Files
reasampler/src/core/package/CLAUDE.md
T
daniel e0b4ec2e21 Tighten RSBK package-format validation for review remediation
Reject NUL/control bytes and Windows-hostile names in entry names, relax
the over-broad ".." substring ban to component-only, close the
trailing-garbage gap on empty manifests, and relocate the package
CMake subdirectory to its ladder home.
2026-08-02 17:19:29 -04:00

5.1 KiB

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.