Merge PR #2400: Issue #2366: a trailer whose /Prev is a reference still reaches the earlier sections

crates/cos/src/document.rs: kept #2220's shifted_subsection_end and object_headers and this PR's prev_offset.
vendor/README.md: hayro-syntax row already has (22) and (23); this PR's patch and tests are numbered (24).
This commit is contained in:
aiongg
2026-10-22 01:46:50 +00:00
3 changed files with 243 additions and 30 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+127 -27
View File
@@ -807,6 +807,34 @@ fn populate_xref_impl<'a>(data: &'a [u8], pos: usize, xref_map: &mut XrefMap) ->
populate_xref_impl_inner(data, pos, xref_map, &mut visited)
}
/// The offset of the previous xref section (`Prev`).
// PdfCraft patch: (#2366) some writers give `Prev` as a reference `N G R`. When this section's
// own entries (`own`) place that object and it holds an offset (iText's `indirectPrev.pdf`), that
// is the offset; otherwise its object number is (tiff2pdf, in JHOVE's PDF-HUL-157 files), as
// `pdfcraft-cos` and other readers take it.
fn prev_offset(data: &[u8], trailer: &Dict<'_>, own: &XrefMap) -> Option<usize> {
if let Some(prev) = trailer.get::<i32>(PREV) {
return usize::try_from(prev).ok();
}
let r = trailer.get_ref(PREV)?;
let id = ObjectIdentifier::new(r.obj_number, r.gen_number);
let held = match own.get(&id) {
Some(EntryType::Normal(at)) => {
let mut reader = Reader::new(data);
reader.jump(*at);
reader
.read_with_context::<IndirectObject<i32>>(&ReaderContext::dummy())
.filter(|o| o.id() == &id)
.map(|o| o.get())
.filter(|p| *p >= 0)
}
_ => None,
};
usize::try_from(held.unwrap_or(r.obj_number)).ok()
}
/// Maximum number of allowed xref `Prev` pointers before we abort.
const MAX_XREF_CHAIN_DEPTH: usize = 256;
@@ -882,17 +910,8 @@ fn populate_from_xref_table<'a>(
reader.skip_white_spaces();
let mut max_obj = 0;
if let Some(prev) = trailer.get::<i32>(PREV) {
// First insert the entries from any previous xref tables.
populate_xref_impl_inner(data, prev as usize, insert_map, visited)?;
}
// In hybrid files, entries in `XRefStm` should have higher priority, therefore we insert them
// after looking at `PREV`.
if let Some(xref_stm) = trailer.get::<i32>(XREF_STM) {
populate_xref_impl_inner(data, xref_stm as usize, insert_map, visited)?;
}
// PdfCraft patch: (#2366) this section's entries are read first, for `prev_offset`.
let mut own = XrefMap::default();
while let Some(header) = reader.read_without_context::<SubsectionHeader>() {
reader.skip_white_spaces();
@@ -908,7 +927,7 @@ fn populate_from_xref_table<'a>(
// Specification says we should ignore any object number > SIZE, but probably
// not important?
if entry.used {
insert_map.insert(
own.insert(
ObjectIdentifier::new(obj_number as i32, entry.gen_number),
EntryType::Normal(entry.offset),
);
@@ -916,6 +935,19 @@ fn populate_from_xref_table<'a>(
}
}
if let Some(prev) = prev_offset(data, &trailer, &own) {
// First insert the entries from any previous xref tables.
populate_xref_impl_inner(data, prev, insert_map, visited)?;
}
// In hybrid files, entries in `XRefStm` should have higher priority, therefore we insert them
// after looking at `PREV`.
if let Some(xref_stm) = trailer.get::<i32>(XREF_STM) {
populate_xref_impl_inner(data, xref_stm as usize, insert_map, visited)?;
}
insert_map.extend(own);
Some(trailer.data())
}
@@ -929,11 +961,6 @@ fn populate_from_xref_stream<'a>(
.read_with_context::<IndirectObject<Stream<'_>>>(&ReaderContext::dummy())?
.get();
if let Some(prev) = stream.dict().get::<i32>(PREV) {
// First insert the entries from any previous xref tables.
let _ = populate_xref_impl_inner(data, prev as usize, insert_map, visited)?;
}
let size = stream.dict().get::<u32>(SIZE)?;
let [f1_len, f2_len, f3_len] = stream.dict().get::<[u8; 3]>(W)?;
@@ -950,6 +977,8 @@ fn populate_from_xref_stream<'a>(
}
let xref_data = stream.decoded().ok()?;
// PdfCraft patch: (#2366) this section's entries are read first, for `prev_offset`.
let mut own = XrefMap::default();
let mut xref_reader = Reader::new(xref_data.as_ref());
if let Some(arr) = stream.dict().get::<Array<'_>>(INDEX) {
@@ -963,21 +992,20 @@ fn populate_from_xref_stream<'a>(
f1_len,
f2_len,
f3_len,
insert_map,
&mut own,
)?;
}
} else {
xref_stream_subsection(
&mut xref_reader,
0,
size,
f1_len,
f2_len,
f3_len,
insert_map,
)?;
xref_stream_subsection(&mut xref_reader, 0, size, f1_len, f2_len, f3_len, &mut own)?;
}
if let Some(prev) = prev_offset(data, stream.dict(), &own) {
// First insert the entries from any previous xref tables.
let _ = populate_xref_impl_inner(data, prev, insert_map, visited)?;
}
insert_map.extend(own);
Some(stream.dict().data())
}
@@ -1434,6 +1462,78 @@ mod tests {
let _result = populate_xref_impl(pdf.as_ref(), xref_pos, &mut xref_map);
}
#[test]
fn prev_as_reference_reaches_the_earlier_section() {
let mut pdf = b"%PDF-1.0\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n".to_vec();
let first_xref_pos = pdf.len();
pdf.extend_from_slice(
format!(
"xref\n\
0 2\n\
0000000000 65535 f \n\
0000000009 00000 n \n\
trailer\n<< /Size 2 /Root 1 0 R >>\n\
startxref\n{first_xref_pos}\n%%EOF\n"
)
.as_bytes(),
);
let second_xref_pos = pdf.len();
pdf.extend_from_slice(
format!(
"xref\n\
0 1\n\
0000000000 65535 f \n\
trailer\n<< /Size 2 /Root 1 0 R /Prev {first_xref_pos} 0 R >>\n\
startxref\n{second_xref_pos}\n%%EOF"
)
.as_bytes(),
);
let mut xref_map = FxHashMap::default();
let xref_pos = find_last_xref_pos(pdf.as_ref()).unwrap();
assert_eq!(xref_pos, second_xref_pos);
populate_xref_impl(pdf.as_ref(), xref_pos, &mut xref_map).unwrap();
assert!(xref_map.contains_key(&ObjectIdentifier::new(1, 0)));
}
#[test]
fn prev_as_reference_to_an_object_holding_the_offset_reads_that_offset() {
let mut pdf = b"%PDF-1.0\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n".to_vec();
let first_xref_pos = pdf.len();
pdf.extend_from_slice(
format!(
"xref\n\
0 2\n\
0000000000 65535 f \n\
0000000009 00000 n \n\
trailer\n<< /Size 2 /Root 1 0 R >>\n\
startxref\n{first_xref_pos}\n%%EOF\n"
)
.as_bytes(),
);
// Object 3 holds the offset; read as offset 3, `Prev` would point inside the header.
let held_pos = pdf.len();
pdf.extend_from_slice(format!("3 0 obj\n{first_xref_pos}\nendobj\n").as_bytes());
let second_xref_pos = pdf.len();
pdf.extend_from_slice(
format!(
"xref\n\
3 1\n\
{held_pos:010} 00000 n \n\
trailer\n<< /Size 4 /Root 1 0 R /Prev 3 0 R >>\n\
startxref\n{second_xref_pos}\n%%EOF"
)
.as_bytes(),
);
let mut xref_map = FxHashMap::default();
let xref_pos = find_last_xref_pos(pdf.as_ref()).unwrap();
assert_eq!(xref_pos, second_xref_pos);
populate_xref_impl(pdf.as_ref(), xref_pos, &mut xref_map).unwrap();
assert!(xref_map.contains_key(&ObjectIdentifier::new(1, 0)));
assert!(xref_map.contains_key(&ObjectIdentifier::new(3, 0)));
}
#[test]
fn find_last_xref_uses_last_startxref() {
let pdf = b"%PDF-1.0\nstartxref\n5\n%%EOF\nstartxref\n42\n%%EOF";