diff --git a/crates/render/src/raster/tests.rs b/crates/render/src/raster/tests.rs index 4c22065196..5677328deb 100644 --- a/crates/render/src/raster/tests.rs +++ b/crates/render/src/raster/tests.rs @@ -9175,19 +9175,37 @@ fn a_complete_update_reads_as_before() { assert_eq!((count, size, model), (2, (300, 150), 2)); } -/// A file with data after `%%EOF` that holds no object (a web page glued to a served file) is read -/// through its table as before, by the renderer and the model alike: the file has a stale copy of -/// its page that only a rebuild would show. +/// A file with data after `%%EOF` that holds no object header (a web page glued to a served file) +/// is read through its table by the renderer, as before: the file has a stale copy of its page that +/// only a rebuild would show. The model, finding no `startxref` in its footer window, rebuilds from +/// object headers and takes that stale 999 x 999 copy, so here the two still differ, as they did +/// before this change. #[test] fn trailing_data_without_objects_reads_as_before() { let mut junk = one_page_file_with(true); - junk.extend_from_slice(b"

1 0 objection: 12 0 endobj, 3 0obj, 4 0 obj_name

"); + junk.extend_from_slice(b"

1 0 objection: 12 0 endobj, 3 0obj, 4 0 obj_name, 4294967296 0 obj

"); junk.extend(std::iter::repeat_n(b' ', 5000)); junk.extend_from_slice(b"\n"); + let doc = pdfcraft_cos::Document::open(Arc::new(junk.clone())).unwrap(); + let catalog = doc.get(doc.root().unwrap()); + let pages = doc.resolve(catalog.as_dict().unwrap().get(b"Pages").unwrap()); + let kid = doc.resolve(pages.as_dict().unwrap().get(b"Kids").unwrap().as_array().unwrap().first().unwrap()); + let media = kid.as_dict().unwrap().get(b"MediaBox").unwrap().as_array().unwrap().iter().filter_map(|x| x.as_f64()).collect::>(); + assert_eq!(media, [0.0, 0.0, 999.0, 999.0], "the model's rebuilt page"); let ((count, size), model) = renderer_and_model_pages(junk); assert_eq!((count, size, model), (1, (200, 100), 1)); } +/// A cut-off update whose object headers run straight into their dictionaries (`4 0 obj<<`) is +/// still found: a delimiter ends the keyword (ISO 32000-2 §7.2.2), and the renderer shows the +/// update's new first page. +#[test] +fn a_cut_off_update_with_obj_followed_by_a_delimiter_is_rebuilt() { + let file = String::from_utf8(with_update(&one_page_file(), 5000, false)).unwrap().replace(" obj\n<<", " obj<<").into_bytes(); + let ((count, size), model) = renderer_and_model_pages(file); + assert_eq!((count, size, model), (2, (300, 150), 2)); +} + /// A cut-off update that ends within the footer window leaves the older `startxref` near the end, /// which the model reads too: both keep the older revision there. #[test] diff --git a/vendor/hayro-syntax/src/xref.rs b/vendor/hayro-syntax/src/xref.rs index 92d69ca5d3..c70cc554b4 100644 --- a/vendor/hayro-syntax/src/xref.rs +++ b/vendor/hayro-syntax/src/xref.rs @@ -918,8 +918,9 @@ pub(crate) fn cut_off_update_follows(data: &[u8]) -> bool { /// PdfCraft patch: (hayro-syntax (43)) whether `data` holds an `N G obj` header: two runs of /// digits and the keyword, separated by white space, at the start of the data or after white /// space or `>`, the keyword ended by white space, a delimiter or the end of the data (ISO -/// 32000-2 §7.2.2), as the document model's repair scan reads it: `1 0 obj_name` is no header. -/// One linear pass, no allocation. +/// 32000-2 §7.2.2), as the document model's repair scan reads it: `1 0 obj_name` is no header, +/// and neither is one whose object number doesn't fit 32 bits (`4294967296 0 obj`), which that +/// scan skips too. One linear pass, no allocation. fn has_object_header(data: &[u8]) -> bool { let digits = |from: usize| { data.get(from..).map_or(0, |rest| { @@ -951,7 +952,13 @@ fn has_object_header(data: &[u8]) -> bool { at += g; let s2 = spaces(at); at += s2; - if s1 > 0 + // The object number as the model reads it, a 32-bit unsigned integer. + let number_fits = data + .get(i..i + n) + .and_then(|d| core::str::from_utf8(d).ok()) + .is_some_and(|d| d.parse::().is_ok()); + if number_fits + && s1 > 0 && g > 0 && s2 > 0 && data.get(at..).is_some_and(|rest| rest.starts_with(b"obj"))