Commit Graph
6 Commits
Author SHA1 Message Date
11ffedcc98 XFA: run the forms' JavaScript: calculations, validation, rows, visibility (#60) (#230)
* M12: run XFA forms' JavaScript: calculations, validation, rows, visibility (#60)

Laid-out XFA forms had their fields but none of their behaviour: totals
didn't compute, "Add row" buttons did nothing, and subforms a check box
should reveal stayed hidden.

The js crate gains the XFA scripting object model as native boa objects
over a snapshot of the form tree: xfa.form navigation by name, SOM
resolution (absolute, relative with the containing-subform search,
descendants, indexes and wildcards), rawValue/presence/access, instance
managers (_name, addInstance, removeInstance, setInstances, count),
xfa.host (messageBox, resetData, print, setFocus, gotoURL), xfa.layout
page numbers, xfa.event, app, console and util, plus unqualified sibling
names as XFA resolves them. Scripts run with `this` bound to the node and
yield their completion value; everything they change or ask for comes
back as effects.

The xfa crate builds the live form tree (instances from the data, values,
presence and access with overrides) and the event table from the
template, keeps presence and access overrides on the AcroForm
(/PCXfaOverrides) so undo and reopening preserve them, adds and removes
row instances in the datasets packet through the existing merge
pipeline, and lays the form out again on demand.

The engine runs initialize and calculate scripts on open (an appended
revision, nothing dirty), exit, validate and calculate after a field
changes (a failing validate shows its message and keeps the value, as
Acrobat does), and click scripts for buttons through run_javascript, so
the app's buttons and the js_run tool work unchanged. Values a script
sets go to the fields and the data at once; rows and visibility changes
re-lay the form out. All of it is one undo step. JavaScript off turns it
off. FormCalc scripts are recognised and reported, not run.

Bounds: script length and nesting as before, loop and recursion limits,
at most 1000 instances added per run, 2000 scripts and 50 re-layouts per
event; a calculate script that adds rows, or a click script looping on
addInstance/setInstances, ends quickly with a bounded form (tested).

Checked on IRCC's IMM 5669: opens with its page-number scripts, the Print
button's xfa.host.print runs through js_run, fill and save keep the
original bytes.

* M12: Save and SaveAs named actions run the file commands (#60)

XFA forms' Save buttons are app.execMenuItem("SaveAs"), which the layout
turns into a SaveAs named action; the shell answered it with "the SaveAs
action isn't supported yet". Named Save and SaveAs now run file.save and
file.save_as, like Named Print opens the print dialog.

The buttons test covers Save against a file on disk (SaveAs opens the
native dialog, which a headless test must not).

* M12: bound XFA form scripts against hostile forms (#60)

Review fixes for the XFA scripting PR (#230):

- Effects: values, presence and access set again on the same object merge
  (last wins, behind order-dependent effects); 10k effects, 100 message boxes
  and 1000 console lines per script and per event, reported when hit. An
  event's data writes are kept in memory and written as one rewrite of the
  datasets packet; later writes in the same edit replace that stream in place.
- form_tree stops at 200k objects and says so; forms without scripts build no
  live form and run nothing.
- Only click scripts may print, save, open links or move the focus; others are
  noted in the console. Open-time messages and errors show right after open.
- Lower loop limits for initialize/calculate/validate, a time budget per event
  after which remaining scripts are skipped and reported, and a per-script time
  limit after which a runaway script is abandoned and the document's scripts
  turned off.
- What open-time scripts change is noted in the XFA warnings.
- A document's Named /Save no longer writes the file on click (SaveAs opens
  the Save As dialog, as a script's execMenuItem does).
- XFA click scripts are no longer written as /S /JavaScript actions: the
  button carries /PCXfaClick and runs by SOM path. Private keys documented.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 04:16:54 -04:00
Brandon ThomasandClaude Opus 5.5 d3db99818f Rename PrintCraft to PdfCraft (#174)
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:03:57 -04:00
Brandon Thomas 414060a3ff Merge pull request #16 from storytold/pr/never-crash-small
Never crash: filters, fonts, js, optimize and a11y deny the panic lints
2026-10-04 14:46:43 -04:00
Brandon ThomasandClaude Opus 5.5 18c1acdb31 Never crash: hostile form scripts can't overflow the stack
boa's parser and compiler recurse once per nesting level and have no limit of their own: about
100 nested parentheses in a field or document script overflowed a 2 MiB stack and aborted the
app, which `catch_unwind` can't stop. `printcraft_js::run` now:
- refuses a script longer than 256 KiB, or one that nests brackets more than 64 deep, chains
  more than 64 prefix operators, or has more than 4096 `?`. Strings and comments are skipped.
  The script fails with an error, as a syntax error would;
- runs the engine on its own thread with a 1 GiB stack, which is address space, committed only
  as it's used. That covers every unbracketed construct a 256 KiB script can nest. A panic
  inside the engine becomes a script error. On wasm the script still runs inline.

Regression tests: `deeply_nested_scripts_fail_instead_of_crashing`, which aborted the test
process before this change, and `ordinary_nesting_still_runs`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:06:24 -04:00
Brandon ThomasandClaude Opus 5.5 9fc3354fe6 Never crash: filters, fonts, js, optimize and a11y deny the panic lints
- js: `host()` returns a script error instead of panicking when the host state is missing.
- optimize, a11y: the graphics-state stack is read with `last`/`last_mut` instead of `expect`.
- fonts: the standard-14 ASCII map is built from `u8`, which can't fail.
- filters: zlib encoding into a `Vec` keeps the standard's narrow, documented `expect` exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:06:23 -04:00
Brandon ThomasandClaude Opus 5.5 06f8d73ec7 M6: Acrobat JavaScript: sandboxed boa engine with the forms object model (printcraft-js); custom keystroke/validate/calculate/format scripts, button scripts, document JavaScripts, JavaScript console (⌘J), Preferences ▸ Enable JavaScript; js_run, js_document_scripts, js_set_document_script, form_set_script, js_enabled tools
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 04:41:47 -04:00