mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user