// Standalone tests over the FROZEN package-compat corpus — no REAPER, no framework. // Nothing here builds a package: every byte comes off disk exactly as committed, which // is the only shape in which a later format change can be caught breaking a // compatibility direction. A test that re-derived its own fixture would prove only that // the codec agrees with itself. See tests/fixtures/package_compat/README.md. // // The values pinned below are the FIXTURE's facts, not this build's — the writer semver // especially must never be compared against version::stampVersion(), or a version bump // would silently re-anchor the corpus. #include "../src/core/package/bank_package.h" #include #include #include #include #include "../src/core/package/package_format.h" #include "../src/core/package/package_manifest.h" #include "package_fixtures.h" using namespace reasampler::package; using namespace reasampler::model; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // --- what the v1 fixture's bytes say ----------------------------------------- static constexpr const char* kV1File = "v1_shipping.rsbank"; static constexpr const char* kV1WriterSemver = "1.4.0"; static constexpr const char* kV1EntryName = "kick.wav"; static constexpr const char* kV1EntryHash = "8df36e805531e4af"; static constexpr std::uint64_t kV1PrefixSize = 907; static constexpr std::uint64_t kV1PayloadLength = 300; static constexpr std::uint64_t kV1TotalSize = 1207; // Every Sample field the shipping build wrote, with every optional PRESENT — so // "every known field intact" is a claim about the whole record, not a sampled few. // relativePath is the BARE transport name: export_plan normalizes it (see // src/core/package/CLAUDE.md), so a package carrying a directory component here would // mean the fixture predates that rule. static Sample expectedV1Sample() { Sample s; s.id = "cap-kick"; s.displayName = "Kick (wet)"; s.relativePath = "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 = 1; s.sampleRate = 48000; s.lengthSeconds = 0.0026666666666666666; s.lengthBeats = 0.00533; s.captureTempo = 120.5; s.captureTimeSigNum = 7; s.captureTimeSigDenom = 8; s.key = "F#m"; s.rootNote = 60; s.loop = LoopPoints{8, 120}; s.levels = {-0.3, -12.7, -14.0}; s.clipped = true; s.tier = Tier::Archive; s.contentHash = "Wcompatcorpus0001"; s.provenance = Provenance{"cap-parent", "fx-snapshot"}; s.createdTimestamp = 1754000000; return s; } static PackageManifest expectedV1Manifest() { PackageManifest m; m.bankDisplayName = "Compat Corpus"; m.exportTimestamp = 1754100000; m.entries.push_back({kV1EntryName, kV1PayloadLength, kV1EntryHash, expectedV1Sample()}); m.slots.append("cap-kick"); return m; } // A fixture that failed to open reads as an empty buffer, and an empty buffer decodes // Malformed — which would let half this file pass vacuously. Every suite loads through // here. static std::vector load(const char* name, std::size_t expectedSize) { std::vector bytes = packageFixtureBytes(name); if (bytes.size() != expectedSize) { std::printf("FAIL: fixture %s is %zu bytes, expected %zu (path: %s)\n", name, bytes.size(), expectedSize, packageFixturePath(name).c_str()); ++g_fail; } return bytes; } static bool containsToken(const std::vector& bytes, const std::string& token) { const std::string text(reinterpret_cast(bytes.data()), bytes.size()); return text.find(token) != std::string::npos; } static std::uint32_t le32At(const std::vector& bytes, std::size_t at) { return static_cast(bytes[at]) | (static_cast(bytes[at + 1]) << 8) | (static_cast(bytes[at + 2]) << 16) | (static_cast(bytes[at + 3]) << 24); } // --- direction 1: the shipping build reads what it wrote --------------------- // The header pair as the bytes carry it, classified without going through decode — the // ladder rule stated against the file rather than against the decoder's reading of it. static PackageReadability classifyFixture(const std::vector& bytes) { return classifyPackageVersion(le32At(bytes, 4), le32At(bytes, 8)); } static void testV1FixtureDecodesToTheRecordTheShippingBuildWrote() { const std::vector bytes = load(kV1File, kV1TotalSize); CHECK(classifyFixture(bytes) == PackageReadability::Readable); const DecodedPackage dec = decodePackage(bytes, bytes.size()); CHECK(dec.status == PackageReadability::Readable); CHECK(dec.header.formatVersion == 1); CHECK(dec.header.minReaderVersion == 1); CHECK(dec.header.writerVersion == kV1WriterSemver); CHECK(dec.manifest.entries.size() == 1); CHECK(dec.manifest.bankDisplayName == "Compat Corpus"); if (dec.manifest.entries.size() == 1) { CHECK(dec.manifest.entries[0].fileName == kV1EntryName); CHECK(dec.manifest.entries[0].byteLength == kV1PayloadLength); CHECK(dec.manifest.entries[0].byteHash == kV1EntryHash); CHECK(dec.manifest.entries[0].sample == expectedV1Sample()); } CHECK(dec.manifest == expectedV1Manifest()); CHECK(dec.prefixSize == kV1PrefixSize); CHECK(dec.layout.size() == 1); if (dec.layout.size() == 1) { CHECK(dec.layout[0].name == kV1EntryName); CHECK(dec.layout[0].offset == kV1PrefixSize); CHECK(dec.layout[0].length == kV1PayloadLength); } } // --- direction 1: a NEWER additive writer still reads ------------------------ // formatVersion N+1, minReaderVersion unchanged: the whole reason two integers exist. // The fixture carries three keys this build has never heard of — one at the manifest // root, one on the entry, one inside the nested Sample blob — and must still decode to // exactly what the v1 fixture decodes to. static void testAdditiveForwardFixtureReadsWithEveryKnownFieldIntact() { const std::vector bytes = load("additive_forward.rsbank", 1283); // Non-vacuity: the unknown keys are genuinely in the bytes, so the equality below // is "skipped without error", not "there was nothing to skip". CHECK(containsToken(bytes, "\"exportTool\"")); CHECK(containsToken(bytes, "\"futureEntryKey\"")); CHECK(containsToken(bytes, "\"futureSampleKey\"")); CHECK(classifyFixture(bytes) == PackageReadability::Readable); const DecodedPackage dec = decodePackage(bytes, bytes.size()); CHECK(dec.status == PackageReadability::Readable); CHECK(dec.header.formatVersion == kPackageFormatVersion + 1); CHECK(dec.header.minReaderVersion == kPackageMinReaderVersion); CHECK(dec.header.writerVersion == "1.9.0"); // Every known field, end to end: same manifest the v1 fixture yields. CHECK(dec.manifest == expectedV1Manifest()); CHECK(dec.layout.size() == 1); if (dec.layout.size() == 1) CHECK(dec.layout[0].length == kV1PayloadLength); } // --- direction 2: a structural newer writer is refused whole ----------------- static void testRefuseFixtureIsTooNewAndStillNamesTheWriter() { const std::vector bytes = load("refuse_structural.rsbank", kV1TotalSize); CHECK(classifyFixture(bytes) == PackageReadability::TooNew); const DecodedPackage dec = decodePackage(bytes, bytes.size()); CHECK(dec.status == PackageReadability::TooNew); // The three facts the refusal message owes the user. CHECK(dec.header.formatVersion == kPackageFormatVersion + 1); CHECK(dec.header.minReaderVersion == kPackageFormatVersion + 1); CHECK(dec.header.writerVersion == "1.9.0"); // Nothing else: no manifest, no layout, no partial success. CHECK(dec.manifest == PackageManifest{}); CHECK(dec.layout.empty()); CHECK(dec.prefixSize == 0); // The refusal is a DECISION, not an inability: this fixture's body is the v1 // fixture's own manifest, which parses. Walk the frozen header by hand to lift it // out — a reader at this version is forbidden from doing so, which is the point. const std::size_t manifestLenAt = 16 + le32At(bytes, 12); const std::uint32_t manifestLen = le32At(bytes, manifestLenAt); const std::string body(reinterpret_cast(bytes.data() + manifestLenAt + 4), manifestLen); CHECK(deserializeManifest(body).has_value()); } // --- truncation: Malformed at every site, never TooNew ----------------------- // One fixture per DISTINCT decode failure site rather than an arithmetic spread. The // RSBK layout is derived from the manifest's entries, not stored as its own section, so // the site a "mid-layout" cut maps to is the payload boundary: the manifest parses, the // layout computes, and the exact-size proof is what fails. struct Truncation { const char* file; std::size_t size; // the cut offset — the committed file IS this many bytes const char* site; }; static const Truncation kTruncations[] = { {"trunc_magic.rsbank", 2, "inside the 4-byte magic"}, {"trunc_version_pair.rsbank", 10, "inside the frozen header's minReaderVersion u32"}, {"trunc_writer_semver.rsbank", 18, "inside the frozen header's writer semver"}, {"trunc_manifest_length.rsbank", 23, "inside the manifest-length u32"}, {"trunc_manifest_body.rsbank", 466, "inside the manifest JSON"}, {"trunc_payload_start.rsbank", 907, "at the payload boundary — manifest parses, layout computes, exact-size proof fails"}, {"trunc_payload_middle.rsbank", 1057, "inside the first payload"}, {"trunc_one_short.rsbank", 1206, "one byte short of the total"}, }; static void testEveryTruncationIsMalformedNeverTooNew() { for (const Truncation& t : kTruncations) { const std::vector bytes = load(t.file, t.size); const DecodedPackage dec = decodePackage(bytes, bytes.size()); if (dec.status != PackageReadability::Malformed) { std::printf("FAIL: %s (%s) classified %s, expected Malformed\n", t.file, t.site, dec.status == PackageReadability::TooNew ? "TooNew" : "Readable"); ++g_fail; } // A refusal never half-succeeds, at any site. CHECK(dec.manifest.entries.empty()); CHECK(dec.layout.empty()); CHECK(dec.prefixSize == 0); // The shell hands decode a PREFIX plus the observed file size, not the whole // file — so the verdict has to survive that call shape too, at every cut the // incremental seam can actually satisfy. const auto need = requiredPrefixSize(bytes); if (need && *need <= bytes.size()) { const std::vector head(bytes.begin(), bytes.begin() + static_cast(*need)); CHECK(decodePackage(head, bytes.size()).status == PackageReadability::Malformed); } } } // --- hostile names: refused at decode, before any planner exists ------------- // The two fields carry DIFFERENT rules (src/core/package/CLAUDE.md): the entry name may // not express a path at all, while the nested relativePath is a path and is refused only // for traversal and absolute forms. Each fixture keeps the other field clean, so the // refusal is attributable to the field under test. struct HostileFixture { const char* file; std::size_t size; const char* offending; // the form as it reads once JSON-unescaped bool inEntryName; // false: the nested Sample's relativePath }; static const HostileFixture kHostiles[] = { {"hostile_name_dotdot.rsbank", 624, "..", true}, {"hostile_name_parent_slash.rsbank", 633, "../evil.wav", true}, {"hostile_name_parent_backslash.rsbank", 634, "..\\evil.wav", true}, {"hostile_name_subdir_slash.rsbank", 634, "sub/evil.wav", true}, {"hostile_name_drive_absolute.rsbank", 643, "C:\\Windows\\evil.wav", true}, {"hostile_name_unc_absolute.rsbank", 646, "\\\\srv\\share\\evil.wav", true}, {"hostile_path_dotdot_slash.rsbank", 641, "bank/../../evil.wav", false}, {"hostile_path_dotdot_backslash.rsbank", 640, "bank\\..\\evil.wav", false}, {"hostile_path_rooted.rsbank", 635, "/etc/evil.wav", false}, {"hostile_path_drive_absolute.rsbank", 643, "C:\\Windows\\evil.wav", false}, {"hostile_path_unc_absolute.rsbank", 646, "\\\\srv\\share\\evil.wav", false}, }; // A backslash rides the manifest JSON doubled; the corpus table above spells the // unescaped form, since that is what the naming rules are asked about. static std::string jsonEscaped(const std::string& s) { std::string out; for (char c : s) { if (c == '\\') out += '\\'; out += c; } return out; } static void testEveryHostileNameIsRefusedAtDecode() { // The clean spelling both fields hold in the fixture they are NOT under test in — // without this, a refusal could be coming from the wrong field. CHECK(isValidEntryName("kick.wav")); CHECK(isValidNestedSamplePath("kick.wav")); for (const HostileFixture& h : kHostiles) { const std::vector bytes = load(h.file, h.size); if (!containsToken(bytes, jsonEscaped(h.offending))) { std::printf("FAIL: %s does not carry the form it is named for (%s)\n", h.file, h.offending); ++g_fail; } // The format's own rule on the offending field, stated directly. if (h.inEntryName) CHECK(!isValidEntryName(h.offending)); else CHECK(!isValidNestedSamplePath(h.offending)); // Refused by the CODEC: decode yields no manifest at all, so there is nothing // for a planner to have been handed. planImport takes a PackageManifest, and // decode produced none. const DecodedPackage dec = decodePackage(bytes, bytes.size()); if (dec.status != PackageReadability::Malformed) { std::printf("FAIL: %s classified %d, expected Malformed\n", h.file, static_cast(dec.status)); ++g_fail; } CHECK(dec.manifest == PackageManifest{}); CHECK(dec.layout.empty()); CHECK(dec.prefixSize == 0); } } int main() { testV1FixtureDecodesToTheRecordTheShippingBuildWrote(); testAdditiveForwardFixtureReadsWithEveryKnownFieldIntact(); testRefuseFixtureIsTooNewAndStillNamesTheWriter(); testEveryTruncationIsMalformedNeverTooNew(); testEveryHostileNameIsRefusedAtDecode(); if (g_fail == 0) { std::printf("package_compat_tests: all passed\n"); return 0; } std::printf("package_compat_tests: %d failure(s)\n", g_fail); return 1; }