Freeze the package compatibility corpus: real .rsbank bytes proving both ladder directions, every truncation site, and the round trip

This commit is contained in:
2026-08-02 15:16:55 -04:00
parent b99027bc4c
commit 9521b5339f
31 changed files with 907 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
# 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
RS
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.