mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-06 10:47:27 +00:00
* A rebuilt xref finds an object whose header names another generation (#5580) `poppler-85140-0.pdf` is 340 bytes with no cross-reference table, and the header of its one page reads `3 18446744073709551616 obj` while the page tree points at `3 0 R`. Before #427 the digits wrapped and 2^64 wrapped to exactly 0, so the header read as `3 0 obj` and the file opened by accident. Once an overflowing integer became a real, the object was unreachable and the file stopped opening at all, which `cargo xtask check` reports as a regression. The generation is not really the point. `3 99999 obj` with the same page tree is refused in exactly the same way on main -- the control for the new test fails on that row first, with "page 1 does not exist" -- so this is a mismatched generation in a rebuilt table, not an integer overflow. A table rebuilt by scanning the file has no revision history to tell two generations of an object apart, and a file that needs rebuilding references everything as `N 0 R` whatever its headers say. So, in the reconstruction path only: - a generation number that cannot be read is taken as 0, the value such a file's references use (`read_object_header`). A generation is at most 65,535 (ISO 32000-2 §7.5.4), so there is nothing to lose by it; - every scanned object is also filed under generation 0, with `or_insert` so a real `N 0 obj` is never displaced by the alias; - the object is read back behind the same lenient header, since reading it parses the header again and the strict identifier check rejected it (`read_repaired_body`, gated on `repaired`). Reading numbers anywhere else is unchanged, and a table read from the file's own xref keeps the exact identifier match. The scan already files objects found in object streams under generation 0, and cos's table parser already takes an unreadable generation as 0, so this follows what the code does elsewhere. The test covers generations 0, 99999 and 2^64 with no table and no `startxref`, and asserts the page is found at its own size. Its fixture is built in the test rather than taken from the corpus, so it holds without the pinned checkout. * The generation-0 stand-in is one key, not a second object Two consequences of filing a scanned object under generation 0 as well, both from review. `XRef::objects` walks the table's keys, so an object with a stand-in came out once per key: a document whose page tree cannot be read is recovered by looking for objects that are pages, and it reported the page twice. Enumeration now yields one object per location, after the sort it already does. Both keys stay for lookups; only this walk is deduplicated. And `or_insert` never replaces, so a later `3 1 obj` moved the exact `3 1` key to the later body while the `3 0` stand-in stayed on the earlier one -- `3 0 R` and `3 1 R` would then read different objects. The scan now tracks which generation-0 keys are stand-ins: a real `N 0 obj` takes the key and keeps it, and a later header for the same number replaces a stand-in. The tests check both through what comes out: page 2 must not exist for a single-page file recovered without a page tree, and with two `3 1 obj` headers the page comes back at the later body's 300x150 rather than the earlier 200x100. * M2: the repair scan keeps its place on a non-header, and an object stream's first member is enumerated (#5580) - read_object_header's lenient path works on a clone, so `42 3 0 obj` no longer costs the header after the stray number (CodeRabbit, minor). - XRef::objects deduplicates by location and object number: an object stream and its first member share (offset, 0) and are two objects (CodeRabbit, minor). - vendor/README.md: hayro-syntax patch (34) and its tests. Both new tests fail with their fix reverted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>