mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -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>
208 lines
8.4 KiB
C++
208 lines
8.4 KiB
C++
// test_disc_path_resolve.cpp — either member of a disc dump is a valid pick.
|
|
//
|
|
// The contract under test (runtime/include/disc_path.h): whichever file the
|
|
// player hands us — the .cue or the .bin — we mount the same disc and read
|
|
// identity/CRC from the same data track. Regression anchor for the old
|
|
// behavior, which swapped a .cue for its same-named .bin unconditionally and
|
|
// silently dropped the cue's CD-DA tracks.
|
|
|
|
#include "cue_sheet.h"
|
|
#include "disc_path.h"
|
|
#include "iso_reader.h"
|
|
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace fs = std::filesystem;
|
|
using PSXRecompV4::resolve_disc_path;
|
|
|
|
namespace {
|
|
|
|
int g_checks = 0;
|
|
|
|
void check(bool cond, const char* what) {
|
|
++g_checks;
|
|
if (!cond) {
|
|
std::fprintf(stderr, "FAIL: %s\n", what);
|
|
std::abort();
|
|
}
|
|
}
|
|
|
|
void write_sectors(const fs::path& path, int count, uint8_t marker) {
|
|
std::ofstream out(path, std::ios::binary);
|
|
std::vector<uint8_t> sector(2352, marker);
|
|
for (int i = 0; i < count; ++i)
|
|
out.write(reinterpret_cast<const char*>(sector.data()), (std::streamsize)sector.size());
|
|
}
|
|
|
|
void write_text(const fs::path& path, const std::string& body) {
|
|
std::ofstream out(path);
|
|
out << body;
|
|
}
|
|
|
|
bool same(const fs::path& a, const fs::path& b) {
|
|
return a.lexically_normal() == b.lexically_normal();
|
|
}
|
|
|
|
// ---- 1. single-file dump: cue and bin are interchangeable -------------------
|
|
void test_single_file_dump(const fs::path& root) {
|
|
const fs::path dir = root / "single";
|
|
fs::create_directories(dir);
|
|
write_sectors(dir / "game.bin", 4, 0x11);
|
|
write_text(dir / "game.cue",
|
|
"FILE \"game.bin\" BINARY\n"
|
|
" TRACK 01 MODE2/2352\n"
|
|
" INDEX 01 00:00:00\n");
|
|
|
|
const auto from_cue = resolve_disc_path(dir / "game.cue");
|
|
check(from_cue.from_cue, "single: picking the cue mounts the cue");
|
|
check(same(from_cue.mount, dir / "game.cue"), "single: cue mount path");
|
|
check(same(from_cue.data, dir / "game.bin"), "single: cue -> bin data track");
|
|
|
|
const auto from_bin = resolve_disc_path(dir / "game.bin");
|
|
check(from_bin.upgraded_to_cue, "single: picking the bin finds the owning cue");
|
|
check(same(from_bin.mount, dir / "game.cue"), "single: bin upgrades to the cue");
|
|
|
|
// The whole point: both picks agree on what to read identity/CRC from.
|
|
check(same(from_cue.data, from_bin.data), "single: both picks share a data track");
|
|
}
|
|
|
|
// ---- 2. redump multi-track: picking any track file finds the cue ------------
|
|
void test_multi_track_dump(const fs::path& root) {
|
|
const fs::path dir = root / "multi";
|
|
fs::create_directories(dir);
|
|
write_sectors(dir / "Game (Track 1).bin", 4, 0x11);
|
|
write_sectors(dir / "Game (Track 2).bin", 3, 0x22);
|
|
write_text(dir / "Game.cue",
|
|
"FILE \"Game (Track 1).bin\" BINARY\n"
|
|
" TRACK 01 MODE2/2352\n"
|
|
" INDEX 01 00:00:00\n"
|
|
"FILE \"Game (Track 2).bin\" BINARY\n"
|
|
" TRACK 02 AUDIO\n"
|
|
" INDEX 00 00:00:00\n"
|
|
" INDEX 01 00:00:01\n");
|
|
|
|
const auto from_cue = resolve_disc_path(dir / "Game.cue");
|
|
check(from_cue.from_cue, "multi: cue mounts as a cue");
|
|
check(same(from_cue.data, dir / "Game (Track 1).bin"), "multi: data track is track 1");
|
|
|
|
// Picking the DATA track's bin: must upgrade to the cue, or the audio track
|
|
// disappears. This is the case the old normalize_disc_path_for_launch got
|
|
// wrong in the other direction.
|
|
const auto from_data_bin = resolve_disc_path(dir / "Game (Track 1).bin");
|
|
check(from_data_bin.upgraded_to_cue, "multi: data bin upgrades to the cue");
|
|
check(same(from_data_bin.mount, dir / "Game.cue"), "multi: data bin -> Game.cue");
|
|
|
|
// Picking an AUDIO track's bin still resolves identity to the data track,
|
|
// so the verify verdict does not depend on which file was clicked.
|
|
const auto from_audio_bin = resolve_disc_path(dir / "Game (Track 2).bin");
|
|
check(from_audio_bin.upgraded_to_cue, "multi: audio bin upgrades to the cue");
|
|
check(same(from_audio_bin.data, dir / "Game (Track 1).bin"),
|
|
"multi: audio bin still identifies via the data track");
|
|
|
|
// And the mounted TOC really does carry both tracks, for every pick.
|
|
for (const fs::path& pick : {dir / "Game.cue",
|
|
dir / "Game (Track 1).bin",
|
|
dir / "Game (Track 2).bin"}) {
|
|
PS1::ISOReader reader;
|
|
check(reader.Open(resolve_disc_path(pick).mount.string()), "multi: mount opens");
|
|
check(reader.TrackCount() == 2, "multi: both tracks present regardless of pick");
|
|
check(reader.TrackIsAudio(2), "multi: track 2 is audio regardless of pick");
|
|
reader.Close();
|
|
}
|
|
}
|
|
|
|
// ---- 3. broken cue: fall back to the sibling image instead of failing -------
|
|
void test_cue_with_missing_bin(const fs::path& root) {
|
|
const fs::path dir = root / "broken";
|
|
fs::create_directories(dir);
|
|
write_sectors(dir / "game.bin", 4, 0x11);
|
|
// The cue names a payload that is not there (renamed dump, partial copy).
|
|
write_text(dir / "game.cue",
|
|
"FILE \"game (Track 01).bin\" BINARY\n"
|
|
" TRACK 01 MODE2/2352\n"
|
|
" INDEX 01 00:00:00\n");
|
|
|
|
const auto r = resolve_disc_path(dir / "game.cue");
|
|
check(r.cue_fallback, "broken cue: reports the fallback");
|
|
check(same(r.mount, dir / "game.bin"), "broken cue: falls back to the sibling bin");
|
|
check(same(r.data, dir / "game.bin"), "broken cue: identity reads the sibling bin");
|
|
check(!r.note.empty(), "broken cue: explains itself");
|
|
}
|
|
|
|
// ---- 4. no cue at all: a bare image mounts as itself ------------------------
|
|
void test_bare_image(const fs::path& root) {
|
|
const fs::path dir = root / "bare";
|
|
fs::create_directories(dir);
|
|
write_sectors(dir / "game.bin", 4, 0x11);
|
|
|
|
const auto r = resolve_disc_path(dir / "game.bin");
|
|
check(!r.from_cue, "bare: no cue to upgrade to");
|
|
check(same(r.mount, dir / "game.bin"), "bare: mounts itself");
|
|
check(same(r.data, dir / "game.bin"), "bare: identity reads itself");
|
|
check(r.note.empty(), "bare: nothing to report");
|
|
|
|
// .iso is equally acceptable.
|
|
write_sectors(dir / "game2.iso", 4, 0x33);
|
|
const auto iso = resolve_disc_path(dir / "game2.iso");
|
|
check(same(iso.mount, dir / "game2.iso"), "bare: iso mounts itself");
|
|
}
|
|
|
|
// ---- 5. surface-syntax tolerance in the shared cue parser -------------------
|
|
void test_cue_syntax_tolerance(const fs::path& root) {
|
|
const fs::path dir = root / "syntax";
|
|
fs::create_directories(dir);
|
|
write_sectors(dir / "game.bin", 4, 0x11);
|
|
|
|
// Lowercase keywords, unquoted filename, CRLF line endings.
|
|
write_text(dir / "lower.cue",
|
|
"file game.bin binary\r\n"
|
|
" track 01 mode2/2352\r\n"
|
|
" index 01 00:00:00\r\n");
|
|
const auto lower = PSXRecompV4::parse_cue_sheet(dir / "lower.cue");
|
|
check(lower.usable(), "syntax: lowercase + unquoted + CRLF parses");
|
|
check(same(lower.files.front().path, dir / "game.bin"), "syntax: resolves the payload");
|
|
|
|
// A WAVE payload has no fixed sector geometry — refuse rather than guess.
|
|
write_text(dir / "wave.cue",
|
|
"FILE \"game.bin\" BINARY\n"
|
|
" TRACK 01 MODE2/2352\n"
|
|
" INDEX 01 00:00:00\n"
|
|
"FILE \"track2.wav\" WAVE\n"
|
|
" TRACK 02 AUDIO\n"
|
|
" INDEX 01 00:00:00\n");
|
|
const auto wave = PSXRecompV4::parse_cue_sheet(dir / "wave.cue");
|
|
check(wave.has_non_binary_file(), "syntax: WAVE payload is flagged");
|
|
check(!wave.usable(), "syntax: WAVE cue is not mountable");
|
|
|
|
PS1::ISOReader reader;
|
|
check(!reader.Open((dir / "wave.cue").string()), "syntax: ISOReader refuses a WAVE cue");
|
|
|
|
// ...and the resolver recovers to the sibling bin rather than dying.
|
|
const auto r = resolve_disc_path(dir / "wave.cue");
|
|
check(same(r.mount, dir / "game.bin"), "syntax: WAVE cue falls back to the bin");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
const fs::path root = fs::temp_directory_path() / "psxrecomp-disc-path-test";
|
|
fs::remove_all(root);
|
|
fs::create_directories(root);
|
|
|
|
test_single_file_dump(root);
|
|
test_multi_track_dump(root);
|
|
test_cue_with_missing_bin(root);
|
|
test_bare_image(root);
|
|
test_cue_syntax_tolerance(root);
|
|
|
|
fs::remove_all(root);
|
|
std::printf("disc_path_resolve: %d checks passed\n", g_checks);
|
|
return 0;
|
|
}
|