Commit Graph
15 Commits
Author SHA1 Message Date
pkibiegoandBrandon Thomas adad3aae2c xfa, xfdf: refuse XML nested past 1024 levels instead of aborting the process (#987)
* xfa, xfdf: refuse XML nested past 1024 levels (roxmltree recurses with nesting depth)

roxmltree parses by recursive descent, so its stack use grows with the nesting depth of the
document it is handed. Ten thousand nested elements overflow the main thread's stack and abort
the process ("fatal runtime error: stack overflow, aborting", SIGABRT, core dumped); three
thousand still parse on the default stack. A stack overflow is `abort()`, so `catch_unwind`
cannot intercept it and the whole application dies.

Two of our XML entry points hand attacker-supplied text straight to it:

- XFDF/FDF form data import. A 1.4 MB file with 50,000 nested `<field>` elements aborted
  `pdfcraft-cli run doc_import_data` in 288 ms (SIGABRT, core dumped). The same file now returns
  "the file nests XML elements 50003 deep, past the 1024 the reader accepts" in 54 ms, at 19 MB.
- The XFA template parser (`crates/xfa/src/parse.rs`), reached when a document with an XFA form
  is opened or laid out. A 10,000-deep template aborted this crate's own test run before the fix.

`crates/xfa/src/data.rs` parses the datasets packet and the data written back into it, so it is
guarded the same way.

The bound has to be applied before parsing, not while walking the result: the overflow happens
inside the parser, so a depth cap on our own walks (64, unchanged) cannot prevent it. Both
crates measure the nesting with a small scanner that skips comments, CDATA, processing
instructions, the DOCTYPE and quoted attribute values, and counts a tag that closes itself
(`<draw/>`) as one level while it is read rather than as a level left open, so a template full of
styling elements is two levels deep rather than two thousand. It cannot under-count a real
element; a stray `<` in text is invalid XML and only makes it over-count, so it errs towards
refusing a file rather than trusting it.

The limit is 1024 because of measurement, not taste: 3000 levels still parse on the default stack
and 10000 abort, and `hostile_templates_fail_without_panicking` keeps asserting that a 500-deep
template parses, so the bound has to sit well above that.

Tests: `deeply_nested_fields_are_refused_instead_of_overflowing_the_stack` and
`deeply_nested_template_is_refused_instead_of_overflowing_the_stack` both abort with a stack
overflow before this change and pass after it; `nesting_depth_scanner_counts_what_matters` and
`self_closing_elements_do_not_accumulate_in_the_depth` cover the scanner (comments, CDATA, a
quoted attribute holding `<`, and 2,000 sibling `<draw/>` elements, which a scanner that leaves
self-closing tags open counts as 2,000 deep);
`many_self_closing_elements_do_not_accumulate_into_the_bound` parses a template holding two
thousand of them; and `nested_fields_within_the_limit_still_read` proves a 16-level field tree
still reads with the same dotted names. Both suites pass: xfa 36, xfdf 18.

The parse options also carry `nodes_limit` now, which was already the shape the XFA parser used:
a 93 MB XFDF with two million sibling elements went from 1082 MB and 2323 ms to 289 MB and
1299 ms ("nodes limit reached").

The scanner is duplicated between the two crates because nothing between them owns XML. If it
should live in one place, say where and it moves.

* xfa: the depth limit holds on a default-stack worker thread

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 13:38:10 -04:00
Nigel Ihza FirdausyandBrandon Thomas 77241397d6 fix(xfdf): combine imported annotation flags with bitwise OR (#931)
* fix(xfdf): combine annotation flags as a bitmask

* Run cargo fmt

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 12:32:17 -04:00
MarekSpodBiedryandBrandon Thomas f6caba43b2 XFDF keeps a callout's leader, text-box inset, intent and arrowhead (#819) (#854)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 15:38:27 -04:00
4b74f655de XFDF keeps polygon and polyline vertices (#809) (#848)
Export wrote polygon and polyline comments without their points and import
had nowhere to read them from, so an XFDF round trip left the comments with
no /Vertices and no appearance. Export now writes the points as XFDF
<vertices>x,y;x,y;…</vertices>, and import reads them back (whole x,y pairs
only) before the appearance is drawn.

Co-authored-by: DWAA1660 <DWAA1660@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 15:35:25 -04:00
MarekSpodBiedry 7608632db1 fix(xfdf): drop the replaced note's popup when re-importing a comment (#807) (#860) 2026-10-10 15:08:32 -04:00
f78f74164c M6.2: check boxes with non-UTF-8 state names show their mark when clicked (#536)
Japanese forms often name a check box's on state in Shift-JIS (「はい」 is
/#82#CD#82#A2). State names were read with from_utf8_lossy, so ticking such a
box wrote a different name to /AS and /V, no appearance matched, and the box
was drawn with no mark (or not at all).

Names now map to text losslessly (forms::name_text / name_bytes: UTF-8 as
itself, other bytes and `#` as #XX) wherever states and values are read and
written: /V, /DV and /AS, PdfCraft's own state appearances, and FDF import and
export, so such values also survive an FDF or XFDF round trip.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 12:58:38 -04:00
d4d2e83273 FDF: replies and review status keep their parent comment (fixes #333) (#388)
FDF export dropped /IRT, so a note's replies and its review-status reply
imported as unrelated comments. Each comment is now an indirect object of
the FDF (with a cross-reference table), and a reply's /IRT points at its
parent's object there, as ISO 32000-2 §12.7.8.3.1 allows. Import maps the
FDF objects to the placed comments and links replies in a second pass,
falling back to a parent of the same /NM already in the document.

FDFs with direct comment dictionaries still import. Self-references,
missing parents and cycles are ignored without panicking.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 21:03:30 -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
890ae8d3ed XFDF: gracefully ignore invalid Unicode annotation colours (#48)
Use checked string slices when reading annotation colour components. Cover colour and interior-colour imports with a synthetic XFDF regression.

Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
2026-10-05 16:36:08 -04:00
Brandon ThomasandClaude Opus 5.5 ac222e818b Never crash: edit and xfdf have no expect calls
- edit: the substitute font name is a `&str` constant, so there's no `from_utf8(..).expect`.
  Paragraph grouping uses `let … else` / `if let` chains, and the reuse-font path takes the
  metrics with `filter` instead of `expect("checked")`.
- xfdf: a comment key that vanished between listing and lookup is skipped.

Both crates now deny the panic lints. Adds the root clippy.toml (identical to #11) so tests may
still unwrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:23:06 -04:00
95af23d3a5 Forms: multi-select list values survive XML, CSV and text import (#5)
Export data writes a multi-select list's values into one cell joined by ", " ("A, C"), but
import passed the cell on as a single choice, which the list refused, so the selection was
silently lost on an export → import round trip (the field was only counted as rejected).
A multi-select list now splits such a cell into its options, unless the whole cell is itself
one of them.

Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:00:23 -04:00
096b515719 Forms: CSV import keeps values that span lines (#4)
* Forms: CSV import keeps values that span lines

A quoted CSV value may hold line breaks, and export_data writes multiline text fields that
way. The importer split the file into lines before reading quotes, so such a value was cut
at its first line break and every later column was lost (an export, then an import, of a form
with a multiline field silently dropped data). CSV records now end only at unquoted line
breaks; tab-delimited text is read as before.

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

* Forms: build CSV records without unwrap (never-panic rule)

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

---------

Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-04 13:24:11 -04:00
Brandon ThomasandClaude Opus 5.5 ab47bf4b9a CI: as_chunks instead of constant-size chunks_exact (clippy 1.99 chunks_exact_to_as_chunks)
Stable clippy 1.99 denies chunks_exact/chunks_exact_mut with a constant
size (32 sites), which failed the test job on main and every open PR.
Behaviour unchanged; MSRV 1.90 has as_chunks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:48:43 -04:00
Brandon ThomasandClaude Opus 5.5 6ff0bf8f64 M6: Merge data files into spreadsheet (FDF, XFDF, filled PDF forms → CSV); form_merge_data tool
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 04:52:59 -04:00
Brandon ThomasandClaude Opus 5.5 22a8691475 M5.6/M6.7: Comment and form data exchange: new xfdf crate (L4): XFDF (ISO 19444-1) export/import of comments (replies, pop-ups, ink, quads, line ends, flags, colours) and nested field values; FDF both ways; form data as Acrobat XML, CSV and tab-delimited text. Import replaces comments with the same name and sets values through formats/validation (read-only fields take data). Import/Export commands; doc_export_data/doc_import_data tools (69 tools)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:31:56 -04:00