CodeRabbit's review of 674a20899:
- Each budgeted read of a font's stream went through
`Document::with_direct_parms`, which copies the whole stream when its
/DecodeParms is given by reference: an unrelated value in the
dictionary, however large, was copied again for every read.
`DecodeBudget::decode_stages_within` now takes the document and reads
through `Document::decoding_view`, as `decode_strict` and
`decode_with_damage` do: the data is shared, and only /Filter and the
parameters made direct are copied. `Reads::decode` and the editor's
program read use it, a Type 3 font's glyph descriptions read through
`decode_with_damage`, and so does Repair Thai maps' program read.
- A Type 3 font's glyph descriptions resolved one description's
parameters a font before the spent allowance refused it. They now ask
`DecodeBudget::refuses` first, as `decode_stages_within` does for the
others: once the allowance is spent a stream with data is refused, and
noted, before its parameters are resolved.
- `DecodeBudget::refused` counts the decodes refused for want of the
allowance, where `remaining() == 0` can't tell a refusal from an
allowance spent to the byte.
Test: `reading_a_fonts_streams_copies_no_dictionary` (`pdfcraft-fonts`,
a test binary of its own): a font whose program and ToUnicode hold an
unrelated array of a million numbers and /DecodeParms by reference is
read eight times by the editor's program read and the metrics read,
and the process's peak grows by less than half of what one array
holds. That a Type 3 font refuses before resolving parameters changes
no result, only the time it takes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Editor, export and redaction must not read or rewrite text that the page view and Find never see, which a mismatched cap allows on large streams.
Lower MAX_DECODED to 256 MiB to match the renderer cap, so every Stream::decoded-based content reader stops at the same byte and oversized streams error instead of exposing later text.
Test: decoded_stops_at_renderer_cap_of_256_mib failed before the change and passes after.
Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
The Legal forms review of b7224cbc9..4d6ff291d:
- Nothing pinned that `recovered_thai_map`'s rewritten guard still
refuses a /CIDToGIDMap that isn't Identity: deleting it left every
Thai test passing. `recovered_thai_map_checks_every_kept_glyph_against_
the_font_program` now asserts no map for a map stream that moves glyphs
and for another name.
- The guard's comment and the test's cited Identity as the default of
ISO 32000-2 Table 115. It is the default of PDF 1.x (ISO 32000-1);
PDF 2.0 gives it none and requires the map with a /FontFile2. Both now
say so, and that the renderer takes a missing map as Identity too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Since 0a00862e9, what an image is stored again with (its size, filters and parameters) is
remembered by number and generation, so a value reached through a reference of another
generation is fetched again rather than taken from what the reference of its object's own
generation read. The README's list of what may be read again now says so. What an image names
is still remembered by object number, so a dictionary named that way isn't read again.
The same sentence said a chain of more than 32 references may be read again. `names_in` follows
33 references before it gives up on a chain, as `a_chain_of_references_is_named_to_the_limit`
says, so it is more than 33.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The Consumers review of 64dfe1fd4. The README said a surviving form loses
the resource entries for removed XObjects its own content does not draw.
It also keeps those a form without resources of its own, drawn through
them, still draws, and since this PR a redacted page keeps them the same
way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The Claims review of the description at b7224cbc9. `Reads::decode` said a
stream past its limit is unread, not refused, "when what is left can pay
for its stages". What `DecodeBudget::decode_stages_within` does, and
`a_stage_past_its_limit_charges_what_came_before_and_the_limit` pins:
with more left than what the stages before the stopped one made and the
limit, it is charged those two; otherwise, with more left than the
stages' total (behind one filter, the limit), it is charged that total;
with no more left than that total, it spends all of it and counts as
refused.
`MAX_THAI_PROGRAMS` also names the second read of a subset's program,
for which of its glyphs have outlines, among what it pays for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The Consumers review of ba28458ad..b7224cbc9:
- `recovered_thai_map` read the descendant's /CIDToGIDMap with
`Dict::name`, on the line after e9142b03b's /Encoding check, so a map
given by reference made Repair Thai maps pass the font over, and so
did a missing one, which is Identity (ISO 32000-2 Table 115), as
`composite_glyph_maps`, `LegacyThaiFont` and the renderer read it. It
now resolves the map and takes a missing one as Identity.
- `MAX_THAI_PROGRAMS` says what it pays for: the programs, and the CMaps
and glyph maps read to adapt their cmaps. The metrics read to find and
measure the subsets aren't charged to it. Its refusal ("MiB of font
data") still says what it counts.
Test: `recovered_thai_map_checks_every_kept_glyph_against_the_font_program`
recovers the same map with the font's /CIDToGIDMap given by reference and
with none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
One conflict, in redact lib.rs: the module list, where main added `elsewhere` and
`identification` (#6714) and this branch `globals`. All three stay, in order.
Main's #2448 work also added a sixth step to `apply`, after the verification and the pattern
guard: `metadata::scrub` removes the `/Info` entries and the catalog's and pages' XMP that repeat
the removed text. An image that only such a packet refers to leaves the file then, after
`unshare` had looked for what left, so its shared dictionary stayed. `unshare` now runs after
the scrub, last of what can take something out. Storing images again changes no page's content
and no pattern cell, so the verification and the pattern guard before it check the same pages
they did. The crate's doc says the dictionary goes last. Remove Hidden Information's order is
unchanged: main's changes to `sanitize.rs` only read (`owned_values`).
Test: `an_image_only_the_scrubbed_metadata_refers_to_takes_a_shared_jbig2_dictionary_too`. An
image under the mark is also named by the catalog's XMP, which repeats the text removed beside
it. After the redaction the packet is gone, the image on the other page is stored again, and
the dictionary is gone too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A third merge of main, at ba28458ad. Main's #2448 (in #6714) collects
the text a redaction removes from the walk's scope, which this branch
also reads after the walk for what it left of the page's allowance.
- `interp.rs`: `Scope` keeps this branch's `into_budget` and gains
main's `with_removed_room`, `flush` and `removed`.
- `lib.rs`: the page's walk starts with main's room for removed text,
and after it the scope's removed text is collected as on main, then
what it left of the page's allowance is taken for the cleanups, last,
as taking it consumes the scope.
Main's new code reads no content stream or form in the walk or the
cleanups, so nothing of this branch's allowance or one-operator reads
needed carrying into it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The Legal forms review of 8fb534ad4. A Type 0 font's /Encoding may be
given by reference (a name or a CMap stream, either indirect), and the
metrics read resolve it: an indirect /Identity-H reads two-byte codes.
The editor's check before reading a composite font's metrics, and
`main`'s `composite_glyph_maps` and `recovered_thai_map`, read it with
`Dict::name`, which doesn't resolve: such a font counted as another
encoding, so the editor adapted no copy of it and Repair Thai maps
passed it over. All three now read it through `identity_h`, which
resolves it as the metrics do.
Tests: `a_composite_fonts_encoding_given_by_reference_is_read` (the
editor adapts a copy of a composite subset with `/Encoding 5 0 R`, object
5 `/Identity-H`, that maps 'Ω', and its metrics read two-byte codes), and
`recovered_thai_map_checks_every_kept_glyph_against_the_font_program`
recovers the same map with the font's /Encoding by reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
In `a_shared_jbig2_dictionary_that_cant_leave_refuses_the_redaction`, the case of a copy of
the dictionary whose `/Filter` is given by reference may end `Ok`, the image stored again
without it. That branch checked only that the dictionary, object 9, left, and that its marker
was nowhere in the file. But `assert_gone` can't decode the copy, object 10, with its filter by
reference, and its bytes are Flate, so a run that never found the copy, and left it in the
file, passed. The branch now checks that object 10 left too, through `reachable_objects`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The forms editor's test draft sets the focus's obj and list, which main added, and the focus scripts look up the focused field with focused_field so a same-name twin is read as main reads it.
Mutation testing of 063f8629e missed eight mutants in
`DecodeBudget::decode_stages_within`: nothing pinned what a read
stopped at a stage's limit, at the stages' total or at what is left of
the allowance charges, which limit it reports, or where one case gives
way to the next.
`a_stage_past_its_limit_charges_what_came_before_and_the_limit` reads
each way, and each boundary on both sides: a byte more room than the hex
stage and the limit (charged them, a byte left), exactly that (past the
allowance: all of it spent, its limit reported), a hex stage making
exactly the limit so the Flate stage reaches the stages' total with more
left (charged the total), with exactly the total left of a larger
allowance and with less (spent, the allowance's limit reported), and,
with nothing left, a stream with data that decodes to nothing (refused)
and an empty one (read).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The Resources review of 8eead8ecd:
- `embedded_sfnt_within` read a composite font's metrics within the
editor's allowance before `composite_glyph_maps` refused any `/Encoding`
but Identity-H, so an Identity-V or UniJIS font spent its ToUnicode and
`/CIDToGIDMap` for nothing. Its `/Encoding` is now checked first; a
program with a cmap of its own is kept as it is, as when the rebuild
finds nothing.
- The Thai edit's allowance, `MAX_THAI_PROGRAMS`, now also pays for the
CMaps and glyph maps its subsets' metrics and codes are read from: its
doc and its refusal say "font data", not "font programs".
- `a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names`
also reads its font, whose `post` table names its glyphs, within an
allowance: the program is paid for by the glyph-name reading, by the
Mac OS Roman one and for the widths, and a byte short of the first the
names go unread.
Tests: `a_composite_font_that_cant_be_adapted_spends_only_its_program`,
and `adapting_a_programs_cmap_is_paid_for` again expects a composite font
whose codes aren't Identity-H to pay for its program only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The Resources review of 8eead8ecd. The program read and the metrics reads
passed their limit times the number of filter stages as one total, which
any one stage could use: a FontFile2 of Flate making 128 MiB of
whitespace, under seven ASCIIHex stages (a 130 KB file), made all of it
before being refused, about 0.1 s a font where `main`'s bound stopped it
at 16 MiB. `edit`'s `matching_thai_outline` reads such a program for each
of a page's fonts with no total, and every `Metrics::from_dict` up to
three times.
Now each stage is held to the limit, as a decode without an allowance
holds it, and the stages together to the limit for each of them, so every
stream the stage scaling was for still reads: a full `/CIDToGIDMap` or a
12 MiB program behind ASCIIHex and Flate. The two bounds take an entry
point of their own: `pdfcraft_filters::decode_tolerant_counted_each`,
which `run` already supports, and `DecodeBudget::decode_stages_within`,
which `Reads::decode` and the editor's program read use. A stage past the
limit is charged what the stages before it decoded and the limit; past
the stages' total, or past what is left, it is charged as
`decode_within` charges. The program, the last stage, is so within the
limit without a check of its own.
Tests: `each_stage_is_held_to_its_own_bound_within_the_total` in
`pdfcraft-filters` (a Flate stage stopped at its bound hands the next
stage nothing), `each_stage_is_held_to_the_limit` in `pdfcraft-cos`, and
in `pdfcraft-fonts` `an_editors_program_read_holds_each_stage_to_its_limit`
and `each_stage_of_a_metrics_stream_is_held_to_its_limit`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
main's #3962 (symbolic TrueType glyph names, 63a6f8ace and nearby) reads a
symbolic TrueType font's codes by the `post` names of the glyphs its cmap
selects, before the Mac OS Roman reading:
- crates/fonts/src/pdf.rs, `unicode_map`: both readings stay, in main's
order, each given the metrics read's allowance. The program read main
added (`symbolic_glyph_texts`) goes through `Reads::decode`, as
`symbolic_mac_roman_texts` does since the last merge: paid for from the
allowance, `past_allowance` set when it can't be, behind several
filters within the stream's own limit.
- crates/fonts/src/pdf/tests.rs: both sides added tests at the same place;
all are kept.
Tests: `adapting_a_programs_cmap_is_paid_for` now expects the program to
be paid for a fourth time, and `the_symbolic_mac_roman_reading_is_paid_for`
becomes `the_symbolic_readings_of_a_program_are_paid_for`, paying for both
readings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
One conflict, in redact sanitize.rs. Main's #6021 (930b44131) split `remove_hidden` in two:
- it now counts only what the chosen categories need (`scan_only`);
- it hands the removal to `remove_counted`, which tests also call with `every_page`.
This branch had added one line to the old body: `kept_before`, what a full save keeps before
anything is removed. Main's side is taken whole, and that line is now the first thing
`remove_counted` does after finding the catalog, before any change to the document. So every
caller of `remove_counted` has it. `unshare_unmarked` stays last, after `unlink_undrawn`, and
merged cleanly there. The only early return after a change is `NothingToApply` when nothing
was removed and nothing unlinked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The merge gave `prune_font_forms`' walk of each Form a kept glyph
draws the allowance of a page, which its call of `resource_ops` now
needs; no test pinned it. In
`the_form_a_kept_type3_glyph_draws_is_walked_within_the_allowance_of_a_page`
such a Form draws two forms without resources of their own, each past
half of the allowance, and the redaction is refused with the decoder's
limit; with two small ones it goes on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Main's #1143 (863af864a), merged in 77b9cfa31, has cos accept both generations of a slot whose
cross-reference row and object header disagree: `8 0` in the row and `8 1 obj` in the file, a
reference of either reads the object. `unshare` stored an image again with `Document::set` under
the row's generation, which keeps that one alone. A page that named the image by its header's,
`/Im 8 1 R`, then read null, in the document and after a full save: a redaction on another page
took the image off a page no mark reached.
It now stores it with `Document::set_existing`, which keeps both. It was the one object this
PR's code writes back after reading.
Test: `an_image_stored_again_keeps_the_generation_its_header_gives` patches the second page's
image to `8 1 obj` behind a row of generation 0, and its page to `/Im 8 1 R`. After a redaction
on the first page, that page still draws the scan, in the document and after `write_full` and
reopening, and the dictionary is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
- engine: the on_changes test's XfaRun literals set highlight; the script
runner reads its template before borrowing the live form for to_js; has_event
reads the paged live form, as the rest of the runner does, so focus scripts
find a page-master copy by its own SOM path.
- js: property reads compare and copy the shared (Arc<str>) node text, and the
property tests build XHost with its current signature.
- xfa: the Emitter test literal sets groups and used; a field split that is
undone also restores the names handed out; rustfmt on a merged import.
- parity: drop the use/usehref gap from form.xfa-dynamic, as prototypes are now
resolved before the template is parsed.