# 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 `..` component, no drive/UNC/rooted form, no control bytes, no Windows-reserved character, no trailing dot/space, no DOS device name), enforced on encode AND decode because a package can arrive from anywhere. There is no field in the format capable of expressing a path. The rule set is the full authority; see `package_format.h`'s doc comment for the itemized list. - **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. - The format carries no algorithm tag for `byteHash` — it is FNV-1a (`capture::hashBytes`) implicitly. Changing the digest algorithm is a `minReaderVersion` bump, not additive: an old reader would otherwise compare a stored digest against bytes hashed the new way and silently misjudge corruption.