Skip to content

Drop the source /XRefStm from a written trailer - #726

Open
hyunhee-jo wants to merge 1 commit into
veraPDF:integrationfrom
hyunhee-jo:fix/drop-stale-xrefstm-from-written-trailer
Open

Drop the source /XRefStm from a written trailer#726
hyunhee-jo wants to merge 1 commit into
veraPDF:integrationfrom
hyunhee-jo:fix/drop-stale-xrefstm-from-written-trailer

Conversation

@hyunhee-jo

@hyunhee-jo hyunhee-jo commented Sep 1, 2026

Copy link
Copy Markdown

Problem

Writer.setTrailer(COSTrailer, long) copies the source trailer dictionary into the output and then adjusts only the keys it manages: it sets /Prev itself and removes /ID on a full rewrite. /XRefStm is not handled. Since the writer only ever emits a classic cross-reference table, an inherited /XRefStm advertises a cross-reference stream for a section the writer did not write.

On a full rewrite the offset lands on arbitrary bytes, so a file veraPDF has just written cannot be reopened:

PDDocument doc = new PDDocument("hybrid.pdf");
doc.saveAs("out.pdf");
// out.pdf trailer: <</XRefStm 589 /Root 2 0 R /Size 7 >>
new PDDocument("out.pdf");   // IOException: can not locate xref table

Hybrid-reference files (ISO 32000-1, 7.5.8.4) are what word processors commonly produce — the last classic section is empty (xref 0 0) and its trailer delegates through /XRefStm — so any such document is affected. qpdf 12.3.2 reports such an output as file is damaged with xref not found at exactly that offset; tolerant readers still reconstruct it, strict ones do not.

Fix

One unconditional removeKey(ASAtom.XREF_STM) in setTrailer. /Prev and /XRefStm are the only byte-offset keys a trailer carries, and /Prev is already managed here, so this fills in the missing one. The removal happens while the source trailer is copied, so a deliberate setXRefStm after setTrailer would still take effect.

The removal is not conditional on prev, unlike the /ID removal above it, because the reason differs: /ID carries document identity, which an incremental update has to preserve, while /XRefStm describes the section being written, and that section is a classic table on both paths.

To be precise about what is demonstrated: on the incremental path I could not observe a reader failure. With the stale key present, both veraPDF and qpdf still resolve incrementally changed objects correctly, so the appended section merely advertises the previous revision's stream rather than corrupting anything. That resolution depends on how a reader orders a section's table against the stream the trailer points at, which the specification describes for a stream that belongs to that section — not for an inherited one.

Verification

  • New WriterTest, 3 tests. Reverting the one-line change fails 2 of them (can not locate xref table on the rewrite, stale offset on the incremental update); the fixture guard still passes.
  • mvn verify green on JDK 25 and 21: 378 tests, 0 failures.
  • Two real third-party hybrid documents, both save paths: the only byte difference against the unpatched build is the removed key. qpdf warnings go 3 -> 0 on the rewrite, object and page counts are preserved, page content streams are byte-identical, and the incremental output still preserves the original bytes as a prefix.
  • Documents without /XRefStm produce byte-identical output.
  • Nothing is lost: in these documents 652 of 683 objects live in object streams, and the rewritten classic table lists all of them individually. The read path that follows an input's /XRefStm is untouched.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed PDF rewriting to prevent invalid cross-reference stream references from being retained in classic cross-reference tables.
    • Full and incremental rewrites now produce valid trailers without unintended /XRefStm entries.
    • Preserved document page counts during hybrid-reference PDF processing.
  • Tests

    • Added coverage for full and incremental rewriting of hybrid-reference PDF files.

Writer copies the source trailer dictionary wholesale, managing /Prev
itself and dropping /ID on a full rewrite, but it never looked at
/XRefStm. Since writeXRefInfo only ever emits a classic cross-reference
table, an inherited /XRefStm advertises a cross-reference stream for a
section the writer did not write: on a full rewrite the offset lands on
arbitrary bytes, so the file veraPDF just wrote cannot be reopened
("can not locate xref table"); on an incremental update it advertises
the previous revision's stream for the appended section.

Hybrid-reference files (ISO 32000-1, 7.5.8.4) are what word processors
commonly produce, so any such document was affected.

Output for documents without /XRefStm is byte for byte unchanged.
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: b8f60761-f287-4aa2-ba69-827534ac3903

📥 Commits

Reviewing files that changed from the base of the PR and between 815f8cc and 7b14870.

📒 Files selected for processing (2)
  • src/main/java/org/verapdf/cos/visitor/Writer.java
  • src/test/java/org/verapdf/cos/visitor/WriterTest.java

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Writer.setTrailer now removes /XRefStm before writing a classic cross-reference table. WriterTest adds hybrid-reference fixtures and verifies full and incremental rewrites.

Changes

Hybrid reference trailer handling

Layer / File(s) Summary
Remove inherited /XRefStm and validate rewrites
src/main/java/org/verapdf/cos/visitor/Writer.java, src/test/java/org/verapdf/cos/visitor/WriterTest.java
Writer.setTrailer removes /XRefStm. Tests cover hybrid-reference fixture validity, full rewrites, incremental rewrites, and page-count preservation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 7b148

The PR prevents written PDF trailers from advertising an unrelated cross-reference stream while preserving existing save behavior and document contents. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: removing the source /XRefStm entry from written trailers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant