From 1dbfa251000956dc968680ebf7f53a47d7f932f9 Mon Sep 17 00:00:00 2001 From: aiongg Date: Thu, 22 Oct 2026 15:22:52 +0000 Subject: [PATCH] fix(xfa): bound the script text the live form's events hold Every instance of a repeated subform records its own copy of each script, and only the number of events was capped. A long script inside nested repeating subforms was copied once per instance before any script ran. The events hold at most as much script text as the XFA packets may; past that the live form is marked truncated, as it is past the event cap. --- crates/xfa/src/script.rs | 52 +++++++++++++++++++--------------------- crates/xfa/src/tests.rs | 27 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs index d39d53e37a..cb555dc81d 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -15,6 +15,9 @@ pub const OVERRIDES_KEY: &[u8] = b"PCXfaOverrides"; const MAX_DEPTH: usize = 64; const MAX_EVENTS: usize = 20_000; +/// Most bytes of script the events hold together: each instance of a repeated subform holds its +/// own copy, so a long script repeated many times stops here (as much as the XFA packets hold). +pub(crate) const MAX_EVENT_SCRIPT_BYTES: usize = 64 << 20; /// Most objects of the live form built (the scripting engine's snapshot holds as many): /// nested repeating subforms multiply, so the walk stops here and says so. pub const MAX_FORM_NODES: usize = 200_000; @@ -67,8 +70,8 @@ pub struct LiveForm { pub root: FormNode, /// Every scripted event, in document order. pub events: Vec, - /// The form was larger than [`MAX_FORM_NODES`] objects (or [`MAX_EVENTS`] events): what - /// is past that was left out. + /// The form was larger than [`MAX_FORM_NODES`] objects (or [`MAX_EVENTS`] events, or + /// [`MAX_EVENT_SCRIPT_BYTES`] of their scripts): what is past that was left out. pub truncated: bool, } @@ -172,6 +175,8 @@ struct Walker<'a> { data: Option<&'a DataNode>, ov: &'a Overrides, events: Vec, + /// Bytes of script the events hold. + script_bytes: usize, /// Objects built so far. nodes: usize, truncated: bool, @@ -190,43 +195,34 @@ impl Walker<'_> { /// Record the scripts of the object at `som` as events: each non-empty event script, then its /// calculate and validate scripts (the validate one with `message`). Once [`MAX_EVENTS`] events - /// are recorded, nothing more is and the form is marked truncated. + /// or [`MAX_EVENT_SCRIPT_BYTES`] of their scripts are recorded, nothing more is and the form is + /// marked truncated. fn push_events(&mut self, som: &str, scripts: &[Script], calculate: Option<&Script>, validate: Option<&Script>, message: Option<&str>) { if self.events.len() >= MAX_EVENTS { self.truncated = true; return; } - for s in scripts { + let all = scripts + .iter() + .map(|s| (s, s.activity.as_str(), None)) + .chain(calculate.map(|c| (c, "calculate", None))) + .chain(validate.map(|v| (v, "validate", message))); + for (s, activity, message) in all { if s.text.trim().is_empty() { continue; } + match self.script_bytes.checked_add(s.text.len()).filter(|n| *n <= MAX_EVENT_SCRIPT_BYTES) { + Some(n) => self.script_bytes = n, + None => { + self.truncated = true; + return; + } + } self.events.push(ScriptEvent { som: som.to_string(), - activity: s.activity.clone(), + activity: activity.to_string(), script: s.text.clone(), formcalc: s.formcalc, - message: None, - }); - } - if let Some(c) = calculate - && !c.text.trim().is_empty() - { - self.events.push(ScriptEvent { - som: som.to_string(), - activity: "calculate".into(), - script: c.text.clone(), - formcalc: c.formcalc, - message: None, - }); - } - if let Some(v) = validate - && !v.text.trim().is_empty() - { - self.events.push(ScriptEvent { - som: som.to_string(), - activity: "validate".into(), - script: v.text.clone(), - formcalc: v.formcalc, message: message.map(str::to_string), }); } @@ -410,7 +406,7 @@ impl Walker<'_> { /// from the template and `ov`) and every scripted event, in document order, within /// [`MAX_FORM_NODES`] objects. pub fn form_tree(tpl: &Template, data: Option<&DataNode>, ov: &Overrides) -> LiveForm { - let mut w = Walker { tpl, data, ov, events: Vec::new(), nodes: 0, truncated: false }; + let mut w = Walker { tpl, data, ov, events: Vec::new(), script_bytes: 0, nodes: 0, truncated: false }; let root = Node::Subform(Box::new(tpl.root.clone())); let mut nodes = w.children(std::slice::from_ref(&root), "", 0); let mut root_node = nodes.pop().unwrap_or_default(); diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 31a3684814..a101f219b5 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -2684,3 +2684,30 @@ fn embedded_field_values_expand_to_a_bounded_amount_of_text() { assert!(kept <= text::MAX_EMBEDDED, "{kept} bytes of embedded text laid out"); assert!(text::natural_width(&rich, &Para::default(), &face, &|_| value.clone()) <= face.width(&value) * 70.0); } + +/// Repeated subforms each carry their own copy of a long script: the live form holds a bounded +/// amount of script text and says it left the rest out. +#[test] +fn repeated_script_events_hold_a_bounded_amount_of_script() { + let script = format!("var a = 1; //{}", "x".repeat(32 * 1024)); + let xml = format!( + r##" + +"## + ); + let (tpl, _) = parse(&xml).unwrap(); + let live = form_tree(&tpl, None, &Overrides::default()); + assert!(!live.events.is_empty()); + let held: usize = live.events.iter().map(|e| e.script.len()).sum(); + assert!(held <= crate::script::MAX_EVENT_SCRIPT_BYTES, "{held} bytes of script held by {} events", live.events.len()); + assert!(live.truncated, "{held} bytes, {} events", live.events.len()); +}