Files
psxrecomp/runtime/tests/test_mod_packages.cpp
T
2026-07-25 21:14:28 -07:00

258 lines
12 KiB
C++

#include "mod_packages.h"
#include "psx_sha256.h"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
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<uint8_t> 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<uint8_t> 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 package must not silently disable another package");
check(!reload.resolve("SLUS-TEST").ok,
"declared conflicts must fail resolution");
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.a", false, &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;
}