// Standalone tests for reasampler::package::bank_package — no REAPER, no test // framework. Byte-level suites hand-roll RSBK images with wire::putLE rather // than calling encodePackage, so a layout regression in encode cannot hide from // decode (the two sides are pinned against each other AND against raw bytes). #include "../src/core/package/bank_package.h" #include #include #include #include #include "../src/core/version/app_version.h" #include "../src/core/wire/bytes.h" using namespace reasampler::package; using namespace reasampler::model; namespace wire = reasampler::wire; namespace version = reasampler::version; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // --- fixtures ---------------------------------------------------------------- // Every Sample field populated, every optional PRESENT. static Sample fullSample() { Sample s; s.id = "smp-full"; s.displayName = "Kick (wet)"; s.relativePath = "reasampler_bank/kick.wav"; s.sourceMode = SourceMode::RazorArea; s.sourceRange = {1.25, 3.5, 480.0, 1920.0}; s.trackGuids = {"{AAA}", "{BBB}"}; s.wetDry = 0.75; s.channelCount = 2; s.sampleRate = 48000; s.lengthSeconds = 2.25; s.lengthBeats = 4.5; s.captureTempo = 120.5; s.captureTimeSigNum = 7; s.captureTimeSigDenom = 8; s.key = "F#m"; s.rootNote = 60; s.loop = LoopPoints{100, 4800}; s.levels = {-0.3, -12.7, -14.0}; s.clipped = true; s.tier = Tier::Archive; s.contentHash = "W0123456789abcdef"; s.provenance = Provenance{"smp-parent", "fx-snapshot"}; s.createdTimestamp = 1754000000; return s; } // Every Sample optional ABSENT (key, rootNote, loop, provenance). static Sample bareSample() { Sample s; s.id = "smp-bare"; s.displayName = "Snare"; s.relativePath = "reasampler_bank/snare.wav"; s.sourceMode = SourceMode::Realtime; s.contentHash = "Wfedcba9876543210"; s.createdTimestamp = 1754000001; return s; } static PackageManifest fixture(std::uint64_t len0, std::uint64_t len1) { PackageManifest m; m.bankDisplayName = "Drums \"live\""; m.exportTimestamp = 1754100000; m.entries.push_back({"kick.wav", len0, "1111222233334444", fullSample()}); m.entries.push_back({"snare.wav", len1, "5555666677778888", bareSample()}); m.slots.append("smp-full"); m.slots.append("smp-bare"); return m; } // A hand-rolled RSBK image: frozen region + a raw tail (manifest framing or // deliberate garbage), independent of encodePackage. static std::vector rawHeader(std::uint32_t fv, std::uint32_t mv, const std::string& semver) { std::vector out; out.insert(out.end(), kPackageMagic, kPackageMagic + 4); wire::putLE(out, fv); wire::putLE(out, mv); wire::putLE(out, static_cast(semver.size())); out.insert(out.end(), semver.begin(), semver.end()); return out; } static void appendManifest(std::vector& out, const std::string& json) { wire::putLE(out, static_cast(json.size())); out.insert(out.end(), json.begin(), json.end()); } // One-entry manifest JSON with `extra` spliced in as additional root content // ("" for none) and `sampleExtra` spliced into the nested Sample object — for // images a current writer would never emit. static std::string handManifest(const std::string& name, int length, const std::string& extra, const std::string& sampleExtra = "") { return std::string("{") + extra + "\"entries\":[{\"name\":\"" + name + "\",\"length\":" + std::to_string(length) + ",\"hash\":\"h\"," "\"index\":{\"version\":1,\"samples\":[{\"id\":\"s1\"," + sampleExtra + "\"relativePath\":\"bank/a.wav\"}]}}]}"; } // --- encode / decode round trip ---------------------------------------------- static void testEncodeDecodeRoundTrip() { const PackageManifest m = fixture(96000, 48000); auto enc = encodePackage(m); CHECK(enc.has_value()); // Layout arithmetic: payloads start at the prefix end, in manifest order. CHECK(enc->layout.size() == 2); CHECK(enc->layout[0].name == "kick.wav"); CHECK(enc->layout[0].offset == enc->prefix.size()); CHECK(enc->layout[0].length == 96000); CHECK(enc->layout[1].offset == enc->prefix.size() + 96000); CHECK(enc->layout[1].length == 48000); CHECK(enc->totalSize == enc->prefix.size() + 96000 + 48000); // decodePackage(encodePackage(x)) == x — payloads are never read by the // codec, so the prefix plus the true total size is the whole input. const DecodedPackage dec = decodePackage(enc->prefix, enc->totalSize); CHECK(dec.status == PackageReadability::Readable); CHECK(dec.manifest == m); CHECK(dec.layout == enc->layout); CHECK(dec.prefixSize == enc->prefix.size()); // The header stamps this build's ladder pair and its informational semver. CHECK(dec.header.formatVersion == kPackageFormatVersion); CHECK(dec.header.minReaderVersion == kPackageMinReaderVersion); CHECK(dec.header.writerVersion == version::stampVersion()); } static void testEncodeRefusesWhatManifestRefuses() { PackageManifest m = fixture(1, 1); m.entries[0].fileName = "../evil.wav"; CHECK(!encodePackage(m).has_value()); // See src/core/package/CLAUDE.md for the shell seam that forces this. PackageManifest zeroLen = fixture(1, 1); zeroLen.entries[1].byteLength = 0; CHECK(!encodePackage(zeroLen).has_value()); } // --- truncation: every byte offset ------------------------------------------- static void testTruncationAtEveryByteOffsetIsMalformed() { const PackageManifest m = fixture(3, 5); auto enc = encodePackage(m); CHECK(enc.has_value()); // The complete on-disk image: prefix + both payloads. std::vector file = enc->prefix; for (std::uint8_t b : {1, 2, 3, 10, 20, 30, 40, 50}) file.push_back(b); CHECK(file.size() == enc->totalSize); CHECK(decodePackage(file, file.size()).status == PackageReadability::Readable); for (std::size_t cut = 0; cut < file.size(); ++cut) { const std::vector truncated(file.begin(), file.begin() + cut); const DecodedPackage dec = decodePackage(truncated, truncated.size()); if (dec.status != PackageReadability::Malformed) { std::printf("FAIL: truncation at %zu not Malformed\n", cut); ++g_fail; break; } // A refused decode must not half-succeed at any cut either. CHECK(dec.manifest.entries.empty()); CHECK(dec.layout.empty()); } // One byte extra (trailing garbage) is as Malformed as one byte missing. std::vector extended = file; extended.push_back(0); CHECK(decodePackage(extended, extended.size()).status == PackageReadability::Malformed); // And a file size that disagrees with the same bytes. CHECK(decodePackage(file, file.size() + 1).status == PackageReadability::Malformed); CHECK(decodePackage(file, file.size() - 1).status == PackageReadability::Malformed); } // --- header defaults --------------------------------------------------------- // The 0/0 defaults are not the current ladder pair, so a header that never // parsed cannot be mistaken for a plausible 1/1. They mean "unset", NOT "the // decode failed": a decode that got past the header reports the real pair // alongside its Malformed verdict. static void testHeaderDefaultsMeanUnparsed() { CHECK(PackageHeader{}.formatVersion == 0); CHECK(PackageHeader{}.minReaderVersion == 0); CHECK(PackageHeader{}.writerVersion.empty()); // Failed before the header: bad magic, and a semver truncated mid-string. std::vector badMagic = rawHeader(1, 1, "1.0.0"); appendManifest(badMagic, "{}"); badMagic[0] = 'Z'; const DecodedPackage magic = decodePackage(badMagic, badMagic.size()); CHECK(magic.status == PackageReadability::Malformed); CHECK(magic.header == PackageHeader{}); std::vector cutSemver = rawHeader(1, 1, "1.0.0"); cutSemver.resize(18); CHECK(decodePackage(cutSemver, cutSemver.size()).header == PackageHeader{}); // Failed after it: a same-version package with a corrupt manifest is // Malformed, and its header is fully populated. std::vector corrupt = rawHeader(kPackageFormatVersion, kPackageMinReaderVersion, "1.0.0"); appendManifest(corrupt, "not json"); const DecodedPackage late = decodePackage(corrupt, corrupt.size()); CHECK(late.status == PackageReadability::Malformed); CHECK(late.header.formatVersion == kPackageFormatVersion); CHECK(late.header.minReaderVersion == kPackageMinReaderVersion); CHECK(late.header.writerVersion == "1.0.0"); } // --- version ladder: TooNew refuses whole ------------------------------------ static void testTooNewProducesNoManifest() { // A future structural format: only the frozen region is trustworthy, so the // tail is deliberate garbage that would crash a parser that kept reading. std::vector bytes = rawHeader(9, 9, "9.9.9"); for (int i = 0; i < 32; ++i) bytes.push_back(0xFF); const DecodedPackage dec = decodePackage(bytes, bytes.size()); CHECK(dec.status == PackageReadability::TooNew); // The refusal message's three facts survive... CHECK(dec.header.formatVersion == 9); CHECK(dec.header.minReaderVersion == 9); CHECK(dec.header.writerVersion == "9.9.9"); // ...and nothing else is produced: no manifest, no layout, no half-success. CHECK(dec.manifest.entries.empty()); CHECK(dec.manifest == PackageManifest{}); CHECK(dec.layout.empty()); CHECK(dec.prefixSize == 0); // Boundary: minReader exactly one past this build. auto boundary = rawHeader(kPackageFormatVersion + 1, kPackageFormatVersion + 1, "2.0.0"); CHECK(decodePackage(boundary, boundary.size()).status == PackageReadability::TooNew); // A TooNew header truncated inside the frozen region cannot name the // writer, so it is Malformed, not an unactionable refusal. std::vector cut = rawHeader(9, 9, "9.9.9"); cut.resize(18); // mid-semver CHECK(decodePackage(cut, cut.size()).status == PackageReadability::Malformed); } // An additively-tagged package (fv > ours, minReader still within reach) whose // manifest fails to parse: the header classifies Readable, so decode reads // into the manifest and fails there. That failure must still report TooNew — // the header is valid and already carries the writer's semver — not the // unactionable Malformed a genuinely corrupt header produces. static void testAdditiveUnparseableManifestIsTooNew() { std::vector bytes = rawHeader(kPackageFormatVersion + 1, kPackageMinReaderVersion, "1.9.0"); appendManifest(bytes, "not json"); const DecodedPackage dec = decodePackage(bytes, bytes.size()); CHECK(dec.status == PackageReadability::TooNew); CHECK(dec.header.formatVersion == kPackageFormatVersion + 1); CHECK(dec.header.minReaderVersion == kPackageMinReaderVersion); CHECK(dec.header.writerVersion == "1.9.0"); CHECK(dec.manifest.entries.empty()); CHECK(dec.layout.empty()); // Same-version unparseable manifest stays Malformed: nothing "newer" // excuses it, so this is not a blanket "unparseable == TooNew" rule. std::vector sameVersion = rawHeader(kPackageFormatVersion, kPackageMinReaderVersion, "1.0.0"); appendManifest(sameVersion, "not json"); CHECK(decodePackage(sameVersion, sameVersion.size()).status == PackageReadability::Malformed); } // --- version ladder: additive forward compatibility -------------------------- // The reason two integers exist: a NEWER formatVersion whose minReaderVersion // still reaches back to this build must read, with its unknown keys skipped — // at the manifest level AND inside the nested Sample blob, the actual // motivating case for the two-integer ladder (see this directory's CLAUDE.md). static void testNewerAdditiveFormatReads() { const std::string manifest = handManifest( "a.wav", 4, "\"instrumentState\":{\"future\":[1,2,3]},\"anotherNewKey\":\"x\",", "\"someFutureSampleField\":42,"); std::vector bytes = rawHeader(kPackageFormatVersion + 1, kPackageMinReaderVersion, "1.9.0"); appendManifest(bytes, manifest); const std::uint64_t total = bytes.size() + 4; // the one entry's payload const DecodedPackage dec = decodePackage(bytes, total); CHECK(dec.status == PackageReadability::Readable); CHECK(dec.header.formatVersion == kPackageFormatVersion + 1); CHECK(dec.header.writerVersion == "1.9.0"); CHECK(dec.manifest.entries.size() == 1); CHECK(dec.manifest.entries[0].fileName == "a.wav"); CHECK(dec.manifest.entries[0].sample.id == "s1"); CHECK(dec.layout.size() == 1); CHECK(dec.layout[0].offset == bytes.size()); CHECK(dec.layout[0].length == 4); } // --- hostile headers --------------------------------------------------------- static void testHostileHeadersAreMalformed() { // Wrong magic. std::vector bad = rawHeader(1, 1, "1.0.0"); appendManifest(bad, "{}"); bad[0] = 'Z'; CHECK(decodePackage(bad, bad.size()).status == PackageReadability::Malformed); // Incoherent version pairs (the classify rules, proven through the framing). for (auto [fv, mv] : {std::pair{0, 0}, {0, 1}, {1, 0}, {1, 2}}) { std::vector b = rawHeader(fv, mv, "1.0.0"); appendManifest(b, "{}"); CHECK(decodePackage(b, b.size()).status == PackageReadability::Malformed); } // A forged semver length over the cap must not read past the buffer. std::vector overSemver; overSemver.insert(overSemver.end(), kPackageMagic, kPackageMagic + 4); wire::putLE(overSemver, std::uint32_t{1}); wire::putLE(overSemver, std::uint32_t{1}); wire::putLE(overSemver, kMaxWriterVersionBytes + 1); CHECK(decodePackage(overSemver, overSemver.size()).status == PackageReadability::Malformed); // A forged manifest length over the cap: refused before any allocation. std::vector overManifest = rawHeader(1, 1, "1.0.0"); wire::putLE(overManifest, kMaxManifestBytes + 1); CHECK(decodePackage(overManifest, overManifest.size()).status == PackageReadability::Malformed); // A manifest whose entry name expresses a path: rejected on decode even // though no current encoder would write it. std::vector traversal = rawHeader(1, 1, "1.0.0"); appendManifest(traversal, handManifest("../evil.wav", 4, "")); CHECK(decodePackage(traversal, traversal.size() + 4).status == PackageReadability::Malformed); } // --- requiredPrefixSize ------------------------------------------------------ static void testRequiredPrefixSizeGrowsToTheFullPrefix() { auto enc = encodePackage(fixture(3, 5)); CHECK(enc.has_value()); const auto& prefix = enc->prefix; // Empty: the fixed region first. CHECK(requiredPrefixSize({}) == std::uint64_t{16}); // With the fixed region: asks through the semver + manifest-length field. const std::string& semver = version::stampVersion(); std::vector first16(prefix.begin(), prefix.begin() + 16); CHECK(requiredPrefixSize(first16) == std::uint64_t{16 + semver.size() + 4}); // With that much: the full prefix size. And the answer is a fixpoint. std::vector upToManifestLen( prefix.begin(), prefix.begin() + 20 + static_cast(semver.size())); CHECK(requiredPrefixSize(upToManifestLen) == std::uint64_t{prefix.size()}); CHECK(requiredPrefixSize(prefix) == std::uint64_t{prefix.size()}); // The returned count is exactly enough for decodePackage. CHECK(decodePackage(prefix, enc->totalSize).status == PackageReadability::Readable); } static void testRequiredPrefixSizeRefusals() { // Bad magic: stop reading. std::vector bad(16, 0); CHECK(!requiredPrefixSize(bad).has_value()); // Incoherent versions: stop reading. std::vector zeroed = rawHeader(0, 0, "1.0.0"); CHECK(!requiredPrefixSize(zeroed).has_value()); // TooNew: asks only through the frozen region — the manifest-length field // belongs to the newer format and is never trusted. std::vector tooNew = rawHeader(9, 9, "9.9.9"); for (int i = 0; i < 8; ++i) tooNew.push_back(0xFF); // garbage where M would be CHECK(requiredPrefixSize(tooNew) == std::uint64_t{16 + 5}); // Oversize length fields: stop reading. std::vector overSemver; overSemver.insert(overSemver.end(), kPackageMagic, kPackageMagic + 4); wire::putLE(overSemver, std::uint32_t{1}); wire::putLE(overSemver, std::uint32_t{1}); wire::putLE(overSemver, kMaxWriterVersionBytes + 1); CHECK(!requiredPrefixSize(overSemver).has_value()); std::vector overManifest = rawHeader(1, 1, "1.0.0"); wire::putLE(overManifest, kMaxManifestBytes + 1); CHECK(!requiredPrefixSize(overManifest).has_value()); } int main() { testEncodeDecodeRoundTrip(); testEncodeRefusesWhatManifestRefuses(); testTruncationAtEveryByteOffsetIsMalformed(); testHeaderDefaultsMeanUnparsed(); testTooNewProducesNoManifest(); testAdditiveUnparseableManifestIsTooNew(); testNewerAdditiveFormatReads(); testHostileHeadersAreMalformed(); testRequiredPrefixSizeRefusals(); testRequiredPrefixSizeGrowsToTheFullPrefix(); if (g_fail == 0) { std::printf("bank_package_tests: all passed\n"); return 0; } std::printf("bank_package_tests: %d failure(s)\n", g_fail); return 1; }