// The corpus driven through both verbs — no REAPER, no framework. Where // test_package_compat asserts what the frozen bytes DECODE to, this file asserts what // the import and export verbs DO with them: the payload bytes survive a full // export -> import -> export, and every refusal in the corpus refuses before a planner // or a filesystem write is reached. #include "../src/shell/package/import_landing.h" #include #include #include #include #include #include #include "../src/core/package/bank_package.h" #include "../src/shell/package/export_bank.h" #include "../src/shell/package/package_path.h" #include "../src/shell/persist/session.h" #include "package_fixtures.h" using namespace reasampler; namespace fs = std::filesystem; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) static const char* kTag = "1754000000"; static const char* kImportedBankId = "bank-imported"; // --- scratch project --------------------------------------------------------- // One project directory per suite, torn down after, so no suite observes another's // bank folder. class Scratch { public: explicit Scratch(const std::string& name) { std::error_code ec; dir_ = pathToUtf8(fs::temp_directory_path(ec) / utf8Path("reasampler_compat_" + name)); fs::remove_all(utf8Path(dir_), ec); fs::create_directories(utf8Path(dir_), ec); } ~Scratch() { std::error_code ec; fs::remove_all(utf8Path(dir_), ec); } const std::string& projectDir() const { return dir_; } std::string bankDir() const { return package::bankFolderDir(dir_); } std::string importPath() const { return dir_ + "/in.rsbank"; } std::string exportPath() const { return dir_ + "/out.rsbank"; } private: std::string dir_; }; static std::vector readBytes(const std::string& path) { std::ifstream f(utf8Path(path), std::ios::binary); return std::vector(std::istreambuf_iterator(f), std::istreambuf_iterator()); } static void writeBytes(const std::string& path, const std::vector& bytes) { std::ofstream f(utf8Path(path), std::ios::binary | std::ios::trunc); f.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); } // Copies a committed fixture to the path the verb will be pointed at. Fails loudly on a // size mismatch (empty included): an unreadable OR mangled corpus would otherwise let // every "must refuse" suite pass, since a garbled fixture still refuses, just not for // the reason under test. static bool stageFixture(const Scratch& scratch, const char* fixture, std::size_t expectedSize) { const std::vector bytes = packageFixtureBytes(fixture); if (bytes.size() != expectedSize) { std::printf("FAIL: fixture %s is %zu bytes, expected %zu (path: %s)\n", fixture, bytes.size(), expectedSize, packageFixturePath(fixture).c_str()); ++g_fail; return false; } writeBytes(scratch.importPath(), bytes); return true; } // Each entry's payload bytes, sliced out of a whole package file by its own layout. static std::vector> payloadsOf(const std::vector& file) { std::vector> out; const package::DecodedPackage dec = package::decodePackage(file, file.size()); if (dec.status != package::PackageReadability::Readable) return out; for (const package::PackageEntrySpan& span : dec.layout) { const auto begin = file.begin() + static_cast(span.offset); out.emplace_back(begin, begin + static_cast(span.length)); } return out; } // --- the sequence the import verb runs, minus REAPER ------------------------- static ImportLanding runImport(const Scratch& scratch, ReaSamplerSession& session, int* outBirths = nullptr) { LandedFileJournal journal; ImportLanding landing = landPackage(scratch.importPath(), scratch.projectDir(), session.book(), kTag, journal); if (landing.outcome != ImportOutcome::Landed) return landing; int births = 0; const bool applied = applyImportedBank(session.book(), kImportedBankId, landing.plan, [&births](const model::Sample&) { ++births; }); CHECK(applied); if (applied) journal.markIndexCommitted(); if (outBirths) *outBirths = births; return landing; } // --- the round-trip anchor --------------------------------------------------- // The fixture IS the first export (written by the shipping build's own export verb), so // importing and re-exporting it closes export -> import -> export over frozen bytes. static void testV1FixtureReExportsByteIdenticalPayloads() { Scratch scratch("roundtrip"); if (!stageFixture(scratch, "v1_shipping.rsbank", 1207)) return; ReaSamplerSession session; int births = 0; const ImportLanding landing = runImport(scratch, session, &births); CHECK(landing.outcome == ImportOutcome::Landed); CHECK(births == 1); CHECK(landing.plan.landCount == 1); if (landing.plan.entries.size() != 1) { CHECK(false); return; } const std::vector> sourcePayloads = payloadsOf(packageFixtureBytes("v1_shipping.rsbank")); if (sourcePayloads.size() != 1) { CHECK(false); return; } // The landed file is the package's payload verbatim — the first half of the claim. const std::string landed = scratch.bankDir() + "/" + landing.plan.entries[0].destFileName; CHECK(readBytes(landed) == sourcePayloads[0]); ExportRequest req; req.projectDir = scratch.projectDir(); req.bankId = kImportedBankId; req.destAbsPath = scratch.exportPath(); req.exportTimestamp = 1754200000; const ExportOutcome out = exportBank(session, req); CHECK(out.status == ExportStatus::Written); CHECK(out.entriesWritten == 1); // The second half: the re-export's payloads, byte for byte. Entry NAMES may legally // differ across the trip — the importer re-spells a bank file and the exporter mints // its own transport name (src/core/package/CLAUDE.md) — the payload bytes may not. CHECK(payloadsOf(readBytes(scratch.exportPath())) == sourcePayloads); } // --- the refusals, at the verb rather than the codec ------------------------- static void testRefuseFixtureRefusesTheWholeImportAndNamesTheWriter() { Scratch scratch("refuse"); if (!stageFixture(scratch, "refuse_structural.rsbank", 1207)) return; ReaSamplerSession session; const BankBook before = session.book(); LandedFileJournal journal; const ImportLanding landing = landPackage(scratch.importPath(), scratch.projectDir(), session.book(), kTag, journal); CHECK(landing.outcome == ImportOutcome::TooNew); // Literal, not kPackageFormatVersion-relative: this is the FIXTURE's frozen pair // (2/2), not this build's (see test_package_compat.cpp's file header note). CHECK(landing.header.formatVersion == 2); CHECK(landing.header.minReaderVersion == 2); CHECK(landing.header.writerVersion == "1.9.0"); // Nothing planned, nothing on disk, nothing in the index. CHECK(landing.plan.entries.empty()); CHECK(!fs::exists(utf8Path(scratch.bankDir()))); CHECK(session.book() == before); } // The same eight cuts test_package_compat classifies, driven through the verb's // incremental prefix reader — the one caller that can ask requiredPrefixSize for more // bytes than the file holds. Sizes match test_package_compat.cpp's kTruncations. struct TruncationFixture { const char* file; std::size_t size; }; static const TruncationFixture kTruncationFixtures[] = { {"trunc_magic.rsbank", 2}, {"trunc_version_pair.rsbank", 10}, {"trunc_writer_semver.rsbank", 18}, {"trunc_manifest_length.rsbank", 23}, {"trunc_manifest_body.rsbank", 466}, {"trunc_payload_start.rsbank", 907}, {"trunc_payload_middle.rsbank", 1057}, {"trunc_one_short.rsbank", 1206}, }; static void testEveryTruncationRefusesTheImportAsMalformed() { for (const TruncationFixture& fixture : kTruncationFixtures) { Scratch scratch(std::string("trunc_") + fixture.file); if (!stageFixture(scratch, fixture.file, fixture.size)) continue; ReaSamplerSession session; const BankBook before = session.book(); LandedFileJournal journal; const ImportLanding landing = landPackage(scratch.importPath(), scratch.projectDir(), session.book(), kTag, journal); if (landing.outcome != ImportOutcome::Malformed) { std::printf("FAIL: %s imported as outcome %d, expected Malformed\n", fixture.file, static_cast(landing.outcome)); ++g_fail; } CHECK(landing.plan.entries.empty()); CHECK(!fs::exists(utf8Path(scratch.bankDir()))); CHECK(session.book() == before); } } // Sizes match test_package_compat.cpp's kHostiles. struct HostileFixtureFile { const char* file; std::size_t size; }; static const HostileFixtureFile kHostileFixtures[] = { {"hostile_name_dotdot.rsbank", 624}, {"hostile_name_parent_slash.rsbank", 633}, {"hostile_name_parent_backslash.rsbank", 634}, {"hostile_name_subdir_slash.rsbank", 634}, {"hostile_name_drive_absolute.rsbank", 643}, {"hostile_name_unc_absolute.rsbank", 646}, {"hostile_path_dotdot_slash.rsbank", 641}, {"hostile_path_dotdot_backslash.rsbank", 640}, {"hostile_path_rooted.rsbank", 635}, {"hostile_path_drive_absolute.rsbank", 643}, {"hostile_path_unc_absolute.rsbank", 646}, }; static void testEveryHostileNameIsRefusedBeforeThePlannerRuns() { for (const HostileFixtureFile& fixture : kHostileFixtures) { Scratch scratch(std::string("hostile_") + fixture.file); if (!stageFixture(scratch, fixture.file, fixture.size)) continue; ReaSamplerSession session; const BankBook before = session.book(); LandedFileJournal journal; const ImportLanding landing = landPackage(scratch.importPath(), scratch.projectDir(), session.book(), kTag, journal); if (landing.outcome != ImportOutcome::Malformed) { std::printf("FAIL: %s imported as outcome %d, expected Malformed\n", fixture.file, static_cast(landing.outcome)); ++g_fail; } // planImport is the ONLY producer of a non-empty plan and it runs after the // decode — an empty one is how "refused before any planner" is observed here. CHECK(landing.plan.entries.empty()); CHECK(landing.plan.bankDisplayName.empty()); CHECK(!fs::exists(utf8Path(scratch.bankDir()))); CHECK(session.book() == before); } } int main() { testV1FixtureReExportsByteIdenticalPayloads(); testRefuseFixtureRefusesTheWholeImportAndNamesTheWriter(); testEveryTruncationRefusesTheImportAsMalformed(); testEveryHostileNameIsRefusedBeforeThePlannerRuns(); if (g_fail == 0) { std::printf("package_round_trip_tests: all passed\n"); return 0; } std::printf("package_round_trip_tests: %d failure(s)\n", g_fail); return 1; }