mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 02:32:02 -04:00
68667cd58b
Two path-resolution defects where the runtime silently accepted only one of
two equally-valid spellings of the same input.
1. Disc: picking the .cue threw away the disc's TOC.
normalize_disc_path_for_launch() swapped any .cue for its same-named .bin
unconditionally, and every disc entry point (game.toml, --disc, the cached
disc.cfg, the launcher's Change Disc, the first-run dialog) went through it.
That discarded the cue's table of contents -- every CD-DA track and pregap
with it -- even though iso_reader.cpp has full multi-FILE cue support that
the swap defeated before it was ever reached. In the other direction,
picking a redump audio-track bin mounted that track as the whole disc, which
cannot boot.
resolve_disc_path() (runtime/include/disc_path.h) now canonicalizes any pick:
.cue that resolves -> mount the cue (TOC intact)
.cue with a missing bin -> the payload it still names, else a
stem-matched sibling image
.bin/.iso/.img owned by a cue -> mount that cue
bare image, no cue -> mount it
identify_disc() resolves through the same code, so the header/serial/CRC32
verdict is read from the data track no matter which member was picked.
While here: the mount path and the verify path had two divergent private cue
parsers (quoted-only + case-sensitive vs quoted-or-bare + case-insensitive),
so they could disagree about the same file. Both now use one parser
(runtime/include/cue_sheet.h), which additionally handles CRLF, backslash
paths, and case-insensitive payload lookup for case-sensitive filesystems.
2. BIOS: the multi-BIOS rename made configs and dump folders incompatible.
SCPH*.toml name "US-PSX-SCPH1001.BIN" while every pre-existing dump folder,
bios.cfg and tools/regen_bios.sh default name "SCPH1001.BIN". Either
combination fataled with "cannot open BIOS file" despite the right ROM being
present. resolve_bios_rom() (recompiler/include/bios_rom_alias.h) matches on
the model token -- the last '-'-separated component of the stem -- so both
conventions resolve in both directions without a hardcoded alias table.
Applied at the config loader, the positional psxrecomp-bios CLI, and each
rung of the runtime's BIOS search.
The positional CLI also derived out_stem from the raw filename, so a
region-qualified ROM would have emitted US-PSX-SCPH1001_full.c -- a name
runtime.cmake cannot link. It now derives the model token.
Also fixes SCPH5552.toml, whose rom field omitted the bios/ directory that
its two siblings have.
Tests: test_disc_path_resolve (36 checks, five dump layouts) and
test_bios_rom_alias (15 checks, both directions + negative cases), both
registered in runtime/CMakeLists.txt. Registered the pre-existing
test_iso_reader_cdda, which was wired into no build.
Verified on real dumps: Ape Escape .cue and .bin both give
SCUS-94423 / NTSC-U / crc=C6F455BC; Tomba 2's multi-track dump gives
crc=1FB8C151 and a 2-track TOC from the .cue, the data bin, and the CD-DA
bin alike.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
199 lines
7.1 KiB
C++
199 lines
7.1 KiB
C++
// disc_path.cpp — see disc_path.h.
|
|
|
|
#include "disc_path.h"
|
|
|
|
#include "cue_sheet.h"
|
|
|
|
#include <system_error>
|
|
#include <vector>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace PSXRecompV4 {
|
|
|
|
namespace {
|
|
|
|
// Raw image extensions we are willing to mount on their own, in preference
|
|
// order for the cue-fallback search.
|
|
const char* const kImageExtensions[] = { ".bin", ".img", ".iso" };
|
|
|
|
bool is_cue(const fs::path& p) { return path_has_extension_ci(p, ".cue"); }
|
|
|
|
bool is_raw_image(const fs::path& p) {
|
|
for (const char* ext : kImageExtensions) {
|
|
if (path_has_extension_ci(p, ext)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool same_file_path(const fs::path& a, const fs::path& b) {
|
|
std::error_code ec;
|
|
if (fs::exists(a, ec) && fs::exists(b, ec)) {
|
|
ec.clear();
|
|
if (fs::equivalent(a, b, ec) && !ec) return true;
|
|
ec.clear();
|
|
}
|
|
return a.lexically_normal() == b.lexically_normal();
|
|
}
|
|
|
|
fs::path absolute_or_self(const fs::path& p) {
|
|
std::error_code ec;
|
|
fs::path abs = fs::absolute(p, ec);
|
|
return ec ? p : abs;
|
|
}
|
|
|
|
// The BINARY payload an unusable cue still points at: the data track when it
|
|
// is present, else the first existing BINARY FILE entry. Empty when the cue
|
|
// names nothing that is both BINARY and on disk.
|
|
fs::path first_existing_binary(const CueSheet& sheet) {
|
|
const size_t di = sheet.data_file_index();
|
|
if (di < sheet.files.size() && sheet.files[di].is_binary && sheet.files[di].exists)
|
|
return sheet.files[di].path;
|
|
for (const CueFileRef& f : sheet.files) {
|
|
if (f.is_binary && f.exists) return f.path;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// <stem>.bin / .img / .iso next to a cue we could not use.
|
|
fs::path find_sibling_image(const fs::path& cue) {
|
|
std::error_code ec;
|
|
for (const char* ext : kImageExtensions) {
|
|
fs::path cand = cue;
|
|
cand.replace_extension(ext);
|
|
ec.clear();
|
|
if (fs::exists(cand, ec) && !fs::is_directory(cand, ec)) return cand;
|
|
}
|
|
// Case-insensitive sweep for filesystems where ".BIN" != ".bin".
|
|
const fs::path dir = cue.parent_path();
|
|
ec.clear();
|
|
if (fs::is_directory(dir, ec)) {
|
|
const fs::path stem = cue.stem();
|
|
ec.clear();
|
|
for (fs::directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec)) {
|
|
if (it->path().stem() != stem) continue;
|
|
if (is_raw_image(it->path())) return it->path();
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// The cue that owns `image`: try <stem>.cue first (the common single-bin
|
|
// layout), then any cue in the same directory whose FILE list names it (the
|
|
// redump multi-track layout, where the cue is "Game.cue" and the payloads are
|
|
// "Game (Track 01).bin", ...).
|
|
fs::path find_owning_cue(const fs::path& image) {
|
|
std::error_code ec;
|
|
|
|
auto cue_owns_image = [&](const fs::path& cue) {
|
|
const CueSheet sheet = parse_cue_sheet(cue);
|
|
if (!sheet.usable()) return false;
|
|
for (const CueFileRef& f : sheet.files) {
|
|
if (same_file_path(f.path, image)) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
fs::path same_stem = image;
|
|
same_stem.replace_extension(".cue");
|
|
if (fs::exists(same_stem, ec) && cue_owns_image(same_stem)) return same_stem;
|
|
|
|
const fs::path dir = image.parent_path();
|
|
ec.clear();
|
|
if (!fs::is_directory(dir, ec)) return {};
|
|
|
|
ec.clear();
|
|
for (fs::directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec)) {
|
|
if (!is_cue(it->path())) continue;
|
|
if (same_file_path(it->path(), same_stem)) continue; // already tried
|
|
if (cue_owns_image(it->path())) return it->path();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
DiscPathResolution resolve_disc_path(const fs::path& picked) {
|
|
DiscPathResolution r;
|
|
r.picked = absolute_or_self(picked);
|
|
r.mount = r.picked;
|
|
r.data = r.picked;
|
|
|
|
if (r.picked.empty()) return r;
|
|
|
|
if (is_cue(r.picked)) {
|
|
const CueSheet sheet = parse_cue_sheet(r.picked);
|
|
if (sheet.usable()) {
|
|
const size_t di = sheet.data_file_index();
|
|
r.from_cue = true;
|
|
r.mount = r.picked;
|
|
r.data = di < sheet.files.size() ? sheet.files[di].path : r.picked;
|
|
return r;
|
|
}
|
|
|
|
// The cue is unusable. Recover to a raw image rather than failing the
|
|
// launch outright — a renamed or missing bin is the single most common
|
|
// way a hand-assembled dump folder is broken.
|
|
//
|
|
// Prefer a BINARY payload the cue itself names and that actually
|
|
// exists: a cue whose only defect is a WAVE audio track still points
|
|
// at a perfectly good data track. Only fall back to a stem-matched
|
|
// sibling when the cue names nothing usable at all.
|
|
fs::path sibling = first_existing_binary(sheet);
|
|
if (sibling.empty()) sibling = find_sibling_image(r.picked);
|
|
if (!sibling.empty()) {
|
|
r.mount = absolute_or_self(sibling);
|
|
r.data = r.mount;
|
|
r.cue_fallback = true;
|
|
r.note = "The cue sheet \"" + r.picked.filename().string() +
|
|
"\" does not resolve to a usable BINARY payload; falling back to \"" +
|
|
r.mount.filename().string() +
|
|
"\". CD audio tracks described by the cue will not be available.";
|
|
return r;
|
|
}
|
|
|
|
if (!sheet.opened) {
|
|
r.note = "The cue sheet \"" + r.picked.filename().string() + "\" could not be read.";
|
|
} else if (sheet.has_non_binary_file()) {
|
|
r.note = "The cue sheet \"" + r.picked.filename().string() +
|
|
"\" references a non-BINARY track payload (WAVE/MP3), which has no fixed "
|
|
"sector geometry and cannot be mounted.";
|
|
} else if (sheet.files.empty()) {
|
|
r.note = "The cue sheet \"" + r.picked.filename().string() + "\" names no FILE.";
|
|
} else {
|
|
std::string missing;
|
|
for (const CueFileRef& f : sheet.files) {
|
|
if (f.exists) continue;
|
|
if (!missing.empty()) missing += ", ";
|
|
missing += "\"" + f.name + "\"";
|
|
}
|
|
r.note = "The cue sheet \"" + r.picked.filename().string() +
|
|
"\" references files that are not next to it: " + missing + ".";
|
|
}
|
|
return r;
|
|
}
|
|
|
|
if (is_raw_image(r.picked)) {
|
|
const fs::path cue = find_owning_cue(r.picked);
|
|
if (!cue.empty()) {
|
|
const CueSheet sheet = parse_cue_sheet(cue);
|
|
const size_t di = sheet.data_file_index();
|
|
r.mount = absolute_or_self(cue);
|
|
r.data = di < sheet.files.size() ? sheet.files[di].path : r.picked;
|
|
r.from_cue = true;
|
|
r.upgraded_to_cue = true;
|
|
if (sheet.tracks.size() > 1) {
|
|
r.note = "Mounting \"" + r.mount.filename().string() + "\" instead of \"" +
|
|
r.picked.filename().string() + "\" so the disc's " +
|
|
std::to_string(sheet.tracks.size()) +
|
|
" tracks (including CD audio) are available.";
|
|
}
|
|
return r;
|
|
}
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
} // namespace PSXRecompV4
|