Land src/core/package: the pure RSBK container — format ladder, JSON manifest, framing/layout codec
Two-integer ladder (formatVersion/minReaderVersion), bare-name-only entries validated on encode and decode, prefix decode that proves exact file size without ever reading a payload.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# src/core/package — the pure RSBK bank-package codec
|
||||
|
||||
## Scope
|
||||
|
||||
The hand-rolled `RSBK` bank-package container, entirely pure (REAPER-free,
|
||||
unit-tested outside the DAW): the format contract and version ladder, the JSON
|
||||
manifest, and the framing/layout codec. No filesystem — the shell
|
||||
(`src/shell/package`) streams bytes against the layouts produced here. The
|
||||
export/import *decisions* (`export_plan` / `import_plan`) are separate modules
|
||||
landing after the format.
|
||||
|
||||
## Invariants
|
||||
|
||||
- **The container is the proprietary `RSBK` — ruled, not revisitable here.** No
|
||||
ZIP, no compressor, no link edge to `vendor/WDL/WDL/zlib/`. The version
|
||||
ladder, not a format swap, is how the format moves.
|
||||
- **Two version integers, two jobs.** `formatVersion` = what the writer
|
||||
emitted; `minReaderVersion` = the oldest reader that can read it safely. The
|
||||
reader's whole rule is `minReaderVersion <= kPackageFormatVersion`. Additive
|
||||
changes (a new optional manifest key, a new enum value with a defined
|
||||
degrade) bump `formatVersion` only; structural changes bump both. The full
|
||||
ladder lives as a comment in `package_format.h` and is READ and validated,
|
||||
never merely written.
|
||||
- **TooNew refuses whole.** A `minReaderVersion` above this build yields the
|
||||
header (so the refusal can name the writer's semver and version) and nothing
|
||||
else — no manifest, no layout, no half-success. The fields through the writer
|
||||
semver are FROZEN for all future versions to keep that refusal producible.
|
||||
- **Path expression is structurally impossible.** Entry names are bare file
|
||||
names (`isValidEntryName`: no separators, no `..`, no drive/UNC/rooted form),
|
||||
enforced on encode AND decode because a package can arrive from anywhere.
|
||||
There is no field in the format capable of expressing a path.
|
||||
- **Framing only, never a payload.** `bank_package` produces header bytes and
|
||||
an ordered `{name, offset, length}` layout; it never holds, copies, or hashes
|
||||
an entry's audio. `decodePackage` proves prefix + payload lengths equal the
|
||||
observed file size exactly, so truncation and trailing garbage are Malformed
|
||||
without any payload being read.
|
||||
- **Per-sample shape has one owner.** Each entry nests a one-sample `BankModel`
|
||||
blob emitted/parsed by `bank_model`'s own codec (the `bank_book_json`
|
||||
precedent), so a future `Sample` field reaches packages with no change here.
|
||||
- **Hostile input: error signaled, never UB** — the `bank_model.h` deserialize
|
||||
standard, plus allocation caps on every length field so a forged header
|
||||
cannot demand gigabytes.
|
||||
|
||||
## Modules
|
||||
|
||||
- `package_format` — the contract: magic, `kPackageFormatVersion` /
|
||||
`kPackageMinReaderVersion`, the ladder comment, the three-way
|
||||
`classifyPackageVersion` (`Readable` / `TooNew` / `Malformed`), the
|
||||
entry-name rule, and `PackageHeader`.
|
||||
- `package_manifest` — the manifest model (`PackageEntry` / `PackageManifest`)
|
||||
and its JSON codec. Per entry: bare name, byte length, and a whole-file
|
||||
`capture::hashBytes` digest (deliberately NOT `hashWavContent`, which skips
|
||||
chunks and cannot answer "did these bytes survive") — the digest is carried
|
||||
here, computed where payloads are streamed (shell). The bank's `slot_map`
|
||||
rides along. Unknown keys skip at every level; duplicate entry names are
|
||||
rejected both ways.
|
||||
- `bank_package` — framing and arithmetic composing the two above:
|
||||
`encodePackage` (prefix bytes + layout + total size, stamping this build's
|
||||
ladder pair and `version::stampVersion()`), `decodePackage` (prefix + observed
|
||||
file size in; header/manifest/layout out), and `requiredPrefixSize` (the
|
||||
incremental-read seam for the shell). Framing rides `core/wire/bytes.h`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Enums nested inside the `BankModel` blob follow `bank_model`'s own rule — an
|
||||
out-of-range `sourceMode`/`tier` REJECTS the parse — so growing one of those
|
||||
vocabularies is a `minReaderVersion` bump for packages, not an additive
|
||||
change. Any enum integer the manifest itself ever adds must instead follow
|
||||
the degrade-to-`Unknown` rule (`core/wire`'s `BakeStatus` precedent) to stay
|
||||
additive. The manifest carries no enum of its own today.
|
||||
- Sample-id rules (remap, collision, dedup across the destination) are
|
||||
deliberately NOT enforced by the codec — they are `import_plan` decisions. The
|
||||
codec rejects only what makes the container itself incoherent (duplicate
|
||||
entry names, invalid names, a non-single-sample nested index).
|
||||
- `requiredPrefixSize` trusts fields beyond the frozen region only when the
|
||||
version pair classifies `Readable`; for `TooNew` it stops at the semver —
|
||||
don't "fix" it to read the manifest length there, a future structural format
|
||||
may have moved it.
|
||||
Reference in New Issue
Block a user