iso_reader: support multi-FILE cue sheets (redump track-split dumps)

The cue parser kept a single bin_filename that every FILE line
overwrote, so a redump-style dump (one .bin per track) mounted only
the LAST track file - the CD-DA audio bin - as the entire disc: no
ISO9660 filesystem, no boot EXE, black screen at startup (reported
against Tomba! 2, whose USA disc is data + one CD-DA track).

Replace the single ifstream with a BinSegment table: one entry per
FILE line, opened for the reader's lifetime and laid out at cumulative
disc-relative sector offsets in cue order. ReadSector/ReadRawSector
resolve a disc LBA to its owning segment; INDEX times, which a cue
states relative to the owning file, are converted to disc-relative
track starts once the segment layout is known. Single-file images are
a one-segment table with unchanged addressing.

Also fixed in passing: Open() never cleared tracks_ from a previous
open (reopening a reader accumulated stale TOC entries), per-read
sector-geometry probing is now computed once per segment, a cue whose
FILE is missing or non-BINARY (WAVE/MP3) fails Open() instead of
mis-mapping the TOC, and GetSectorCount() reports the whole-disc
total.

Verified: Tomba! 2 single-bin dump split at the track-2 boundary
(concatenation SHA1-identical to the original); a host-side test
opens both cues and confirms all 186430 sectors byte-identical and
the TOC identical (track 2 at LBA 174674, audio); the game boots to
intro FMV, title, and attract gameplay from the multi-file dump with
CD audio flowing, and the single-file dump boots identically on the
same binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-07-12 18:29:32 -07:00
parent c94fcd5e1a
commit 5a2c6fd3f0
2 changed files with 172 additions and 87 deletions
+26 -3
View File
@@ -46,7 +46,24 @@ struct RootDirectoryInfo {
struct CDTrack {
int number; // 1-based track number
bool is_audio; // true = CD-DA audio (Red Book); false = data
uint32_t start_lba; // .bin-relative start LBA (cue INDEX 01; track 1 = 0)
uint32_t start_lba; // disc-relative start LBA (cue INDEX 01; track 1 = 0).
// Single-FILE cues: equals the .bin-relative INDEX time.
// Multi-FILE cues (redump "(Track N).bin" dumps): the
// owning file's first disc sector plus its INDEX time.
};
/**
* One BINARY file backing a contiguous run of disc sectors. Single-file
* images have exactly one segment; redump-style dumps have one per track
* file, concatenated in cue order to form the flat disc-sector space that
* ReadSector()/ReadRawSector() address.
*/
struct BinSegment {
std::string path; // resolved on-disk path
std::ifstream file; // opened for the reader's lifetime
uint32_t start_lba; // first disc-relative sector of this file
uint32_t sector_count; // sectors stored in this file
bool raw; // 2352-byte raw sectors (BIN) vs 2048 (ISO)
};
class ISOReader {
@@ -187,12 +204,18 @@ private:
*/
std::vector<ISOFileEntry> ListFilesByLBA(uint32_t lba, uint32_t dir_size);
std::ifstream file_;
/**
* Helper: find the BinSegment containing a disc-relative LBA.
* @return segment pointer, or nullptr when lba is past the disc end
*/
BinSegment* SegmentForLBA(uint32_t lba);
bool is_open_;
std::string volume_id_;
std::string bin_path_;
std::string bin_path_; // first (data) segment; kept for callers
RootDirectoryInfo root_dir_;
std::vector<CDTrack> tracks_; // from the .cue TOC; >=1 entry after Open()
std::vector<BinSegment> segments_; // cue FILE entries in disc order; >=1 after Open()
};
} // namespace PS1
+146 -84
View File
@@ -29,7 +29,7 @@ ISOReader::~ISOReader() {
}
bool ISOReader::Open(const std::string& filename) {
// Close any previously opened file
// Close any previously opened image (also resets tracks/segments)
Close();
// Check if file exists
@@ -37,17 +37,33 @@ bool ISOReader::Open(const std::string& filename) {
return false;
}
// Handle .cue files - parse to find .bin file
std::string bin_filename = filename;
// Ordered list of BINARY files backing the disc. A bare .bin/.iso is a
// single-entry list; a .cue contributes one entry per FILE line (redump
// multi-track dumps ship one file per track).
std::vector<std::string> bin_files;
// Tracks parsed from the cue. INDEX times in a cue are relative to the
// OWNING FILE, so remember which file each track belongs to and convert
// to disc-relative LBAs once the segment table (with each file's first
// disc sector) is built below.
struct PendingTrack {
int number;
bool is_audio;
size_t file_index; // index into bin_files
uint32_t index01; // INDEX 01 as a file-relative LBA
};
std::vector<PendingTrack> pending_tracks;
auto ends_with = [](const std::string& s, const std::string& suffix) {
return s.size() >= suffix.size() &&
s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
};
if (ends_with(filename, ".cue") || ends_with(filename, ".CUE")) {
// Parse the .cue: resolve the FILE, AND build the track TOC from the
// TRACK/INDEX lines. A bare .bin/.iso (no .cue) falls through to the
// single-track synthesis below; a multi-track disc (data + CD-DA audio)
// gets each track so the CD model's GetTN/GetTD report them correctly.
// Parse the .cue: resolve every FILE, AND build the track TOC from
// the TRACK/INDEX lines. Single-file cues keep their historical
// behavior; multi-file cues (data track + CD-DA audio tracks in
// separate .bin files) map each file to a contiguous run of disc
// sectors, concatenated in cue order.
std::ifstream cue_file(filename);
if (!cue_file.is_open()) {
return false;
@@ -59,18 +75,24 @@ bool ISOReader::Open(const std::string& filename) {
while (std::getline(cue_file, line)) {
// FILE "filename.bin" BINARY
size_t file_pos = line.find("FILE");
size_t binary_pos = line.find("BINARY");
if (file_pos != std::string::npos && binary_pos != std::string::npos) {
if (file_pos != std::string::npos) {
size_t quote1 = line.find('"', file_pos);
size_t quote2 = line.find('"', quote1 + 1);
size_t quote2 = (quote1 == std::string::npos)
? std::string::npos : line.find('"', quote1 + 1);
if (quote1 != std::string::npos && quote2 != std::string::npos) {
// Only raw BINARY payloads are supported (WAVE/MP3 audio
// files have no fixed sector geometry). Mis-mapping the
// TOC would be worse than refusing the image.
if (line.find("BINARY", quote2) == std::string::npos) {
return false;
}
std::string bin_name = line.substr(quote1 + 1, quote2 - quote1 - 1);
std::filesystem::path cue_path(filename);
std::filesystem::path bin_path(bin_name);
if (bin_path.is_relative()) {
bin_filename = (cue_path.parent_path() / bin_name).string();
bin_files.push_back((cue_path.parent_path() / bin_name).string());
} else {
bin_filename = bin_name;
bin_files.push_back(bin_name);
}
}
continue;
@@ -88,16 +110,61 @@ bool ISOReader::Open(const std::string& filename) {
// INDEX 01 MM:SS:FF — the track's start (INDEX 00 is its pregap).
int idx = 0, mm = 0, ss = 0, ff = 0;
if (std::sscanf(line.c_str(), " INDEX %d %d:%d:%d", &idx, &mm, &ss, &ff) == 4
&& idx == 1 && cur_track_num >= 1) {
CDTrack t;
t.number = cur_track_num;
t.is_audio = cur_track_audio;
t.start_lba = (uint32_t)(((mm * 60 + ss) * 75) + ff); // .bin-relative LBA
tracks_.push_back(t);
&& idx == 1 && cur_track_num >= 1 && !bin_files.empty()) {
PendingTrack t;
t.number = cur_track_num;
t.is_audio = cur_track_audio;
t.file_index = bin_files.size() - 1;
t.index01 = (uint32_t)(((mm * 60 + ss) * 75) + ff);
pending_tracks.push_back(t);
cur_track_num = -1;
}
}
cue_file.close();
// A cue with no FILE line has nothing to mount.
if (bin_files.empty()) {
return false;
}
} else {
bin_files.push_back(filename);
}
// Build the segment table: open every file and lay it out at the next
// disc-relative sector. Every file must open — a multi-file dump with a
// missing track file would otherwise silently read the wrong sectors.
uint32_t next_lba = 0;
for (const std::string& bin_name : bin_files) {
BinSegment seg;
seg.path = bin_name;
seg.file.open(bin_name, std::ios::binary);
if (!seg.file.is_open()) {
Close();
return false;
}
seg.file.seekg(0, std::ios::end);
const std::streampos file_size = seg.file.tellg();
seg.file.clear();
if (file_size <= 0) {
Close();
return false;
}
const uint64_t size = static_cast<uint64_t>(file_size);
seg.raw = (size % RAW_SECTOR_SIZE) == 0;
seg.sector_count = static_cast<uint32_t>(size / (seg.raw ? RAW_SECTOR_SIZE
: SECTOR_SIZE));
seg.start_lba = next_lba;
next_lba += seg.sector_count;
segments_.push_back(std::move(seg));
}
// Convert the pending cue tracks to disc-relative LBAs.
for (const PendingTrack& p : pending_tracks) {
CDTrack t;
t.number = p.number;
t.is_audio = p.is_audio;
t.start_lba = segments_[p.file_index].start_lba + p.index01;
tracks_.push_back(t);
}
// Synthesize a single data track for a bare .bin/.iso or a .cue with no
@@ -108,14 +175,8 @@ bool ISOReader::Open(const std::string& filename) {
tracks_.push_back(t);
}
// Store the resolved bin path for callers (e.g. xa_audio)
bin_path_ = bin_filename;
// Open the ISO/BIN file in binary mode
file_.open(bin_filename, std::ios::binary);
if (!file_.is_open()) {
return false;
}
// Store the resolved data-track path for callers
bin_path_ = segments_.front().path;
is_open_ = true;
@@ -123,7 +184,6 @@ bool ISOReader::Open(const std::string& filename) {
// present. Runtime CD-ROM access only needs sector reads, so keep the
// image mounted even if an ISO9660 header is missing or nonstandard.
if (!ParseVolumeDescriptor()) {
file_.clear();
volume_id_.clear();
root_dir_.lba = 0;
root_dir_.size = 0;
@@ -133,59 +193,74 @@ bool ISOReader::Open(const std::string& filename) {
}
void ISOReader::Close() {
if (file_.is_open()) {
file_.close();
for (BinSegment& seg : segments_) {
if (seg.file.is_open()) {
seg.file.close();
}
}
segments_.clear();
tracks_.clear();
bin_path_.clear();
volume_id_.clear();
root_dir_.lba = 0;
root_dir_.size = 0;
is_open_ = false;
}
BinSegment* ISOReader::SegmentForLBA(uint32_t lba) {
// Linear scan: images carry a handful of segments (one per track file).
for (BinSegment& seg : segments_) {
if (lba >= seg.start_lba && lba - seg.start_lba < seg.sector_count) {
return &seg;
}
}
return nullptr;
}
bool ISOReader::ReadSector(uint32_t lba, uint8_t* buffer) {
if (!is_open_ || !buffer) {
return false;
}
// Clear any error flags
file_.clear();
BinSegment* seg = SegmentForLBA(lba);
if (!seg) {
return false;
}
const uint32_t local_lba = lba - seg->start_lba;
// Determine sector format by checking file size
file_.seekg(0, std::ios::end);
std::streampos file_size = file_.tellg();
// Clear any error flags from a previous failed read
seg->file.clear();
// Clear error flags after seeking
file_.clear();
// Check if file uses raw sectors (2352 bytes) or cooked sectors (2048 bytes)
bool is_raw_format = (file_size % RAW_SECTOR_SIZE == 0);
if (is_raw_format) {
if (seg->raw) {
// Raw BIN format - read full sector, extract user data
std::streampos offset = static_cast<std::streampos>(lba) * RAW_SECTOR_SIZE + RAW_DATA_OFFSET;
file_.seekg(offset, std::ios::beg);
std::streampos offset =
static_cast<std::streampos>(local_lba) * RAW_SECTOR_SIZE + RAW_DATA_OFFSET;
seg->file.seekg(offset, std::ios::beg);
if (!file_.good()) {
file_.clear();
if (!seg->file.good()) {
seg->file.clear();
return false;
}
file_.read(reinterpret_cast<char*>(buffer), SECTOR_SIZE);
std::streamsize bytes_read = file_.gcount();
seg->file.read(reinterpret_cast<char*>(buffer), SECTOR_SIZE);
std::streamsize bytes_read = seg->file.gcount();
bool success = (bytes_read == SECTOR_SIZE);
file_.clear();
seg->file.clear();
return success;
} else {
// ISO format - sectors are already 2048 bytes
std::streampos offset = static_cast<std::streampos>(lba) * SECTOR_SIZE;
file_.seekg(offset, std::ios::beg);
std::streampos offset = static_cast<std::streampos>(local_lba) * SECTOR_SIZE;
seg->file.seekg(offset, std::ios::beg);
if (!file_.good()) {
file_.clear();
if (!seg->file.good()) {
seg->file.clear();
return false;
}
file_.read(reinterpret_cast<char*>(buffer), SECTOR_SIZE);
std::streamsize bytes_read = file_.gcount();
seg->file.read(reinterpret_cast<char*>(buffer), SECTOR_SIZE);
std::streamsize bytes_read = seg->file.gcount();
bool success = (bytes_read == SECTOR_SIZE);
file_.clear();
seg->file.clear();
return success;
}
}
@@ -195,26 +270,24 @@ bool ISOReader::ReadRawSector(uint32_t lba, uint8_t* buffer) {
return false;
}
file_.clear();
file_.seekg(0, std::ios::end);
std::streampos file_size = file_.tellg();
file_.clear();
BinSegment* seg = SegmentForLBA(lba);
if (!seg || !seg->raw) {
return false;
}
const uint32_t local_lba = lba - seg->start_lba;
if (file_size <= 0 || (file_size % RAW_SECTOR_SIZE) != 0) {
seg->file.clear();
std::streampos offset = static_cast<std::streampos>(local_lba) * RAW_SECTOR_SIZE;
seg->file.seekg(offset, std::ios::beg);
if (!seg->file.good()) {
seg->file.clear();
return false;
}
std::streampos offset = static_cast<std::streampos>(lba) * RAW_SECTOR_SIZE;
file_.seekg(offset, std::ios::beg);
if (!file_.good()) {
file_.clear();
return false;
}
file_.read(reinterpret_cast<char*>(buffer), RAW_SECTOR_SIZE);
std::streamsize bytes_read = file_.gcount();
seg->file.read(reinterpret_cast<char*>(buffer), RAW_SECTOR_SIZE);
std::streamsize bytes_read = seg->file.gcount();
bool success = (bytes_read == RAW_SECTOR_SIZE);
file_.clear();
seg->file.clear();
return success;
}
@@ -231,23 +304,12 @@ std::string ISOReader::GetBinPath() const {
}
uint32_t ISOReader::GetSectorCount() {
if (!is_open_) {
if (!is_open_ || segments_.empty()) {
return 0;
}
file_.clear();
file_.seekg(0, std::ios::end);
std::streampos file_size = file_.tellg();
file_.clear();
if (file_size <= 0) {
return 0;
}
const uint64_t size = static_cast<uint64_t>(file_size);
if ((size % RAW_SECTOR_SIZE) == 0) {
return static_cast<uint32_t>(size / RAW_SECTOR_SIZE);
}
return static_cast<uint32_t>(size / SECTOR_SIZE);
const BinSegment& last = segments_.back();
return last.start_lba + last.sector_count;
}
int ISOReader::TrackCount() const {