diff --git a/crates/pdf/src/pages.rs b/crates/pdf/src/pages.rs index 17618280e..7bc47c9bd 100644 --- a/crates/pdf/src/pages.rs +++ b/crates/pdf/src/pages.rs @@ -46,6 +46,11 @@ const MAX_LINKS: usize = 16; /// The application's calculator-procedure depth policy, layered over the shared reader's limits. const MAX_PROCEDURES: usize = 64; +/// The values of `d` that the reader can parse (`Dict::entries` panics on one it can't). +fn values<'a>(d: &Dict<'a>) -> impl Iterator>> { + d.keys().filter_map(|k| d.get_raw::>(k.as_ref())) +} + /// What the function or colour space `o` is built on: the functions a stitching function joins; /// the base, alternate and tint of a colour space; the alternate of a colour profile. Nothing for /// any other object. @@ -144,7 +149,7 @@ fn nests_without_end(pdf: &Pdf) -> bool { } match &o { Object::Array(a) => pending.extend(a.raw_iter().filter_map(direct)), - _ => pending.extend(dict_or_stream(&o).into_iter().flat_map(|(d, _)| d.entries()).filter_map(|(_, v)| direct(v))), + _ => pending.extend(dict_or_stream(&o).into_iter().flat_map(|(d, _)| values(d)).filter_map(direct)), } } false diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index a868dec79..28e02d633 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -796,3 +796,26 @@ fn functions_and_colour_spaces_built_on_themselves_are_refused() { assert!(d.layers.iter().any(|l| l.children().is_some_and(|c| !c.is_empty())), "{what}: its art is read"); } } + +/// A dictionary value the reader can't parse (a name with a broken `#` escape, a number with a +/// stray `-`) is left out of the nesting check, which goes on with the rest of the file: a file +/// whose shading has a plain function is read, and one whose function stitches itself is refused. +#[test] +fn dictionary_values_the_reader_cannot_parse_are_skipped() { + use vectorcraft_testkit::pdf::{PdfPage, first_extra, pdf_with}; + let me = format!("{} 0 R", first_extra(1)); + let shaded = PdfPage { + resources: format!("/Shading << /S << /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function {me} >> >>"), + ..PdfPage::new(100.0, 100.0, "/S sh") + }; + let ramp = "<< /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >>"; + let stitch = format!("<< /FunctionType 3 /Domain [0 1] /Functions [{me}] /Bounds [] /Encode [0 1] >>"); + for junk in ["<< /A /#zz >>", "<< /A 12-.5 >>"] { + let [plain, looped] = [ramp, stitch.as_str()].map(|f| pdf_with(std::slice::from_ref(&shaded), &[f, junk], None)); + assert_eq!(info(&plain, None).map(|i| i.pages.len()).ok(), Some(1), "{junk}"); + let d = import(&plain).unwrap_or_else(|e| panic!("{junk}: {e}")); + assert!(d.layers.iter().any(|l| l.children().is_some_and(|c| !c.is_empty())), "{junk}: its art is read"); + let e = import(&looped).err().map(|e| e.to_string()); + assert!(e.as_deref().is_some_and(|e| e.contains("built on one another without end")), "{junk}: {e:?}"); + } +}