mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -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>
191 lines
6.2 KiB
C++
191 lines
6.2 KiB
C++
// cue_sheet.cpp — see cue_sheet.h.
|
|
|
|
#include "cue_sheet.h"
|
|
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace PSXRecompV4 {
|
|
|
|
namespace {
|
|
|
|
std::string upper_ascii(std::string s) {
|
|
for (char& c : s) c = (char)std::toupper((unsigned char)c);
|
|
return s;
|
|
}
|
|
|
|
bool equal_ci(const std::string& a, const std::string& b) {
|
|
if (a.size() != b.size()) return false;
|
|
for (size_t i = 0; i < a.size(); i++) {
|
|
if (std::toupper((unsigned char)a[i]) != std::toupper((unsigned char)b[i])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Extract the FILE line's payload name and type token. Handles both the quoted
|
|
// form (`FILE "game.bin" BINARY`) and the bare form (`FILE game.bin BINARY`)
|
|
// that some hand-written cues use.
|
|
bool parse_file_line(const std::string& line, size_t file_kw_pos,
|
|
std::string& out_name, std::string& out_type) {
|
|
const size_t after_kw = file_kw_pos + 4; // strlen("FILE")
|
|
const size_t q1 = line.find('"', after_kw);
|
|
size_t rest_pos;
|
|
|
|
if (q1 != std::string::npos) {
|
|
const size_t q2 = line.find('"', q1 + 1);
|
|
if (q2 == std::string::npos || q2 <= q1 + 1) return false;
|
|
out_name = line.substr(q1 + 1, q2 - q1 - 1);
|
|
rest_pos = q2 + 1;
|
|
} else {
|
|
std::istringstream iss(line.substr(after_kw));
|
|
if (!(iss >> out_name) || out_name.empty()) return false;
|
|
rest_pos = after_kw;
|
|
// Advance past the name we just consumed so the type token scan below
|
|
// starts after it.
|
|
const size_t name_at = line.find(out_name, after_kw);
|
|
if (name_at != std::string::npos) rest_pos = name_at + out_name.size();
|
|
}
|
|
|
|
std::istringstream rest(line.substr(rest_pos));
|
|
out_type.clear();
|
|
rest >> out_type;
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool CueSheet::usable() const {
|
|
if (!opened || files.empty()) return false;
|
|
for (const CueFileRef& f : files) {
|
|
if (!f.is_binary || !f.exists) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CueSheet::has_non_binary_file() const {
|
|
for (const CueFileRef& f : files) {
|
|
if (!f.is_binary) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
size_t CueSheet::data_file_index() const {
|
|
for (const CueTrackRef& t : tracks) {
|
|
if (!t.is_audio && t.file_index < files.size()) return t.file_index;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool path_has_extension_ci(const fs::path& p, const char* dotted_ext) {
|
|
return equal_ci(p.extension().string(), std::string(dotted_ext));
|
|
}
|
|
|
|
fs::path resolve_cue_relative_file(const fs::path& dir, const std::string& name) {
|
|
fs::path named(name);
|
|
// Cues written on Windows may use backslashes; std::filesystem on POSIX
|
|
// treats those as ordinary characters, so normalize them first.
|
|
if (name.find('\\') != std::string::npos) {
|
|
std::string slashed = name;
|
|
for (char& c : slashed) if (c == '\\') c = '/';
|
|
named = fs::path(slashed);
|
|
}
|
|
|
|
const fs::path literal = named.is_absolute() ? named : (dir / named);
|
|
std::error_code ec;
|
|
if (fs::exists(literal, ec)) return literal;
|
|
|
|
// Case-insensitive fallback: scan the containing directory for a filename
|
|
// that differs only by case.
|
|
const fs::path parent = literal.parent_path();
|
|
const std::string want = literal.filename().string();
|
|
ec.clear();
|
|
if (fs::is_directory(parent, ec)) {
|
|
ec.clear();
|
|
for (fs::directory_iterator it(parent, ec), end; !ec && it != end; it.increment(ec)) {
|
|
if (equal_ci(it->path().filename().string(), want)) return it->path();
|
|
}
|
|
}
|
|
return literal;
|
|
}
|
|
|
|
CueSheet parse_cue_sheet(const fs::path& cue_path) {
|
|
CueSheet sheet;
|
|
|
|
std::ifstream cue(cue_path);
|
|
if (!cue.is_open()) return sheet;
|
|
sheet.opened = true;
|
|
|
|
const fs::path dir = cue_path.parent_path();
|
|
|
|
std::string line;
|
|
int cur_track_num = -1;
|
|
bool cur_track_audio = false;
|
|
uint32_t cur_index00 = 0;
|
|
bool cur_has_index00 = false;
|
|
|
|
while (std::getline(cue, line)) {
|
|
// Strip a trailing CR so cues with CRLF endings parse on POSIX.
|
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
|
|
|
const std::string upper = upper_ascii(line);
|
|
|
|
const size_t file_pos = upper.find("FILE");
|
|
if (file_pos != std::string::npos) {
|
|
std::string name, type;
|
|
if (parse_file_line(line, file_pos, name, type)) {
|
|
CueFileRef ref;
|
|
ref.name = name;
|
|
ref.is_binary = equal_ci(type, "BINARY");
|
|
ref.path = resolve_cue_relative_file(dir, name);
|
|
std::error_code ec;
|
|
ref.exists = fs::exists(ref.path, ec) && !fs::is_directory(ref.path, ec);
|
|
sheet.files.push_back(std::move(ref));
|
|
}
|
|
// A FILE line resets any track state carried from the previous file.
|
|
cur_track_num = -1;
|
|
cur_has_index00 = false;
|
|
cur_index00 = 0;
|
|
continue;
|
|
}
|
|
|
|
int tn = 0;
|
|
char type_buf[32] = {0};
|
|
if (std::sscanf(upper.c_str(), " TRACK %d %31s", &tn, type_buf) == 2) {
|
|
cur_track_num = tn;
|
|
cur_track_audio = (std::string(type_buf).find("AUDIO") != std::string::npos);
|
|
cur_index00 = 0;
|
|
cur_has_index00 = false;
|
|
continue;
|
|
}
|
|
|
|
int idx = 0, mm = 0, ss = 0, ff = 0;
|
|
if (std::sscanf(upper.c_str(), " INDEX %d %d:%d:%d", &idx, &mm, &ss, &ff) == 4 &&
|
|
cur_track_num >= 1 && !sheet.files.empty()) {
|
|
const uint32_t index_lba = (uint32_t)(((mm * 60 + ss) * 75) + ff);
|
|
if (idx == 0) {
|
|
cur_index00 = index_lba;
|
|
cur_has_index00 = true;
|
|
continue;
|
|
}
|
|
if (idx != 1) continue;
|
|
CueTrackRef t;
|
|
t.number = cur_track_num;
|
|
t.is_audio = cur_track_audio;
|
|
t.file_index = sheet.files.size() - 1;
|
|
t.index01 = index_lba;
|
|
t.index00 = cur_index00;
|
|
t.has_index00 = cur_has_index00;
|
|
sheet.tracks.push_back(t);
|
|
cur_track_num = -1;
|
|
}
|
|
}
|
|
|
|
return sheet;
|
|
}
|
|
|
|
} // namespace PSXRecompV4
|