Commit Graph
4844 Commits
Author SHA1 Message Date
aiongg a0e82da864 fix(render): bound the replacement text copied into redraws
Each unmarked redraw of an /ActualText span's glyphs took its own copy
of the replacement text. The work budget counts comparisons, not
bytes, so a long replacement around one glyph followed by many redraws
at the same place copied it once per redraw: gigabytes from a small
page. Redraws on one page share 4 MiB of replacement text; past it a
redraw keeps its own glyphs, as it does past the work budget.
2026-10-22 17:43:04 +00:00
aiongg 6905acad2c fix(model): bound the inherited values a page walk copies
Each leaf page gets its own copy of every attribute it inherits, so a
large direct /Resources on the root of a page tree with thousands of
leaves was copied thousands of times: a compressed file of a few
kilobytes could need tens of gigabytes. The walk weighs each inherited
value once where it is declared and charges every copy to a 1 GiB
budget; past it the walk is cut off, as an over-large tree already is.
2026-10-22 17:43:04 +00:00
aiongg 1127a66031 fix(crypt): resolve crypt filter methods once per handler
Every string and stream decrypted searched the document's /CF filters
for the method by name. A file controls both the number of filters and
the number of strings, so the search multiplied the two when the
filter in use was listed last. The handler resolves the string, stream
and embedded-file methods when it is built, and looks named filters up
in a map.
2026-10-22 17:43:04 +00:00
aiongg 196be22b64 fix(cos): read a hybrid cross-reference stream once per chain
The materialized cross-reference reader reread a section's /XRefStm
for every revision that named it, decoding the stream and inserting
its rows each time, although the first read had already merged every
entry. Up to 10,000 revisions naming one large stream repeated that
work 10,000 times. A stream already read is skipped, with one repair
log line.
2026-10-22 17:43:04 +00:00
aiongg b122455705 fix(cos): find endstream once per document, not once per stream
A stream whose /Length is wrong or missing is ended at the next
endstream, which the parser found by searching the rest of the file.
Many such streams before one late endstream (a damaged file being
reconstructed reads every one) cost the file size per stream. The
document lists where endstream occurs the first time it needs it, and
each stream looks its end up there.
2026-10-22 17:43:04 +00:00
aiongg 73d15cd8b2 fix(cos): count strings at every depth against the object caches
The object caches weigh what each cached object owns, but the walk
stopped four levels down and counted anything deeper as nothing. Many
objects holding a large string inside a few nested arrays stayed cached
far past the byte budget (34 MiB against 32 MiB in the test, and without
limit as objects grow). The walk now goes past the parser's nesting
limit; anything deeper counts as too large to keep.
2026-10-22 17:43:04 +00:00
aiongg 92e713feb1 fix(cos): a /Filter that is not a name does not decode as unfiltered
Stream::filters reads only a direct name or array of names, so an
indirect /Filter (or an array holding a reference) gave an empty chain
and every decode returned the encoded bytes as if they were the data:
a strict decode reported success on content it never decoded, while a
viewer resolving the reference shows something else. Decoding such a
stream is an error, so callers keep it encoded or refuse it.
2026-10-22 17:43:03 +00:00
aiongg e2c71b60e5 fix(filters): refuse a filter chain longer than eight
Every stage of a stream's filter chain could produce up to the output
limit and nothing bounded the number of stages, so a long /Filter array
over data that decodes to itself (FF FF under RunLengthDecode) repeated
the full work once per array entry. Chains of more than eight filters
are unsupported, like an image codec, and stay encoded.
2026-10-22 17:43:03 +00:00
aiongg 07aea983d0 fix(content): splice copies only the pieces a range touches
Pieces::splice copied the unedited bytes between edits by visiting
every piece for every edit, so a page of many content pieces and many
replaced operators cost pieces x edits steps. The copy starts at the
piece holding its first byte and stops past its last one.
2026-10-22 17:43:03 +00:00
aiongg 3c64f9314e fix(content): skip a long unreadable number whole
When an operand failed to parse, the content parser went back to the
byte after its start and tried again, so a run of n bytes that never
reads as a number (a long run of '+', or digits ending in a letter) was
read again from every one of its bytes: n^2/2 bytes from a tiny
compressed stream. A failed number token longer than 64 bytes is now
skipped whole; shorter ones still recover byte by byte.
2026-10-22 17:43:03 +00:00
aiongg 58dc10c227 fix(cos): read a damaged real's numeric prefix in one pass
To read the longest valid prefix of a token like 1.2.3 the parser
retried the conversion on each shorter prefix, so a long digit run
followed by long junk was converted once per junk byte: quadratic in
the token. The prefix is found in one scan and converted once. A
prefix that spells infinity or NaN no longer reads as a number.
2026-10-22 17:43:03 +00:00
aiongg a374d16515 fix(cos): index the keys of a large dictionary
Dict::set searched every existing entry before inserting a key, so a
parsed dictionary of n distinct keys cost n^2/2 key comparisons: a
few megabytes of keys stalled parsing for minutes. From 32 entries a
dictionary keeps a key index, so each insert and lookup is one hash
probe. Equality and debug output still compare and show the entries
alone.
2026-10-22 17:43:03 +00:00
Combiler 5c8db57e5c Merge branch 'storytold:main' into main 2026-10-22 17:32:38 +00:00
aiongg 241c5512d6 fix(redact): charge repeated name-tree kids to the node budget
The name-tree count cloned each node's /Kids array and skipped repeated
references without charging the budget, so a node listing the same kid
many times cost work proportional to every entry while the budget saw one
step. The walk now borrows the array, charges every entry it does not
descend into, and stops once the budget is spent.
2026-10-22 13:28:40 -04:00
aiongg 4d64ca8e2d docs(redact): say which objects hidden-information removal reaches 2026-10-22 13:28:40 -04:00
aiongg 94563649e6 fix(redact): name the owner sweep's holder list for clippy 2026-10-22 13:28:40 -04:00
aiongg 822179ccf2 fix(redact): remove hidden data from every object that owns it
Metadata, private application data, associated files and additional
actions were removed from the catalog and pages only. The same entries on
an image or font (XMP), a Form XObject (PieceInfo), a page or annotation
(/AF with its embedded file) or a form field (/AA scripts) survived the
full rewrite, and the scan did not count them, so removing only such a
category reported nothing to remove. A walk of every object reachable from
the catalog now counts and strips these entries wherever they are, leaving
name maps such as resource dictionaries alone (their keys are names a
producer chose), and refuses a graph nested too deeply to walk in full.
2026-10-22 13:28:40 -04:00
aiongg 750bbe27f1 fix(redact): clean the structure tree after removing hidden content
Removing hidden text or hidden layers rewrote page and Form content but
left the structure tree alone, unlike applying redactions. An element's
ActualText, Alt or expansion text still described the removed content,
and a marked-content reference into a Form kept the original Form, with
the removed text, in the full rewrite. The hidden-content pass now cleans
the tags of what each page no longer shows and moves Form references to
the rewritten copy, refusing when the tree can't be cleaned completely.
2026-10-22 13:28:40 -04:00
aiongg 3fe9fe218e fix(redact): remove the widgets flattening keeps when removing form fields
Removing form fields flattens them and then drops the interactive form,
but flattening keeps hidden and no-view widgets (they draw nothing) and
widgets with no appearance. The pages still listed them, so their values
and their parent fields' values survived the full rewrite. Every widget
left after flattening now leaves the pages and the structure tree, inline
annotations included.
2026-10-22 13:28:40 -04:00
aiongg ee010fbbe5 fix(redact): read content strictly before sanitizing or redacting it
Page and Form content was decoded tolerantly, keeping what came before a
corrupt spot and dropping the rest without saying so. A page whose content
broke at its first bytes therefore read as empty: the hidden-text scan
counted nothing, the removal was skipped and the original stream, which
other viewers still draw, stayed in the file. Content is now decoded
strictly; an unreadable page counts as something to remove, so the removal
runs and refuses the page, and a Form read only in part is treated like an
undecodable one.
2026-10-22 13:28:40 -04:00
aiongg f1c56ef865 fix(redact): count a name tree's shared nodes once
The hidden-information count followed every /Kids entry of the attachment
and document-script name trees with only a depth limit, so a node listed
twice by each level was read once per path: a few hundred bytes made the
scan take 2^32 steps. Indirect nodes are now read once each, under a total
node budget.
2026-10-22 13:28:40 -04:00
aiongg 256097023c fix(xfdf): use an imported redaction mark's /Rect only when it is four finite numbers
The /Rect fallback dropped non-numbers before checking the length, so a
malformed /Rect such as [0 0 /Bad 100 100] read as a valid rectangle and
gave the mark quads. The array itself must now hold exactly four finite
numbers.
2026-10-22 13:27:52 -04:00
aiongg 63f7932d06 fix(xfdf): fall back to the /Rect when an imported redaction mark has a zero-area quad
A quad whose corners all lie on one line (or on one point) encloses
nothing, yet such /QuadPoints counted as drawable, replaced the mark's
/Rect and left an imported redaction that covers nothing. Any quad without
area now makes the mark's quads unusable, so its /Rect is used instead.
2026-10-22 13:27:52 -04:00
aiongg 8a8dab6c00 fix(xfdf): keep merged spreadsheet cells from starting a formula
Merge Data Files into Spreadsheet writes field names and values from other
people's files as CSV cells, escaping only the delimiters. A cell starting
with =, +, - or @ is evaluated as a formula when the CSV is opened in a
spreadsheet program. Such cells, after any leading whitespace, are now
written with a leading apostrophe; finite numbers such as -12.5 are left
as they are.
2026-10-22 13:27:52 -04:00
aiongg d266222339 fix(xfdf): draw every imported redaction mark's review outline, opaque
A redaction mark's outline is drawn from its /QuadPoints, but applying the
mark falls back to its /Rect when it has none. An imported mark with only a
/Rect, or with quads the outline can't draw, therefore had no visible
outline while still removing content when applied, and an imported opacity
of 0 hid the outline too. Imported marks now get quads from their /Rect
when their own are unusable (or none, so they remove nothing), and their
/CA and /ca are dropped.
2026-10-22 13:27:52 -04:00
aiongg f0e3880c35 fix(xfdf): never let a same-named imported comment replace a form widget
Comment import replaces an existing annotation with the same /NM in place.
The check for annotations it must keep covered locked comments and
redaction marks but not widgets or field dictionaries, so an imported
comment could overwrite a field's widget object, setting its value and
validation entries without the form value setter while the AcroForm field
list still pointed at it. Widgets and field dictionaries are now kept, and
the imported comment is added beside them under no name.
2026-10-22 13:27:52 -04:00
aiongg 78a562bc08 fix(optimize): bound a merged fragment image's height before sizing its buffer
The fragment merge multiplied the merged width by a height cast from a
float without checking either fit. A tiny fragment rectangle gives a huge
height: the multiplication overflowed (a panic in debug builds, a wrap past
the pixel limit in release builds) and the u32 cast saturated, sizing an
enormous allocation. The height must now fit u32 before the pixel limit is
checked, and the buffer size uses checked multiplication.
2026-10-22 13:27:52 -04:00
aiongg 4aad36cf19 fix(create): refuse an indexed colour space whose base is indexed before following it
Image export parsed an Indexed colour space's base before checking whether
that base was itself Indexed. A base that refers back to the same array
recursed until the stack overflowed. The base is now checked first; since
Indexed is the only colour space that recurses, this also ends any cycle.
2026-10-22 13:27:52 -04:00
Eric Kaden 6c0fd0f353 Merge remote-tracking branch 'upstream/main' into fix/util-scand
# Conflicts:
#	crates/js/src/lib.rs
2026-10-22 13:23:32 -04:00
aiongg ee33337627 fix(xfa): bound the name bytes of data nodes a write creates
Writing a value at a high index creates every missing sibling before it,
each with its own copy of the name. The node budget counted siblings but not
their names, so a long name with a high index made thousands of copies of
it. The write's budget also counts the bytes of the names it creates; a
path past it is skipped with the existing warning.
2026-10-22 17:22:50 +00:00
aiongg bac0e610db fix(xfa): bound the bytes a data import copies out of its file
Converting an imported data file copied each element's and attribute's
resolved namespace URI, so a long namespace declared once on the root was
copied into every element that inherits it; only the node count was
limited. The import counts the names, namespaces, attributes and text it
copies against a byte budget of four times the file-size limit, and refuses
the file as too large past it.
2026-10-22 17:22:50 +00:00
aiongg a07ad60d52 fix(xfa): queue each added data instance once per pass
write_data_ops expanded every instance-count op into one queued node per
instance, then deduplicated the pass. A script adding rows one at a time
asks for one more instance each time, so n rows queued about n*n/2 nodes
before the deduplication. Each op queues only the instances the pass has
not queued yet, and a budget bounds the instances queued across passes
that removals separate, with a warning past it.
2026-10-22 17:22:50 +00:00
aiongg 1dbfa25100 fix(xfa): bound the script text the live form's events hold
Every instance of a repeated subform records its own copy of each script,
and only the number of events was capped. A long script inside nested
repeating subforms was copied once per instance before any script ran. The
events hold at most as much script text as the XFA packets may; past that
the live form is marked truncated, as it is past the event cap.
2026-10-22 17:22:50 +00:00
aiongg 3412437191 fix(xfa): bound the embedded field text one text block expands
Rich text resolved every embedded-field reference to a fresh copy of the
field's value, so a template repeating one reference to a long value many
times laid out a copy per reference. One text block expands at most as many
bytes of embedded values as a text of its own may hold; past that the
values are cut.
2026-10-22 17:22:50 +00:00
aiongg 17f5d80f44 fix(xfa): break a long word in linear time
Text layout broke a word wider than its line by measuring the whole growing
piece after every character, and counting its characters again each time.
A long unbroken word in a wide box with a small font made that quadratic.
The piece's advance is kept as a running sum of character widths instead.
2026-10-22 17:22:50 +00:00
aiongg 72fd2a1b24 fix(js): bound the text an XFA script's effects and values hold
The XFA script host capped the number of effects but not their size: URLs,
focus targets and resetData lists were kept whole, a resetData list kept
every repeated name, and values written to fields were each cut but not
counted together. A loop repeating one long script-built string kept a copy
per effect or field. URLs, focus targets and reset names are cut to a
message's length, reset lists name each field once, and every effect,
console line and value written charges a byte budget for the run; past it
they are left out and noted. FormCalc charges an assignment to many fields
once per field copied.
2026-10-22 17:22:50 +00:00
aiongg a5cfc3eec4 fix(js): cap a form script's alerts, console lines and requests
The AcroForm script host kept every alert, console line and request a
script made, each as long as the script made it. A loop printing a long
script-built string held a copy per iteration. The host applies the XFA
host's limits: message and console counts, the effect count for requests,
each message, URL and field name cut to a line, and a byte budget across
all of them, with a closing console line saying how many were left out.
2026-10-22 17:22:50 +00:00
aiongg 2e9b247f3f fix(js): read printd literals in linear time
util.printd measured the run of identical characters at every position of
the format, literals included, but copied a literal one character at a time.
A format with a long stretch of one literal character rescanned the rest of
that stretch at every step: quadratic work from one script-built string.
Only the date letters are read as runs now.
2026-10-22 17:22:50 +00:00
PerchAnton c5279baf35 M6 PR #2648: retain active-XFA script isolation with current main 2026-10-22 20:21:39 +03:00
aiongg 77480498b2 fix(engine): low-resolution printing prints only the chosen content
Pages printed as pictures were drawn with everything on them, so form fields only
printed the page content, and the document with stamps printed every comment. The
pictures are drawn from a copy holding only what the vector print draws.
2026-10-22 13:18:26 -04:00
aiongg c3bc881607 fix(platform): a saved file keeps its group along with its mode
The atomic writer gave the new file the replaced file's mode but left it in
the saver's own group, so a file readable by one group became readable by
another. The staging file joins the replaced file's group before it gets the
mode and before anything is written; where it can't, group and others get no
access.
2026-10-22 13:18:26 -04:00
aiongg 73f11b2a29 fix(engine): fail an action whose information-removal step fails
A step that applies an edit logged any error and carried on, which is right
for a flatten with nothing to flatten but not for removing hidden
information or sanitizing: on a signed file the removal was refused, and the
unchanged file was written out as the action's successful result. Those
steps now fail the action unless there was nothing to remove.
2026-10-22 13:18:26 -04:00
aiongg 28ab616eb6 fix(engine): print as low-resolution pictures when only that is allowed
Printing checked the print permission but not the separate high-quality one
(Table 22, bit 12), so a document limited to low-resolution printing came out
as its original vector pages, text included. Such a document now always
prints as pictures of at most 150 dpi, whatever the settings ask for.
2026-10-22 13:18:26 -04:00
aiongg 30300ae71b fix(engine): compare only documents that allow copying their text
Compare read both documents' words out with no check of the copying
permission, and its result, report and difference comments carry that text,
so a copy-restricted document's text could be taken out through a comparison.
Both documents must now allow copying.
2026-10-22 13:18:26 -04:00
aiongg 5dc64face0 fix(automation): keep page text from tools when copying is not allowed
text_lines returned a copy-restricted document's lines, ocr_recognize returned
the words it recognised, and text_edit on a paragraph echoed the paragraph's
own text, all without checking the copying permission that the other text
tools check. text_lines is refused like them; recognising and restyling still
work with permission to change the document, and leave the text out.
2026-10-22 13:18:26 -04:00
aiongg a525cf1d42 fix(js): refuse code compiled from strings that skipped the script checks
eval and the Function constructors compiled code built at run time, which never
passed the size and nesting checks a script passes before it runs, so a short
form script could hand boa's recursive parser a deeply nested string and
overflow the stack. Form scripts now refuse to compile code from strings. XFA
scripts, which the runner itself starts through eval, hold such code to the
same checks as the script.
2026-10-22 13:18:26 -04:00
MarekSpodBiedry db0689e381 Save keeps the open text box or paragraph the document refused, and still applies the other (#3496) 2026-10-22 19:18:09 +02:00
aiongg 7eaa20799c fix(fonts): let auto-deref pass the candidate objects to the program match
Clippy's explicit_auto_deref rejects the explicit derefs.
2026-10-22 17:12:57 +00:00
PerchAnton d323e86706 M7 PR #3000: retain batched styled fallbacks with current main 2026-10-22 20:11:21 +03:00
aiongg 306627ee77 fix(xfa): read a packet stream the XFA array lists again only once
The packet reader decoded the stream of every /XFA array entry and
charged only the decoded output to its 64 MB budget, so one stream that
decodes to almost nothing (Flate over hex whitespace), listed many
times, repeated a large decode for free. A stream reference already
read is skipped.
2026-10-22 17:08:55 +00:00