Review on #3943, point 1. has_password_values ran a full fields() walk
on every save and recovery snapshot, and clear_password_values walked
every field's ancestors again for each ancestor of each target.
has_password_flag now answers first, reading only /Ff, /Kids and /Parent:
the field tree from /Fields (seen set, 32 levels) and, as fields() does,
the widgets on the pages with the fields above them. A form without such
a field returns before any Field is built. clear_password_values finds
each field's ancestors once.
The test puts a page-only widget, a flag inherited from an unlisted
parent and a nested field whose widget is on no page each alone in a
document. Mutants that drop the page scan, the parent chain, the tree
walk or the descent into kids each fail it, as does treating every
ancestor as exclusive. Dropping the seen set or the early-out changes
only speed (equivalent).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #3943. A black-box test of Acrobat (synthetic form, Save As and
Ctrl+S, every decoded object searched) showed how it saves a Password
field: the typed value is nowhere in the file; a field with /DV is saved
holding /DV, one without is left empty; the asterisks are drawn from the
saved value (5 for a 5-letter default, not 13 for the typed password);
/DV and /AA are kept.
clear_password_values now does the same: it restores the (inherited)
/DV as the value, or leaves the field empty, and redraws the widgets, so
the appearance no longer shows how long the typed password was. A field
already at its default no longer counts as holding a password, so such
a form needs no cleaned copy. The parity note for form.fill-text says so
and lists the two limits Acrobat shares (a script copying the password
into an ordinary field, and an earlier revision kept by an incremental
save).
Mutants, each failing a test: no /DV restore, no redraw, the default
ignored in password_fields, emptiness ignored (an empty string in an
array). Dropping the empty-default filter was equivalent, so the filter
is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #3943: sign, sign_with_timestamp, timestamp_document and dss::embed
each write a new incremental file from editor.cos, outside the writable() boundary
saves use, so a typed Password value was signed into the file. Each now writes from
the writable() copy. The new test signs a form with a typed value and checks the
signed file holds the masked appearance but not the value; the three TSA/DSS paths
need a timestamp server or validation data, and use the same one-line change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #3943: Extract Pages copies the fields of the pages it takes, and
write_new (Combine, Extract, Split, Create) wrote that copy without the writable()
boundary saves use, so the new file kept a typed Password value. write_unchecked, which
write_new and Split's size measuring both go through, now writes the sanitized copy. The
other write_full/write_incremental calls in the engine build in-memory bytes (display,
previews, the working copy, print rasters), not files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up: the helpers checked only the field dictionary's own /V. A value
inherited through /Parent or carried by a separate widget was missed, so the
document could be written as it was. Detection now uses the resolved value and
the widgets; clearing removes /V from the field, its widgets and every ancestor
only password fields inherit from, and gives the field an empty value of its own
where a shared ancestor value has to stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A text field with the Password flag kept its typed value in /V, so Save, a full
rewrite, Reduce File Size and the recovery snapshots all wrote it in plain text.
ISO 32000-2 §12.7.5.3, Table 231 (note to the Password flag): PDF processors
should never store the value in the PDF file. Each of those paths now writes a
copy without the values; the open document keeps them in memory, and the masked
appearance is kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(xfdf, sign, preflight, engine, forms, measure, annot): bound repeated work in form exchange, preflight and export, and check what imports and time stamps claim
Every packaging script runs under bash, so [[ ]] is available. Name the positional
parameters of sha256, render and has, end env.sh's functions with an explicit return
(bare, so the exit status is unchanged), and give the build-label case a default arm.
vendor/ is third-party code kept close to upstream (vendor/README.md), so SonarCloud's
186 findings there are not ours to restyle. MD5 in crates/crypt is what ISO 32000's
standard security handler mandates, and RIPEMD-160 in crates/sign is a digest existing
signatures can name; both S4790 findings are scoped to those two files.
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.
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.
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.
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.
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.
The fixture's image stream held six bytes under /Length 4, and the test
only checked that the picture was missing at a zero budget, which a
picture that could not be prepared also passes. The stream is four
bytes, and the picture is first shown to export at a 1 MiB budget.
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.
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.