M12: XFA open-time scripts start from one shared copy of the form

On a large dynamic XFA form, every script of an event copied the whole form
twice before it ran (the engine's live form to the script engine's tree, then
that tree to the flattened form the host works on), and every resetData call
rewrote and rescanned the whole document. On a 1,300-script form opening spent
its 3 s budget on these copies and on twenty resets, and skipped the last 850
initialize and calculate scripts.

The engine now sends the scripts of an event one shared tree (Arc) until the
live form changes in any way, and the script thread keeps the flattened form
it built from that tree; each script starts from a copy whose text is shared
(Arc<str>), so it costs a fraction of a flatten. Each script still gets its
own copy: nothing one script changes reaches the next except through the
engine. Resets in a row in one script's effects are applied as one reset of
the names together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
This commit is contained in:
mtlcl410
2026-10-31 13:57:32 +08:00
co-authored by Claude Opus 5.5
parent bc5aff5ff6
commit d65c465b1b
13 changed files with 294 additions and 71 deletions
+51
View File
@@ -6576,6 +6576,57 @@ fn xfa_initialize_scripts_that_hide_subforms_lay_the_form_out_once() {
assert_eq!(names(&s, again), opened);
}
/// A script that resets several subforms one after another (`xfa.host.resetData` once for
/// each, as forms with optional pages do) resets every one of them, and nothing else; resets
/// in a row are applied together.
#[test]
fn xfa_resets_in_a_row_reset_every_named_subform_and_nothing_else() {
let sections = 5;
let mut body = String::from(
r#"<field name="wanted" w="0.3in" h="0.3in"><ui><checkButton/></ui><items><integer>1</integer><integer>0</integer></items><value><integer>0</integer></value>"#,
);
let resets: String = (0..sections).map(|n| format!("xfa.host.resetData('form.extra{n}');")).collect();
body.push_str(&format!(
r#"<event activity="initialize"><script contentType="application/x-javascript">{resets} keep.rawValue = "kept";</script></event></field>"#
));
body.push_str(r#"<field name="keep" w="3in" h="0.3in"><ui><textEdit/></ui></field>"#);
for n in 0..sections {
body.push_str(&format!(
r#"<subform name="extra{n}" layout="tb"><field name="note" w="3in" h="0.3in"><ui><textEdit/></ui><value><text>default</text></value></field></subform>"#
));
}
let notes: String = (0..sections).map(|n| format!("<extra{n}><note>typed {n}</note></extra{n}>")).collect();
let tpl = 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">
<pageSet><pageArea name="front"><occur max="-1"/><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
{body}
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><form><keep>typed</keep>{notes}</form></xfa:data></xfa:datasets>
</xdp:xdp>
"#
);
let mut s = Session::new().with_clock(|| 1_700_000_000).with_xfa_time_scale(100);
// Without the resets the sections show what was typed.
let untouched = Arc::new(pdfcraft_xfa::fixtures::shell(&tpl.replace(&resets, "")));
let plain = s.open("plain.pdf", None, untouched, None).expect("opens");
let typed: Vec<_> = s.get(plain).unwrap().form.iter().filter(|f| f.name.contains("note")).map(|f| f.value.clone()).collect();
assert_eq!(typed, (0..sections).map(|n| vec![format!("typed {n}")]).collect::<Vec<_>>());
let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&tpl));
let id = s.open("resets.pdf", None, bytes, None).expect("opens");
let out = s.take_js_output(id);
assert!(out.errors.is_empty(), "{:?}", out.errors);
let form = &s.get(id).unwrap().form;
let notes: Vec<_> = form.iter().filter(|f| f.name.contains("note")).map(|f| f.value.clone()).collect();
assert_eq!(notes.len(), sections, "{form:?}");
assert!(notes.iter().all(|v| v == &vec!["default".to_string()]), "every section is reset: {notes:?}");
let keep = form.iter().find(|f| f.name.contains("keep")).map(|f| f.value.clone());
assert_eq!(keep, Some(vec!["kept".to_string()]), "the script's later write stands");
}
/// The scripts of one event share the form as the engine holds it: a failed script's
/// half-done writes reach nobody, and a script reads the saved value a choice list was given
/// by its shown text.
+48 -3
View File
@@ -159,6 +159,23 @@ fn to_js(n: &FormNode) -> XfaNode {
}
}
/// Add the names of a `resetData` call to the run of resets before it. No names means the
/// whole form, which takes in every other.
fn merge_reset(merged: &mut Vec<String>, names: &[String]) {
if merged.is_empty() {
return;
}
if names.is_empty() {
merged.clear();
return;
}
for n in names {
if !merged.contains(n) {
merged.push(n.clone());
}
}
}
fn find_container<'a>(n: &'a FormNode, som: &str, depth: usize) -> Option<&'a FormNode> {
if depth > 64 {
return None;
@@ -275,6 +292,9 @@ struct Runner<'a, 'b> {
fields: Option<HashMap<String, FieldInfo>>,
/// The live form as last built; dropped when a script changed the form.
cached: Option<LiveForm>,
/// `cached` as the scripts get it, made once and sent to each script until the live form
/// changes in any way (a value too): the script engine reuses what it built from it.
js_root: Option<Arc<XfaNode>>,
/// The script engine for this event: one thread for all its scripts, each run in a fresh
/// context on the form as it is then (`cached`, kept up to date in place for values and
/// rebuilt when the structure changed).
@@ -309,6 +329,7 @@ impl<'a, 'b> Runner<'a, 'b> {
by_path: None,
fields: None,
cached: None,
js_root: None,
session: None,
changes: Changes::default(),
}
@@ -318,6 +339,7 @@ impl<'a, 'b> Runner<'a, 'b> {
/// script.
fn invalidate(&mut self) {
self.cached = None;
self.js_root = None;
}
/// The script engine for this event.
@@ -334,6 +356,8 @@ impl<'a, 'b> Runner<'a, 'b> {
let op = pdfcraft_xfa::DataOp::Value { path, text };
self.data.get_or_insert_with(Default::default).apply(&op);
self.pending.push(op);
// The value changes in the live form below: the scripts' copy is made again.
self.js_root = None;
let refreshed = match self.cached.as_mut() {
Some(live) => pdfcraft_xfa::refresh_value(&mut live.root, self.tpl, self.data.as_ref(), som).is_some(),
None => true,
@@ -591,7 +615,9 @@ impl<'a, 'b> Runner<'a, 'b> {
}
}
}
for e in effects {
// Resets in a row, applied together when the run of them ends.
let mut pending_reset: Option<Vec<String>> = None;
for (at, e) in effects.iter().enumerate() {
// Only a click script may print, save, open links or move the focus (not the
// calculations a click sets off, nor anything run on open or on a change).
if let Some(what) = click_only(e)
@@ -640,7 +666,19 @@ impl<'a, 'b> Runner<'a, 'b> {
relayout = true;
}
XfaEffect::MessageBox(m) => self.alert(m.clone()),
XfaEffect::ResetData(names) => self.reset(names)?,
XfaEffect::ResetData(names) => {
// A script resetting several subforms in a row (`resetData` once for each)
// resets them together: the same fields end up at their defaults.
if let Some(merged) = pending_reset.as_mut() {
merge_reset(merged, names);
} else {
pending_reset = Some(names.clone());
}
let next_is_reset = effects.get(at.saturating_add(1)).is_some_and(|e| matches!(e, XfaEffect::ResetData(_)));
if !next_is_reset && let Some(names) = pending_reset.take() {
self.reset(&names)?;
}
}
XfaEffect::Print => self.run.out.requests.push(Request::Print),
XfaEffect::SaveAs => self.run.out.requests.push(Request::SaveAs),
XfaEffect::LaunchUrl(u) => self.run.out.requests.push(Request::LaunchUrl(u.clone())),
@@ -669,7 +707,14 @@ impl<'a, 'b> Runner<'a, 'b> {
#[cfg(test)]
self.check_live();
let event = XfaEvent { activity: ev.activity.clone(), target: ev.som.clone(), new_text: new_text.to_string(), prev_text: String::new() };
let root = to_js(&self.live().root);
let root = match &self.js_root {
Some(r) => Arc::clone(r),
None => {
let r = Arc::new(to_js(&self.live().root));
self.js_root = Some(Arc::clone(&r));
r
}
};
let script = Script {
source: ev.script.clone(),
formcalc: ev.formcalc,
+5 -4
View File
@@ -109,7 +109,7 @@ type R<T> = Result<T, String>;
/// Run a FormCalc `script` for `event` on the form `root`, as [`crate::xfa::run_xfa`] runs a
/// JavaScript one.
pub fn run_formcalc(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, limits: Limits) -> XfaOutcome {
run_formcalc_at(script, event, doc, root.clone(), limits, None)
run_formcalc_at(script, event, doc, std::sync::Arc::new(root.clone()), false, limits, None)
}
/// [`run_formcalc`] on an owned form, giving up at `deadline` (the clock is read every
@@ -118,7 +118,8 @@ pub(crate) fn run_formcalc_at(
script: &str,
event: &XfaEvent,
doc: &XfaDoc,
root: XfaNode,
root: std::sync::Arc<XfaNode>,
reuse: bool,
limits: Limits,
deadline: Option<std::time::Duration>,
) -> XfaOutcome {
@@ -133,7 +134,7 @@ pub(crate) fn run_formcalc_at(
Ok(b) => b,
Err(e) => return XfaOutcome { error: Some(e), ..Default::default() },
};
let mut host = XHost::new(root, &event.target, doc);
let mut host = XHost::new(root, reuse, &event.target, doc);
#[cfg(target_arch = "wasm32")]
let _ = deadline;
let mut interp = Interp {
@@ -177,7 +178,7 @@ pub fn run_formcalc_within(script: &str, event: &XfaEvent, doc: &XfaDoc, root: X
// FormCalc has no engine to start: its time (and its own deadline) begins at once.
run_within("pdfcraft-formcalc", timeout, move |started| {
started();
run_formcalc_at(&script, &event, &doc, root, limits, Some(timeout))
run_formcalc_at(&script, &event, &doc, std::sync::Arc::new(root), false, limits, Some(timeout))
})
}
+6 -6
View File
@@ -313,16 +313,16 @@ impl Interp<'_> {
let Some(n) = self.node(i) else { return Ok(Val::Null) };
Ok(match name {
"formattedValue" => match &n.formatted {
Some(f) => Val::Str(f.clone()),
Some(f) => Val::Str(f.to_string()),
None => self.node_value(i),
},
"rawValue" | "value" => self.node_value(i),
"name" => Val::Str(n.name.clone()),
"somExpression" => Val::Str(n.som.clone()),
"name" => Val::Str(n.name.to_string()),
"somExpression" => Val::Str(n.som.to_string()),
"className" => Val::Str(n.kind.class_name().into()),
"index" | "instanceIndex" => Val::Num(n.index as f64),
"presence" => Val::Str(n.presence.clone()),
"access" => Val::Str(n.access.clone()),
"presence" => Val::Str(n.presence.to_string()),
"access" => Val::Str(n.access.to_string()),
"isNull" => Val::Num(if n.value.is_empty() { 1.0 } else { 0.0 }),
"mandatory" => Val::Str("disabled".into()),
"parent" => n.parent.map_or(Val::Null, Val::Node),
@@ -332,7 +332,7 @@ impl Interp<'_> {
None => Val::Node(i),
},
"instanceManager" => match n.parent {
Some(p) => Val::Im(p, n.name.clone()),
Some(p) => Val::Im(p, n.name.to_string()),
None => Val::Null,
},
"border" | "ui" | "font" | "caption" | "fillColor" | "borderColor" | "fontColor" | "margin" | "para" | "assist" | "validate"
+3 -3
View File
@@ -277,11 +277,11 @@ impl Interp<'_> {
Some(e) => {
let raw = self.eval(e)?;
match self.nodes_of(&raw).first() {
Some(i) => self.node(*i).map(|n| n.som.clone()).unwrap_or_default(),
Some(i) => self.node(*i).map(|n| n.som.to_string()).unwrap_or_default(),
None => {
let s = self.to_str(&raw);
let found = resolve_som(self.h, self.h.current, &s);
found.first().and_then(|i| self.node(*i)).map(|n| n.som.clone()).unwrap_or(s)
found.first().and_then(|i| self.node(*i)).map(|n| n.som.to_string()).unwrap_or(s)
}
}
}
@@ -396,7 +396,7 @@ impl Interp<'_> {
match target {
Val::Node(i) => {
let Some(n) = self.node(i) else { return Ok(()) };
let som = n.som.clone();
let som = n.som.to_string();
match prop {
"rawValue" | "value" | "formattedValue" => {
let text = self.to_str(&v);
+45 -3
View File
@@ -1542,7 +1542,8 @@ $host.gotoURL("https://example.org")"#,
"for i = 1 upto 100000 do Eval(\"1\") endfor",
&ev,
&doc,
form(),
std::sync::Arc::new(form()),
false,
Limits { loop_iterations: u64::MAX, recursion: 32 },
Some(std::time::Duration::ZERO),
);
@@ -1660,12 +1661,53 @@ mod session {
source: source.into(),
formcalc,
event: XfaEvent { activity: activity.into(), target: target.into(), ..Default::default() },
root: form(),
root: std::sync::Arc::new(form()),
limits: Limits::default(),
timeout: std::time::Duration::from_secs(3),
}
}
/// The scripts of an event given the same form start from one flattened copy of it: the
/// second host shares the first one's text, and each host changes only its own copy.
#[test]
fn hosts_given_the_same_form_reuse_its_flattened_copy_and_change_only_their_own() {
let root = std::sync::Arc::new(form());
let doc = XfaDoc::default();
let mut first = crate::xfa::XHost::new(std::sync::Arc::clone(&root), true, "form1[0].page1[0].qty[0]", &doc);
let second = crate::xfa::XHost::new(std::sync::Arc::clone(&root), true, "form1[0].page1[0].qty[0]", &doc);
let shared = std::sync::Arc::clone(&first.nodes.get(1).expect("form1").som);
assert!(std::sync::Arc::ptr_eq(&shared, &second.nodes.get(1).expect("form1").som), "the second host starts from the first one's copy");
let qty = first.current;
let before = second.nodes.get(qty).map(|n| n.value.clone());
first.set_state(qty, XfaEffect::SetPresence { som: "form1[0].page1[0].qty[0]".into(), presence: "hidden".into() });
if let Some(n) = first.nodes.get_mut(qty) {
n.value = "99".into();
}
let third = crate::xfa::XHost::new(std::sync::Arc::clone(&root), true, "form1[0].page1[0].qty[0]", &doc);
for h in [&second, &third] {
assert_eq!(h.nodes.get(qty).map(|n| n.value.clone()), before);
assert_eq!(h.nodes.get(qty).map(|n| n.presence.to_string()).as_deref(), Some("visible"));
}
// Another form (a new `Arc`, even with the same content) is flattened again.
let other = crate::xfa::XHost::new(std::sync::Arc::new(form()), true, "form1[0].page1[0].qty[0]", &doc);
assert!(!std::sync::Arc::ptr_eq(&other.nodes.get(1).expect("form1").som, &shared));
}
/// One session given the same form for two scripts: what the first changed on its copy is
/// not in the second's.
#[test]
fn a_form_sent_again_does_not_carry_the_last_scripts_changes() {
let mut s = XfaSession::new(XfaDoc::default());
let root = std::sync::Arc::new(form());
let mut a = script("qty.rawValue = 10; details.presence = 'visible'; qty.rawValue", false, "form1[0].page1[0].total[0]", "calculate");
a.root = std::sync::Arc::clone(&root);
assert_eq!(s.run(a).result.as_deref(), Some("10"));
let mut b = script("qty.rawValue + ':' + details.presence", false, "form1[0].page1[0].total[0]", "calculate");
b.root = std::sync::Arc::clone(&root);
let o = s.run(b);
assert_eq!(o.result.as_deref(), Some("4:hidden"), "{o:?}");
}
/// JavaScript and FormCalc scripts run in turn in one session, each on the form the caller
/// sends: nothing an earlier script changed or defined leaks into the next, a failing script
/// doesn't end the session, and hundreds of scripts run on its one thread.
@@ -1683,7 +1725,7 @@ mod session {
let o = s.run(script("qty * price", true, "form1[0].page1[0].total[0]", "calculate"));
assert_eq!(o.result.as_deref(), Some("10"), "{o:?}");
let mut with = script("qty * price", true, "form1[0].page1[0].total[0]", "calculate");
with.root.children[0].children[0].value = "10".into();
std::sync::Arc::make_mut(&mut with.root).children[0].children[0].value = "10".into();
assert_eq!(s.run(with).result.as_deref(), Some("25"));
// Nor do variables, globals, prototype changes or kept objects.
s.run(script("leak = 5; String.prototype.trim = function () { return 'P' }; kept = qty; 1", false, "form1[0].page1[0].qty[0]", "calculate"));
+2
View File
@@ -24,6 +24,8 @@ mod model;
mod nodes;
mod run;
#[cfg(target_arch = "wasm32")]
pub(crate) use host::forget_base;
pub(crate) use host::{HNode, XHost};
pub use model::{MAX_ALERTS, MAX_CONSOLE, MAX_EFFECTS, XfaDoc, XfaEffect, XfaEvent, XfaKind, XfaNode, XfaOutcome};
pub(crate) use model::{MAX_LINE, MAX_PAYLOAD, clip, clip_result, reset_names};
+3 -3
View File
@@ -69,13 +69,13 @@ fn set_focus(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsVal
let som = match target.as_object() {
Some(_) if node_index(&target, ctx).is_ok() => {
let i = node_index(&target, ctx)?;
host(ctx)?.borrow().nodes.get(i).map(|n| n.som.clone()).unwrap_or_default()
host(ctx)?.borrow().nodes.get(i).map(|n| n.som.to_string()).unwrap_or_default()
}
_ => {
let expr = text(&target, ctx)?;
let h = host(ctx)?.borrow();
let found = resolve_som(&h, h.current, &expr);
found.first().and_then(|i| h.nodes.get(*i)).map(|n| n.som.clone()).unwrap_or(expr)
found.first().and_then(|i| h.nodes.get(*i)).map(|n| n.som.to_string()).unwrap_or(expr)
}
};
effect(ctx, XfaEffect::SetFocus(som))?;
@@ -329,7 +329,7 @@ pub(super) fn install(ctx: &mut Context, event: &XfaEvent, doc: &XfaDoc, current
let h = host(ctx)?.borrow();
h.nodes
.get(scope)
.map(|n| n.children.iter().filter_map(|&c| h.nodes.get(c).map(|k| (c, k.name.clone(), k.repeatable && !k.gone))).collect())
.map(|n| n.children.iter().filter_map(|&c| h.nodes.get(c).map(|k| (c, k.name.to_string(), k.repeatable && !k.gone))).collect())
.unwrap_or_default()
};
for (c, name, repeatable) in kids {
+80 -22
View File
@@ -2,27 +2,71 @@ use super::model::{
MAX_ALERTS, MAX_CONSOLE, MAX_DEPTH, MAX_EFFECTS, MAX_NODES, MAX_PAYLOAD, MAX_VALUE, XfaDoc, XfaEffect, XfaKind, XfaNode, clip, clip_value,
effect_key, payload,
};
use std::sync::Arc;
use super::{Context, HashMap, JsResult, Message, RefCell, error};
/// One node of the flattened form, shared by the JavaScript and FormCalc engines.
/// One node of the flattened form, shared by the JavaScript and FormCalc engines. Its text is
/// shared (`Arc<str>`), so the copy each script starts from costs little: the scripts of one
/// event start from the same flattened form until it changes (see [`XHost::new`]).
#[derive(Clone)]
pub(crate) struct HNode {
pub(crate) name: String,
pub(crate) som: String,
pub(crate) name: Arc<str>,
pub(crate) som: Arc<str>,
pub(crate) kind: XfaKind,
pub(crate) value: String,
pub(crate) numeric: bool,
pub(crate) presence: String,
pub(crate) access: String,
pub(crate) presence: Arc<str>,
pub(crate) access: Arc<str>,
pub(crate) repeatable: bool,
pub(crate) occur_min: usize,
pub(crate) occur_max: Option<usize>,
pub(crate) index: usize,
pub(crate) parent: Option<usize>,
pub(crate) children: Vec<usize>,
pub(crate) children: Arc<[usize]>,
/// Removed by `removeInstance`: skipped everywhere.
pub(crate) gone: bool,
/// See [`XfaNode::formatted`]; dropped when a script sets the value, which it no longer shows.
pub(crate) formatted: Option<String>,
pub(crate) formatted: Option<Arc<str>>,
}
thread_local! {
/// The form the last script on this thread started from, flattened: the next script of the
/// event starts from a copy of it while the engine sends the same form.
static BASE: std::cell::RefCell<Option<(Arc<XfaNode>, Vec<HNode>)>> = const { std::cell::RefCell::new(None) };
}
/// Let go of the form the scripts on this thread started from.
#[cfg(target_arch = "wasm32")]
pub(crate) fn forget_base() {
BASE.with(|b| b.borrow_mut().take());
}
/// `root` flattened under a synthetic `form` node (index 0).
fn flattened(root: &XfaNode) -> Vec<HNode> {
let mut nodes = vec![HNode {
name: "form".into(),
som: "".into(),
kind: XfaKind::Form,
value: String::new(),
numeric: false,
presence: "visible".into(),
access: "open".into(),
repeatable: false,
occur_min: 0,
occur_max: None,
index: 0,
parent: None,
children: Arc::from([]),
gone: false,
formatted: None,
}];
if let Some(ci) = flatten(root, Some(0), &mut nodes, 1)
&& let Some(f) = nodes.get_mut(0)
{
f.children = Arc::from([ci]);
}
nodes
}
/// The form as a script sees and changes it, with the effects it records.
@@ -53,11 +97,25 @@ pub(crate) struct XHost {
impl XHost {
/// The host for a script on `root` (the template's root subform), with `this` at the node
/// named by `target`. `xfa.form` is a synthetic root whose child is the root subform.
pub(crate) fn new(root: XfaNode, target: &str, doc: &XfaDoc) -> XHost {
let form = XfaNode { name: "form".into(), kind: Some(XfaKind::Form), children: vec![root], ..Default::default() };
let mut nodes = Vec::new();
flatten(&form, None, &mut nodes, 0);
let current = nodes.iter().position(|n| n.som == target && n.kind != XfaKind::Form).unwrap_or(1.min(nodes.len().saturating_sub(1)));
/// With `reuse`, the flattened form is kept on this thread for the next script given the
/// same `root` (the scripts of one event, [`crate::xfa_session`]).
pub(crate) fn new(root: Arc<XfaNode>, reuse: bool, target: &str, doc: &XfaDoc) -> XHost {
let nodes = if !reuse {
flattened(&root)
} else {
BASE.with(|b| {
let mut b = b.borrow_mut();
match b.as_ref() {
Some((r, nodes)) if Arc::ptr_eq(r, &root) => nodes.clone(),
_ => {
let nodes = flattened(&root);
*b = Some((root, nodes.clone()));
nodes
}
}
})
};
let current = nodes.iter().position(|n| &*n.som == target && n.kind != XfaKind::Form).unwrap_or(1.min(nodes.len().saturating_sub(1)));
XHost {
nodes,
effects: Vec::new(),
@@ -122,8 +180,8 @@ impl XHost {
}
if let Some(n) = self.nodes.get_mut(i) {
match &e {
XfaEffect::SetPresence { presence, .. } => n.presence = presence.clone(),
XfaEffect::SetAccess { access, .. } => n.access = access.clone(),
XfaEffect::SetPresence { presence, .. } => n.presence = presence.as_str().into(),
XfaEffect::SetAccess { access, .. } => n.access = access.as_str().into(),
_ => {}
}
}
@@ -220,7 +278,7 @@ impl XHost {
if n.gone || !matches!(n.kind, XfaKind::Field | XfaKind::ExclGroup) {
return false;
}
let som = n.som.clone();
let som = n.som.to_string();
// Cut here, not only in the effect: later reads in the script see what is saved, and
// the snapshot stays bounded however often a long value is written.
let (text, cut) = clip_value(text);
@@ -257,21 +315,21 @@ fn flatten(n: &XfaNode, parent: Option<usize>, out: &mut Vec<HNode>, depth: usiz
}
let idx = out.len();
out.push(HNode {
name: n.name.clone(),
som: n.som.clone(),
name: n.name.as_str().into(),
som: n.som.as_str().into(),
kind: n.kind.unwrap_or(XfaKind::Subform),
value: n.value.clone(),
numeric: n.numeric,
presence: if n.presence.is_empty() { "visible".into() } else { n.presence.clone() },
access: if n.access.is_empty() { "open".into() } else { n.access.clone() },
presence: if n.presence.is_empty() { "visible".into() } else { n.presence.as_str().into() },
access: if n.access.is_empty() { "open".into() } else { n.access.as_str().into() },
repeatable: n.repeatable,
occur_min: n.occur_min,
occur_max: n.occur_max,
index: n.index,
parent,
children: Vec::new(),
children: Arc::from([]),
gone: false,
formatted: n.formatted.clone(),
formatted: n.formatted.as_deref().map(Arc::from),
});
let mut kids = Vec::new();
for c in &n.children {
@@ -280,7 +338,7 @@ fn flatten(n: &XfaNode, parent: Option<usize>, out: &mut Vec<HNode>, depth: usiz
}
}
if let Some(h) = out.get_mut(idx) {
h.children = kids;
h.children = kids.into();
}
Some(idx)
}
+16 -12
View File
@@ -1,3 +1,5 @@
use std::sync::Arc;
use super::host::{HNode, XHost, host};
use super::model::{MAX_ADDED, MAX_DEPTH, MAX_NODES, XfaEffect};
use super::nodes::{instances, node_object};
@@ -112,14 +114,14 @@ fn renumber(h: &mut XHost, idx: usize, depth: usize) {
}
let (parent_som, name, index, kids) = {
let Some(n) = h.nodes.get(idx) else { return };
let parent_som = n.parent.and_then(|p| h.nodes.get(p)).map(|p| p.som.clone()).unwrap_or_default();
let parent_som = n.parent.and_then(|p| h.nodes.get(p)).map(|p| p.som.to_string()).unwrap_or_default();
(parent_som, n.name.clone(), n.index, n.children.clone())
};
let som = child_som(parent_som, &name, index);
if let Some(n) = h.nodes.get_mut(idx) {
n.som = som;
n.som = som.into();
}
for k in kids {
for k in kids.iter().copied() {
renumber(h, k, depth + 1);
}
}
@@ -145,7 +147,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option<usize>, depth: usize)
(
HNode {
name: n.name.clone(),
som: String::new(),
som: "".into(),
kind: n.kind,
value: String::new(),
numeric: n.numeric,
@@ -156,7 +158,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option<usize>, depth: usize)
occur_max: n.occur_max,
index: n.index,
parent,
children: Vec::new(),
children: Arc::from([]),
gone: false,
// The copy holds no value, so nothing is shown in its picture either.
formatted: None,
@@ -167,7 +169,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option<usize>, depth: usize)
let idx = h.nodes.len();
h.nodes.push(template);
let mut copied = Vec::new();
for k in kids {
for k in kids.iter().copied() {
// Nested repeating subforms copy their first instance only.
let skip = h.nodes.get(k).is_some_and(|c| c.gone || c.repeatable && c.index > 0);
if skip {
@@ -178,7 +180,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option<usize>, depth: usize)
}
}
if let Some(n) = h.nodes.get_mut(idx) {
n.children = copied;
n.children = copied.into();
}
Some(idx)
}
@@ -206,8 +208,8 @@ impl XHost {
return None;
}
// The copy's SOM, as `renumber` will give it, so its effect is reserved before the copy.
let name = h.nodes.get(first).map(|n| n.name.clone()).unwrap_or_default();
let parent_som = h.nodes.get(parent).map(|p| p.som.clone()).unwrap_or_default();
let name = h.nodes.get(first).map(|n| n.name.to_string()).unwrap_or_default();
let parent_som = h.nodes.get(parent).map(|p| p.som.to_string()).unwrap_or_default();
let som = child_som(parent_som, &name, live.len());
if !h.reserve(&XfaEffect::AddInstance { som: String::new() }, som.len()) {
return None;
@@ -219,8 +221,10 @@ impl XHost {
}
// Keep instances together: right after the last one.
if let Some(p) = h.nodes.get_mut(parent) {
let pos = p.children.iter().position(|&c| c == last).map_or(p.children.len(), |i| i + 1);
p.children.insert(pos, new);
let mut kids = p.children.to_vec();
let pos = kids.iter().position(|&c| c == last).map_or(kids.len(), |i| i + 1);
kids.insert(pos, new);
p.children = kids.into();
}
renumber(h, new, 0);
h.added += 1;
@@ -260,7 +264,7 @@ impl XHost {
if live.len() <= min {
return;
}
let som = h.nodes.get(gone).map(|n| n.som.clone()).unwrap_or_default();
let som = h.nodes.get(gone).map(|n| n.som.to_string()).unwrap_or_default();
if !h.reserve(&XfaEffect::RemoveInstance { som: String::new() }, som.len()) {
return;
}
+6 -6
View File
@@ -70,14 +70,14 @@ fn value_js(h: &HNode) -> JsValue {
/// `(idx, name)` of the child of `parent` named `name`, first instance.
pub(crate) fn child_named(h: &XHost, parent: usize, name: &str) -> Option<usize> {
let p = h.nodes.get(parent)?;
p.children.iter().copied().find(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && n.name == name))
p.children.iter().copied().find(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && &*n.name == name))
}
/// Every live instance named `name` under `parent`, in order.
pub(crate) fn instances(h: &XHost, parent: usize, name: &str) -> Vec<usize> {
h.nodes
.get(parent)
.map(|p| p.children.iter().copied().filter(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && n.name == name)).collect())
.map(|p| p.children.iter().copied().filter(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && &*n.name == name)).collect())
.unwrap_or_default()
}
@@ -179,11 +179,11 @@ pub(crate) fn descendants_named(h: &XHost, from: usize, name: &str, depth: usize
return out;
}
let Some(n) = h.nodes.get(from) else { return out };
for &c in &n.children {
for &c in n.children.iter() {
if let Some(k) = h.nodes.get(c)
&& !k.gone
{
if k.name == name {
if &*k.name == name {
out.push(c);
} else {
out.extend(descendants_named(h, c, name, depth + 1));
@@ -211,7 +211,7 @@ fn node_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValu
Ok(match key.as_str() {
IDX => JsValue::from(idx as f64),
"rawValue" | "value" => host(ctx)?.borrow().nodes.get(idx).map_or(JsValue::null(), value_js),
"formattedValue" => s(&host(ctx)?.borrow().nodes.get(idx).and_then(|n| n.formatted.clone()).unwrap_or(value)),
"formattedValue" => s(&host(ctx)?.borrow().nodes.get(idx).and_then(|n| n.formatted.as_deref().map(str::to_string)).unwrap_or(value)),
"name" => s(&name),
"somExpression" => s(&som),
"className" => s(kind.class_name()),
@@ -289,7 +289,7 @@ fn node_set(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValu
let value = if value.is_null() || value.is_undefined() { String::new() } else { text(&value, ctx)? };
let mut h = host(ctx)?.borrow_mut();
let Some(n) = h.nodes.get(idx) else { return Ok(JsValue::from(true)) };
let som = n.som.clone();
let som = n.som.to_string();
match key.as_str() {
"rawValue" | "value" | "formattedValue" => {
// Not a field: the property doesn't exist, as in Acrobat, and nothing is recorded.
+15 -5
View File
@@ -1,3 +1,5 @@
use std::sync::Arc;
use super::globals::install;
use super::host::{Shared, XHost};
use super::model::{XfaDoc, XfaEvent, XfaNode, XfaOutcome, clip_result};
@@ -15,7 +17,7 @@ pub fn run_xfa(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, lim
let spawned = std::thread::Builder::new()
.name("pdfcraft-xfa-js".into())
.stack_size(crate::SCRIPT_STACK)
.spawn_scoped(scope, || run_here(script, event, doc, root.clone(), limits, &|| {}));
.spawn_scoped(scope, || run_here(script, event, doc, Arc::new(root.clone()), false, limits, &|| {}));
match spawned {
Ok(thread) => thread.join().unwrap_or_else(|_| failed("the script stopped with an internal error".into())),
Err(e) => failed(format!("the script engine could not start: {e}")),
@@ -23,7 +25,7 @@ pub fn run_xfa(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, lim
})
}
#[cfg(target_arch = "wasm32")]
run_here(script, event, doc, root.clone(), limits, &|| {})
run_here(script, event, doc, Arc::new(root.clone()), false, limits, &|| {})
}
/// [`run_xfa`] with a time limit: a script still running `timeout` after the engine is ready
@@ -36,7 +38,7 @@ pub fn run_xfa_within(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNod
return XfaOutcome { error: Some(why), ..Default::default() };
}
let (script, event, doc) = (script.to_string(), event.clone(), doc.clone());
run_within("pdfcraft-xfa-js", timeout, move |started| run_here(&script, &event, &doc, root, limits, started))
run_within("pdfcraft-xfa-js", timeout, move |started| run_here(&script, &event, &doc, Arc::new(root), false, limits, started))
}
/// Longest the script engine may take to be ready for a script (its thread and context, and
@@ -125,7 +127,15 @@ pub(crate) fn warm_up() {
}
/// `started` is called once the engine is ready: a time limit on the script runs from then.
pub(crate) fn run_here(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNode, limits: Limits, started: &dyn Fn()) -> XfaOutcome {
pub(crate) fn run_here(
script: &str,
event: &XfaEvent,
doc: &XfaDoc,
root: Arc<XfaNode>,
reuse: bool,
limits: Limits,
started: &dyn Fn(),
) -> XfaOutcome {
warm_up();
let mut ctx = match Context::builder().host_hooks(std::rc::Rc::new(crate::CheckedCodeFromStrings)).build() {
Ok(ctx) => ctx,
@@ -133,7 +143,7 @@ pub(crate) fn run_here(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNo
};
ctx.runtime_limits_mut().set_loop_iteration_limit(limits.loop_iterations);
ctx.runtime_limits_mut().set_recursion_limit(limits.recursion);
let host_state = XHost::new(root, &event.target, doc);
let host_state = XHost::new(root, reuse, &event.target, doc);
let current = host_state.current;
ctx.insert_data(RefCell::new(host_state));
let mut out = XfaOutcome::default();
+14 -4
View File
@@ -36,8 +36,10 @@ pub struct Script {
pub source: String,
pub formcalc: bool,
pub event: XfaEvent,
/// The form as the caller holds it now.
pub root: XfaNode,
/// The form as the caller holds it now. Send the same `Arc` while the form is unchanged:
/// the next script then starts from the copy the last one was given, without flattening the
/// form again.
pub root: std::sync::Arc<XfaNode>,
pub limits: Limits,
pub timeout: std::time::Duration,
}
@@ -58,13 +60,13 @@ fn run_script(s: Script, doc: &XfaDoc, started: &dyn Fn()) -> XfaOutcome {
if s.formcalc {
// FormCalc has no engine to start: its time (and its own deadline) begins at once.
started();
return crate::formcalc::run_formcalc_at(&s.source, &s.event, doc, s.root, s.limits, Some(s.timeout));
return crate::formcalc::run_formcalc_at(&s.source, &s.event, doc, s.root, true, s.limits, Some(s.timeout));
}
if let Some(why) = crate::refuse(&s.source) {
started();
return XfaOutcome { error: Some(why), ..Default::default() };
}
crate::xfa::run_here(&s.source, &s.event, doc, s.root, s.limits, started)
crate::xfa::run_here(&s.source, &s.event, doc, s.root, true, s.limits, started)
}
#[cfg(not(target_arch = "wasm32"))]
@@ -236,6 +238,14 @@ mod native {
run_script(script, &self.doc, &|| {})
}
}
impl Drop for XfaSession {
/// The scripts ran on this thread: the form they started from is let go of with the
/// event (on other targets it goes with the session's thread).
fn drop(&mut self) {
crate::xfa::forget_base();
}
}
}
pub use native::XfaSession;