Added
- (aiff) fold oxideav-aiff into oxideav-iff::aiff
Other
- add SSND (Sound Data) chunk writer — §5.0 block-aligning body encoder
- frame_chunk framing helper + write_fver_chunk (FVER) writer
- ANIM op-1 (XOR ILBM mode) full-frame decode + encode
- COMM chunk writer + public 80-bit extended sample-rate encoder (§2.1/§3.2)
- ANIM op-4 (Generalized short/long Delta) decode + encode
- op-2/op-3 Long/Short Delta mode decode + encode (spec §1.2.2-§1.2.3, §2.2.1)
- EA IFF 85 §5 LIST/CAT group-children walker
- surface chunk::ReservedId §3 reserved-ckID classifier
- surface EA IFF 85 §3 universally-reserved ckID classifier
- generic top-level group probe primitive (FORM/LIST/CAT envelope)
- typed PCHG header surface + derived-hint consistency check
- typed Sham row-palette accessors mirroring Pchg::palette_at_line
- drop release-plz.toml — use release-plz defaults across the workspace
- cargo-fuzz harness — aiff_decode + anim_decode + pchg_parse
- §14 chunk-precedence surface (ChunkClass + Form helpers)
- structured SPRT (sprite-precedence) chunk surfacing
- structured DEST (destination-merge) chunk surfacing
- structured SAXL (Sound Accelerator) chunk surfacing
- structured §13.0 text chunks (NAME / AUTH / (c) / ANNO)
- structured MIDI (MIDI Data) chunk surfacing
- ANIM op-7 encoder + AIFF COMT/AESD/APPL surfacing + MARK/INST write
- structured INST (Instrument) chunk parsing
- structured MARK (Marker) chunk parsing
Added
-
AIFF/AIFF-C
SSND(Sound Data) chunk writer
(docs/audio/aiff/aiff-c.txt§5.0).aiff::write_sound_data(&SoundData)
emits the §5.0SoundDataChunkdata portion —offset(u32) +
blockSize(u32) +offsetbytes of block-alignment padding +
soundData— completing the AIFF round-trip story:SSNDwas the
one read-path chunk class (Form::sound) without a body writer. Per
§5.0 "offset determines where the first sample frame in the soundData
starts", a non-zerooffsetinserts that many zero alignment bytes
before the samples (the §5.0 "Block-Aligning Sound Data" mechanism),
so the result round-trips through theSSNDreader, whosesamples
slice begins at byte8 + offset. The common case
(§5.0 "Applications that don't care about block alignment should set
blockSize and offset to zero") emits eight zero header bytes followed
by the raw samples. Like every otheraiff::write_*helper it emits a
chunk body; pair it withaiff::frame_chunk(b"SSND", body)for the
full header + odd-length pad. Covered by 4 new unit tests in
src/aiff/form.rs(zero-offset layout, full-FORMparseround-trip,
non-zero-offset alignment-gap round-trip, and aframe_chunk→
ChunkIterframing round-trip). -
AIFF/AIFF-C
frame_chunkframing helper +write_fver_chunk
(FVER) writer (docs/audio/aiff/aiff-aifc-format.md§1 / §3.1).
Every per-chunkaiff::write_*helper emits a chunk body and
leaves the 8-byteckID + ckSizeheader and the odd-length pad byte
to the caller.aiff::frame_chunk(id, body)factors that into one
place — the exact inverse ofaiff::ChunkIter: it prepends the
4-byteckID+ big-endianint32ckSizeheader and appends a
single0x00pad byte iff the body length is odd (the §1 16-bit
alignment rule; the pad is not counted inckSize), returning
AiffError::OversizedChunkwhen the body exceedsu32::MAX. This
closes the last write-side gap:FVERwas the one read-path chunk
class (Form::fver_timestamp) without a body writer.
aiff::write_fver_chunk(timestamp)emits the 4-byte big-endian
timestamp, andaiff::AIFC_VERSION_1(0xA280_5140) is the §3.1
AIFF-C v1 spec timestamp every AIFC file carries. Covered by 6 new
unit tests insrc/aiff/chunk.rs(even/odd/empty framing, pad
transparency through aframe_chunk→ChunkIterround-trip, and an
FVERframed round-trip). -
ANIM op-1 (XOR ILBM mode) decode + encode for the full-frame
case (docs/image/iff/anim.txt§1.2.1 / §1.3 / §2.1). op-1 is the
original ANIM compression method: the encoder XORs every byte of the
new frame against the previous frame's planar bitmap, producing a
bitmap that is0where the frames agreed, and stores it
run-length-encoded (anim::encode_op1_body/anim::encode_anim_op1,
honouringBMHD.compressionfor ByteRun1 or uncompressed BODY). The
decoder (apply_op1, wired intoparse_animviaANHD.operation = 1) expands the BODY and XORs it into the running planar state — a
zero byte in the XOR bitmap leaves the running state unchanged per
§1.3. The §2.1 "XOR mode only"mask/w/h/x/yANHD
fields narrow the BODY to a plane subset / sub-rectangle "to
eliminate unnecessary un-changed data"; the staged spec gives no wire
description of that partial-BODY layout, so a plane-masked or
partial-rectangle ANHD is rejected withError::unsupportedand the
full-frame case (all planes, whole bitmap) is decoded. Covered by
tests/anim_op1.rs(8 tests: ByteRun1 + uncompressed round-trips,
sparse / 2-plane / multi-frame sequences, all-plane-mask tagging,
no-op-XOR on identical planar buffers, partial-rectangle rejection). -
AIFF / AIFF-C
COMMchunk writer + 80-bit extended sample-rate
encoder (docs/audio/aiff/aiff-aifc-format.md§2.1, §3.2). Adds
aiff::write_common_chunk, the round-trip inverse of
parse_common: it emits the fixed 18-byte AIFF body
(numChannels/numSampleFrames/sampleSize/ 10-byte
sampleRate) and, when theCommonChunkcarries a
compression_type, the AIFF-C extension — the 4-byte
compressionTypeFourCC followed by thecompressionNamePascal
string padded so its total length (length byte + chars) is even per
§3.2. ANonecompression name collapses to the canonical
zero-length pstring. The writer follows the body-only convention of
the otherwrite_*_chunkfunctions (the FORM header / whole-chunk
pad byte are the muxer's job). Backing this, the 80-bit IEEE-754
extended encoder previously available only to the test suite was
promoted to the publicaiff::encode_extended(the exact inverse
ofdecode_extendedfor finite normalised values) plus
aiff::encode_sample_rate, the validating wrapper that rejects
NaN / infinite / non-positive rates withInvalidSampleRateso a
writer can never emit a COMM the parser would refuse. With this the
requiredCOMMchunk joins the existing optional-chunk writers, so
every AIFF chunk class except the top-level FORM/SSND muxer now has
symmetric read + write paths. -
ANIM op-4 (Generalized short/long Delta mode) decode + encode
(spec §1.2.4, wire format §2.2.2). Implemented from the §2.2.2
SetDLTAshortreference routine, the only normative description of
the op-4 wire format. The DLTA opens with 16 big-endian u32
pointers — 8 data-list pointers then 8 op-list pointers — and these
pointers (plus the per-op column offsets) are measured in 16-bit
words, not bytes, because the reference routine performsWORD*
pointer arithmetic (data = deltaword + deltadata[i],dest = planeptr + *ptr); this is the key behavioural difference from the
byte-offset ops 5 / 7. Each plane's op list is a flat run of
(offset, size)pairs terminated by0xFFFF:offsetis the
absolute word position where the run begins (non-cumulative),
size > 0copiessizedata words one-per-row (Uniq) and
size < 0copies one data word to|size|rows (Same), with the
dest steppingnw = row_bytes / word_sizewords per row down each
vertical column.ANHD.bitsselects the variant — bit 0
short/long data, bit 2 separate-vs-shared info list (both
supported), bit 5 short/long op offsets — while the XOR (bit 1) and
horizontal (bit 4 clear) variants and any reserved high bit are
rejected withError::Unsupportedsince the spec gives them no
separate wire format (§2.1 directs players to verify undefined bits
are zero).parse_anim/ theiff_animdemuxer now accept
ANHD.operation = 4; the write-side surface is
[anim::encode_anim_op4] (container-level) plus the lower-level
[anim::encode_op4_body], both emitting the short/long-data,
vertical, RLC, separate-info, non-XOR configuration the reference
routine reads. Covered bytests/anim_op4.rs(10 tests:
hand-built Same / Uniq decode in short + long data modes, word-unit
pointer semantics, shared-info-list, unsupported-variant rejection,
and encode→decode + full-container round-trips). -
ANIM op-2 / op-3 (Long / Short Delta mode) decode + encode
(spec §1.2.2 / §1.2.3, wire format §2.2.1). The DLTA opens with 8
big-endian u32 plane pointers (0= plane unchanged; the §2.2.1
worked value for the first list is 32); each plane's payload is a
list of groups whose offsets and counts are big-endian shorts and
whose data words are longs (op 2) or shorts (op 3). A word cursor
starts at the plane's first word; a positive offset advances the
cursor and places one data word, a negative offset (absolute value
= offset + 2) advances the cursor and a count short introduces that
many contiguous data words, and0xFFFFterminates the plane's
list. The bitplane is addressed as the contiguousheight × row_bytesbyte array it occupies in memory, so op-2 long words may
straddle row boundaries — the decoder gathers each plane into a
contiguous buffer, applies the groups, and scatters the rows back.
parse_anim/ theiff_animdemuxer now acceptANHD.operation
2 and 3; the new write-side surface is [anim::encode_anim_op2] /
[anim::encode_anim_op3] (container-level, mirroring the op-5 /
op-7 encoders) plus the lower-level [anim::encode_op23_body].
The encoder collapses runs of ≥ 2 changed words into a
negative-offset group per §1.2.2 ¶ "Strings of 2 or more long-words
in a row which change can be run together", emits single-word
groups otherwise, and bridges offsets wider than a positive short
by rewriting an unchanged word in place. After a run group the
cursor convention is "last written word" (the spec prose tracks
the pointer at the position the data word "would be placed at" and
never says it advances past a write); the in-tree encoder and
decoder share that reading. 14 new integration tests in
tests/anim_op23.rs— hand-crafted DLTA byte vectors pinning the
group grammar (single words, runs, terminator, zero pointers,
straddling long words, truncation/overrun rejection) plus
encode →parse_animround-trips for both modes. -
EA IFF 85 §5 LIST/CAT group-children walker
([chunk::GroupChild] + [chunk::parse_group_children] +
[chunk::prop_for_form_type]). Appendix A's productions close the
child grammar of the two outer group kinds —LIST ::= "LIST" #{ ContentsType PROP* (FORM | LIST | CAT)* }andCAT ::= "CAT " #{ ContentsType (FORM | LIST | CAT)* }— so a generic walker can
decode every LIST/CAT child without per-form knowledge. The new
[chunk::parse_group_children] takes a [chunk::GroupKind] plus
the group's payload after its ContentsType (the caller bounds the
slice by the declared ckSize per §5 Group CAT ¶ "programs must
respect it's ckSize as a virtual end-of-file for reading the nested
objects") and returns typed [chunk::GroupChild] entries —Prop { form_type, body }for §5 shared-property sets,Group { kind, inner_type, body }for nested FORM/LIST/CAT — enforcing every
structural rule §5 states: PROPs only in LISTs (¶ "PROP chunks may
appear in LISTs (not in FORMs or CATs)" / Rules for Writer Programs
¶ "PROPs may only appear inside LISTs"), PROPs before any nested
group (¶ "all the PROPs must appear before any of the FORMs or
nested LISTs and CATs"), at most one PROP per FORM type (¶ "A LIST
may have at most one PROP of a FORM type"). §3 FILLER children are
walked past without being surfaced ("chunks that fill space but
have no meaningful contents"); reserved-future-version IDs and bare
data ckIDs are rejected since the grammar admits no other child.
GroupKind::Formis refused outright — §4's production admits
LocalChunkchildren whose IDs are form-type-specific, so FORM
bodies stay with the per-form walkers. [chunk::prop_for_form_type]
is the §5 ¶ "Here are the shared properties for FORM type
<FormType>" lookup joining a FORM type against the parsed child
list. Nine new unit tests insrc/chunk.rs(worked-example LIST
decode, PROP-after-group rejection, duplicate-FormType rejection,
PROP-in-CAT rejection, FORM-kind rejection, data/future-version
ckID rejection, FILLER skip, bounds checks, odd-size pad handling)
plus 3 integration tests intests/group_children.rsbuilding the
§5 worked example (LIST { PROP TEXT { FONT } FORM TEXT … }), a
CATof heterogeneous FORMs with the blankJJJJcontents ID,
and a LIST nested inside a CAT walked recursively — all end-to-end
fromprobe_top_level_groupthrough the child walk. Doc reference:
docs/image/iff/ea-iff-85.txt§5 "LISTs, CATs, and Shared
Properties" lines 842–986; §6 reader/writer rules lines 1119–1196;
Appendix A grammar lines 1244–1253. -
EA IFF 85 §3 universally-reserved ckID classifier
(chunk::ReservedId). §3 ¶ "the following ckIDs are universally
reserved to identify chunks with particular IFF meanings: 'LIST',
'FORM', 'PROP', 'CAT ', and ' '. […] The IDs 'LIS1' through
'LIS9', 'FOR1' through 'FOR9', and 'CAT1' through 'CAT9' are
reserved for future 'version number' variations" enumerates the
full reserved set every conforming IFF reader must recognise. The
new [chunk::ReservedId] enum maps any 4-byte ckID to one of
Group(GroupKind)(the three group-chunk IDs already surfaced
by [chunk::GroupKind]),Prop(the §3 PROP property-set group
that only appears as the first child of a LIST),Filler(the
four-space ID for "chunks that fill space but have no meaningful
contents"), orReservedFuture { parent, digit }(the
twenty-seven LIS1..9 / FOR1..9 / CAT1..9 version-number
variants, carrying back the parent group kind and the ASCII
digit). Two new module-level constants — [chunk::FILLER_ID]
(b" ") and [chunk::PROP_ID] (b"PROP") — give the two
previously-unnamed reserved IDs a typed home alongside the
existing [chunk::GROUP_FORM] / [chunk::GROUP_LIST] /
[chunk::GROUP_CAT] constants. [chunk::ReservedId::classify]
is the free entry point; [chunk::ChunkHeader::reserved] and
[chunk::ChunkHeader::is_filler] are the convenience accessors
the per-form walkers can route on. Three predicates
([ReservedId::is_group], [is_filler], [is_reserved_future])
cover the §3-aware fall-through cases, and
[ReservedId::all_reserved_ids] enumerates the full set in
spec-listed order. The classifier rejects every FORM-local
property surfaced elsewhere in the crate (BMHD / CMAP / BODY /
CAMG / GRAB / DEST / SPRT / CRNG / CCRT / DRNG / SHAM / PCHG /
VHDR / CHAN / ANNO / NAME / AUTH / COMM / SSND / MARK / INST /
COMT / AESD / APPL / MIDI / SAXL / FVER / ANHD / DLTA) so the
data-chunk dispatch path is unaffected. Sixteen new tests cover
the classifier surface — 9 unit tests insrc/chunk.rs
(reserved_id_classifies_three_groups,
reserved_id_classifies_prop_and_filler,
reserved_id_classifies_all_twenty_seven_future_versions,
reserved_id_rejects_non_reserved_ckid,
reserved_id_rejects_boundary_version_digits,
reserved_id_predicates,
all_reserved_ids_covers_every_id_classify_recognises,
chunk_header_reserved_and_is_filler,
filler_chunk_walks_past_body_without_decoding) plus 5
integration tests intests/reserved_ids.rscovering the
per-id-round-trip, the constant surfaces, an end-to-end
FILLER-before-FORM stream walk via [chunk::skip_chunk_body],
the future-version parent-group routing, and the FORM-local
rejection sweep. Doc reference:docs/image/iff/ea-iff-85.txt
§3 "Chunks" lines 524–531; Appendix A C macros
ID_FORM/ID_LIST/ID_PROP/ID_CAT/ID_FILLERlines
1230–1234. The current spec defines no decoder for the
reserved-future-version IDs; the predicate is offered so callers
can route them to a versioning-aware fall-back path instead of
misclassifying them as ordinary data chunks. The §3 paragraph
cites "23 chunk IDs" as the magic number callers must account
for, but the explicit enumeration in the same paragraph spells
out 5 base + 3×9 = 32 distinct IDs; the enum and the helper
match the explicit list, with the discrepancy flagged in the
[all_reserved_ids] doc comment. -
Top-level group probe primitive ([
chunk::probe_top_level_group]- [
chunk::read_top_level_group]). EA IFF 85 §6 restricts a
conforming file to a singleFORM/LIST/CATgroup at offset 0
whose first 12 bytes encodekind+ckSize+ the 4-byte inner
type ID (FormTypefor FORM/PROP,ContentsTypefor LIST/CAT).
Today the four forms this crate handles (8SVX/ILBM/ANIM/
AIFF/AIFC) each open with a near-identical hand-rolled magic
check (&buf[0..4] == b"FORM" && &buf[8..12] == b"<type>"); the
new primitive lifts that into one tested entry point that returns a
typed [chunk::TopLevelGroup] (with [chunk::GroupKind] +
inner_type4CC +size+ [TopLevelGroup::declared_total_len])
and surfaces "starts with a non-group FourCC" as
Error::invalid("IFF: not a top-level group chunk (got XXXX)")
so callers can fall through cleanly to a non-IFF container probe.
Cross-form regression coverage lives in
tests/top_level_group_probe.rs(5 envelope checks, one per
shipped form plus aCursorround-trip).
- [
-
ILBM
PCHGtyped-header surface. The PCHG (Palette CHanGe)
chunk now exposes its 20-byte fixed-layout header via a typed
[Pchg::header] accessor returning anOption<PchgHeader>
carrying every field the parser reads off the wire:
[PchgHeader::compression], [PchgHeader::flags],
[PchgHeader::start_line], [PchgHeader::line_count],
[PchgHeader::changed_lines], [PchgHeader::min_reg],
[PchgHeader::max_reg], [PchgHeader::max_changes], and
[PchgHeader::total_changes]. A companion [PchgKind]
(Small/Big) enum decodes the change-record encoding
selector out of theflagsword; [Pchg::kind] is the
convenience accessor (zero-flag bodies reportSmallper the
annex's documented default), and [PchgHeader::is_compressed]
flags theCompression == 1(Huffman-compressed-records)
variant — that wire shape still isn't decoded into per-line
changes, but it now surfaces through the typed header for
callers needing to fall back to [Pchg::raw]. The new
[Pchg::derive_header_hints] re-derives the four annex hint
fields (changed_lines,min_reg,max_reg,max_changes,
total_changes) directly from the decoded [Pchg::lines],
matching the annex's canonical semantics including the
"empty-PCHG ⇒ MinReg == MaxReg == 0" rule, and
[Pchg::header_matches_payload] gates the on-wire header
against the canonical re-derivation so editors can flag hint
drift after modifying the change list before re-encoding.
Fifteen new tests intests/ilbm_pchg_header.rscover the
Small + Big header round-trips, both payloads decoded through
the typed accessor, the four-field re-derivation against
Small/Big/empty PCHGs, the consistency check on both
round-trip and deliberately stale-hint inputs (incorrect
TotalChanges/MinReg), the zero-flag default-to-Small
rule, theis_compressedflag on a synthetic
Compression == 1body, and the short-raw"header is
None" path for hand-craftedPchgvalues. No behaviour
change for existing call sites; the helpers are pure
additions on top of the existing parser. Doc reference:
header-layout comment insrc/ilbm.rsPCHG section (the
annex layout was already documented inline in the parser;
this round only surfaces the fields the parser already reads). -
ILBM
Shamtyped row-palette accessors. The Sliced-HAM
per-scanline palette descriptor now exposes typed accessors that
match the [Pchg::palette_at_line] shape: [Sham::row_palette(y)]
borrows the 16-entry RGB888 palette for scanlineywithout
allocating (returningNonepast the parsed-row count, so callers
can spot the explicit-vs-padded boundary on hand-builtSham
values), [Sham::palette_at_line(base, y)] returns an owned
16-entry palette mirroring thePchgaccessor's shape (SHAM row
verbatim when present;basetruncated/padded to 16 entries
otherwise — the fallback always emits a 16-entry CMAP suitable for
feeding directly into [expand_ham_row]), and
[Sham::is_empty] / [Sham::rows] surface the
"any explicit rows?" / "how many?" predicates without forcing
callers to reach into thepalettesfield. Three new tests in
tests/ilbm_round2.rsexercise the explicit-row path, the
past-end fallback path (both full-length and shortbase
palettes — the helper pads with[0, 0, 0]whenbasehas fewer
than 16 entries), the empty-chunk path, and the parser's
short-chunk row-padding behaviour as observed through the typed
accessors. No behaviour change for existing call sites; the
helpers are pure additions andSham::palettesremains
publicly readable. -
cargo-fuzzharness with three libFuzzer targets. Newfuzz/
subdirectory wires the standardcargo-fuzzlayout into the crate
with three targets covering the highest-risk parser surfaces:
aiff_decode(the top-of-stack
aiff::demuxer::AiffDemuxer::from_bytesFORM AIFF / AIFC walker),
anim_decode(theanim::parse_animFORM ANIM walker with its
op-0 / op-5 / op-7 delta decoders), andpchg_parse(the
failure-mode-denseilbm::Pchg::parsePCHG palette-change-per-line
chunk decoder). The contract under fuzz is purely that each call
returns aResultand never panics / aborts / OOMs, regardless of
how malformed the input is. Run with
cargo +nightly fuzz run <target>from the crate root; see the
README "Fuzzing" section for the full target catalogue and the
failure-mode notes each target was authored to keep honest. -
AIFF-C §14 chunk-precedence surface. A new
aiff::ChunkClass
enum ranks the §3.1 / §4..§13 chunk classes per the §14 ordering
("Highest precedence Common Chunk … Lowest precedence Application
Specific Chunk"). The enum'srepr(u8)is the precedence rank
(FVER = 0; the §14 ranked block runs1..=13fromCOMMto
APPL), with [ChunkClass::rank] returning that rank and
[ChunkClass::higher_precedence_than] giving the §14 ¶ "the
loop points in the Instrument Chunk take precedence over
conflicting loop points found in the MIDI Data Chunk" predicate
in one call. [ChunkClass::ck_id] returns the on-wire 4-byte
ckID (with the §13.0 ¶ "the 'c' is lowercase and there is a
space [0x20] after the close parenthesis" Copyright case
honoured exactly asb"(c) "), and
[ChunkClass::all_in_precedence_order] enumerates the full
fourteen-entry table for callers that want to iterate by §14
rank. The matching [aiff::Form::precedence_order] helper
walks a parsedFormand emits aVec<ChunkClass>of the
classes the FORM actually contains in §14 order (regardless of
the on-wire chunk layout — §4 of the staged AIFF-AIFC layout
doc is explicit that chunk order inside a FORM is unspecified);
multi-instance classes (§8.0SAXL, §10.0MIDI, §12.0
APPL, §13.0ANNO) appear once per instance and preserve the
document-order semantics §14 ¶ "Annotation Chunk[s] -- in the
order they appear in the FORM" requires. The companion
[aiff::Form::highest_precedence_class] returns the top entry
(alwaysSomebecauseCOMMis mandatory) and identifies an
AIFF-C FORM with a §3.1FVERchunk asFormatVersion-led.
Eight unit tests insrc/aiff/precedence.rsand seven
integration tests intests/aiff_precedence.rscover the
rank-vs-spec-order invariant, the ckID round-trip, the
§13.0 Copyright(c)byte literal, the §14 worked example
Instrument-outranks-MIDI, the §14 "Common always outranks" /
"APPL always loses" envelope, irreflexivity of
higher_precedence_than,Ordagreement withrank, a
minimal AIFF FORM ordering, a minimal AIFF-C FORM with
FVERordering, a full thirteen-class FORM with deliberately
scrambled on-wire layout, multi-instance multiplicities for
ANNO/APPL/MIDI/SAXL, and the
highest_precedence_classswitch fromCommonto
FormatVersionwhenFVERis present.
Doc reference:docs/audio/aiff/aiff-c.txt§14 ("Chunk
Precedence"), lines 1209–1259 of the staged spec text. -
ILBM
SPRT(sprite-precedence) chunk surfacing.parse_ilbm
now lifts theSPRTproperty (ILBM supplement §2.7) into a
structured [ilbm::Sprt] (singleUWORD precedence) and
exposes it throughIlbmImage::sprt. The supplement defines
the chunk as "presence flags the ILBM as intended as a sprite"
with aUWORD SpritePrecedencewhere "0 is the highest"
(foremost). The Appendix A grammar slots SPRT between[DEST]
and[CAMG](BMHD [CMAP] [GRAB] [DEST] [SPRT] [CAMG]); §6
also notes the property chunks "may actually be in any order
but all must appear before the BODY chunk".encode_ilbmemits
the two-byte payload immediately afterDEST, beforeBODY.
A const sentinel [ilbm::Sprt::FOREMOST] =0plus a
[ilbm::Sprt::is_foremost] predicate surface the §2.7
"0 is the highest" convention without forcing callers to
remember the bare-int sentinel. The full unsigned-16 range
0..=0xFFFFround-trips. Eleven new tests intests/ilbm_sprt.rs
cover the two-byte wire layout, foremost-zero handling,
max-UWORD handling, short-payload rejection, the implicit
no-SPRT default, the grammar-ordering invariant (DEST precedes
SPRT precedes BODY), full-property-set coexistence with GRAB +
DEST + CAMG, and parse → encode → parse byte-stability. Doc
reference:docs/image/iff/ilbm.txt§2.7 + Appendix A. -
ILBM
DEST(destination-merge) chunk surfacing.parse_ilbm
now lifts theDESTproperty (ILBM §2.6) into a structured
[ilbm::Dest] (depth/pad1/plane_pick/plane_on_off/
plane_mask) and exposes it throughIlbmImage::dest. The §6
grammar fixes the property order asBMHD [CMAP] [GRAB] [DEST] [SPRT] [CAMG] ... BODY;encode_ilbmhonours that slot, writing
an eight-byte payload (UBYTE depth,UBYTE pad1, three big-endian
UWORDmasks) right afterGRAB. A helper
[ilbm::Dest::pick_count_matches_depth] surfaces the §2.6 soft
expectation "the number of '1' bits should equal nPlanes" without
rejecting non-conforming inputs at parse time (the spec frames the
equality as an expectation, not a requirement). Round-trip is
byte-stable, including a non-zeropad1byte (§2.6: "unused; for
consistency put 0 here"). Eight new tests intests/ilbm_dest.rs
cover the wire layout, the implicit(1 << nPlanes) - 1default
case, mismatch detection, and the FORM-envelope ordering invariant. -
AIFF / AIFF-C
SAXL(Sound Accelerator) chunk surfacing. The
FORM walker now decodes everySAXLchunk
(docs/audio/aiff/aiff-c.txt§8.0 + Appendix D) into a structured
[aiff::SaxelChunk] (with aVec<aiff::Saxel>of(id, data)
pairs) and exposes them through [aiff::Form::saxels] in document
order. §8.0 explicitly permits "any number of Saxel Chunks" per
FORM AIFC (and "Multiple Saxel Chunks are allowed in a single FORM
AIFC file"), so the surface is aVecrather than anOption—
matching how §10.0 MIDI and §12.0 APPL handle the
"any-number-per-FORM" rule. The chunk body is preserved verbatim
as a raw byte stream — Appendix D ¶ "saxelData contains the
specific sound accelerator data which is compression-type specific"
and §8.0 ¶ "Under Construction" / Appendix D ¶ "Caution" emphasise
the mechanism remained a "rough proposal" in the 1991 draft, so
this crate does not interpretdataagainst any particular
decompressor's state-priming convention. Lightweight observers
[aiff::Saxel::len] / [aiff::Saxel::is_empty] cover the common
"what's the priming-data length?" inspection without re-parsing.
Lookups are provided in both directions: [aiff::Saxel::resolve_marker]
joins a saxel'sidagainst a supplied [aiff::MarkerChunk] per
§8.0 ¶ "id identifies the marker for which the sound accelerator
data is to be used" (returningNonewhen the id isn't a positive
MarkerIdper §6.0 or no marker with that id is present), and
[aiff::SaxelChunk::by_marker_id] scans the chunk's saxel list
for a matching id. The per-saxel pad byte (Appendix D ¶ "The data
must be padded with a byte at the end as needed to make it an even
number of bytes long. This pad byte, if present, is not included
in size.") is honoured on parse and written on encode; end-of-chunk
pad on the last saxel is tolerated as either present or absent,
mirroring the MARK / COMT pstring-tail tolerance for legacy
encoders that elided the trailing pad. The matching
[aiff::write_saxel_chunk] write-side helper completes the
round-trip story; an encoder building a FORM AIFC can now emit
every chunk class the read path surfaces (MARK, INST, COMT, AESD,
APPL, MIDI, SAXL + the four §13.0 text chunks). New
tests/aiff_saxel.rscovers single-chunk + multiple-chunk-in-FORM- empty-Vec-when-absent surfacing, the empty-saxel-list intra-chunk
case, odd-size saxelData per-saxel pad handling, the write-side
round-trip,resolve_markeragainst a FORM-level MARK chunk plus
the zero/negative-id MarkerId-sentinel rejection per §6.0,
by_marker_idlookup, and SAXL coexisting with MARK + COMT + APPL - MIDI + ANNO in a single FORM. Internal
saxel.rstests exercise
the same surfaces against the lower-level helpers (empty list,
single-saxel even/odd data, empty-data, multiple saxels in
document order with mixed pad,by_marker_idhappy-path, three
truncation classes, end-of-chunk pad tolerance, write-helper
round-trip, write-helper empty-chunk, byte-for-byte write layout
match, and write document-order preservation). Doc reference:
docs/audio/aiff/aiff-c.txt§8.0 + Appendix D.
- empty-Vec-when-absent surfacing, the empty-saxel-list intra-chunk
-
AIFF / AIFF-C §13.0 text chunks (
NAME/AUTH/(c)/ANNO).
The FORM walker now decodes the four §13.0 Text Chunks of
docs/audio/aiff/aiff-c.txtinto a structured [aiff::TextChunk]
(with a [aiff::TextKind] discriminant tagging which of the four
ckIDs the chunk came from) and surfaces them through new
[aiff::Form::name] / [aiff::Form::author] /
[aiff::Form::copyright] / [aiff::Form::annotations] fields.
Per §13.0 ¶ "No more than one Name / Author / Copyright Chunk may
exist within a FORM AIFC",NAME/AUTH/(c)are
duplicate-checked singletons (a second occurrence raises
[aiff::AiffError::DuplicateChunk]); per §13.0 ¶ "Any number of
Annotation Chunks may exist within a FORM AIFC",ANNOis
accumulated into aVec<TextChunk>in document order, matching how
§10.0 MIDI and §12.0 APPL handle the "any-number-per-FORM" rule.
The text body is preserved byte-for-byte (§13.0 ¶ "text contains
pure ASCII characters. It is neither a pstring nor a C string");
[aiff::TextChunk::as_str] returns a borrowed&strfor valid
UTF-8 bodies and [aiff::TextChunk::as_string_lossy] decodes the
full body withU+FFFDsubstitution so MacRoman / Latin-1 bodies
produced by older encoders are still salvageable. Empty text
bodies (ckDataSize == 0) are accepted — §13.0 places no
minimum on the text field. A matching [aiff::write_text_chunk]
write-side helper completes the round-trip story; an encoder
building a FORM AIFF / AIFC can now emit every §13.0 ckID
alongside the COMT / MARK / INST / AESD / APPL / MIDI write paths
added in earlier rounds. The(c)ckID uses the canonical
four-byte ASCII form0x28 0x63 0x29 0x20per §13.0 ¶ "the 'c' is
lowercase and there is a space [0x20] after the close parenthesis";
the spec uses the round-bracket character itself as the ckID glyph
standing in for ©. Newtests/aiff_text_chunks.rscovers
standalone parse/write round-trips, the four-kind happy path with
one file carrying NAME + AUTH +(c)+ 2 × ANNO, three
duplicate-chunk rejection paths, a document-order check for ANNO,
empty-body acceptance, odd-length pad-byte round-trip, and the
(c)ckID variant-resistance check. Internalform.rstests
exercise the same surfaces against the lower-level helpers. -
AIFF / AIFF-C
MIDI(MIDI Data) chunk surfacing. The FORM
walker now decodes everyMIDIchunk (docs/audio/aiff/aiff-c.txt
§10.0) into a structured [aiff::MidiDataChunk] and exposes them
through [aiff::Form::midi] in document order. §10.0 explicitly
permits "any number of MIDI Data Chunks" per FORM AIFC, so the
surface is aVecrather than anOption. The chunk body is
preserved verbatim as a raw MIDI byte stream — the spec calls
MIDIdata"a stream of MIDI data" and imposes no internal
framing, so an MMA Standard MIDI File-style decode (MThd / MTrk
/ variable-length quantity / running status) remains the job of
theoxideav-midisibling crate. Lightweight observers
[aiff::MidiDataChunk::len] /
[aiff::MidiDataChunk::is_empty] /
[aiff::MidiDataChunk::is_sysex] cover the common "is this a
SysEx patch dump or something else?" classification without
re-parsing (is_sysexmatches the leading0xF0status byte
the spec calls out as the chunk's "primary purpose"). The
matching [aiff::write_midi_chunk] write-side helper completes
the round-trip story for encoders building a FORM AIFC. Empty
chunks (ckDataSize == 0) are accepted per §10.0 ¶ "MIDIData
contains a stream of MIDI data." — the spec sets no minimum
body length. Newtests/aiff_optional_chunks.rscases
(surfaces_single_midi_chunk,
surfaces_multiple_midi_chunks_in_document_order,
surfaces_zero_midi_chunks_as_empty_vec,
midi_chunk_with_odd_length_round_trips_through_chunk_walker,
midi_chunk_write_helper_roundtrips,
empty_midi_chunk_is_accepted,
midi_chunk_coexists_with_other_optional_chunks) exercise the
full surface plus 6 module-level unit tests
(parses_empty_chunk,preserves_byte_stream_verbatim,
is_sysex_false_when_first_byte_is_not_f0,write_round_trips,
write_round_trips_empty_chunk,classifies_odd_length_stream,
accepts_large_body). Doc reference:
docs/audio/aiff/aiff-c.txt§10.0 MIDI DATA CHUNK. -
ANIM op-7 (Short / Long Vertical Delta) encoder. New
[anim::encode_op7_body] builds the 64-byte pointer table + 8
per-plane opcode lists + 8 per-plane data lists from aprev/
curplanar-frame pair, picking Skip / Same / Uniq ops per column
to minimise byte cost (Same for runs ≥ 2 items, Uniq otherwise,
Skip for unchanged runs). [anim::encode_anim_op7] wraps it into a
full FORM/ANIM file with leadingFORM ILBM(seed) + per-delta
FORM ILBM { ANHD(op=7, bits=long_data?1:0) + DLTA }frames. The
short (2-byte items,ANHD.bitsbit 0 cleared) and long (4-byte
items, bit set) variants both round-trip through the in-tree
[anim::parse_anim] decoder. The parser was extended to accept the
DLTAchunk id alongsideBODYso op-7 / op-5 / op-0 streams all
decode via the same path. Newtests/anim_op7_encode.rsexercises
identical-frame elimination, sparse and full-change deltas, short
vs long mode, and rejectslong_data=truewithrow_bytes
unaligned to 4. Doc reference:docs/image/iff/anim.txtAppendix
Anim7 §#.# (Wolfgang Hofer, 23.6.92). -
AIFF / AIFF-C
COMT(Comments) chunk parsing. The FORM walker
now decodes theCOMTchunk into a structured
[aiff::CommentsChunk] surfaced through [aiff::Form::comments]
perdocs/audio/aiff/aiff-c.txt§7.0. Each comment carries a
timestamp(seconds since 1904-01-01 UTC, the Mac epoch), a
MarkerId(0 = comment is not linked to any marker, otherwise
references the FORM's MARK entry), and a UTF-8-lossy decoded text
body. The accompanying [aiff::Comment::linked_marker] returns
Option<i16>so callers can distinguish linked vs free-floating
comments without checking the marker field directly, and
[aiff::Comment::resolve_marker] joins the linkage against a
supplied [aiff::MarkerChunk]. At most oneCOMTper FORM per
§7.0 — duplicates are rejected asAiffError::DuplicateChunk ("COMT"). The per-commenttextpad byte rule (pad to even byte
count, pad NOT included incount) is honoured with the same
end-of-buffer tolerance asMARK. -
AIFF / AIFF-C
AESD(Audio Recording) chunk parsing. The FORM
walker now decodes theAESDchunk into a structured
[aiff::AesdChunk] surfaced through [aiff::Form::aesd] per
§11.0. The 24-byte AES channel-status block is preserved verbatim
instatus; [aiff::AesdChunk::emphasis] extracts the 3-bit
recording-emphasis field from byte 0 bits 2..=4 the spec calls out
as "of general interest". At most oneAESDper FORM per §11.0;
duplicates rejected asAiffError::DuplicateChunk("AESD"). The
spec's "ckDataSize is always 24" invariant is enforced — shorter
isTruncated, longer is
InvalidValue { what: "AESD ckSize", ... }. -
AIFF / AIFF-C
APPL(Application Specific) chunk parsing. The
FORM walker now decodes everyAPPLchunk into an
[aiff::ApplicationChunk] and collects them into
[aiff::Form::applications] in document order (§12.0 explicitly
permits any number of APPL chunks per FORM, unlike the other
optional chunks). [aiff::ApplicationChunk::dialect] classifies
the four-byteapplicationSignatureinto the three §12.0
dialects (pdosApple II,stocnon-Apple, anything else =
Macintosh); [aiff::ApplicationChunk::application_name] decodes
the leading Pascal-string application name forpdos/stoc
chunks (Macintosh dialect carries raw bytes with no required
leading structure) and [aiff::ApplicationChunk:: payload_after_name] returns the slice after the name, stepping
by exactly1 + length_byte(§12.0 specifies chunk-level
pad-to-even on the whole APPL but not an inner pad after the
leading pstring). -
MARKandINSTwrite-side encoders. Encoders building AIFF /
AIFF-C files can now construct the exact wire body for these
chunks via [aiff::write_marker_chunk] / [aiff:: write_instrument_chunk]. The marker writer preserves document
order, honours the §6.0 pstring pad-to-even discipline, and caps
oversize names / lists at the wire field widths (u8 length, u16
numMarkers); the instrument writer emits exactly 20 bytes in spec
field order and accepts arbitraryLoopsubstructures. Companion
[aiff::write_comments_chunk] / [aiff::write_appl_chunk] /
[aiff::write_aesd_chunk] cover the other newly-surfaced
chunks. All five round-trip through the FORM-level [aiff::parse]
walker (verified bytests/aiff_optional_chunks.rs). -
AIFF / AIFF-C
INST(Instrument) chunk parsing. The FORM
walker now decodes theINSTchunk into a structured
[aiff::InstrumentChunk] surfaced through
[aiff::Form::instrument]. Every wire field is preserved:
baseNote/lowNote/highNote(MIDI 0..=127),
detune(cents -50..=+50),lowVelocity/highVelocity
(1..=127), signed-dBgain, plus the twoLoop
substructures (sustainLoop, releaseLoop) which carry a decoded
[aiff::PlayMode] (NoLooping/ForwardLooping/
ForwardBackwardLooping) and the twoMarkerIds referencing the
FORM's MARK chunk. The parser enforces every §9 invariant — at
most oneINSTchunk per FORM (rejected as
AiffError::DuplicateChunk("INST")), exact 20-byte ckDataSize
("ckDataSize is always 20" — shorter isTruncated, longer is
InvalidValue { what: "INST ckSize", ... }), MIDI-note range,
detune range, velocity range, and a knownplayMode. The
accompanying [aiff::InstrumentChunk::resolve_sustain_loop] /
[aiff::InstrumentChunk::resolve_release_loop] helpers join the
loop endpoints against the FORM's [aiff::MarkerChunk] and apply
§9 ¶ "beginLoop and endLoop": "The begin position must be less
than the end position so the loop segment will have a positive
length. [If this is not the case, then ignore this loop segment.
No looping takes place.]" — returningNonewhenever
playMode == None, an endpoint id isn't a positive marker id,
either id isn't present in the supplied MARK list, or the begin
marker's frame position isn't strictly less than the end marker's.
22 new tests across theaiff::instrument,aiff::formand
tests/aiff_instrument.rssurfaces. -
AIFF / AIFF-C
MARK(Marker) chunk parsing. The FORM walker
now decodes theMARKchunk into a structured
[aiff::MarkerChunk] surfaced through [aiff::Form::markers].
Each [aiff::Marker] carries the spec'sid(big-endiani16,0, unique within the FORM),
position(big-endianu32sample
frame; for compressed AIFF-C streams the spec defines this in
expanded-domain frames per §6.0 ¶3), and pstringname
(length-prefixed with pad-to-even total). The parser enforces
every AIFF-C §6.0 invariant — at most oneMARKchunk per FORM
(rejected asAiffError::DuplicateChunk("MARK")), positive
MarkerId(rejected asAiffError::InvalidValue { what: "MarkerId", ... }), and unique-id-within-chunk (rejected as
AiffError::DuplicateMarkerId). Markers are exposed in document
order;MarkerChunk::by_idprovides the typical
lookup-by-id needed when the AIFF-CINST(instrument) chunk
references loop endpoints by marker id. 17 new tests across the
aiff::marker,aiff::formandtests/aiff_markers.rs
surfaces.
Changed
aiff::Formgains amarkers: Option<MarkerChunk>field —
Nonewhen the FORM has noMARKchunk,Some(MarkerChunk{ markers: vec![] })for an empty marker list (the encoder
declared markers but had none).aiff::Formgains aninstrument: Option<InstrumentChunk>
field —Nonewhen the FORM has noINSTchunk,Some(_)
otherwise. New since the previous Unreleased entry.