Files
pdfcraft/crates/cos/README.md
2fa110e185 M1: repair exhausted xref hints without object-number overflow (#1337) (#1359)
* M7: share Windows local-link fixture portability fixes (#1265)

Use the identical test-only prerequisite already proposed in #1263: Unix-only mutable vectors, ordinary Windows drive paths and escaped PDF literal strings. This is shared CI support for the separate xref overflow fix; production link handling is unchanged.

Generated with Codex.

* M1: return an error for exhausted xref object numbers (#1337)

Check the next-object addition and return CosError at the xref offset rather than overflowing or wrapping to object zero. Classic-table and stream regressions reproduced the original panic; ordinary, zero-offset and repaired-range controls preserve catalog access and allocation.

Validation: 2732 passed, 0 failed, 13 ignored; strict all-target Clippy, fmt, 31 WASM crates, layering, assets, deny and parity passed. The rebuilt CLI opened and rendered a clean synthetic page.

Generated with Codex.

* M1: place overflow notes beside stable milestone entries (#1337)

Keep the tested code unchanged while avoiding progress-note conflicts with newly landed main updates. Formatting and diff checks passed.

Generated with Codex.

* Merge main: adapt exhausted-xref repair to the compact xref index

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>
2026-10-15 03:02:16 -04:00

5.2 KiB
Raw Permalink Blame History

pdfcraft-cos

The PDF object layer (ISO 32000-2 §7): the COS object graph, parsed lazily and tolerantly, edited through a copy-on-write overlay, and written back incrementally or as a full rewrite.

  • Layer: L1, standalone. The layering table lets it use only pdfcraft-filters, pdfcraft-crypt and pdfcraft-geom (xtask/src/layers.rs::STANDALONE_DEPS); it currently uses the first two. That keeps the parser and the crypto independently auditable and reusable.
  • #![forbid(unsafe_code)], and the crate denies unwrap/expect/panic!/todo!. Builds for wasm32-unknown-unknown.

The object graph is the document model. Nothing is lifted into a tidier representation and written back out, because that is how data gets silently dropped. Typed views live above, in pdfcraft-model.

API

let doc = Document::open(bytes)?;                               // Arc<Vec<u8>>
let doc = Document::open_with_password(bytes, Some("owner"))?;  // encrypted files

let root = doc.root();                               // Option<ObjRef> — the catalog
let obj  = doc.get(ObjRef { num: 12, gen: 0 });      // load (and cache): Arc<Object>
let real = doc.resolve(&obj);                        // follow one indirect reference
let d    = doc.dict(&obj);                           // Option<Dict>, resolving first
let data = stream.decoded()?;                        // filters applied on demand

doc.set(r, Object::Dict(d));                         // edit: lands in the overlay
let r = doc.add(obj);                                // append a new object
doc.free(r);

let bytes = write_incremental(&doc, &SaveOptions::default())?;  // append a revision
let bytes = write_full(&doc, &SaveOptions::default())?;         // garbage-collected rewrite

Both writers return the bytes; the caller decides where they go, which is what lets the engine produce a working file in memory after every edit and keep saving atomic.

Useful alongside: doc.repair_log() (what was reconstructed), doc.revisions() and doc.revision_ends() (the incremental history), doc.is_edited(num) and doc.modified_objects(), doc.permissions() and doc.security(), doc.set_encryption() / doc.remove_encryption(), and doc.require_full_save() for edits that cannot be expressed incrementally.

Document is cheap to clone: the original bytes, the xref index and the parse caches are shared, so a clone is a snapshot. Undo/redo and background jobs hold clones while the UI edits another. Edits never touch the original bytes.

Reading

  • Every cross-reference form: classic tables with their quirky subsections, cross-reference streams, hybrid-reference files (/XRefStm), and object streams.
  • Repair. A file whose xref is wrong or missing is reconstructed by scanning for obj headers. Repairs are recorded, never silent, so the UI can say the file was damaged.
  • Tolerant, never fatal. The lexer works on a byte slice and a position; it allocates nothing for skipped data, bounds-checks every index, and caps nesting depth. Malformed input yields CosError, never a panic — this crate is the first thing a hostile file meets. Exhausted xref entries are skipped with a repair log; exhausted /Size hints allocate unused numbers without overflowing or replacing existing objects.
  • Lazy. Objects are parsed on first use and cached. (Whole-file buffering is still the case; ByteSource streaming is core.lazy-loading, not yet done.)
  • Encryption is handled on load and save through pdfcraft-crypt: every standard-security revision R2–R6 (RC4, AES-128/256), crypt filters, /EFF embedded-file encryption, and /EncryptMetadata. security.rs knows what the spec exempts — the /Encrypt dictionary, xref streams, the trailer /ID, and objects inside an object stream (the stream is encrypted whole).

Writing

write_incremental appends only the edited objects, a cross-reference section in the same style as the file's last one, and a trailer with /Prev. The original bytes are never modified, which is what keeps a signature over an earlier revision valid.

write_full writes only what the trailer can reach, renumbered densely — the Save As and garbage-collecting path. By default (SaveOptions::object_streams) it packs non-stream objects into compressed object streams with a cross-reference stream (PDF 1.5+); turning that off writes a classic table any reader accepts.

Dictionaries keep their key order and streams keep their encoded bytes, so rewritten objects stay recognisable in a diff and byte-stable when re-serialized.

Errors

CosError: NotPdf, Syntax { offset, detail }, MissingObject, NotADictionary, Filter, NeedsPassword, WrongPassword, Security, Poisoned.

Not done here

  • Streaming very large files within a memory budget (ByteSource, core.lazy-loading); files are read whole today.
  • Linearized output, Fast Web View (core.linearization, M11).
  • Structural validation against the Arlington model (core.arlington-validation), which will live in the reserved arlington crate.
  • Image codecs. pdfcraft-filters carries the non-image filters; DCT, JPX, JBIG2 and CCITT are still decoded through the bootstrap renderer (core.image-filters, M2.6).