Commit Graph

2443 Commits

Author SHA1 Message Date
mtlcl410 f41abf921f M6: dotted field names make parent fields (#2696)
A field's own (partial) name can't contain a period (ISO 32000-2
§12.7.4.2), so Prepare a form refused MASTER.first. Acrobat reads such a
name as a full name: the field `first` under a parent field `MASTER`.

- add_field: a dotted name makes or joins the parent fields it names.
- set_props: a dotted name is the full name and moves the field there,
  copying what it inherited onto it first; a plain name still renames
  the field within its parent. Either way the new full name may not be
  taken by, or hold or sit inside, another field.
- A field joining an existing parent doesn't take its Ff or Q.
- Field Properties and the Prepare a form name tags show the full name
  instead of the last part.
- Names are capped at 32 parts; empty parts are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:45:13 +08:00
Dylan M. Taylor de91632934 Merge pull request #4151 from aiongg/fix/coderabbit-c2-edit-organize
fix(edit,organize,create): bound the work hostile files cause in editing, page organizing and image import
2026-10-22 21:57:13 -04:00
Dylan M. Taylor d31a96f46f Merge pull request #4121 from aiongg/fix/coderabbit-c1-js-xfa
fix(js, xfa): bound the text and work that form scripts and XFA data can multiply
2026-10-22 21:39:08 -04:00
Dylan M. Taylor 8ee1ac0c58 Merge pull request #4117 from aiongg/fix/coderabbit-b-caps
fix(limits): bound the decoding and listing work that repeated references and small inputs multiply
2026-10-22 21:38:59 -04:00
Dylan M. Taylor 3cee9f338f Merge pull request #4128 from aiongg/fix/coderabbit-c4-core
fix(cos, content, filters, crypt, model, render, print, a11y): bound the work that repeated keys, streams and page values multiply
2026-10-22 21:38:37 -04:00
Dylan M. Taylor f7a3a52fad Merge pull request #4114 from aiongg/fix/coderabbit-b-shared
fix(cos): one decode budget per page walk, so repeated stream references cannot multiply decoding
2026-10-22 21:38:15 -04:00
Brandon Thomas 53199fb4fa Release 0.13.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.13.0
2026-10-22 20:06:13 -04:00
aiongg ba024eb0dd fix(edit): flatten on a copy so a page refused part way changes nothing 2026-10-23 00:02:33 +00:00
aiongg 0d216199b9 fix(redact): apply redactions on a copy so a failure leaves the document as it was 2026-10-22 23:40:55 +00:00
aiongg 2430d53f9e fix(print): read the text after a form the budget refuses 2026-10-22 23:37:07 +00:00
aiongg 5473d354a9 fix(forms): skip a field of an unread kind before it claims shared widgets 2026-10-22 23:36:31 +00:00
aiongg 54152aed87 fix(edit): refuse a mark on top of over-deep content before changing the page 2026-10-22 23:35:40 +00:00
aiongg 03366c3c70 A counted decode bounds all filter stages together and counts output before the predictor 2026-10-22 23:33:37 +00:00
aiongg 4362449fb2 fix(render): find an embedded file by key without the listing's budget
Extraction walked the name tree again with a fresh key budget, so keys
the listing had left out could fill it and a listed embedded file came
back as not found. The lookup compares each key with the one wanted and
keeps none, so it needs no budget; a key object or /Names array that
several places reference is converted once.
2026-10-22 23:32:42 +00:00
aiongg 7a466ff5fc fix(edit): prune a field again when a shorter path reaches it past the depth cutoff 2026-10-22 23:32:11 +00:00
aiongg 8bcf83faef fix(render): spend the raw length of text that converts shorter
A kept web address or attachment text spent only its converted length,
so a long raw value that trimmed short was converted in full for every
entry sharing it. A kept conversion now spends the longer of its raw
and converted lengths.
2026-10-22 23:31:43 +00:00
aiongg 7a73c2ec3a fix(render): keep embedded-file keys within the text budget
The embedded-files name tree copied every key, and an attachment
without a file name copied its key again as the name, so many entries
sharing one long key string multiplied it. Keys, and a key kept again
as the name, now spend the 16 MiB link and attachment text budget; an
embedded file whose key does not fit is not listed.
2026-10-22 22:03:43 +00:00
aiongg a0a66b982b fix(render): read an annotation attachment's Contents only when it names it
An annotation attachment's /Contents was read and charged to the text
budget before the file specification's /UF or /F, which then named the
attachment, so the discarded text could leave a later web address
unlisted. /Contents is now read only when neither name is there.
2026-10-22 22:02:10 +00:00
aiongg 0c09000f7b A decode charges the page's decode budget for every filter stage's output 2026-10-22 22:00:51 +00:00
aiongg c704c1089a fix(create): keep the TIFF total budget in range on 32-bit targets 2026-10-22 21:55:12 +00:00
aiongg aaa4b99eb9 fix(render): spend the text budget on a rejected conversion
A web address or attachment text that converted to more than the budget
left spent nothing, so every link or attachment sharing it converted it
again. A conversion that comes out too long now spends the raw length,
and so does attachment text that trims to nothing.
2026-10-22 21:54:39 +00:00
skylar taylor-barrick adea2c908a M14: recover existing macOS PDF associations after open and save (#3558)
* M14: recover existing macOS PDF associations

* M14: address macOS association recovery review

* M14: reuse upstream save metadata preservation
2026-10-22 15:49:13 -06:00
aiongg 82721ed44c Merge #4114 (fix/coderabbit-b-shared): over-budget page content reads as an error 2026-10-22 21:41:53 +00:00
aiongg d67c4b3059 fix(js): an XFA value past the effect cap leaves the field unchanged 2026-10-22 21:41:50 +00:00
aiongg e541151389 fix(js): XFA presence and access change the snapshot only when their effect is kept 2026-10-22 21:41:01 +00:00
aiongg 11d2a576d6 Filters README lists decode_counted 2026-10-22 21:40:10 +00:00
aiongg 0ae289c762 A failed strict decode charges the page's decode budget for what it decoded 2026-10-22 21:37:19 +00:00
aiongg fd2e13f332 M8: XFA instance changes reserve their effect before changing the snapshot 2026-10-22 21:35:52 +00:00
aiongg 426a609795 fix(render): keep link addresses and attachment text within 16 MiB
The link and attachment listings copied a link's web address and an
attachment's name and description for every annotation or name-tree
entry, so many entries sharing one long string multiplied it in
memory. Together they keep at most 16 MiB of that text, and a string
past what is left is not converted. A link past it lists as a URI
action without its address, an attachment under its generic name
without a description.
2026-10-22 21:35:23 +00:00
aiongg 4e1457ed4f Marking a tag as an artifact refuses content past the page's decode budget 2026-10-22 21:33:43 +00:00
aiongg c82dc91abf fix(render): list an annotation reached through a reference chain once
The annotation listing skipped a repeated /Annots entry by its raw
reference, so an indirect object holding only a reference to a link
listed that link a second time. Entries and page arrays are now told
apart by the object their reference chain ends at. The lopdf reader
reported the first object of a chain, because lopdf's get_object
already follows it; it now reports the last, as the lazy reader does.
2026-10-22 21:33:20 +00:00
aiongg 473b0a4371 Merge fix/coderabbit-b-shared into fix/coderabbit-c4-core 2026-10-22 21:32:09 +00:00
aiongg 4fc56fd303 Reading blocks documents that over-budget page content is an error and truncation covers forms 2026-10-22 21:29:04 +00:00
aiongg 6384381127 fix(forms): read each widget once when enumerating fields
A /Kids array listing one widget many times, or shared by several fields,
built a Widget for every listing, so a small file expanded into fields times
listings widgets. A widget now belongs to the first field that lists it, and a
page widget already owned is not adopted again.
2026-10-22 21:20:10 +00:00
aiongg a52e39a5e2 Merge #4114 (fix/coderabbit-b-shared): shared DecodeBudget
The tests each branch appends to the audit, edit and organize test files are
kept side by side; prune.rs merges cleanly, with the form memo on top of the
budgeted page content.
2026-10-22 21:17:55 +00:00
aiongg 9188cf59eb fix(create): don't decode a JPEG carrying thousands of Extended XMP chunks
The JPEG decoder re-sorts and rescans every pending Extended XMP chunk after
each marker, so a file of many small chunks cost time quadratic in their
number before any pixel limit applied. Turning a photo upright now counts
the chunks first and leaves a JPEG with more than 1024 as it is.
2026-10-22 21:17:15 +00:00
aiongg cad0fe4526 fix(create): refuse a GIF screen past 4096x4096 and convert its canvas in place
A one-pixel GIF on a huge logical screen decodes to a canvas that size,
which the conversion then copied before splitting it into colour and alpha,
so a few bytes could hold over a gigabyte. Screens past 16 Mpx are refused
before decoding, and the decoded canvas is converted without a copy.
2026-10-22 21:17:15 +00:00
aiongg 7e9f5a4435 fix(create): bound a TIFF's decoded pages together, and read one when one is wanted
A TIFF's pages were each capped at 2 GiB but all kept in memory until
embedded, so many small directories sharing one compressed strip could ask
for many times that; embedding one image also decoded every page. The pages
of one TIFF are now refused past 4 GiB together, and image_xobject reads
only the first page.
2026-10-22 21:17:15 +00:00
aiongg b979d4d9c2 fix(audit): audit the glyphs on each page held, not their ActualText
The redaction audit read each page's text through text_lines, which walked
the whole page tree again for every page (quadratic in the page count) and
let /ActualText replace the glyphs, so an empty or substitute ActualText hid
covered text from the audit. Edit gains glyph_text_lines, which reads the
page dictionary it is given and ignores ActualText; the audit uses it.
2026-10-22 21:17:15 +00:00
aiongg fdcfe16b7d fix(edit): bound the text one page's extraction can expand into
Text extraction copies a ToUnicode destination for every glyph showing its
code, and the page's total text was not bounded, so a small page could expand
into gigabytes. One page's extraction (its forms included) stops with an error
past 16 MiB of text.
2026-10-22 21:17:15 +00:00
aiongg c9090b00ef fix(edit): refuse to put content on top when marked-content depth is unknown
Past the marked-content depth budget the number of open sequences is unknown,
and the mark on top was appended with none of them closed, so the page's own
optional-content scopes (possibly hidden) could hide a redaction box, a
header or a watermark. Placing on top of such a page is now refused before
anything changes; behind it is still possible.
2026-10-22 21:16:27 +00:00
aiongg dc7a195dda fix(edit): prune each form-field node once after flattening
Removing flattened widgets from the field tree recursed into every /Kids
entry with only a depth limit, so a node listing its child twice per level
(or itself) made the walk exponential. Each node's answer is now remembered;
a node met again while it is still being visited survives, as it did when
the depth limit stopped a cycle.
2026-10-22 21:16:21 +00:00
aiongg f1a89a4842 fix(edit): wrap added text once per write, not once per line
Writing an added text item re-wrapped the whole text inside the loop over its
lines to learn the line count, so multiline text cost time and temporary
memory quadratic in its length. The wrapped lines are computed once.
2026-10-22 21:16:21 +00:00
aiongg cb1ddc899b fix(organize): find XMP attributes in one pass over the packet
Each candidate attribute match searched the whole packet before it for the
last < and >, so a long text node repeating the attribute name cost time
quadratic in the packet size (up to the 16 MiB decode cap). The scan now
keeps the last < and > as it moves forward.
2026-10-22 21:16:21 +00:00
aiongg 1f6ce5ecc7 fix(organize): analyse each form once when pruning unused XObjects
The pruner decoded and walked a form XObject again on every Do that drew
it, so a form drawing itself a few times per level was analysed
exponentially often within the depth limit. Forms are now remembered with
the resources owner they were drawn against, and a repeat records nothing
new.
2026-10-22 21:16:20 +00:00
aiongg cab1dc3eb5 fix(organize): read each name-tree node once when importing
Looking up a named destination and listing attachments in a source's name
trees followed every /Kids entry with only a depth limit, so a node listing
the same child twice per level made the walk (and the attachment list)
exponential in the depth. Both walks now skip a node already read.
2026-10-22 21:16:20 +00:00
aiongg 59dbdee428 fix(organize): copy annotation actions with one shared reference map
Fixing up copied annotations cloned the whole source-to-destination map for
every non-GoTo action and dropped the copies it made, so many annotations
cost quadratic memory and time and an object shared by their actions was
copied once per action. The copier now lends its map to the action copy and
takes it back with the new entries.
2026-10-22 21:16:20 +00:00
aiongg da288517e6 fix(organize): resume the field-rename suffix search where it stopped
Renaming a colliding field tried Name-1, Name-2, ... from 1 every time, so
many fields of one name cost a quadratic number of attempts during import,
combine and their conflict previews. Taken names only grow, so the search
now resumes after the last suffix handed out for that name; the result is
still the first free variant.
2026-10-22 21:16:20 +00:00
aiongg d39b55dd6d fix(organize): register imported form fields in linear time
Each imported top-level field was checked against every field already in
/AcroForm /Fields before it was appended, so a page with many distinct
widget fields cost time quadratic in their number. Membership is now kept
in a set beside the list.

Adds a test-only work counter to the crate, so tests can bound the work a
hostile document causes without timing it.
2026-10-22 21:16:20 +00:00
aiongg f63f509647 fix(cos): compare each section's table with a repeated hybrid stream
A section naming an /XRefStm that a newer section had already read
skipped the stream entirely, so an entry in its own table that
disagreed with the stream was no longer recorded in xref_oddities.
Signature validation reads those records to refuse an appended section
another reader could follow. The reader keeps each hybrid stream's
entries (up to MAX_XREF_OBJECTS in all) and compares every later
naming section's table with them, without decoding the stream again.
2026-10-22 20:37:04 +00:00