Commit Graph
253 Commits
Author SHA1 Message Date
Dylan M. Taylor ed18238d74 crypt, filters, cos, export, xfdf: migrate to craft-pdf, craft-rtf, craft-csv 2026-11-14 04:02:21 +00:00
c04925c580 fix(cos): a clone that edits an object stream or a /Length stops reading what other clones cached (#6910)
* fix(cos): a clone that edits an object stream or a /Length stops reading what other clones cached

Clones share the object cache and the parsed object streams. #5802 keeps
results that read a clone's edits out of those shared maps, but a clone
could still read entries cached before its edit, by another clone or by
itself: after replacing object stream 4, a member of it still read as the
file's copy, and a stream or object stream framed by an indirect /Length
kept the file's framing after the length was edited.

Each cached load now records the other objects it read (a member's object
stream, an indirect /Length, and what those read in turn), and a document
whose edits include one of them reads the object again. Documents with no
edits read the shared entries as before. This also replaces forgetting a
stream's cached member failures on every edit, which the same check covers
for the editing document without costing other clones their entries.

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

* fix(cos): count a reference read as null in what a load read

A reference whose generation doesn't match the file reads as null, but an
edit that sets that generation makes it match. A load that read null there
(an object stream's /DecodeParms by reference, say) now counts the object
as read, so a clone that sets it decodes the stream again instead of
taking the one another clone cached. Reads keep a shared slice, and the
read bound has its own test.

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

* test(optimize): edit the object stream without breaking it

space_audit_shares_an_object_stream_..._edited_members set the stream's
/First to 999999 and still expected its members to read, which only held
while a stale shared-cache entry answered for them. Mark the stream with
an unrelated key instead: it is still edited, so its members are sized
on their own, and they stay readable.

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

* fix(cos): keep an object stream decoded by edited parameters out of the shared cache

Whether an object stream is the loading document's own was settled
before its indirect /DecodeParms were read. A clone that edited them
decoded the stream by its edit and put the result in the cache its
clones share, so the document it came from read the clone's data.
The check now runs after decoding.

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

---------

Co-authored-by: KenM76 <KenM76>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 08:56:34 +01:00
Michael Higgins de1fe40da5 fix(cos): cap the total entries a materialized xref-stream chain merges (#5082)
## What this changes

`read_xref_chain_materialized` in `crates/cos/src/document/reader.rs` keeps a running total of the rows it merges across the cross-reference chain. Past `MAX_XREF_OBJECTS` (8,388,607) it returns `CosError::Syntax` ("cross-reference chain lists more than 8388607 entries"). The open path then rebuilds the index from object headers and logs the repair, as it already does when a single section decodes past that cap. Before this, the per-section cap and the 10,000-section chain limit multiplied, so the merged map had no overall bound.

Each section adds `sect.len() + odd.len()`: its entries plus the rows it set aside, including those from a hybrid `/XRefStm`. Set-aside rows are kept for the whole chain, and a section can set aside nearly everything it lists. The count is of rows merged, not the map's size, because a chain that relists the same objects costs as much to merge. A section's rows never outnumber what the compact reader counts for it, and that reader stops at 2,000,000 rows. So no chain the compact reader accepts is refused here.

The limit itself lives in `read_xref_chain_materialized_with_limit` (`pub(super)`). The existing entry point, `read_xref_chain_materialized`, calls it with `MAX_XREF_OBJECTS`, so tests can reach the limit in a few rows. Both functions have doc comments.

Fixes #5082

## How you verified it

Three regression tests in `crates/cos/src/document/tests.rs`. The two small-limit tests were suggested by @Harrison-Booth in review.

1. `a_cross_reference_chain_past_the_total_cap_is_refused_and_the_file_reconstructed` runs end to end at the production cap. It builds a synthetic file:
   - a catalog, a page tree with one page, and a chain of three cross-reference streams;
   - the newest stream locates objects 0 to 6;
   - two older Flate-compressed streams each list 4,200,000 rows (`/W [1 0 0]`, disjoint `/Index` ranges). Each is half the per-section cap and too large for the compact reader. Their rows are in use at offset 0, so they are set aside, which keeps the test at a few bytes a row.

   It asserts that `Document::open` logs the cap with "rebuilding from object headers" and then "rebuilt cross-reference index ...". The rebuilt document must keep `/Root 1 0 R` and the `/Page`. `read_xref_chain_materialized` must return `CosError::Syntax` at the oldest stream's offset.
2. `the_chain_row_limit_counts_disjoint_and_relisted_rows` uses three classic tables of 2, 1 and 1 rows. A limit of 4 passes, and a limit of 3 refuses the chain at the oldest table. This holds when the sections list different objects, and when one relists an object a newer section has: that is a map of three entries but four rows merged.
3. `the_chain_row_limit_counts_hybrid_rows_set_aside` uses a hybrid file whose table puts object 3 at an offset while its `/XRefStm` puts it in an object stream. It merges six rows and sets object 3 aside. A limit of 7 passes, and a limit of 6 refuses it.

Seen failing, each by reverting part of the fix in the working tree:

| Revert | Test 1 | Test 2 | Test 3 |
|---|---|---|---|
| no chain check at all | fails (empty repair log) | fails | fails |
| count without `sect.len()` | passes | fails | fails |
| count without `odd.len()` (state before 780764cb) | fails | passes | fails |
| fix restored | passes | passes | passes |

All three run in about a second together.

Commands, all with `-j 2` and a separate `CARGO_TARGET_DIR`:
- `cargo fmt -p pdfcraft-cos --check`: clean
- `cargo clippy -p pdfcraft-cos -j 2 --all-targets --locked -- -D warnings`: clean
- `cargo test -p pdfcraft-cos -j 2 --locked`: 95 unit tests and 39 integration tests pass (2 ignored)

- [x] `cargo fmt --check`
- [x] clippy with `-D warnings` on the crates I changed
- [x] tests pass on the crates I changed
- [x] n/a — `cargo xtask layers` / `cargo xtask assets`: no crate boundary or asset touched

## Never-crash

- [x] No new `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!` outside tests (the offset conversion uses `unwrap_or(usize::MAX)`)
- [x] Checked arithmetic: `saturating_add` per section, `checked_add` for the running total; the change bounds an allocation sized by input
- [x] No `unsafe`
- [x] The unbounded allocation this fixes has a small synthetic regression test

## Provenance

- [x] n/a — no assets of any kind
- [x] n/a — no font files
- [x] No GPL/AGPL code copied in
- [x] Behaviour comes from ISO 32000 (§7.5.8 cross-reference streams, the Annex C object limit) and the crate's existing escalation path
- [x] n/a — no new assets, so no `ATTRIBUTION.toml` entry

## Security

- [x] n/a — no file paths or tool dispatch touched (`--root` confinement unaffected)
- [x] n/a — MCP and control channel untouched
- [x] No new outbound network access

## Parity and reach

- [x] n/a — hardening of the cross-reference reader, not a parity feature; no `parity/` entry changes
- [x] n/a — no new behaviour to expose; every `automation` tool that opens a document gets the cap
- [x] n/a — no documented behaviour changes

## Anything a reviewer should look at first

- The check runs after each section is read. So one section, already under the per-section cap, is decoded before the chain is refused. Peak memory stays bounded by the cap plus one section.
- The row limit is a parameter of a `pub(super)` function so tests can set it small. Production always passes `MAX_XREF_OBJECTS`, and the error text names whichever limit applied.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
  * Documents with cross-reference chains that exceed the supported total size now report a syntax error at the section where the limit is exceeded.
  * The limit applies across the entire chain, including repeated object numbers and set-aside entries, so individually valid sections cannot exceed the total cap.
  * When opening an affected document, the index can be rebuilt from object headers, allowing available catalog and page content to be recovered.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-11 07:31:46 +01:00
Dylan M. Taylor 9345b5caaa cos: drop the second decoded_strict_within that #6046 merged in (#6933)
main fails to compile (E0592): the merge of #6046 added Stream::decoded_strict_within next to the one already there. Both decode the whole chain bounded by max and map the error to CosError::Filter; the earlier one stays, with #6046's test.
2026-11-10 18:54:55 -05:00
Dylan M. Taylor ad3e6d4b85 fix(optimize): bound each unused-resource decode by the remaining shared budget (#6046) 2026-11-10 17:53:58 -05:00
BFlat 2b46f8bf8c Merge pull request #5698 from Piohrun/fix/jbig2-dictionary-any-route
fix(redact): a shared JBIG2 dictionary leaves with whatever used it, by any route
2026-11-10 14:24:09 +07:00
bflatastic f6fd408419 Merge remote-tracking branch 'origin/main' into merge/fonts-budget-6239
# Conflicts:
#	crates/edit/src/tests.rs
2026-11-10 10:09:08 +07:00
Grzegorz MartensandClaude Opus 5.5 8c5333bfe7 M7: font reads decode through a view of the stream, and refuse before resolving its parameters
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
2026-11-09 21:45:10 +01:00
eliets2andeliets2 a1ea2d3763 Align stream decode cap with renderer at 256 MiB (#6585)
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>
2026-11-09 18:54:53 +01:00
Grzegorz MartensandClaude Opus 5.5 8fb534ad4c M7: tests pin what a stage past its limit charges the allowance
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
2026-11-09 14:08:29 +01:00
Grzegorz MartensandClaude Opus 5.5 063f8629e4 M7: each filter stage of a font's stream is held to its limit, as without an allowance
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
2026-11-09 13:36:29 +01:00
Grzegorz MartensandClaude Opus 5.5 b9e83d8b67 Merge main again into fix/jbig2-dictionary-any-route
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
2026-11-09 13:05:41 +01:00
Grzegorz MartensandClaude Opus 5.5 77b9cfa310 Merge main into fix/jbig2-dictionary-any-route
Conflicts, and how they were resolved:

- cos writer.rs and lib.rs: main now numbers a full save in `full_save_order` and exports
  `reachable_objects`, the object numbers a full save writes, for the pattern guard. This
  branch had added the same walk as `walk` and `reachable`. Main's is kept, with its alias
  handling, and this branch's is dropped: `globals::kept` is now `reachable_objects`, so the
  JBIG2 cleanup and the pattern guard read one walk. `kept` returns the walk's error (a save
  that can't number its objects), which redaction and Remove Hidden Information pass on. The
  cos test of what is reachable, and two redaction tests, read `reachable_objects` instead.
  The exports keep this branch's `DECODE_PARMS_KEYS` and main's `TextChars`,
  `civil_from_days` and `new_file_id`.
- redact lib.rs: main unlinks pruned patterns from `/Pages` nodes after `unlink_doomed`; this
  branch replaced `restore_shared_jbig2` with `globals::unshare`. Both stay, the unlink first:
  an image a pruned pattern's cell drew leaves the file with it, and is a user of a dictionary
  that left too.
- redact sanitize.rs: main's `strip_outline_actions` and this branch's `unshare_unmarked` both
  stay. At the end of `remove_hidden`, `unshare_unmarked` runs after main's `unlink_undrawn`,
  so an image unlinked for being drawn nowhere takes a shared dictionary with it, and is
  followed by main's `require_full_save_with_new_id`.
- redact image.rs: `MAX_PIXELS` is `pub(crate)` on both sides; the comment names
  `globals::without_dictionary`, where this branch moved `without_globals`.

Tests: `an_undrawn_image_unlinked_takes_a_shared_jbig2_dictionary_too` (Remove Hidden
Information, through `unlink_undrawn`) and
`an_image_a_pruned_patterns_cell_drew_takes_a_shared_jbig2_dictionary_too` (a redaction,
through `unlink_pruned_patterns`) pin the order of the two new routes before `unshare`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 11:59:18 +01:00
mtlcl410 9e29af7849 fix(render): a stream ending in endstrea is not read into a later object's stream (hayro-syntax patch 36) (#6669) 2026-11-09 18:03:41 +08:00
bflatastic 8dfb3bdc5b Merge origin/main into merge/preflight-pdfa-checks
# Conflicts:
#	crates/engine/src/tests.rs
2026-11-09 09:01:00 +07:00
bflatastic 2992146abb Merge #6312: M1: a document that encrypts only its embedded files opens without a password (core.attachment-only-encryption)
# Conflicts:
#	crates/render/src/inspect/tests.rs
#	vendor/README.md
2026-11-09 08:12:28 +07:00
bflatastic e70cacf36f Merge #6290: fix(render): a stream with no endstream after it draws from its /Length instead of a blank page (hayro-syntax patch 36)
# Conflicts:
#	crates/render/src/raster/tests.rs
#	vendor/README.md
2026-11-09 08:11:43 +07:00
bflatastic 9e2c8130ad Merge #6561: M1: a file whose last update has no /Prev opens with all its pages and a repair note (core.repair-broken-files)
# Conflicts:
#	parity/acrobat-features.toml
#	vendor/README.md
#	vendor/hayro-syntax/src/pdf.rs
2026-11-09 08:11:01 +07:00
bflatastic 6be3850177 Merge #6214: M11: PDF/A verify checks the file identifier and Save as PDF/A adds one (optimize.pdfa-validate)
# Conflicts:
#	crates/automation/src/tools.rs
#	crates/cos/src/lib.rs
#	crates/preflight/src/lib.rs
#	crates/preflight/src/tests.rs
#	parity/acrobat-features.toml
2026-11-09 08:10:01 +07:00
bflatastic e7ecc84544 Merge #6506: fix(render): a fax image with more /Rows than its /Height is decoded to its height (hayro-syntax patch 41)
# Conflicts:
#	vendor/README.md
2026-11-09 08:05:40 +07:00
bflatastic 5c42721193 Merge #6501: fix(render): an image whose PNG predictor was declared but never applied draws its samples (hayro-syntax patch 40)
# Conflicts:
#	parity/acrobat-features.toml
#	vendor/README.md
2026-11-09 08:05:23 +07:00
quincy-jc d0b2a32744 perf(optimize): work out pictures on threads and skip needless page reads (#6230) 2026-11-09 08:53:27 +08:00
mtlcl410 0a029f0aad M12: floating fields show the values scripts give them, and the sheet number and count (#6160) (#6166) 2026-11-09 08:45:03 +08:00
jonathanwxh-cell 1f48670fce refactor(cos): consolidate duplicate civil_from_days onto pdfcraft_cos::civil_from_days (#6540)
Fixes #6466
2026-11-09 08:32:08 +08:00
mtlcl410andClaude Opus 5.5 b702338cc2 M1: the save test checks every reopened page's width in order (core.repair-broken-files)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-09 00:28:36 +08:00
mtlcl410andClaude Opus 5.5 64dd381bc4 M1: the unlinked-update tests check the objects the update wins with (core.malformed-input-safety)
The hybrid update test now checks that the update's page and catalog
win and that the repair log says why, and that the linked file keeps
both revisions; the save test checks that the reopened file keeps the
update's page and catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-09 00:04:59 +08:00
mtlcl410 fae489acdb Merge upstream/main into fix/update-without-prev-is-refused 2026-11-08 23:27:12 +08:00
mtlcl410andClaude Opus 5.5 90b3c41bc6 Merge upstream/main into fix/flate-predictor-untagged-rows
Conflict: crates/edit/src/rules.rs, where main joins the content streams with
crate::text::joined; that line replaces this branch's earlier build fix, so the file is main's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 23:17:19 +08:00
mtlcl410 7d1dea0517 M7: Ungroup draws Form artwork in place, so its text can be edited (#3067)
Refs #2964, #3005
2026-11-08 22:14:30 +08:00
mtlcl410andClaude Opus 5.5 d6ffacbd19 Merge upstream/main into fix/update-without-prev-is-refused
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 20:50:01 +08:00
mtlcl410 ca146cd23f Merge remote-tracking branch 'upstream/main' into lane1-6501 2026-11-08 20:31:22 +08:00
FredApps 6fa72b914c Merge pull request #4876 from vangel76/perf/write-full-retain-parsed-objects
perf(cos): write_full keeps the objects it parsed while numbering
2026-11-08 13:22:45 +01:00
FredApps efe75d3a30 Merge pull request #4874 from vangel76/perf/cos-reconstruct-4868
perf(cos): speed up byte searches during cross-reference reconstruction
2026-11-08 13:22:36 +01:00
mtlcl410 67ac32a2a1 Merge remote-tracking branch 'upstream/main' into lane1-6501
# Conflicts:
#	crates/filters/src/lib.rs
2026-11-08 20:21:16 +08:00
FredApps ed71db7c0f Merge pull request #4878 from vangel76/perf/cos-serialize-fmt-4870
perf(cos): write hex strings, integers and references without the formatter
2026-11-08 13:19:42 +01:00
FredApps 0a7798707b Merge pull request #5825 from Piohrun/fix/flate-input-work
fix(filters): inflate with zlib-rs everywhere, and charge the image allowance for what Flate reads
2026-11-08 13:11:25 +01:00
FredApps 8287ad0d06 Merge pull request #6289 from Piohrun/fix/partial-content-rewrite
M2.6: never rewrite a content stream from the part before its damage
2026-11-08 13:10:56 +01:00
mtlcl410andClaude Opus 5.5 d6750ee660 M11: ensure_id's doc says it keeps any two-element /ID array, even a malformed one (optimize.pdfa-validate)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 11:58:19 +08:00
vangel76andClaude Sonnet 5.5 f19ecc4635 Merge upstream/main; drop the object-stream pre-check from reconstruct
Main now reports unreadable objects from the parse of every object in
Document::reconstruct, so the pre-check that skipped that parse is gone.
The single-pass /XRef positions and the rarest-byte find stay.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 18:49:31 +01:00
vangel76 8fba397412 Merge remote-tracking branch 'upstream/main' into perf/cos-reconstruct-4868-w 2026-11-07 18:48:15 +01:00
mtlcl410andClaude Opus 5.5 4e522cd067 Merge upstream/main into fix/update-without-prev-is-refused
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 00:25:48 +08:00
vangel76andClaude Sonnet 5.5 a77cd9cb78 cos: reachable_objects keeps no parsed objects
full_save_order retained up to 64 MiB of parsed objects for the write pass, which
reachable_objects then dropped unused. It now asks for no retention.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 16:24:15 +01:00
vangel76andClaude Sonnet 5.5 8ac51f833b cos: charge the object-stream pre-scan to one budget per rebuild
Overlapping unterminated dictionaries could each be read up to 1 MiB. A shared
budget (the file length) is now taken off on every path; a spent budget keeps the
object a candidate, as before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 16:21:28 +01:00
vangel76andClaude Sonnet 5.5 c9947e76e9 Merge upstream/main into perf/write-full-retain-parsed-objects
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 16:13:40 +01:00
FredApps 33e3945c04 Merge main and preserve unreadable diagnostics with cached failures 2026-11-07 15:48:09 +01:00
mtlcl410andClaude Opus 5.5 7de85d520c M1: a hybrid update without /Prev is rebuilt by the renderer as by cos (core.repair-broken-files)
The renderer (hayro-syntax (44)) counted the sections its chain reached, and
a hybrid section's /XRefStm stream (ISO 32000-2 §7.5.8.4) counted as one, so
a hybrid update without /Prev was rebuilt by cos but not by the renderer. It
now asks what cos asks: whether the newest section names an earlier one
through /Prev. The /XRefStm stream belongs to the same update.

Tests: a synthetic hybrid fixture (pdfcraft-testkit), with and without
/Prev, in cos and render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 22:29:49 +08:00
mtlcl410andClaude Opus 5.5 6847b1ea7e M1: a file whose last update has no /Prev opens with all its pages and a repair note (core.repair-broken-files)
An update whose cross-reference section has no /Prev (ISO 32000-2 §7.5.6,
§7.5.8.2) cuts the earlier revisions, and the page tree with them, off the
chain. The signed PDFBOX-2720 and PDFBOX-2942 files have this shape: the
document model found no page tree and the renderer showed 2 blank pages
found by scanning for page dictionaries.

When the chain reaches only the newest section, the file holds another
startxref and the catalog's /Pages can't be read, both readers now rebuild
the table from the file's objects, as for a damaged file:

- cos: the existing reconstruction (newest in-file definition wins), a
  repair-log line saying why, and `Document::rebuilt_unlinked_update`.
- hayro-syntax (44): `root_xref` repairs the table before the page tree is
  read; `Pdf::rebuilt_unlinked_update` says so.
- render: an open warning, and the lazy structure reader accepts such a
  rebuilt file, so links, fields and bookmarks are read too.

A save rewrites the file in full (main's rule for reconstructed files).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 22:22:47 +08:00
Grzegorz MartensandClaude Opus 5.5 749a323342 M2.6: tests pin damage in an earlier filter stage, on another page, and in the saved file
- cos: a filter chain whose first stage stops at damage and whose
  second reads all it was given is damaged.
- optimize: a page damaged partway stops resampling for the whole
  document, so another page drawing the same image whole doesn't get
  it resampled.
- redact: after redacting content with /DecodeParms by reference, no
  stream in the saved file holds the text, read with its parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 91968610c3 M2.6: redaction reads content with its /DecodeParms given by reference
`DecodeBudget::decode_strict` read a stream's `/DecodeParms` only when
direct, so content behind a predictor whose parameters are an indirect
object decoded with the predictor's tag bytes left in. Redaction then found
no glyph under a mark, left the text in the file and reported the mark
applied. ISO 32000-2 lets those parameters be indirect.

`decode_strict` takes the document and reads through `decoding_view`, as
`decode_with_damage` does. Its other readers read the parameters too:
Remove Hidden Information's hidden text and layers, the audit's covered-text
check, and edit's backdrop scan.

The accessibility_mark_tag_artifact description says that content which is,
or may be, in a stream damaged partway is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 69e26167e4 M2.6: a page's content is read with its /DecodeParms given by reference
`decode_with_damage` and `decode_contents_with_damage` read a stream's
`/DecodeParms` only when direct, as `Stream::decoded` does, so content behind
a predictor whose parameters, or a number in them, are an indirect object
decoded with the predictor's tag bytes left in, and was taken as whole. Edit
then listed and wrote back the wrong bytes, and pruning dropped an image such
a form draws. ISO 32000-2 lets those values be indirect.

- `decode_with_damage` takes the document and reads the parameters through
  it, and so does `decode_contents_with_damage`, and `decode_contents` with
  it: printing, accessibility's reading of a page and a page copied as a form
  read them too. Code that calls `decode` on a stream itself doesn't yet.
- It reads through `Document::decoding_view`: `with_direct_parms`, but a copy
  of the stream holds only its `/Filter` and the parameters, its data shared,
  so a stream a page's `/Contents` lists again and again isn't copied whole
  for each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00