Read the native container beyond its preview: the chain of allocation tables (#FAT, #FT2, #FT3, #FT4) with newest-revision selection, CRC-checked extraction of stored, zlib and zstd entries with their byte predictors, and the self-describing tagged object stream in doc.dat as a generic tree of shared, inline and positional objects. Every declared size is checked against per-entry and per-import budgets before allocation; the zstd window is capped at 64 MiB; table chains are cycle-checked; arrays are checked against the remaining bytes; object nesting stops at 384 levels. The header now accepts container versions 8 to 12 and the flag values 4 and 8 that real Affinity 3 files carry (the previous check rejected the public CC0 Affinity 3 documents); the thumbnail record is the same in all those versions. Checked against 176 public documents saved by Affinity 1.x to 3.x: every doc.dat decompresses with a matching CRC and parses with no bytes left over. A test-only `synth` feature builds synthetic containers for unit tests and fuzz seeds; it is not an Affinity exporter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
vectorcraft-affinity
An independent Rust parser for a narrow part of the Affinity container: the indexed PNG
preview observed in files saved by Affinity 3.3.0.4850 on macOS (container version 12).
It uses the MIT OR Apache-2.0 crc32fast crate already present in the workspace lockfile,
has no application-crate dependencies, and does not decompress or interpret the document graph.
This is preview import, not native editable Affinity support. The preview can be smaller
than the document: a synthetic 1600 × 1200 document saved a 1024 × 768 preview. Files without
the verified preview record fail with an actionable error. The parser makes no claim about
every .af file, Windows saves, older .afdesign/.afphoto files or native export.
Provenance
The container header was researched against VMDevCpp/afread,
MIT licensed, at 04b672334a43e3e37ded6b5ffc57af231d589774. That reader supports container
versions 7–11; it does not establish version-12 compatibility. This Rust implementation was
written independently, with the version-12 preview record observed through synthetic documents
created and saved by the public Affinity scripting SDK. No proprietary application source or
GPL/AGPL reader code was used.
Accepted record
All container integers are little-endian. PNG integers are big-endian.
| File offset | Field checked |
|---|---|
| 0 | Magic 00 ff 4b 41 |
| 4 | u16 version (inspection accepts 7–12; previews require 12) |
| 6 | Zero flags |
| 8 | Document class bytes nsrP (add-ons share the magic) |
| 12 | #Inf |
| 24 | u64 thumbnail record offset |
| 64 | Prot for versions after 7 |
At the thumbnail offset, the accepted record is ff ff ff ff Thmb, followed by u32 record
version 1, record length PNG length + 13, payload offset 29, zero, PNG length, encoding byte 1,
then exactly that many PNG bytes. Unknown versions, flags, classes and record layouts fail.
The parser follows only this offset; it never scans for PNG signatures in other resources or
saved revisions.
Encoded previews are capped at 16 MiB and dimensions at 4096 pixels per side. Every offset,
length and PNG chunk boundary is checked, every chunk CRC is validated (including IEND and
ancillary chunks after IDAT), and the PNG must end exactly at IEND. The consuming
engine additionally decodes the complete image pixels with a 128 MiB allocation
limit before embedding it. It warns about omitted native content and drops the source save path.
Compressed text/profile chunks (zTXt, iTXt, iCCP) and APNG chunks (acTL, fcTL, fdAT)
are rejected: the verified native previews have none, and pixel budgets alone do not bound
aggregate inflated metadata. IHDR is first and unique, IDAT is present and contiguous, PLTE is unique and precedes image data, unknown critical chunks fail, and IEND is last. Supporting additional layouts needs separate bounded decoding and oracles.
Tests and next steps
The leaf tests use synthetic envelopes and exercise truncations, hostile fields and random
mutations. Engine tests exercise Open, placement rejection, warnings, raster validation and native save/reopen;
engine/tests/import_fuzz.rs exercises import followed by render/export.
A standalone cargo-fuzz target exercises both parser entry points:
cd crates/affinity
cargo +nightly fuzz run container -- -max_len=20971520 -max_total_time=60
Seed it with independently generated synthetic envelopes and metadata-reviewed native files; keep local corpus/artifact files uncommitted. A short smoke campaign is not prolonged fuzzing.
Templates and document-derived style/swatch libraries reject Affinity instead of discarding the import warning. Placement is explicitly rejected by signature, even under another extension, in both direct and queued Place commands; their UI does not display successful import warnings. Export SVG/PDF or full-resolution PNG from Affinity before placing.
The ignored native_affinity_oracle engine test accepts an AFFINITY_ORACLE_DIR containing
synthetic-rectangle.af/.png and synthetic-large.af/.png, generated by Affinity. Native
oracles must be published through the shared corpus with a generator, verified metadata,
SHA-256 manifest and pinned commit before becoming a repeatable CI gate. No binary fixtures
belong in this repository.
Full editable import needs a separately bounded FAT/revision reader that resolves the current
doc.dat revision chain, zstd/deflate streams with their predictors and checksums, and a typed-tag
intermediate representation with object-reference, recursion and allocation bounds. Document
mapping then needs geometry, units, transforms, pages/artboards, groups, curves/shapes, fills,
gradients, strokes, clipping, embedded raster blobs, colour management, text/fonts with explicit
fallbacks, effects and unsupported-feature warnings. Unknown data needs an explicit preservation
or loss policy. Each supported version and feature needs independently generated native oracles.
Native export and round-trip fidelity are separate work.