#include "mod_packages.h" #include "psx_sha256.h" #include #include #include #include #include #include namespace fs = std::filesystem; using namespace PSXRecompV4; static int failures; static void check(bool value, const char* message) { if (!value) { std::cerr << "FAIL: " << message << "\n"; failures++; } } static void write_text(const fs::path& path, const std::string& text) { fs::create_directories(path.parent_path()); std::ofstream out(path); out << text; } static void write_deflated_package(const fs::path& path) { static const char* compressed_hex = "4bcb2fca4d2c892f4b2d2acecccf53b05530e4ca4c01524a5599057ab9f929" "4a5c082925433d033d0325aebcc4dc541037ca3340c117a4a428b5383f07a80" "e2498929a9c93589458925996aac4151d5d9258949e5a121bcb950ed4140f313" "ad827345837c4353844890b00"; std::vector compressed; for (const char* p = compressed_hex; *p; p += 2) compressed.push_back((uint8_t)std::stoul(std::string(p, 2), nullptr, 16)); std::vector zip; auto le16 = [&](uint16_t v) { zip.push_back((uint8_t)v); zip.push_back((uint8_t)(v >> 8)); }; auto le32 = [&](uint32_t v) { le16((uint16_t)v); le16((uint16_t)(v >> 16)); }; const std::string name = "manifest.toml"; le32(0x04034b50); le16(20); le16(0); le16(8); le16(0); le16(0); le32(0x7d8454e1); le32((uint32_t)compressed.size()); le32(127); le16((uint16_t)name.size()); le16(0); zip.insert(zip.end(), name.begin(), name.end()); zip.insert(zip.end(), compressed.begin(), compressed.end()); const uint32_t central_offset = (uint32_t)zip.size(); le32(0x02014b50); le16(20); le16(20); le16(0); le16(8); le16(0); le16(0); le32(0x7d8454e1); le32((uint32_t)compressed.size()); le32(127); le16((uint16_t)name.size()); le16(0); le16(0); le16(0); le16(0); le32(0); le32(0); zip.insert(zip.end(), name.begin(), name.end()); const uint32_t central_size = (uint32_t)zip.size() - central_offset; le32(0x06054b50); le16(0); le16(0); le16(1); le16(1); le32(central_size); le32(central_offset); le16(0); fs::create_directories(path.parent_path()); std::ofstream out(path, std::ios::binary); out.write((const char*)zip.data(), (std::streamsize)zip.size()); } static std::string manifest(const std::string& id, const std::string& version, const std::string& extra = {}) { return "format_version = 1\n" "id = \"" + id + "\"\n" "version = \"" + version + "\"\n" "name = \"" + id + "\"\n" "resolver = \"declarative\"\n" "[[target]]\n" "game_id = \"SLUS-TEST\"\n" + extra; } int main() { const fs::path root = fs::temp_directory_path() / "psxrecomp-mod-package-test"; std::error_code ec; fs::remove_all(root, ec); { const uint8_t abc[] = {'a', 'b', 'c'}; uint8_t one_shot[32], streamed[32]; psx_sha256_compute(abc, sizeof(abc), one_shot); psx_sha256_ctx hash; psx_sha256_init(&hash); psx_sha256_update(&hash, abc, 1); psx_sha256_update(&hash, abc + 1, 2); psx_sha256_final(&hash, streamed); check(std::equal(one_shot, one_shot + 32, streamed), "streaming SHA-256 must match one-shot hashing"); } write_text(root / "packages/base.mod/1.0.0/manifest.toml", manifest("base.mod", "1.0.0", "\n[[option]]\n" "id = \"difficulty\"\n" "label = \"Difficulty\"\n" "type = \"choice\"\n" "default = \"normal\"\n" "[[option.choice]]\nvalue = \"normal\"\nlabel = \"Normal\"\n" "[[option.choice]]\nvalue = \"hard\"\nlabel = \"Hard\"\n" "[[patch]]\n" "target = \"main_exe\"\n" "address = 2147487744\n" "expected = \"01 02 03 04\"\n" "replace = \"05 06 07 08\"\n" "when_option = \"difficulty\"\n" "when_value = \"hard\"\n" "[[derived_disc]]\n" "kind = \"vcdiff\"\n" "patch = \"assets/base.xdelta3\"\n" "patch_sha256 = \"0000000000000000000000000000000000000000000000000000000000000000\"\n" "output_size = 123456\n" "output_sha256 = \"1111111111111111111111111111111111111111111111111111111111111111\"\n" "when_option = \"difficulty\"\n" "when_value = \"hard\"\n")); write_text(root / "packages/base.mod/1.0.0/assets/base.xdelta3", "test"); write_text(root / "packages/addon.mod/2.0.0/manifest.toml", manifest("addon.mod", "2.0.0", "\n[[dependency]]\nid = \"base.mod\"\nversion = \"^1.0.0\"\n")); ModPackageManager manager(root); std::string error; check(manager.scan(&error), error.c_str()); write_deflated_package(root / "zip.psxmod"); check(manager.install_archive(root / "zip.psxmod", nullptr, nullptr, &error), error.c_str()); check(manager.packages().count("zip.mod") == 1, "deflated .psxmod must install"); if (const char* external = std::getenv("PSXMOD_TEST_ARCHIVE"); external && external[0]) { std::string installed_id, installed_version; check(manager.install_archive(external, &installed_id, &installed_version, &error), error.c_str()); check(!installed_id.empty() && !installed_version.empty(), "external package must report installed identity"); } check(manager.load_state(&error), error.c_str()); check(manager.set_enabled("addon.mod", true, &error), error.c_str()); ModResolution missing = manager.resolve("SLUS-TEST"); check(!missing.ok, "missing dependency must fail resolution"); check(manager.set_enabled("base.mod", true, &error), error.c_str()); check(manager.set_option("base.mod", "difficulty", "hard", &error), error.c_str()); check(!manager.set_option("base.mod", "difficulty", "impossible", &error), "invalid choice must be rejected"); ModResolution resolved = manager.resolve("SLUS-TEST"); check(resolved.ok, "valid dependency graph must resolve"); check(resolved.ordered.size() == 2, "two packages should resolve"); check(resolved.ordered.size() == 2 && resolved.ordered[0]->id == "base.mod", "dependency must precede dependent"); check(resolved.writes.size() == 1, "selected declarative patch must resolve"); check(resolved.writes.size() == 1 && resolved.writes[0].location == 0x80001000ull && resolved.writes[0].replacement[0] == 5, "resolved write must retain guest address and bytes"); check(resolved.derived_discs.size() == 1 && resolved.derived_discs[0].output_size == 123456, "selected derived-disc recipe must resolve"); check(resolved.fingerprint.size() == 64, "plan fingerprint must be SHA-256 hex"); const std::string fingerprint = resolved.fingerprint; check(manager.save_state(&error), error.c_str()); ModPackageManager reload(root); check(reload.scan(&error), error.c_str()); check(reload.load_state(&error), error.c_str()); check(reload.resolve("SLUS-TEST").fingerprint == fingerprint, "saved state must resolve deterministically"); check(!reload.remove_version("base.mod", "1.0.0", &error), "active package cannot be removed"); check(reload.set_enabled("base.mod", false, &error), error.c_str()); check(!reload.remove_version("base.mod", "1.0.0", &error), "enabled dependent must protect required version"); check(reload.set_enabled("addon.mod", false, &error), error.c_str()); check(reload.remove_version("base.mod", "1.0.0", &error), error.c_str()); write_text(root / "packages/conflict.a/1.0.0/manifest.toml", "format_version = 1\n" "id = \"conflict.a\"\n" "version = \"1.0.0\"\n" "name = \"conflict.a\"\n" "resolver = \"declarative\"\n" "conflicts = [\"conflict.b\"]\n" "[[target]]\n" "game_id = \"SLUS-TEST\"\n"); write_text(root / "packages/conflict.b/1.0.0/manifest.toml", manifest("conflict.b", "1.0.0")); check(reload.scan(&error), error.c_str()); check(reload.set_enabled("conflict.a", true, &error), error.c_str()); check(reload.set_enabled("conflict.b", true, &error), error.c_str()); check(!reload.selections().at("conflict.a").enabled && reload.selections().at("conflict.b").enabled, "enabling a conflicting package must disable the previous package"); write_text(root / "packages/matrix.mod/1.0.0/manifest.toml", manifest("matrix.mod", "1.0.0", "\n[[option]]\n" "id = \"title\"\n" "label = \"Title\"\n" "type = \"choice\"\n" "default = \"mega\"\n" "[[option.choice]]\n" "value = \"mega\"\n" "label = \"Mega\"\n" "[[option.choice]]\n" "value = \"rockman\"\n" "label = \"Rockman\"\n" "\n[[option]]\n" "id = \"script\"\n" "label = \"Script\"\n" "type = \"choice\"\n" "default = \"original\"\n" "[[option.choice]]\n" "value = \"original\"\n" "label = \"Original\"\n" "[[option.choice]]\n" "value = \"retranslation\"\n" "label = \"Retranslation\"\n" "\n[[derived_disc]]\n" "kind = \"vcdiff\"\n" "patch = \"assets/matrix.xdelta3\"\n" "patch_sha256 = \"2222222222222222222222222222222222222222222222222222222222222222\"\n" "output_size = 222222\n" "output_sha256 = \"3333333333333333333333333333333333333333333333333333333333333333\"\n" "when = { title = \"rockman\", script = \"retranslation\" }\n")); write_text(root / "packages/matrix.mod/1.0.0/assets/matrix.xdelta3", "test"); check(reload.scan(&error), error.c_str()); check(reload.set_enabled("conflict.b", false, &error), error.c_str()); check(reload.set_enabled("matrix.mod", true, &error), error.c_str()); check(reload.set_option("matrix.mod", "title", "rockman", &error), error.c_str()); check(reload.set_option("matrix.mod", "script", "retranslation", &error), error.c_str()); ModResolution matrix = reload.resolve("SLUS-TEST"); check(matrix.ok && matrix.derived_discs.size() == 1 && matrix.derived_discs[0].output_size == 222222, "multi-option derived-disc condition must match selected values"); ModPackage invalid; write_text(root / "bad.toml", "format_version=1\nid=\"../bad\"\nversion=\"1.0.0\"\nname=\"Bad\"\n" "[[target]]\ngame_id=\"SLUS-TEST\"\n"); check(!ModPackageManager::read_manifest(root / "bad.toml", invalid, &error), "unsafe package id must be rejected"); fs::remove_all(root, ec); if (failures) { std::cerr << failures << " mod package test(s) failed\n"; return 1; } std::cout << "mod package tests passed\n"; return 0; }