Files
vectorcraft/crates/affinity
callmeBallochandClaude Opus 5.5 1c47480a09 AF.2: bounded Affinity archive and object-stream reader
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
2026-10-08 17:09:56 +00:00
..

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.