diff --git a/crates/forms/src/scripting.rs b/crates/forms/src/scripting.rs index 012dbb831..9a857510b 100644 --- a/crates/forms/src/scripting.rs +++ b/crates/forms/src/scripting.rs @@ -6,7 +6,9 @@ //! calculation order, Format when the appearance is drawn — and applies what the script asks //! for: the event's value, `rc`, and changes to other fields. -use pdfcraft_cos::{Document, Object, PdfString}; +use std::collections::HashSet; + +use pdfcraft_cos::{Document, ObjRef, Object, PdfString}; use crate::{Field, FieldKind, FieldValue, FormError, fields, flags}; @@ -172,18 +174,27 @@ pub fn document_scripts_named(doc: &Document) -> Vec<(String, String)> { let Some(root) = doc.root() else { return Vec::new() }; let cat = doc.get(root); let Some(names) = cat.as_dict().and_then(|c| c.get(b"Names")).map(|n| doc.resolve(n)) else { return Vec::new() }; - let Some(tree) = names.as_dict().and_then(|n| n.get(b"JavaScript")).map(|t| doc.resolve(t)) else { return Vec::new() }; + let Some(tree) = names.as_dict().and_then(|n| n.get(b"JavaScript")).cloned() else { return Vec::new() }; let mut out: Vec<(String, String)> = Vec::new(); - let mut stack = vec![(*tree).clone()]; - let mut seen = 0; - while let Some(node) = stack.pop() { - seen += 1; - if seen > 10_000 { + // Kids stay unresolved on the stack, and each node is read once: a node listed many times, + // or among its own kids, must not multiply the work. + let mut stack = vec![tree]; + let mut seen: HashSet = HashSet::new(); + let mut visited = 0; + while let Some(raw) = stack.pop() { + if let Object::Ref(r) = raw + && !seen.insert(r) + { + continue; + } + visited += 1; + if visited > 10_000 { break; } + let node = doc.resolve(&raw); let Some(d) = node.as_dict() else { continue }; if let Some(kids) = d.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()) { - stack.extend(kids.iter().map(|k| (*doc.resolve(k)).clone())); + stack.extend(kids); } if let Some(pairs) = d.get(b"Names").map(|n| doc.resolve(n)).and_then(|n| n.as_array().cloned()) { for pair in pairs.chunks(2) { diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 31340f9dd..feaf25425 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -1997,3 +1997,16 @@ fn auto_text_size_fits_single_and_multiline_text() { } assert_eq!(auto_text_size(long, 50.0, 10.0, false, |_, _| f64::NAN), 10.0 / 1.15); } + +#[test] +fn document_scripts_read_each_name_tree_node_once() { + // The root lists itself and the leaf twice each. + let doc = document(&[ + "<< /Type /Catalog /Pages 2 0 R /Names << /JavaScript 3 0 R >> >>".into(), + "<< /Type /Pages /Kids [] /Count 0 >>".into(), + "<< /Kids [3 0 R 4 0 R 3 0 R 4 0 R] >>".into(), + "<< /Names [(init) 5 0 R] >>".into(), + "<< /S /JavaScript /JS (var ready = true;) >>".into(), + ]); + assert_eq!(document_scripts_named(&doc), [("init".to_string(), "var ready = true;".to_string())]); +}