mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 12:38:58 -06:00
fix(cos): a repaired xref table fills only the numbers the damage leaves out (refs #1628)
This commit is contained in:
@@ -1933,9 +1933,11 @@ impl Document {
|
||||
/// that nothing else replaces. (A number no section lists is not evidence: it is a null
|
||||
/// object, §7.3.10, and valid files have them below an oversized `/Size`.) Viewers rebuild
|
||||
/// the table and show these objects; reading them as null would drop them from every copy.
|
||||
/// When one of those shows, the file is scanned once for object headers, numbers without an
|
||||
/// entry take the last header's location, and wrong offsets are looked up there (`load`).
|
||||
/// Existing entries are never replaced: an object freed by an update stays freed.
|
||||
/// When one of those shows, the file is scanned once for object headers, and wrong offsets
|
||||
/// are looked up there (`load`). Only the numbers the damage leaves out take the last
|
||||
/// header's location: one whose entry was set aside, or the last object of a subsection
|
||||
/// numbered one too low. Any other unlisted number stays null, and existing
|
||||
/// entries are never replaced: an object freed by an update stays freed.
|
||||
fn fill_xref_gaps(&mut self, log: &mut Vec<String>) {
|
||||
let entries = &self.entries;
|
||||
let shifted = matches!(entries.lookup(0, &self.data), Some(XrefEntry::InFile { .. }));
|
||||
@@ -1948,9 +1950,22 @@ impl Document {
|
||||
located.insert(num, (offset, generation));
|
||||
}
|
||||
let mut filled = entries.to_map(&self.data);
|
||||
// A subsection `0 N` whose rows are those of objects 1 to N leaves out object N, whatever
|
||||
// newer sections say about its neighbours.
|
||||
let past_shift: HashSet<u32> = if shifted {
|
||||
self.revisions
|
||||
.iter()
|
||||
.filter(|r| !r.is_stream)
|
||||
.filter_map(|r| usize::try_from(r.xref_offset).ok().map(|o| if r.header_relative { o.saturating_add(self.header_offset) } else { o }))
|
||||
.filter_map(|at| shifted_subsection_end(&self.data, at))
|
||||
.collect()
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
let mut found = 0usize;
|
||||
for (&num, &(offset, generation)) in &located {
|
||||
if num != 0 && !filled.contains_key(&num) {
|
||||
let left_out = past_shift.contains(&num) || self.xref_oddities.iter().any(|(_, n)| *n == num);
|
||||
if num != 0 && !filled.contains_key(&num) && left_out {
|
||||
filled.insert(num, XrefEntry::InFile { offset, generation });
|
||||
found += 1;
|
||||
}
|
||||
@@ -2028,6 +2043,25 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
/// For a classic table at `at` whose first subsection is `0 N` and opens with an in-use row
|
||||
/// (the rows of objects 1 to N), the number N its rows really end at.
|
||||
fn shifted_subsection_end(data: &[u8], at: usize) -> Option<u32> {
|
||||
let mut lx = Lexer::new(data, at);
|
||||
if !lx.eat_keyword(b"xref") {
|
||||
return None;
|
||||
}
|
||||
let (Ok(Object::Int(0)), Ok(Object::Int(count))) = (lx.object(), lx.object()) else { return None };
|
||||
lx.skip_ws();
|
||||
let offset = std::str::from_utf8(lx.token()).ok().and_then(|s| s.parse::<u64>().ok());
|
||||
lx.skip_ws();
|
||||
lx.token();
|
||||
lx.skip_ws();
|
||||
match (offset, lx.token()) {
|
||||
(Some(offset), b"n") if offset != 0 => u32::try_from(count).ok().filter(|n| *n > 0),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `N G obj` header in `data`, in file order, as (number, offset, generation).
|
||||
fn object_headers(data: &[u8]) -> Vec<(u32, u64, u16)> {
|
||||
let mut headers = Vec::new();
|
||||
@@ -2466,6 +2500,54 @@ endobj
|
||||
assert_eq!(doc.xref_entry(5), Some(XrefEntry::Free { next_generation: 1 }));
|
||||
}
|
||||
|
||||
/// Repairing a damaged table fills only the numbers the damage leaves out: an old body of a
|
||||
/// number no section lists, under an oversized `/Size`, stays a null object.
|
||||
#[test]
|
||||
fn a_repaired_table_leaves_an_unlisted_number_null() {
|
||||
let base = scanned_page();
|
||||
let at = String::from_utf8_lossy(&base).rfind("\nxref\n").unwrap() + 1;
|
||||
let entries = table_entries(&base);
|
||||
let mut offset_zero = vec!["0 6".to_string()];
|
||||
offset_zero.extend(entries.iter().cloned());
|
||||
offset_zero[5] = "0000000000 00000 n ".into();
|
||||
let mut off_by_one = vec!["0 5".to_string()];
|
||||
off_by_one.extend(entries[1..6].iter().cloned());
|
||||
for (name, table, found) in [("offset 0", offset_zero, 4), ("off by one", off_by_one, 5)] {
|
||||
let bytes = [&base[..at], b"7 0 obj\n(stale)\nendobj\n", &base[at..]].concat();
|
||||
let doc = Document::open(Arc::new(with_table(&bytes, &table, 9))).unwrap();
|
||||
assert!(matches!(&*doc.get(ObjRef::new(found, 0)), Object::Stream(_)), "{name}: the gap is filled");
|
||||
assert_eq!(*doc.get(ObjRef::new(7, 0)), Object::Null, "{name}: {:?}", doc.repair_log());
|
||||
assert_eq!(doc.xref_entry(7), None, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The last object of a subsection numbered one too low is filled from that subsection,
|
||||
/// whatever later happens to the row before it: an incremental save that rewrites object 4,
|
||||
/// or object 4's own row at offset 0.
|
||||
#[test]
|
||||
fn the_last_object_of_a_shifted_subsection_is_filled_whatever_its_neighbour_says() {
|
||||
let base = scanned_page();
|
||||
let entries = table_entries(&base);
|
||||
let mut off_by_one = vec!["0 5".to_string()];
|
||||
off_by_one.extend(entries[1..6].iter().cloned());
|
||||
let shifted = with_table(&base, &off_by_one, 5);
|
||||
let prev = String::from_utf8_lossy(&shifted).rfind("\nxref\n").unwrap() + 1;
|
||||
let mut updated = shifted.clone();
|
||||
let body = updated.len();
|
||||
updated.extend_from_slice(b"4 0 obj\n<< /Length 21 >>\nstream\n10 0 0 10 0 0 cm /Im0 Do\nendstream\nendobj\n");
|
||||
let section = updated.len();
|
||||
updated.extend_from_slice(
|
||||
format!("xref\n4 1\n{body:010} 00000 n \ntrailer\n<< /Size 6 /Root 1 0 R /Prev {prev} >>\nstartxref\n{section}\n%%EOF\n").as_bytes(),
|
||||
);
|
||||
let mut neighbour_at_zero = off_by_one.clone();
|
||||
neighbour_at_zero[5] = "0000000000 00000 n ".into();
|
||||
for (name, bytes) in [("updated", updated), ("row 4 at offset 0", with_table(&base, &neighbour_at_zero, 5))] {
|
||||
let doc = Document::open(Arc::new(bytes)).unwrap();
|
||||
let image = doc.get(ObjRef::new(5, 0));
|
||||
assert_eq!(image.as_dict().and_then(|d| d.name(b"Subtype")), Some(&b"Image"[..]), "{name}: {:?}", doc.repair_log());
|
||||
}
|
||||
}
|
||||
|
||||
/// The image the first page draws as `/Im1`, followed through the page tree.
|
||||
fn first_page_image(doc: &Document) -> Arc<Object> {
|
||||
let catalog = doc.get(doc.root().unwrap());
|
||||
|
||||
Reference in New Issue
Block a user