mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 22:19:10 -06:00
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:
+24
-28
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user