Importers: a PDF dictionary value the reader can't parse no longer crashes the nesting check

The shared PDF reader (craft_pdf::syntax, craft-libs ca50d63b) indexes
a dictionary by skipping its values. Its skip accepts some values that
its parse refuses, such as a name with a broken # escape (/#zz) and a
number such as 12-.5. Dict::entries unwraps the parse of each value
(vendor/hayro-syntax/src/object/dict.rs:104). The check in pages.rs
that refuses functions and color spaces built on one another without
end (nests_without_end) calls entries on every dictionary of a file
when the file opens. So document.pdfInfo, PDF import,
clipboard.importPdf and loading a .ai panicked on such a file, and
three import fuzz tests fail on main with PROPTEST_RNG_SEED=1182 and
PROPTEST_CASES=20000.

The check now reads a dictionary's values with keys and get_raw and
leaves out a value that doesn't parse.

A test in crates/pdf puts each of those values in a file with a plain
function, which opens, and in a file with a function that stitches
itself, which is still refused.
This commit is contained in:
dotCooCoo
2026-10-16 01:34:09 +07:00
committed by bflatastic
parent b12a52f724
commit a69ddea930
2 changed files with 29 additions and 1 deletions
+6 -1
View File
@@ -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<Item = MaybeRef<Object<'a>>> {
d.keys().filter_map(|k| d.get_raw::<Object<'a>>(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
+23
View File
@@ -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:?}");
}
}