mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 20:18:06 -06:00
Issue #2366: a trailer whose /Prev is a reference still reaches the earlier sections
The JHOVE PDF-HUL-157 files (tiff2pdf output) end their first trailer with
`/Prev 297298 0 R`: the offset written as a reference. The chain reader took
/Prev as an integer only, so the chain stopped after the first section without
a word, and every object of the earlier sections (here pages 2-4 and their
images) read as null. The document showed one page where Poppler and MuPDF
show four, and a full save wrote the missing kids as null, so pages 2-4 were
unreadable afterwards ("cycle in page tree", 1x1 renders).
A reference-valued /Prev is now read as the offset it names, with a repair-log
note; any other non-offset value is noted and ends the chain as before.
Regression test: document::tests::a_prev_written_as_a_reference_still_reaches_the_earlier_sections.
Verified on JA3-1998-PDF.pdf: four pages read, and after a Subject edit and a
full save Poppler renders all four pages without a warning (before: "Kid
object (page 2) is not an indirect reference (null)").
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
ec0a965074
commit
0e8ebf00e6
@@ -1476,7 +1476,7 @@ impl Document {
|
||||
}
|
||||
}
|
||||
sections.push(XrefRevisionSection { primary, hybrid });
|
||||
next = sect_trailer.int(b"Prev").filter(|p| *p >= 0).map(|p| p as u64);
|
||||
next = prev_offset(§_trailer, off, log);
|
||||
if trailer.is_none() {
|
||||
trailer = Some(sect_trailer);
|
||||
}
|
||||
@@ -1764,7 +1764,7 @@ impl Document {
|
||||
for (num, e) in sect {
|
||||
entries.entry(num).or_insert(e);
|
||||
}
|
||||
next = sect_trailer.int(b"Prev").filter(|p| *p >= 0).map(|p| p as u64);
|
||||
next = prev_offset(§_trailer, off, log);
|
||||
if trailer.is_none() {
|
||||
trailer = Some(sect_trailer);
|
||||
}
|
||||
@@ -1990,6 +1990,25 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
/// The offset of the section before the one whose trailer this is (`/Prev`), if any. Some writers
|
||||
/// (tiff2pdf; JHOVE's PDF-HUL-157 files) give it as a reference, `N 0 R`: other readers rebuild
|
||||
/// such a file, and the sections before are at N, so it is read as that, with a note. Any other
|
||||
/// value that is not an offset is noted and ends the chain.
|
||||
fn prev_offset(trailer: &Dict, off: u64, log: &mut Vec<String>) -> Option<u64> {
|
||||
match trailer.get(b"Prev") {
|
||||
Some(Object::Int(p)) if *p >= 0 => Some(*p as u64),
|
||||
Some(Object::Ref(r)) => {
|
||||
log.push(format!("the trailer at {off} gives /Prev as the reference {} 0 R; reading it as offset {}", r.num, r.num));
|
||||
Some(u64::from(r.num))
|
||||
}
|
||||
Some(_) => {
|
||||
log.push(format!("the trailer at {off} has a /Prev that is not an offset; the sections before it are unreachable"));
|
||||
None
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find(hay: &[u8], needle: &[u8], from: usize, to: usize) -> Option<usize> {
|
||||
let to = to.min(hay.len());
|
||||
if from >= to || needle.is_empty() || to - from < needle.len() {
|
||||
@@ -2834,6 +2853,43 @@ endobj
|
||||
}
|
||||
}
|
||||
|
||||
/// A trailer whose /Prev is written as the reference `N 0 R` (tiff2pdf did this; the files
|
||||
/// are JHOVE's PDF-HUL-157 cases, #2366) used to end the cross-reference chain silently, so
|
||||
/// every object of the earlier sections read as null: a four-page file showed one page, and
|
||||
/// a full save wrote the three missing kids as null. The offset is read from the reference.
|
||||
#[test]
|
||||
fn a_prev_written_as_a_reference_still_reaches_the_earlier_sections() {
|
||||
let base = build(
|
||||
&["<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", "<< /Type /Page /Parent 2 0 R >>"],
|
||||
"/Root 1 0 R",
|
||||
);
|
||||
let prev = String::from_utf8_lossy(&base).rfind("xref\n0 4").unwrap();
|
||||
let update = |prev_entry: &str| {
|
||||
let mut bytes = base.clone();
|
||||
let at = bytes.len();
|
||||
bytes.extend_from_slice(b"4 0 obj\n<< /Title (Update) >>\nendobj\n");
|
||||
let xref = bytes.len();
|
||||
bytes.extend_from_slice(
|
||||
format!(
|
||||
"xref\n4 1\n{at:010} 00000 n \ntrailer\n<< /Size 5 /Root 1 0 R /Info 4 0 R /Prev {prev_entry} >>\nstartxref\n{xref}\n%%EOF\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
Document::open(Arc::new(bytes)).unwrap()
|
||||
};
|
||||
let good = update(&prev.to_string());
|
||||
let bad = update(&format!("{prev} 0 R"));
|
||||
assert!(good.repair_log().is_empty(), "{:?}", good.repair_log());
|
||||
assert_eq!(bad.revisions().len(), 2, "{:?}", bad.repair_log());
|
||||
assert_eq!(bad.repair_log().len(), 1, "a note about the /Prev: {:?}", bad.repair_log());
|
||||
assert_eq!(bad.xref_entry(3), good.xref_entry(3), "the page of the first section is reached");
|
||||
assert_eq!(bad.get(ObjRef::new(3, 0)).as_dict().and_then(|d| d.name(b"Type")), Some(&b"Page"[..]));
|
||||
assert_eq!(
|
||||
bad.get(ObjRef::new(4, 0)).as_dict().and_then(|d| d.get(b"Title").and_then(|t| t.as_string().map(|s| s.to_text()))),
|
||||
Some("Update".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damaged_xref_is_reconstructed() {
|
||||
let mut bytes = build(&["<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Kids [] /Count 0 >>"], "/Root 1 0 R");
|
||||
|
||||
Reference in New Issue
Block a user