mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
PDF.R1: never panic on a damaged array or dictionary in an imported PDF
hayro-syntax's Dict::entries still unwraps each value it reads, so a dictionary cut short (or with a stray token) aborts the import. The craft-libs ArrayIter now stops at the first unreadable item instead of panicking, which also means a hostile item after a self-referencing function or colour space is silently dropped from nests_without_end. - import_color: `readable(array)` checks every item parses; `items()` yields nothing for an unreadable array; `entries()` reads a dictionary key by key through get_raw, skipping values that don't parse. - pages/editing/import_scan: use the helpers instead of raw_iter() and Dict::entries(). - nests_without_end refuses a file with an unreadable array. - Tests: two refusal cases in pdf tests and an .ai cut inside an array or dictionary in engine/tests/import_fuzz.rs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcHisW1vt6pkVPYbzL6Cpg
This commit is contained in:
@@ -3448,3 +3448,18 @@ fn a_pattern_painting_its_own_tile_opens_without_the_paint() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A .ai cut short inside an array or dictionary of its PDF part (found by
|
||||
/// `ai_mutated_save_never_panics`): a value can't be read whole, and hayro's iterators over the
|
||||
/// array's items or the dictionary's entries would panic on it.
|
||||
#[test]
|
||||
fn ai_cut_inside_an_array_or_dictionary_opens_without_panicking() {
|
||||
for flip in [None, Some((12262, b'/'))] {
|
||||
let mut b = saved_ai();
|
||||
if let Some((at, c)) = flip {
|
||||
b[at] = c;
|
||||
}
|
||||
b.truncate(44902.min(b.len()));
|
||||
survive("ai cut inside an array or dictionary", || vectorcraft_engine::cmd::fileio::load("saved.ai", &b).ok().map(|l| l.doc)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use krilla::embed::{AssociationKind, EmbeddedFile, MimeType};
|
||||
use krilla::metadata::DateTime;
|
||||
|
||||
use crate::PdfError;
|
||||
use crate::import_color::items;
|
||||
use crate::lab_spot::find;
|
||||
|
||||
/// The embedded file holding the native document (its name among the PDF's attachments).
|
||||
@@ -110,7 +111,7 @@ fn editing_spec<'a>(node: &Dict<'a>, depth: usize, seen: &mut HashSet<ObjectIden
|
||||
}
|
||||
let kids = node.get::<Array<'a>>(b"Kids")?;
|
||||
// Only a referenced kid can be shared (a direct kid's `obj_id` is the object holding it).
|
||||
kids.raw_iter().zip(kids.iter::<Dict<'a>>()).find_map(|(raw, kid)| {
|
||||
items(&kids).zip(kids.iter::<Dict<'a>>()).find_map(|(raw, kid)| {
|
||||
if raw.as_obj_ref().is_some_and(|r| !seen.insert(r.into())) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ use std::fmt::{self, Debug, Write as _};
|
||||
use craft_pdf::interpret::Function;
|
||||
use craft_pdf::interpret::color::ColorSpace;
|
||||
use craft_pdf::syntax::Pdf;
|
||||
use craft_pdf::syntax::object::{Array, MaybeRef, Name, Object, Stream};
|
||||
use craft_pdf::syntax::object::{Array, Dict, MaybeRef, Name, Object, Stream};
|
||||
use craft_pdf::syntax::reader::{Reader, ReaderExt};
|
||||
use smallvec::SmallVec;
|
||||
use vectorcraft_color::swatch::REGISTRATION;
|
||||
use vectorcraft_color::{Color, Paint, Swatch};
|
||||
@@ -304,7 +305,7 @@ impl<'p> Colors<'p> {
|
||||
match o {
|
||||
Object::Array(a) => {
|
||||
self.ink_space(&a);
|
||||
for item in a.raw_iter().filter_map(direct) {
|
||||
for item in items(&a).filter_map(direct) {
|
||||
self.visit(item, depth + 1);
|
||||
}
|
||||
}
|
||||
@@ -379,6 +380,28 @@ impl<'p> Colors<'p> {
|
||||
}
|
||||
|
||||
/// An object written in place (references are visited as objects of their own).
|
||||
/// Can every item of `a` be read? Not in a damaged file, and hayro's own iterator over the items
|
||||
/// unwraps each read and panics there.
|
||||
///
|
||||
/// The items are read without the array's context. That reading fails wherever the array's own
|
||||
/// would (a string it can't decrypt is kept as it is either way; a stream whose `/Length` it can't
|
||||
/// follow falls back to finding `endstream`, as the array's reading does when its length is wrong).
|
||||
pub(crate) fn readable(a: &Array<'_>) -> bool {
|
||||
let mut r = Reader::new(a.data());
|
||||
std::iter::from_fn(|| {
|
||||
r.skip_white_spaces_and_comments();
|
||||
(!r.at_end()).then(|| r.read_without_context::<MaybeRef<Object<'_>>>().is_some())
|
||||
})
|
||||
.all(|read| read)
|
||||
}
|
||||
|
||||
/// The items of `a`, or none when one of them can't be read ([`readable`]). A file with such an
|
||||
/// array is refused before it is imported ([`crate::pages`]), so that none never hides what the
|
||||
/// interpreter would still read of it (its leading items).
|
||||
pub(crate) fn items<'a>(a: &Array<'a>) -> impl Iterator<Item = MaybeRef<Object<'a>>> + use<'a> {
|
||||
readable(a).then(|| a.raw_iter()).into_iter().flatten()
|
||||
}
|
||||
|
||||
pub(crate) fn direct(m: MaybeRef<Object<'_>>) -> Option<Object<'_>> {
|
||||
match m {
|
||||
MaybeRef::NotRef(o) => Some(o),
|
||||
@@ -387,7 +410,7 @@ pub(crate) fn direct(m: MaybeRef<Object<'_>>) -> Option<Object<'_>> {
|
||||
}
|
||||
|
||||
/// The values of `d` that the reader can parse, sorted by key as `Dict::entries` sorts them.
|
||||
pub(crate) fn values<'a>(d: &craft_pdf::syntax::object::Dict<'a>) -> impl Iterator<Item = MaybeRef<Object<'a>>> {
|
||||
pub(crate) fn values<'a>(d: &Dict<'a>) -> impl Iterator<Item = MaybeRef<Object<'a>>> {
|
||||
let mut keys: Vec<Name<'a>> = d.keys().collect();
|
||||
keys.sort_by(|a, b| a.as_ref().cmp(b.as_ref()));
|
||||
keys.into_iter().filter_map(|k| d.get_raw::<Object<'a>>(k.as_ref()))
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::import_color::items;
|
||||
use craft_pdf::interpret::CacheKey;
|
||||
use craft_pdf::syntax::Pdf;
|
||||
use craft_pdf::syntax::content::TypedIter;
|
||||
@@ -78,7 +79,7 @@ impl Ocgs {
|
||||
let config = props.get::<Dict<'_>>(b"D").unwrap_or_default();
|
||||
let refs = |d: &Dict<'_>, key: &[u8]| -> Vec<ObjectIdentifier> {
|
||||
d.get::<Array<'_>>(key)
|
||||
.map(|a| a.raw_iter().filter_map(|i| i.as_obj_ref()).map(ObjectIdentifier::from).take(MAX_GROUPS).collect())
|
||||
.map(|a| items(&a).filter_map(|i| i.as_obj_ref()).map(ObjectIdentifier::from).take(MAX_GROUPS).collect())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let all = refs(&props, b"OCGs");
|
||||
@@ -111,7 +112,7 @@ impl Ocgs {
|
||||
/// stays where it is first listed. `seen`: the entries read so far.
|
||||
fn nest(&mut self, order: &Array<'_>, parent: Option<usize>, depth: usize, xref: &craft_pdf::syntax::xref::XRef, seen: &mut usize) {
|
||||
let mut prev = None;
|
||||
for item in order.raw_iter() {
|
||||
for item in items(order) {
|
||||
*seen += 1;
|
||||
if *seen > 4 * MAX_GROUPS {
|
||||
return;
|
||||
@@ -130,7 +131,7 @@ impl Ocgs {
|
||||
if let Some(sub) = sub
|
||||
&& depth < MAX_NESTING
|
||||
{
|
||||
let label = matches!(sub.raw_iter().next(), Some(MaybeRef::NotRef(Object::String(_))));
|
||||
let label = matches!(items(&sub).next(), Some(MaybeRef::NotRef(Object::String(_))));
|
||||
let under = prev.take().filter(|_| !label).or(parent);
|
||||
self.nest(&sub, under, depth + 1, xref, seen);
|
||||
}
|
||||
@@ -177,7 +178,7 @@ impl Ocgs {
|
||||
/// object) belongs to: a membership dictionary's first group.
|
||||
fn group_of(&mut self, d: &Dict<'_>, id: ObjectIdentifier, xref: &craft_pdf::syntax::xref::XRef) -> Option<usize> {
|
||||
if d.get::<Name<'_>>(b"Type").is_some_and(|t| t.as_ref() == b"OCMD") {
|
||||
let first = d.get::<Array<'_>>(b"OCGs").and_then(|a| a.raw_iter().find_map(|i| i.as_obj_ref())).or_else(|| d.get_ref(b"OCGs"))?;
|
||||
let first = d.get::<Array<'_>>(b"OCGs").and_then(|a| items(&a).find_map(|i| i.as_obj_ref())).or_else(|| d.get_ref(b"OCGs"))?;
|
||||
let id = ObjectIdentifier::from(first);
|
||||
return self.note(id, &xref.get::<Dict<'_>>(id)?);
|
||||
}
|
||||
@@ -355,7 +356,7 @@ impl Walker<'_, '_> {
|
||||
Some(id) if !membership => !self.off.contains(&id),
|
||||
_ => {
|
||||
let ids: Vec<ObjectIdentifier> = match d.get::<Array<'_>>(b"OCGs") {
|
||||
Some(a) => a.raw_iter().filter_map(|i| i.as_obj_ref()).map(ObjectIdentifier::from).collect(),
|
||||
Some(a) => items(&a).filter_map(|i| i.as_obj_ref()).map(ObjectIdentifier::from).collect(),
|
||||
None => d.get_ref(b"OCGs").map(ObjectIdentifier::from).into_iter().collect(),
|
||||
};
|
||||
let on = |i: &ObjectIdentifier| !self.off.contains(i);
|
||||
|
||||
+10
-4
@@ -10,7 +10,7 @@ use craft_pdf::syntax::{DecryptionError, LoadPdfError, Pdf};
|
||||
use kurbo::{Affine, Rect};
|
||||
use vectorcraft_doc::setup::MAX_BLEED;
|
||||
|
||||
use crate::import_color::{direct, values};
|
||||
use crate::import_color::{direct, items, readable, values};
|
||||
use crate::{Choice, CropTo, ImportOptions, PdfError};
|
||||
|
||||
/// What `info` tells about one page.
|
||||
@@ -52,7 +52,7 @@ const MAX_PROCEDURES: usize = 64;
|
||||
fn links<'a>(o: &Object<'a>, xref: &'a XRef) -> Vec<MaybeRef<Object<'a>>> {
|
||||
const SPACES: [&[u8]; 6] = [b"Indexed", b"I", b"Separation", b"DeviceN", b"Pattern", b"ICCBased"];
|
||||
if let Object::Array(a) = o {
|
||||
let mut items = a.raw_iter();
|
||||
let mut items = items(a);
|
||||
let kind = match items.next() {
|
||||
Some(MaybeRef::NotRef(Object::Name(n))) => Some(n),
|
||||
Some(MaybeRef::Ref(r)) => xref.get::<Name<'a>>(r.into()),
|
||||
@@ -61,7 +61,7 @@ fn links<'a>(o: &Object<'a>, xref: &'a XRef) -> Vec<MaybeRef<Object<'a>>> {
|
||||
return if kind.is_some_and(|n| SPACES.contains(&n.as_ref())) { items.collect() } else { vec![] };
|
||||
}
|
||||
let Some((d, _)) = dict_or_stream(o) else { return vec![] };
|
||||
let functions = d.get::<Array<'a>>(b"Functions").map(|a| a.raw_iter().collect::<Vec<_>>()).unwrap_or_default();
|
||||
let functions = d.get::<Array<'a>>(b"Functions").map(|a| items(&a).collect::<Vec<_>>()).unwrap_or_default();
|
||||
functions.into_iter().chain(d.get_raw::<Object<'a>>(b"Alternate")).collect()
|
||||
}
|
||||
|
||||
@@ -121,6 +121,9 @@ const STREAM_NOTE: &str = "a stream in it would decode to more than 256 MB, or i
|
||||
/// (or on themselves), or a calculator function nested past [`MAX_PROCEDURES`].
|
||||
const NESTED_NOTE: &str = "its functions or colour spaces are built on one another without end";
|
||||
|
||||
/// The error of a file with an array whose items can't all be read (a damaged file).
|
||||
const UNREADABLE_NOTE: &str = "an array in it is damaged and can't be read";
|
||||
|
||||
/// Why `pdf` is refused, if it is: a stream the reader would fail on ([`STREAM_NOTE`]), or
|
||||
/// functions, color spaces or calculator procedures nested too deep ([`NESTED_NOTE`]). Every
|
||||
/// object is looked at, and every array, dictionary and stream written inside one, wherever it
|
||||
@@ -140,7 +143,10 @@ fn hostile(pdf: &Pdf) -> Option<&'static str> {
|
||||
return Some(NESTED_NOTE);
|
||||
}
|
||||
match &o {
|
||||
Object::Array(a) => pending.extend(a.raw_iter().filter_map(direct)),
|
||||
// An array with an item that can't be read can't be looked at whole, and the
|
||||
// interpreter would still read its leading items: the file is refused.
|
||||
Object::Array(a) if !readable(a) => return Some(UNREADABLE_NOTE),
|
||||
Object::Array(a) => pending.extend(items(a).filter_map(direct)),
|
||||
_ => pending.extend(dict_or_stream(&o).into_iter().flat_map(|(d, _)| values(d)).filter_map(direct)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -806,6 +806,10 @@ fn functions_and_colour_spaces_built_on_themselves_are_refused() {
|
||||
("procedures in procedures", &shaded, vec![calculator(&format!("{}{}", "{".repeat(100), "}".repeat(100)))]),
|
||||
("a colour space on itself", &filled, vec![indexed(&me)]),
|
||||
("two colour spaces on each other", &filled, vec![indexed(&next), indexed(&me)]),
|
||||
// Its leading items are read by the interpreter all the same: an item that can't be read
|
||||
// (a stream of an external file) after them doesn't hide them.
|
||||
("a function stitching itself, then an item that can't be read", &shaded, vec![stitch(&format!("{me} << /F (x) >> stream"))]),
|
||||
("a colour space on itself, then an item that can't be read", &filled, vec![format!("[/Indexed {me} 1 <000000ffffff> << /F (x) >> stream]")]),
|
||||
] {
|
||||
let objects: Vec<&str> = objects.iter().map(String::as_str).collect();
|
||||
let e = import(&pdf_with(std::slice::from_ref(page), &objects, None)).err().map(|e| e.to_string());
|
||||
|
||||
Reference in New Issue
Block a user