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.
This commit is contained in:
aiongg
2026-10-22 17:22:50 +00:00
parent 3412437191
commit 1dbfa25100
2 changed files with 51 additions and 28 deletions
+24 -28
View File
@@ -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<ScriptEvent>,
/// 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<ScriptEvent>,
/// 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();
+27
View File
@@ -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##"<?xml version="1.0" encoding="UTF-8"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<subform name="outer" layout="tb"><occur min="1" max="-1" initial="50"/>
<subform name="inner" layout="tb"><occur min="1" max="-1" initial="50"/>
<field name="f" w="1in" h="0.3in"><ui><textEdit/></ui>
<event activity="initialize"><script contentType="application/x-javascript">{script}</script></event></field>
</subform>
</subform>
</subform>
</template></xdp:xdp>"##
);
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());
}