Wire warm preboot readiness and cover cache invalidation contracts

This commit is contained in:
Matthew Stanley
2026-10-07 09:25:56 -07:00
parent 0fb60d2617
commit c124f6ba52
6 changed files with 380 additions and 37 deletions
+210
View File
@@ -1,4 +1,5 @@
#include "mod_runtime.h"
#include "host_file_identity.h"
#include "mod_packages.h"
#include "mod_plugins.h"
#include "psx_sha256.h"
@@ -10,6 +11,8 @@
#include <array>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
#include <filesystem>
#include <fstream>
#include <iostream>
@@ -288,6 +291,15 @@ static std::string sha256_hex(const std::vector<uint8_t>& bytes) {
return out;
}
extern "C" int psx_mod_counters_snapshot(const char**, uint64_t*, uint64_t*, int, uint64_t*);
static uint64_t startup_counter(const char* requested) {
const char* names[128]{}; uint64_t counts[128]{}, frames[128]{};
const int count = psx_mod_counters_snapshot(names, counts, frames, 128, nullptr);
for (int i = 0; i < count && i < 128; ++i)
if (std::string(names[i]) == requested) return counts[i];
return 0;
}
int main() {
const fs::path root = fs::temp_directory_path() / "psxrecomp-mod-runtime-test";
std::error_code ec;
@@ -1325,6 +1337,204 @@ int main() {
check(psx_mod_read_disc_file("S0/LEVEL.NSF", nullptr, 0, &bytes) && bytes == 69400576u,
"files grown past 64 MiB keep their effective size");
}
// Reliable identity detects byte replacement even when size/mtime are restored.
{
const auto identity_path = root / "identity.bin";
write_bytes(identity_path, {1, 2, 3, 4});
const auto original_time = fs::last_write_time(identity_path);
std::string before, after;
const bool reliable = PSXRecompV4::host_file_identity(identity_path, before);
write_bytes(identity_path, {4, 3, 2, 1});
fs::last_write_time(identity_path, original_time);
check(!reliable || (PSXRecompV4::host_file_identity(identity_path, after) && before != after),
"file identity rejects same-size restored-mtime replacement");
check(!PSXRecompV4::host_file_identity(root, after), "directories are never file cache identities");
check(!PSXRecompV4::host_file_identity(root / "absent.bin", after), "missing identity is a cache miss");
}
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
{
const auto cached_root = root / "audited-cache";
const auto output_path = root / "audited-output.z64";
const auto input_path = root / "audited-input.bin";
const auto disc = root / "audited-disc.iso";
fs::copy_file(iso_path, disc, fs::copy_options::overwrite_existing);
write_bytes(input_path, {1,2,3,4});
std::string input_receipt, output_receipt;
int prepare_calls = 0;
bool throw_probe = false, mutate_on_resolve = false, throw_resolver = false;
const std::string manifest =
"format_version = 9\nid = \"cache.test\"\nversion = \"1.0.0\"\nname = \"Cache\"\nprepare = \"test.cache.prepare\"\nresolver = \"builtin:test.cache.resolve\"\n"
"[[target]]\ngame_id = \"READER\"\n"
"[[feature]]\nid = \"active\"\nname = \"Active\"\ndefault_enabled = true\n"
"[[resource]]\nfeature = \"active\"\nid = \"source\"\nlabel = \"Source\"\ninput_only = true\nshared_source = \"cache.original\"\nrequired = true\n"
"[[resource]]\nfeature = \"active\"\nid = \"rom\"\nlabel = \"ROM\"\nformat = \"n64-rom\"\nhidden = true\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n"
"[[feature]]\nid = \"second\"\nname = \"Second\"\ndefault_enabled = false\n"
"[[resource]]\nfeature = \"second\"\nid = \"rom\"\nlabel = \"ROM\"\nformat = \"n64-rom\"\nhidden = true\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n";
check(PSXRecompV4::mod_register_builtin_resolver("test.cache.resolve",
[&](const auto&, const auto&, const auto&, auto&, auto&) {
if (throw_resolver) throw std::runtime_error("resolver fixture exception");
if (mutate_on_resolve) write_bytes(input_path, {7,7,7,7});
return true;
}), "register resolver for mutation and unwind regression");
const auto manifest_path = cached_root / "packages/cache.test/1.0.0/manifest.toml";
write_text(manifest_path, manifest);
check(PSXRecompV4::mod_register_media_preparer("test.cache.prepare",
[&](const PSXRecompV4::ModPrepareContext&, std::map<std::string, fs::path>& outputs, std::string&) {
++prepare_calls;
write_bytes(output_path, rom);
PSXRecompV4::host_file_identity(input_path, input_receipt);
PSXRecompV4::host_file_identity(output_path, output_receipt);
outputs["rom"] = output_path;
return true;
},
[&](const PSXRecompV4::ModPrepareContext& context, std::map<std::string, fs::path>& outputs, std::string&) {
if (throw_probe) throw std::runtime_error("probe fixture exception");
std::string input, output;
if (context.feature_id == "second" ||
!PSXRecompV4::host_file_identity(input_path, input) || input != input_receipt ||
!PSXRecompV4::host_file_identity(output_path, output) || output != output_receipt) return false;
outputs["rom"] = output_path; return true;
}), "register audited preparation and cheap identity probe");
PSXRecompV4::ModPackageManager source_selection(cached_root);
check(source_selection.scan(&error) &&
source_selection.set_feature_resource_path("cache.test", "active", "source", input_path, &error) &&
source_selection.save_state(&error), "persist shared input selection for audited fixture");
PSXRecompV4::mod_runtime_end_netplay(); test_match_caps = {};
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "audited initialize");
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 0,
"cold cached-only launch does not prepare missing receipt");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == 1,
"cold launcher creates verified preparation");
const auto reused = startup_counter("startup.plan_reuse");
const auto hashed = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) &&
PSXRecompV4::mod_runtime_commit(disc, &error) && prepare_calls == 1 &&
startup_counter("startup.plan_reuse") == reused + 2 && startup_counter("startup.disc_hash") == hashed,
"duplicate provider and main commit reuse a prepared plan without converter or hash");
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 1 &&
startup_counter("startup.disc_hash") == hashed,
"warm initialize preloads receipts with no preparer or raw hash");
mod_runtime_activate_plugins();
check(PSXRecompV4::mod_runtime_commit(disc, &error) && prepare_calls == 2,
"activation invalidates the preboot ticket");
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
PSXRecompV4::mod_runtime_try_prepare_cached(disc), "restore warm plan for source invalidation");
const auto input_time = fs::last_write_time(input_path);
write_bytes(input_path, {4,3,2,1}); fs::last_write_time(input_path, input_time);
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "external shared input replacement invalidates title receipt");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == 3,
"changed source goes through full preparation");
auto corrupt_rom = rom; corrupt_rom[32] ^= 1; write_bytes(output_path, corrupt_rom);
// Even if the probe claims the file is current, the first warm resolve
// must verify output bytes against the declared SHA.
PSXRecompV4::host_file_identity(output_path, output_receipt);
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 3,
"warm output corruption fails immutable-byte verification");
fs::remove(output_path);
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 3,
"missing output fails without invoking converter");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == 4,
"full preparation repairs missing output");
throw_probe = true;
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "throwing cached probe fails safely");
throw_probe = false;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error), "cached flag restored after failure");
// Probe results are transactional across multiple enabled features.
PSXRecompV4::ModPackageManager transactional(cached_root);
check(transactional.scan(&error) && transactional.load_state(&error) &&
transactional.set_feature_enabled("cache.test", "second", true, &error) &&
transactional.set_feature_resource_path("cache.test", "active", "rom", rom_path, &error), "enable second probe fixture");
const auto original_output = transactional.feature_resource_path("cache.test", "active", "rom");
check(!transactional.prepare_resources("READER", disc, root / "cache", &error, true) &&
transactional.feature_resource_path("cache.test", "active", "rom") == original_output,
"later cache miss discards earlier pending output selections");
// A throw inside resolve must restore cached-only/provider modes.
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "invalidate ticket for resolver exception fixture");
throw_resolver = true;
bool threw = false;
try { PSXRecompV4::mod_runtime_try_prepare_cached(disc); }
catch (const std::runtime_error&) { threw = true; }
throw_resolver = false;
check(threw, "resolver exception reaches caller for unwind fixture");
fs::remove(output_path);
const auto before_unwind = prepare_calls;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == before_unwind + 1,
"cached-only mode restored after resolve exception");
// A dependency changed during resolution must never publish readiness.
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "invalidate ticket for resolver mutation fixture");
const auto before_mutation = prepare_calls;
mutate_on_resolve = true;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error), "full resolution can finish with concurrent input mutation");
mutate_on_resolve = false;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == before_mutation + 2,
"unstable resolve never publishes a reusable ticket");
// Altering a syntactically valid digest must fail its receipt checksum.
std::filesystem::path cache_dir;
#if defined(_WIN32)
if (const char* local = std::getenv("LOCALAPPDATA")) cache_dir = local;
#else
if (const char* cache = std::getenv("XDG_CACHE_HOME")) cache_dir = cache;
else if (const char* home_dir = std::getenv("HOME")) cache_dir = fs::path(home_dir) / ".cache";
#endif
if (cache_dir.empty()) cache_dir = fs::temp_directory_path();
cache_dir /= "psxrecomp/imports/disc-digests-v2";
bool corrupted_receipt = false;
for (const auto& entry : fs::directory_iterator(cache_dir)) {
std::ifstream saved(entry.path(), std::ios::binary);
std::string receipt(std::istreambuf_iterator<char>(saved), {});
if (receipt.find(disc.generic_string()) == std::string::npos || receipt.size() < 130) continue;
const size_t digest_offset = receipt.size() - 130;
receipt[digest_offset] = receipt[digest_offset] == '0' ? '1' : '0';
write_text(entry.path(), receipt); corrupted_receipt = true;
}
check(corrupted_receipt && PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "corrupt well-formed digest receipt misses warm startup");
const auto before_corrupt_hash = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) &&
startup_counter("startup.disc_hash") == before_corrupt_hash + 1,
"full launch repairs corrupted digest with authoritative source hash");
// Catalog changes cannot certify stale in-memory parsed manifests.
write_text(manifest_path, manifest + "\n# external catalog mutation\n");
check(!PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) &&
error.find("catalog changed") != std::string::npos,
"changed manifest requires rescan instead of resealing stale package");
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "explicit initialize rescans changed catalog");
const int before_netplay = prepare_calls;
const auto net_hash = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_commit_for_direct_netplay(disc, &error) &&
prepare_calls == before_netplay + 1 && startup_counter("startup.disc_hash") > net_hash,
"netplay ignores preboot ticket and hashes the source fully");
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "active netplay never preloads an offline ticket");
PSXRecompV4::mod_runtime_end_netplay();
// Legacy preparers have no cheap receipt and must never claim warm readiness.
const auto legacy_root = root / "legacy-cache";
int legacy_calls = 0;
check(PSXRecompV4::mod_register_media_preparer("test.legacy.prepare",
[&](const PSXRecompV4::ModPrepareContext&, std::map<std::string, fs::path>& outputs, std::string&) {
++legacy_calls; outputs["rom"] = output_path; return true;
}), "register legacy preparer without probe");
std::string legacy_manifest = manifest;
legacy_manifest.replace(legacy_manifest.find("test.cache.prepare"), std::string("test.cache.prepare").size(), "test.legacy.prepare");
write_text(legacy_root / "packages/cache.test/1.0.0/manifest.toml", legacy_manifest);
check(PSXRecompV4::mod_runtime_initialize(legacy_root, "READER", 0, {}, &error) &&
!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && legacy_calls == 0,
"legacy preparer always misses cached-only startup");
}
#else
{
const auto defaults = root / "default-hashing";
fs::create_directories(defaults);
check(PSXRecompV4::mod_runtime_initialize(defaults, "READER", 0, {}, &error), "default hashing fixture initialize");
const auto hash_count = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_commit(iso_path, &error) &&
PSXRecompV4::mod_runtime_commit(iso_path, &error) &&
startup_counter("startup.disc_hash") == hash_count + 1,
"default runtime preserves hash-only-on-selected-path-change behavior");
check(!PSXRecompV4::mod_runtime_try_prepare_cached(iso_path), "default runtime does not opt into cached startup");
}
#endif
fs::remove_all(root, ec);
if (failures) return 1;
std::cout << "mod runtime tests passed\n";