diff --git a/crates/xfa/src/pdf/strip.rs b/crates/xfa/src/pdf/strip.rs index b753510653..f7d9747636 100644 --- a/crates/xfa/src/pdf/strip.rs +++ b/crates/xfa/src/pdf/strip.rs @@ -151,6 +151,7 @@ pub fn strip(doc: &mut Document) -> Result { let fields = acro.get(b"Fields").map(|f| doc.resolve(f)).and_then(|f| f.as_array().cloned()).unwrap_or_default(); let mut seen = std::collections::HashSet::new(); let mut todo: Vec = Vec::new(); + let mut kids_seen = std::collections::HashSet::new(); for r in fields.iter().filter_map(Object::as_ref) { if seen.insert(r) && seen.len() <= MAX_STRIP_FIELDS { todo.push(r); @@ -174,7 +175,9 @@ pub fn strip(doc: &mut Document) -> Result { } })?; } - if let Some(kids) = d.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()) { + // An indirect /Kids array several fields share is read once: its entries are already queued. + let shared_seen = d.get(b"Kids").and_then(Object::as_ref).is_some_and(|k| !kids_seen.insert(k)); + if !shared_seen && let Some(kids) = d.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()) { for k in kids.iter().filter_map(Object::as_ref) { if seen.insert(k) && seen.len() <= MAX_STRIP_FIELDS { todo.push(k); @@ -280,3 +283,34 @@ pub fn existing_layout(doc: &Document) -> Option { warnings, }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fields_sharing_a_kids_array_still_lose_their_private_keys_once() { + let mut doc = Document::new_empty(); + let mut kid = Dict::new(); + kid.set(CLICK_KEY.to_vec(), Object::Int(1)); + let kid = doc.add(Object::Dict(kid)); + let kids = doc.add(Object::Array(vec![Object::Ref(kid)])); + let parents: Vec = (0..3) + .map(|_| { + let mut p = Dict::new(); + p.set(b"Kids".to_vec(), Object::Ref(kids)); + Object::Ref(doc.add(Object::Dict(p))) + }) + .collect(); + let mut acro = Dict::new(); + acro.set(b"XFA".to_vec(), Object::Array(Vec::new())); + acro.set(b"Fields".to_vec(), Object::Array(parents)); + let root = doc.root().unwrap(); + let mut catalog = doc.get(root).as_dict().cloned().unwrap(); + catalog.set(b"AcroForm".to_vec(), Object::Dict(acro)); + doc.set(root, Object::Dict(catalog)); + let out = strip(&mut doc).unwrap(); + assert_eq!(out.script_buttons, 1); + assert!(!doc.get(kid).as_dict().unwrap().contains(CLICK_KEY)); + } +}