diff --git a/crates/cos/src/document/objects.rs b/crates/cos/src/document/objects.rs index 2ac18cff03..3fba3fd395 100644 --- a/crates/cos/src/document/objects.rs +++ b/crates/cos/src/document/objects.rs @@ -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 = 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)); diff --git a/crates/cos/src/document/tests.rs b/crates/cos/src/document/tests.rs index cca4b87326..044a410862 100644 --- a/crates/cos/src/document/tests.rs +++ b/crates/cos/src/document/tests.rs @@ -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:?}"); +}