feat(signing): append a cross-reference stream when the document uses one - #224
Merged
Conversation
… one PDF 1.5 replaced the classic table of ISO 32000-1 7.5.4 with a stream object, and Word, print to PDF in Chrome and most modern generators emit that form. Reading it landed last release; signing refused, because appending a classic table to a document whose latest section is a stream produced a file poppler reported as carrying no signatures at all. XrefStreamWriter emits the section and RevisionWriter picks the form from what the document already uses, for the signature revision and for the DSS and archive timestamp revisions alike. Three choices worth naming: the stream indexes itself, since it is an ordinary object and the next revision can only find this one through it; no /Filter, because a revision indexes a handful of objects and zlib overhead would exceed what it compresses; and /Index carries runs rather than one range, since a revision touches the catalog low in the file and its new objects high in it. XrefSubsections computes those runs and now serves both forms. Uncovered a defect that was never about streams. findFirstPage scanned a fixed 400-byte window from each object offset, which in a compact document reaches the objects that follow: the catalog was reported as the first page because a /Type/Page two objects later fell inside its window, and the revision then wrote /AcroForm and /Annots both onto object 1, the second dropping the first. The window is bounded at endobj now. It was latent in any document whose objects sit close together, and only a 434-byte fixture was small enough to expose it. Verified in poppler, not only in the suite: signed once, signed twice and B-LTA all read as valid, and the B-LTA output is indistinguishable from samples/pades-b-lta.pdf. samples/xref-stream.pdf is the committed artefact. The classic-table samples were regenerated and compared: same structure, so they are left as they are. Mutation over the two new classes is 100%, and the Signing namespace holds at 67.30% against measurements of 66.02% and 67.50%. The pre-commit hook now runs PHPStan after Pint, on the paths its own configuration names rather than on the staged ones: a change in one file breaks types in another, and analysing only what was touched would report a clean commit that fails in CI.
The floor is 8.4, which allows new Reader()->parse($der), so wrapping the constructor was the pre-8.4 workaround and PhpStorm reports it as a removable wrapper. 38 occurrences across one source file and six test files. Pint has no fixer for this, so CLAUDE.md records it as a review point rather than a gate, and notes the case that is not a chain: new self(new Encrypter(...)) is already the plain form and keeps its parentheses because they belong to the outer call.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the writing half of 0009. PDF 1.5 replaced the classic table of ISO 32000-1 §7.5.4 with a stream object, and Word, "print to PDF" in Chrome and most modern generators emit that form. Reading it landed in #223; signing refused, because appending a classic table to a document whose latest section is a stream produced a file poppler reported as carrying no signatures at all.
What changed
XrefStreamWriteremits the section andRevisionWriter::crossReference()picks the form from what the document already uses, for the signature revision and for the DSS and archive timestamp revisions alike.Three choices worth naming, each with an alternative:
/Filter. A revision indexes a handful of objects, so its table is tens of bytes and zlib's header and checksum would make the stream larger than what they compress. It also removes a failure path: no compression call that can fail./Indexcarries runs, not one range. A revision touches the catalog and a page low in the file and writes its new objects high in it.XrefSubsectionscomputes the runs and now serves both forms, since the classic table'sfirst countheader says the same thing.The defect this uncovered, which was not about streams
The first signed output still read as unsigned, for an unrelated reason.
DocumentReader::findFirstPage()scanned a fixed 400-byte window from each object's offset, and in a compact document that window reaches the objects that follow. The catalog was reported as the first page because a/Type/Pagetwo objects later fell inside its window. The revision then wrote/AcroFormonto object 1 and/Annotsonto object 1, the second silently dropping the first, so the document had a signature dictionary and no form to reach it from.The window is bounded at
endobjnow. This was latent in any document whose objects sit close together; only a 434-byte fixture was small enough to expose it.Verified in poppler, not only in the suite
pdfsigTimestamp2covering the whole fileThe B-LTA output is indistinguishable from
samples/pades-b-lta.pdf, down to poppler's Unimplemented Feature (0) on the DocTimeStamp, which the classic-table sample has always reported too.samples/xref-stream.pdfis the committed artefact, signed twice. The classic-table samples were regenerated and compared: same structure, so they are left as they are.Mutation over the two new classes is 100%. The Signing namespace holds at 67.30%, against measurements of 66.02% and 67.50% and a floor of 62.
Also here
neware gone, 38 of them. Pint has no fixer for this, soCLAUDE.mdrecords it as a review point rather than a gate.🤖 Generated with Claude Code