e0b4ec2e21
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.
5.1 KiB
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 tovendor/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 isminReaderVersion <= kPackageFormatVersion. Additive changes (a new optional manifest key, a new enum value with a defined degrade) bumpformatVersiononly; structural changes bump both. The full ladder lives as a comment inpackage_format.hand is READ and validated, never merely written. - TooNew refuses whole. A
minReaderVersionabove 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; seepackage_format.h's doc comment for the itemized list. - Framing only, never a payload.
bank_packageproduces header bytes and an ordered{name, offset, length}layout; it never holds, copies, or hashes an entry's audio.decodePackageproves 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
BankModelblob emitted/parsed bybank_model's own codec (thebank_book_jsonprecedent), so a futureSamplefield reaches packages with no change here. - Hostile input: error signaled, never UB — the
bank_model.hdeserialize 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-wayclassifyPackageVersion(Readable/TooNew/Malformed), the entry-name rule, andPackageHeader.package_manifest— the manifest model (PackageEntry/PackageManifest) and its JSON codec. Per entry: bare name, byte length, and a whole-filecapture::hashBytesdigest (deliberately NOThashWavContent, which skips chunks and cannot answer "did these bytes survive") — the digest is carried here, computed where payloads are streamed (shell). The bank'sslot_maprides 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 andversion::stampVersion()),decodePackage(prefix + observed file size in; header/manifest/layout out), andrequiredPrefixSize(the incremental-read seam for the shell). Framing ridescore/wire/bytes.h.
Gotchas
- Enums nested inside the
BankModelblob followbank_model's own rule — an out-of-rangesourceMode/tierREJECTS the parse — so growing one of those vocabularies is aminReaderVersionbump for packages, not an additive change. Any enum integer the manifest itself ever adds must instead follow the degrade-to-Unknownrule (core/wire'sBakeStatusprecedent) 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_plandecisions. The codec rejects only what makes the container itself incoherent (duplicate entry names, invalid names, a non-single-sample nested index). requiredPrefixSizetrusts fields beyond the frozen region only when the version pair classifiesReadable; forTooNewit 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 aminReaderVersionbump, not additive: an old reader would otherwise compare a stored digest against bytes hashed the new way and silently misjudge corruption.