nigig-org/crates/apps/pdf/pdf-cos/fuzz/Cargo.toml
andodeki 6a18886185
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
feat(pdf): Type 3 fonts and streaming interpretation — Phase 2 complete
The last two items of NIGIG_PDF_FEATURE_PARITY_PLAN.md Phase 2. Design and
merge criteria in REVIEWS/adr/0014-pdf-type3-fonts-and-streaming.md.

TYPE 3 FONTS DREW NOTHING

A Type 3 font's glyphs are not outlines - they are content streams, listed
in /CharProcs and mapped to text space by /FontMatrix. Probing a document
with one:

  fonts on page: ["T3"]
    T3: subtype=Type3 base=Unknown
  -> are the glyph procedures reachable? no CharProcs field exists
  -> is /FontMatrix exposed?             no field exists

The font was detected and then nothing could be done with it. /CharProcs and
/FontMatrix appeared nowhere in the crate, so the procedures were unreachable
and the text was silently invisible - a page that renders, reports no error,
and is missing content.

New pdf-document/src/type3.rs parses /FontMatrix, /CharProcs, /Differences,
/Widths, /FontBBox and the font's own /Resources, and resolves a character
code to its glyph procedure's decoded bytes. /FontMatrix is applied as
written rather than assumed to be the common 0.001 scale - Type 3 fonts
routinely use other matrices, which is the point of the entry. A missing
/CharProcs entry is a typed error naming the glyph, not a blank.

A THIRD BUG, FOUND WHILE WIRING d0/d1

The interpreter parsed both operators and discarded them:

  PdfOp::Type3Width(_wx, _wy) => {}
  PdfOp::Type3BBox(_x1, _y1, _x2, _y2) => {}

They are how a Type 3 glyph declares its advance, so even a renderer that
could draw the glyphs would stack them all at one point. Wiring them to the
device exposed that `d1` takes SIX operands - wx wy llx lly urx ury - and the
parser read four, so the "bounding box" was really the advance and the
advance was lost entirely. Now `Type3BBox { wx, wy, bbox }`, reading all six.

STREAMING INTERPRETATION

parse_content_stream materialised every operator into a Vec before
interpreting any of them: peak memory proportional to the whole content
stream, on a stream walked once and discarded. Adds ContentStreamIter and
interpret_streaming, with parse_content_stream reimplemented on top of the
iterator so there is ONE tokeniser rather than two that can drift.

Equivalence is proven, not asserted: a test compares both paths across every
corpus fixture, and a streaming_interpreter fuzz target compares them over
arbitrary bytes, which is where a divergence would actually hide.

4 corpus fixtures, 13 acceptance tests, 9 unit tests. Mutation-checked:
reverting d0 to a no-op fails glyph_advances_reach_the_device.

Phase 2 is now complete; the plan is updated with an item-by-item audit.
Several entries were already done (inline images, Do, text state, shading);
the plan's "biggest gap" was xref streams, closed in ADR 0013.

TEST_TARGET=pdf 615 -> 637, TEST_TARGET=pdf-ui 660 -> 682.
rustfmt and clippy -D warnings clean.
2026-08-16 17:28:44 +00:00

105 lines
1.8 KiB
TOML

[package]
name = "nigig-pdf-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
# Not a workspace member: cargo-fuzz builds this standalone with a nightly
# toolchain, and it must not drag libfuzzer into the normal build graph.
[workspace]
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
nigig-pdf-cos = { path = ".." }
nigig-pdf-document = { path = "../../pdf-document" }
nigig-pdf-graphics = { path = "../../pdf-graphics" }
[profile.release]
debug = 1
[[bin]]
name = "parse_object"
path = "fuzz_targets/parse_object.rs"
test = false
doc = false
[[bin]]
name = "parse_xref"
path = "fuzz_targets/parse_xref.rs"
test = false
doc = false
[[bin]]
name = "parse_content_stream"
path = "fuzz_targets/parse_content_stream.rs"
test = false
doc = false
[[bin]]
name = "parse_document"
path = "fuzz_targets/parse_document.rs"
test = false
doc = false
[[bin]]
name = "decode_stream"
path = "fuzz_targets/decode_stream.rs"
test = false
doc = false
[[bin]]
name = "parse_revision_chain"
path = "fuzz_targets/parse_revision_chain.rs"
test = false
doc = false
[[bin]]
name = "decrypt"
path = "fuzz_targets/decrypt.rs"
test = false
doc = false
[[bin]]
name = "eval_function"
path = "fuzz_targets/eval_function.rs"
test = false
doc = false
[[bin]]
name = "parse_colorspace"
path = "fuzz_targets/parse_colorspace.rs"
test = false
doc = false
[[bin]]
name = "parse_ext_gstate"
path = "fuzz_targets/parse_ext_gstate.rs"
test = false
doc = false
[[bin]]
name = "parse_signature"
path = "fuzz_targets/parse_signature.rs"
test = false
doc = false
[[bin]]
name = "parse_content_dict"
path = "fuzz_targets/parse_content_dict.rs"
test = false
doc = false
[[bin]]
name = "parse_xref_stream"
path = "fuzz_targets/parse_xref_stream.rs"
test = false
doc = false
[[bin]]
name = "streaming_interpreter"
path = "fuzz_targets/streaming_interpreter.rs"
test = false
doc = false