mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Make host disc reads see the effective modded disc
Apply committed sector mods during plugin activation, resolve paths through patched directory records, and bound file size by CD capacity. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+108
-26
@@ -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<const char*>(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<uint8_t*>(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);
|
||||
}
|
||||
|
||||
@@ -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<uint8_t> 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<uint8_t> 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<uint8_t, 2048> 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";
|
||||
|
||||
Reference in New Issue
Block a user