diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 6750d47d..45bb27b9 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -1447,6 +1447,86 @@ extern "C" void mod_runtime_enable_disc_patches(void) { PSXRecompV4::state().disc_enabled = true; } +static void patch_committed_disc_sector(uint32_t lba, int raw_sector, + uint8_t* bytes, uint32_t size); + +/* The largest file a CD can hold (80-minute disc, 360,000 sectors). Mods may + * grow an archive past its original extent, e.g. into a padding file. */ +static constexpr uint32_t kMaxDiscFileBytes = 360000u * 2048u; + +/* One 2048-byte user-data sector of the effective disc: the committed plan's + * raw and user-data writes/overlays applied. Form 2 (XA) sectors are refused. */ +static bool read_effective_user_sector(PS1::ISOReader& reader, uint32_t lba, + uint8_t* sector) { + uint8_t raw[2352]; + if (reader.ReadRawSector(lba, raw)) { + if (raw[15] != 1 && (raw[15] != 2 || (raw[18] & 0x20u))) return false; + patch_committed_disc_sector(lba, 1, raw, sizeof raw); + std::memcpy(sector, raw + (raw[15] == 1 ? 16 : 24), 2048); + if (raw[15] == 1) patch_committed_disc_sector(lba, 0, sector, 2048); + } else { + if (!reader.ReadSector(lba, sector)) return false; + patch_committed_disc_sector(lba, 0, sector, 2048); + } + return !PSXRecompV4::state().disc_guard_failed; +} + +/* ISO 9660 path lookup through the effective directory records. Mods may + * relocate or grow a file by patching its directory record (e.g. an archive + * extended into the following padding file); the emulated drive serves those + * patched records, so host reads must resolve paths through them too. Names + * compare case-insensitively without the ";1" version suffix. */ +static bool find_effective_file(PS1::ISOReader& reader, const std::string& path, + uint32_t& lba, uint32_t& bytes, bool& directory) { + uint8_t sector[2048]; + auto le32 = [](const uint8_t* p) { + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24); + }; + if (!read_effective_user_sector(reader, 16, sector) || sector[0] != 1 || + std::memcmp(sector + 1, "CD001", 5) != 0) return false; + lba = le32(sector + 156 + 2); + bytes = le32(sector + 156 + 10); + directory = true; + size_t at = 0; + while (at < path.size()) { + size_t next = path.find_first_of("/\\", at); + if (next == std::string::npos) next = path.size(); + std::string want = path.substr(at, next - at); + at = next + 1; + if (want.empty()) continue; + if (!directory) return false; + std::transform(want.begin(), want.end(), want.begin(), + [](unsigned char c) { return (char)std::toupper(c); }); + bool found = false; + const uint32_t sectors = (bytes + 2047u) / 2048u; + for (uint32_t i = 0; i < sectors && !found; ++i) { + if (!read_effective_user_sector(reader, lba + i, sector)) return false; + for (uint32_t p = 0; p < 2048u && sector[p] && !found;) { + const uint32_t length = sector[p]; + if (length < 34 || p + length > 2048u || + 33u + sector[p + 32] > length) return false; + std::string name(reinterpret_cast(sector + p + 33), + sector[p + 32]); + name = name.substr(0, name.find(';')); + std::transform(name.begin(), name.end(), name.begin(), + [](unsigned char c) { return (char)std::toupper(c); }); + if (sector[p + 32] == 1 && (sector[p + 33] == 0 || sector[p + 33] == 1)) + name.clear(); + if (!name.empty() && name == want) { + lba = le32(sector + p + 2); + bytes = le32(sector + p + 10); + directory = (sector[p + 25] & 2u) != 0; + found = true; + } + p += length; + } + } + if (!found) return false; + } + return true; +} + extern "C" int psx_mod_read_disc_file(const char* path, void* buffer, uint32_t capacity, uint32_t* size) { using namespace PSXRecompV4; @@ -1457,29 +1537,21 @@ extern "C" int psx_mod_read_disc_file(const char* path, void* buffer, const auto& mount = s.effective_disc_path.empty() ? s.disc_path : s.effective_disc_path; if (mount.empty()) return 0; PS1::ISOReader reader; - PS1::ISOFileEntry entry; - if (!reader.Open(mount.string()) || !reader.FindFile(path, entry) || - entry.is_directory || !entry.size || entry.size > 64u * 1024u * 1024u) + uint32_t lba = 0, bytes = 0; + bool directory = false; + if (!reader.Open(mount.string()) || + !find_effective_file(reader, path, lba, bytes, directory) || + directory || !bytes || bytes > kMaxDiscFileBytes) return 0; - if (!buffer) { *size = entry.size; return 1; } - if (capacity < entry.size) return 0; - uint8_t sector[2048], raw[2352]; - for (uint32_t offset = 0; offset < entry.size; offset += 2048u) { - const uint32_t lba = entry.lba + offset / 2048u; - if (reader.ReadRawSector(lba, raw)) { - if (raw[15] != 1 && (raw[15] != 2 || (raw[18] & 0x20u))) return 0; - mod_runtime_patch_disc_sector(lba, 1, raw, sizeof raw); - std::memcpy(sector, raw + (raw[15] == 1 ? 16 : 24), sizeof sector); - if (raw[15] == 1) mod_runtime_patch_disc_sector(lba, 0, sector, sizeof sector); - } else { - if (!reader.ReadSector(lba, sector)) return 0; - mod_runtime_patch_disc_sector(lba, 0, sector, sizeof sector); - } - if (state().disc_guard_failed) return 0; - const uint32_t count = std::min(2048u, entry.size - offset); + if (!buffer) { *size = bytes; return 1; } + if (capacity < bytes) return 0; + uint8_t sector[2048]; + for (uint32_t offset = 0; offset < bytes; offset += 2048u) { + if (!read_effective_user_sector(reader, lba + offset / 2048u, sector)) return 0; + const uint32_t count = std::min(2048u, bytes - offset); std::memcpy(static_cast(buffer) + offset, sector, count); } - *size = entry.size; + *size = bytes; return 1; } catch (...) { return 0; } } @@ -1783,12 +1855,16 @@ extern "C" int psx_mod_function_entry_active(void) { return PSXRecompV4::function_entry_depth != 0; } -extern "C" void mod_runtime_patch_disc_sector(uint32_t lba, int raw_sector, - uint8_t* bytes, uint32_t size) { +/* Apply the committed plan's disc writes and overlays to one sector. The + * emulated drive reaches this only after mod_runtime_enable_disc_patches() + * (reference reads before then must see the original image); host-side + * preparation through psx_mod_read_disc_file() always sees the effective disc, + * including during plugin activation, which runs before the drive is enabled. */ +static void patch_committed_disc_sector(uint32_t lba, int raw_sector, + uint8_t* bytes, uint32_t size) { using namespace PSXRecompV4; RuntimeMods& s = state(); - if (!s.initialized || !s.disc_enabled || s.disc_guard_failed || - !bytes || size == 0) return; + if (!s.initialized || s.disc_guard_failed || !bytes || size == 0) return; /* Raw Mode2 Form1 reads are also the source of the 2048-byte logical * stream consumed by the emulated CD controller. Apply raw claims to the * complete sector, then user-data claims to its payload window. Form2/XA @@ -1807,7 +1883,7 @@ extern "C" void mod_runtime_patch_disc_sector(uint32_t lba, int raw_sector, const auto overlay_sector = overlay_index.find(lba); if (sector == index.end() && overlay_sector == overlay_index.end()) { if (has_mode2_form1_user_data) - mod_runtime_patch_disc_sector(lba, 0, bytes + 24, 2048); + patch_committed_disc_sector(lba, 0, bytes + 24, 2048); return; } if (sector != index.end()) { @@ -1862,5 +1938,11 @@ extern "C" void mod_runtime_patch_disc_sector(uint32_t lba, int raw_sector, } } if (has_mode2_form1_user_data && !s.disc_guard_failed) - mod_runtime_patch_disc_sector(lba, 0, bytes + 24, 2048); + patch_committed_disc_sector(lba, 0, bytes + 24, 2048); +} + +extern "C" void mod_runtime_patch_disc_sector(uint32_t lba, int raw_sector, + uint8_t* bytes, uint32_t size) { + if (!PSXRecompV4::state().disc_enabled) return; + patch_committed_disc_sector(lba, raw_sector, bytes, size); } diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index c5d0b15c..fc079444 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -956,6 +956,98 @@ int main() { check(PSXRecompV4::mod_runtime_commit(raw_path,&error),"reader mount raw disc"); check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) && std::equal(result.begin(),result.end(),iso.begin()+22*2048),"raw and ISO reads identical"); + /* Plugin activation (native asset preparation) runs before the emulated + * drive's disc patches are enabled. Host reads must already return the + * committed plan's effective bytes, or a prepared cache would silently + * serve original assets under a modded plan. The drive path stays gated. */ + { + const auto patched_root = root / "host-reader-patched"; + const auto patched_iso = patched_root / "original.iso"; + /* A byte past the original 3000-byte extent; a mod that grows the file + * through its directory record exposes it. */ + std::vector grown_iso = iso; + grown_iso[22 * 2048 + 3500] = 0x77; + write_bytes(patched_iso, grown_iso); + write_text(patched_root / "packages/reader.patch/1.0.0/manifest.toml", + "format_version = 5\n" + "id = \"reader.patch\"\n" + "version = \"1.0.0\"\n" + "name = \"Reader Patch\"\n" + "[[target]]\n" + "game_id = \"READER\"\n" + "disc_sha256 = \"" + sha256_hex(grown_iso) + "\"\n" + "[[feature]]\n" + "id = \"asset\"\n" + "name = \"Asset\"\n" + "[[feature]]\n" + "id = \"grow\"\n" + "name = \"Grow\"\n" + "[[patch]]\n" + "feature = \"grow\"\n" + "target = \"disc_user\"\n" + "offset = " + std::to_string(21 * 2048 + 10) + "\n" + "expected = \"b80b0000\"\n" + "replace = \"a00f0000\"\n" + "[[feature]]\n" + "id = \"huge\"\n" + "name = \"Huge\"\n" + "[[patch]]\n" + "feature = \"huge\"\n" + "target = \"disc_user\"\n" + "offset = " + std::to_string(21 * 2048 + 10) + "\n" + "expected = \"b80b0000\"\n" + "replace = \"00f82204\"\n" + "[[patch]]\n" + "feature = \"asset\"\n" + "target = \"disc_user\"\n" + "offset = " + std::to_string(22 * 2048 + 5) + "\n" + "expected = \"23\"\n" + "replace = \"5a\"\n"); + write_text(patched_root / "state.toml", + "format_version = 2\n" + "[[feature]]\n" + "package_id = \"reader.patch\"\n" + "id = \"asset\"\n" + "enabled = true\n" + "[[feature]]\n" + "package_id = \"reader.patch\"\n" + "id = \"grow\"\n" + "enabled = true\n"); + check(PSXRecompV4::mod_runtime_initialize(patched_root, "READER", 0, {}, &error), + "patched reader initialize"); + check(PSXRecompV4::mod_runtime_commit(patched_iso, &error), "patched reader commit"); + check(psx_mod_read_disc_file("S0/LEVEL.NSF", nullptr, 0, &bytes) && bytes == 4000, + "host disc reads resolve paths through patched directory records"); + std::vector effective(4000); + check(psx_mod_read_disc_file("S0/LEVEL.NSF", effective.data(), + (uint32_t)effective.size(), &bytes) && + bytes == 4000 && effective[5] == 0x5a && effective[3500] == 0x77 && + std::equal(effective.begin() + 6, effective.begin() + 3000, + iso.begin() + 22 * 2048 + 6), + "host disc reads apply the committed plan before the drive is enabled"); + check(!psx_mod_read_disc_file("S0", nullptr, 0, &bytes), + "effective lookup still rejects directories"); + std::array drive_sector{}; + std::copy_n(iso.begin() + 22 * 2048, 2048, drive_sector.begin()); + mod_runtime_patch_disc_sector(22, 0, drive_sector.data(), (uint32_t)drive_sector.size()); + check(drive_sector[5] == 0x23, + "the emulated drive path stays original until disc patches are enabled"); + mod_runtime_enable_disc_patches(); + mod_runtime_patch_disc_sector(22, 0, drive_sector.data(), (uint32_t)drive_sector.size()); + check(drive_sector[5] == 0x5a, "enabled drive path applies the same plan"); + /* Grown archives may exceed 64 MiB (an extended MMX6 ROCK_X6.DAT is + * 69,400,576 bytes); the bound is CD capacity, not a fixed cap. */ + write_text(patched_root / "state.toml", + "format_version = 2\n" + "[[feature]]\n" + "package_id = \"reader.patch\"\n" + "id = \"huge\"\n" + "enabled = true\n"); + check(PSXRecompV4::mod_runtime_initialize(patched_root, "READER", 0, {}, &error) && + PSXRecompV4::mod_runtime_commit(patched_iso, &error), "huge plan commit"); + check(psx_mod_read_disc_file("S0/LEVEL.NSF", nullptr, 0, &bytes) && bytes == 69400576u, + "files grown past 64 MiB keep their effective size"); + } fs::remove_all(root, ec); if (failures) return 1; std::cout << "mod runtime tests passed\n";