Files
reasampler/tests/fixtures/package_compat/README.md
T

6.1 KiB

The frozen .rsbank compatibility corpus

Real RSBK bytes, committed. tests/test_package_compat.cpp decodes them; tests/test_package_round_trip.cpp drives them through the import and export verbs.

THE RULE: this corpus is append-only

No file here is ever regenerated or edited. When a future format version ships, add its fixture beside these and leave every existing one alone.

The reason is the whole point of the corpus. These bytes exist to catch a format change that quietly breaks a compatibility direction. A fixture regenerated by the build that broke it agrees with that build by construction and catches nothing — which is exactly the failure mode a version ladder exists to prevent. The same argument forbids a test that builds its own fixture at run time.

If a fixture stops decoding, the answer is never to re-cut the fixture. Either the format change was structural (bump minReaderVersion, add a new fixture, and leave the old one asserting the refusal) or it is a regression.

Provenance

v1_shipping.rsbank was produced by running this repo's own export verb (exportBank) at version 1.4.0 over a one-sample bank, and copying the emitted file here verbatim. Every other fixture is derived from those bytes: the truncations are prefixes of them, and the synthetic packages reuse their manifest region under different version integers or a hand-written hostile manifest (the encoder refuses to write one, which is why those could not come from the verb).

Payloads are one 300-byte 16-bit mono WAV. The properties under test are structural — version integers, framing arithmetic, name validation — so a larger payload proves nothing extra and costs the repo bytes forever. Whole corpus: ~14 KB.

Adding a fixture for a future version means writing it with that version's shipping build, exactly as this one was, and recording the build's version here.

What each fixture proves

The three version fixtures

File formatVersion / minReaderVersion Verdict Proves
v1_shipping.rsbank 1 / 1 Readable This build reads what it wrote: header, one manifest entry, the entry digest, and every Sample field with every optional present. Writer semver 1.4.0 is asserted literally, not against stampVersion() — comparing against the running build would let a version bump re-anchor the fixture silently.
additive_forward.rsbank 2 / 1 Readable An additive newer writer still reads. Carries three keys this build has never heard of — exportTool at the manifest root, futureEntryKey on the entry, futureSampleKey inside the nested Sample blob — and decodes to exactly the manifest v1_shipping.rsbank decodes to. Writer semver 1.9.0.
refuse_structural.rsbank 2 / 2 TooNew A structural newer writer is refused whole. The header through the writer semver still reads, so the refusal can name all three facts (1.9.0, needs format 2, this build reads 1); no manifest, no layout, no partial success. Its body is v1_shipping.rsbank's own manifest, which parses — so the refusal is a decision, not an inability.

Truncation — one file per distinct decode failure site

Each is a prefix of v1_shipping.rsbank (907-byte prefix + 300-byte payload = 1207 bytes). All classify Malformed; none may classify TooNew, since "install a newer build" does not fix a partial download.

File Bytes Site the cut lands in
trunc_magic.rsbank 2 Inside the 4-byte magic.
trunc_version_pair.rsbank 10 Inside the frozen header's minReaderVersion u32.
trunc_writer_semver.rsbank 18 Inside the frozen header's writer semver.
trunc_manifest_length.rsbank 23 Inside the manifest-length u32.
trunc_manifest_body.rsbank 466 Inside the manifest JSON.
trunc_payload_start.rsbank 907 At the payload boundary. RSBK stores no layout section — the layout is derived from the manifest's entries — so this is the cut that exercises "manifest parses, layout computes, exact-size proof fails".
trunc_payload_middle.rsbank 1057 Inside the first payload.
trunc_one_short.rsbank 1206 One byte short of the total.

Hostile names — refused at decode, before any planner

The two naming fields carry different rules (src/core/package/CLAUDE.md), so each fixture keeps the other field spelled cleanly (kick.wav) and the refusal is attributable to the field under test.

Entry name — a bare file name, no path expression possible (isValidEntryName):

File Entry name
hostile_name_dotdot.rsbank ..
hostile_name_parent_slash.rsbank ../evil.wav
hostile_name_parent_backslash.rsbank ..\evil.wav
hostile_name_subdir_slash.rsbank sub/evil.wav
hostile_name_drive_absolute.rsbank C:\Windows\evil.wav
hostile_name_unc_absolute.rsbank \\srv\share\evil.wav

Nested Sample::relativePath — a path by design, refused only for traversal and absolute forms (isValidNestedSamplePath):

File relativePath Guard that fires first inside the codec
hostile_path_dotdot_slash.rsbank bank/../../evil.wav isValidNestedSamplePath
hostile_path_dotdot_backslash.rsbank bank\..\evil.wav isValidNestedSamplePath
hostile_path_rooted.rsbank /etc/evil.wav BankModel::add's absolute-path rejection, which drops the record and leaves the nested blob holding zero samples
hostile_path_drive_absolute.rsbank C:\Windows\evil.wav as above
hostile_path_unc_absolute.rsbank \\srv\share\evil.wav as above

Both guards are inside the codec and both refuse the whole package, so the security property is the same either way; the split is recorded because a change to either guard alone would still leave these fixtures passing.

The round-trip anchor

v1_shipping.rsbank doubles as it: the file is a real export, so importing it and exporting the resulting bank closes export → import → export over frozen bytes. Entry names may legally change across the trip (the importer re-spells a bank file, the exporter mints its own transport name); the payload bytes may not.