diff --git a/runtime/include/iso_reader.h b/runtime/include/iso_reader.h index da0fc21b..9b1b3363 100644 --- a/runtime/include/iso_reader.h +++ b/runtime/include/iso_reader.h @@ -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 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 tracks_; // from the .cue TOC; >=1 entry after Open() + std::vector segments_; // cue FILE entries in disc order; >=1 after Open() }; } // namespace PS1 diff --git a/runtime/src/iso_reader.cpp b/runtime/src/iso_reader.cpp index 3f6baabc..3c60ea75 100644 --- a/runtime/src/iso_reader.cpp +++ b/runtime/src/iso_reader.cpp @@ -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 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 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(file_size); + seg.raw = (size % RAW_SECTOR_SIZE) == 0; + seg.sector_count = static_cast(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(lba) * RAW_SECTOR_SIZE + RAW_DATA_OFFSET; - file_.seekg(offset, std::ios::beg); + std::streampos offset = + static_cast(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(buffer), SECTOR_SIZE); - std::streamsize bytes_read = file_.gcount(); + seg->file.read(reinterpret_cast(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(lba) * SECTOR_SIZE; - file_.seekg(offset, std::ios::beg); + std::streampos offset = static_cast(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(buffer), SECTOR_SIZE); - std::streamsize bytes_read = file_.gcount(); + seg->file.read(reinterpret_cast(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(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(lba) * RAW_SECTOR_SIZE; - file_.seekg(offset, std::ios::beg); - if (!file_.good()) { - file_.clear(); - return false; - } - - file_.read(reinterpret_cast(buffer), RAW_SECTOR_SIZE); - std::streamsize bytes_read = file_.gcount(); + seg->file.read(reinterpret_cast(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(file_size); - if ((size % RAW_SECTOR_SIZE) == 0) { - return static_cast(size / RAW_SECTOR_SIZE); - } - return static_cast(size / SECTOR_SIZE); + const BinSegment& last = segments_.back(); + return last.start_lba + last.sector_count; } int ISOReader::TrackCount() const {