mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
fix(cos): a save after undo keeps objects packed in a dropped object stream
When the working snapshot is restored from an earlier revision (undo after a save), the next save appends to the last saved file through overlay_changes_from. An object stream of that file that the restored graph frees or reuses the number of took its unchanged members with it: they stayed listed in the dropped stream, and the file reread with no pages. Such members are now written again on their own. Adding a PRC model now edits the catalog (the ADBE extension), which made the model_3d automation test (save, undo, save) reach this case on CI. The same change is part of #3305 (skylartaylor), which also covers security changes. Test: cos a_restored_revision_keeps_what_the_saved_file_held_in_object_streams, failing with the fix reverted (the reread catalog's /Pages resolves to nothing). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
1 parent
4c087336aa
commit
eaf06f9cb9
2 files changed
+45
-2
No files matched your search
@@ -682,8 +682,9 @@ impl Document {
|
||||
///
|
||||
/// Used when the working snapshot was restored from an earlier revision and the next
|
||||
/// incremental save must still append to this file (the last saved revision). Objects
|
||||
/// that already match are left untouched. Reads go through private readers so neither
|
||||
/// document's editing cache is filled.
|
||||
/// that already match are left untouched, except members of an object stream of this file
|
||||
/// whose stream number `current` frees or reuses: those are written again on their own.
|
||||
/// Reads go through private readers so neither document's editing cache is filled.
|
||||
pub fn overlay_changes_from(&mut self, current: &Document) -> Result<(), CosError> {
|
||||
let saved_nums = self.object_numbers();
|
||||
let current_nums = current.object_numbers();
|
||||
@@ -707,6 +708,22 @@ impl Document {
|
||||
}
|
||||
}
|
||||
}
|
||||
// An object stream of this file that is freed or replaced takes its members with it, so
|
||||
// a member that is otherwise unchanged is written again on its own.
|
||||
let gone = |n: u32| freed.binary_search(&n).is_ok() || changed.binary_search(&n).is_ok();
|
||||
let orphaned: Vec<u32> = current_nums
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&num| changed.binary_search(&num).is_err())
|
||||
.filter(|&num| {
|
||||
let packed_in = match self.xref_entry(num) {
|
||||
Some(XrefEntry::InStream { stream, .. }) => Some(stream),
|
||||
_ => None,
|
||||
};
|
||||
packed_in.is_some_and(gone)
|
||||
})
|
||||
.collect();
|
||||
changed.extend(orphaned);
|
||||
for num in freed {
|
||||
let generation = self.generation(num);
|
||||
self.free(ObjRef::new(num, generation));
|
||||
|
||||
@@ -1701,3 +1701,29 @@ fn a_stream_whose_length_is_itself_and_never_ends_loads_quickly() {
|
||||
});
|
||||
rx.recv_timeout(std::time::Duration::from_secs(20)).expect("loading must not retry exponentially");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_restored_revision_keeps_what_the_saved_file_held_in_object_streams() {
|
||||
// The earlier revision: an empty document read back from a file whose catalog and pages
|
||||
// sit in an object stream.
|
||||
let earlier = Document::open(Arc::new(crate::write_full(&Document::new_empty(), &crate::SaveOptions::default()).unwrap())).unwrap();
|
||||
// The saved revision adds an object the catalog links to, so a full save numbers its objects
|
||||
// anew; an undo then returns to the earlier graph.
|
||||
let mut later = earlier.clone();
|
||||
let extra = later.add(Object::Int(7));
|
||||
let root = later.root().unwrap();
|
||||
later.update_dict(root, |c| c.set(b"Extra".to_vec(), Object::Ref(extra))).unwrap();
|
||||
let saved = crate::write_full(&later, &crate::SaveOptions::default()).unwrap();
|
||||
let mut base = Document::open(Arc::new(saved)).unwrap();
|
||||
assert!(
|
||||
base.object_numbers().iter().any(|&n| matches!(base.xref_entry(n), Some(XrefEntry::InStream { .. }))),
|
||||
"the saved file holds objects in a stream"
|
||||
);
|
||||
base.overlay_changes_from(&earlier).unwrap();
|
||||
let file = crate::write_incremental(&base, &crate::SaveOptions::default()).unwrap();
|
||||
let reread = Document::open(Arc::new(file)).unwrap();
|
||||
let catalog = reread.get(reread.root().unwrap()).as_dict().unwrap().clone();
|
||||
assert!(catalog.get(b"Extra").is_none(), "the restored catalog");
|
||||
let pages = reread.resolve(catalog.get(b"Pages").unwrap());
|
||||
assert_eq!(pages.as_dict().and_then(|d| d.name(b"Type")), Some(&b"Pages"[..]), "the pages survive: {pages:?}");
|
||||
}
|
||||
Reference in new issue
Block a user