mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 11:08:48 -06:00
fix(forms): read each JavaScript name tree node once (#2040)
document_scripts_named walked the catalog's /Names /JavaScript tree by resolving and deep-cloning every kid onto its stack before looking at it. The 10,000 cap counted nodes taken off the stack, not what went on it, so a node whose /Kids lists itself k times pushed k full copies of itself (each holding its k-entry /Kids array) per visit. A 6.5 KB file with k = 1000 made `pdfcraft-cli run` (doc_open, js_document_scripts) exhaust an 8 GB address space in 8 seconds and abort; the same walk runs when a field edit sets up the script runner. A tree that lists a leaf twice returned its script twice. Keep kids unresolved on the stack and skip references already read, as the /Dests walker in organize does. The cap stays. Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Brandon Thomas
parent
32a397f27a
commit
15ca7e13ac
@@ -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<ObjRef> = 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) {
|
||||
|
||||
@@ -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())]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user