diff --git a/crates/xfa/src/linked_images.rs b/crates/xfa/src/linked_images.rs index ff85a1a431..d9c2e2983b 100644 --- a/crates/xfa/src/linked_images.rs +++ b/crates/xfa/src/linked_images.rs @@ -77,27 +77,57 @@ struct Found<'a> { doc: &'a Document, names: HashMap, read: HashMap>>, + /// Bytes of the pictures in `read`, and of the copies put in places: each at most + /// [`MAX_TOTAL`], however many names the tree holds. + kept: usize, total: usize, warnings: Vec, } impl Found<'_> { /// A copy of the picture linked as `href` for one more place, when the file holds it and the - /// copies so far leave room for it under [`MAX_TOTAL`]. + /// pictures kept and the copies so far leave room for it under [`MAX_TOTAL`]. fn get(&mut self, href: &str) -> Option> { if !self.read.contains_key(href) { - let data = self.load(href); + // Past the total, a picture not read yet is neither read nor kept. + let data = if self.kept >= MAX_TOTAL { None } else { self.load(href) }; + let data = match data { + Some(d) if self.kept.saturating_add(d.len()) > MAX_TOTAL => { + self.over_total(); + None + } + Some(d) => { + self.kept = self.kept.saturating_add(d.len()); + Some(d) + } + None => { + if self.kept >= MAX_TOTAL && self.names.contains_key(href) { + self.over_total(); + } + None + } + }; self.read.insert(href.to_string(), data); } let len = self.read.get(href).and_then(Option::as_ref).map(Vec::len)?; if self.total.saturating_add(len) > MAX_TOTAL { - self.warnings.push("the pictures the form keeps in the file take too much memory in all; the rest are left out".into()); + self.over_total(); return None; } self.total = self.total.saturating_add(len); self.read.get(href).cloned().flatten() } + fn over_total(&mut self) { + self.warnings.push("the pictures the form keeps in the file take too much memory in all; the rest are left out".into()); + } + + /// Bytes of the pictures read so far and kept for reuse. + #[cfg(test)] + fn kept(&self) -> usize { + self.read.values().flatten().map(Vec::len).sum() + } + fn load(&mut self, href: &str) -> Option> { let object = self.names.get(href)?; let object = self.doc.resolve(object); @@ -156,12 +186,22 @@ impl Found<'_> { /// Give the pictures `tpl` links to (and doesn't embed) the data `doc` keeps for them. A link /// the file holds nothing for stays empty. What couldn't be read is returned as warnings. pub fn resolve(tpl: &mut Template, doc: &Document) -> Vec { + resolve_found(tpl, doc).map(|f| f.warnings).unwrap_or_default() +} + +fn resolve_found<'a>(tpl: &mut Template, doc: &'a Document) -> Option> { let names = names(doc); if names.is_empty() { - return Vec::new(); + return None; } - let mut found = Found { doc, names, read: HashMap::new(), total: 0, warnings: Vec::new() }; + let mut found = Found { doc, names, read: HashMap::new(), kept: 0, total: 0, warnings: Vec::new() }; found.subform(&mut tpl.root, 0); found.warnings.dedup(); - found.warnings + Some(found) +} + +/// [`resolve`], and the bytes of pictures it held for reuse at the end. +#[cfg(test)] +pub(crate) fn resolve_keeping(tpl: &mut Template, doc: &Document) -> (Vec, usize) { + resolve_found(tpl, doc).map_or((Vec::new(), 0), |f| (f.warnings.clone(), f.kept())) } diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 282c1bc90e..8038c8d3fa 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -4246,3 +4246,24 @@ fn a_linked_picture_used_in_many_places_counts_each_place() { assert_eq!(filled, (1 << 10) / one, "as many places as the total holds"); assert!(warnings.iter().any(|w| w.contains("too much memory")), "{warnings:?}"); } + +/// Pictures read from the file and kept for reuse count against the total too (small in tests): +/// a tree with many names, each read once, holds no more than the total, however many names +/// there are; past it a name is neither read nor kept. +#[test] +fn pictures_kept_for_reuse_stay_under_the_total_whatever_the_number_of_names() { + let names: String = (0..30).map(|i| format!("(n{i}) 8 0 R ")).collect(); + let kid = format!("<< /Names [{names}] >>"); + let doc = Document::open(Arc::new(crate::fixtures::linked_image_shell_with(b"<< /Kids [7 0 R] >>", kid.as_bytes()))).unwrap(); + let draws: String = + (0..30).map(|i| format!(r#""#)).collect(); + let xml = format!( + r#""# + ); + let (mut tpl, _) = crate::parse(&xml).unwrap(); + let (warnings, kept) = crate::linked_images::resolve_keeping(&mut tpl, &doc); + assert!(kept <= 1 << 10, "kept {kept} bytes"); + assert!(warnings.iter().any(|w| w.contains("too much memory")), "{warnings:?}"); +}