From 37792ac4af65b28cd5d1c217b98d78e993d869c4 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 11 Oct 2026 23:43:34 -0700 Subject: [PATCH] Disc geometry from cue TRACK mode / sector-0 sync, never file length A raw dump with trailing bytes can be a multiple of 2048 and mounted as a cooked ISO (Legend of Dragoon disc 1: PVD read as zeros, BIOS SystemError B/906). Co-Authored-By: Claude Opus 5.5 (1M context) --- runtime/CMakeLists.txt | 10 + runtime/include/cue_sheet.h | 5 + runtime/include/iso_reader.h | 132 +-- runtime/src/cue_sheet.cpp | 16 +- runtime/src/iso_reader.cpp | 1108 +++++++++++--------- runtime/src/mod_runtime.cpp | 10 +- runtime/tests/test_iso_reader_geometry.cpp | 136 +++ runtime/tests/test_mod_runtime.cpp | 3 + 8 files changed, 837 insertions(+), 583 deletions(-) create mode 100644 runtime/tests/test_iso_reader_geometry.cpp diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 5e84519f..f0732674 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1121,6 +1121,16 @@ if(BUILD_TESTING) target_link_libraries(iso_reader_pregap_test PRIVATE chdr-static) add_test(NAME iso_reader_pregap_test COMMAND iso_reader_pregap_test) + # Raw vs cooked geometry comes from the cue TRACK mode / sector-0 sync, + # never from file length. + add_executable(iso_reader_geometry_test + tests/test_iso_reader_geometry.cpp + src/iso_reader.cpp + src/cue_sheet.cpp) + target_include_directories(iso_reader_geometry_test PRIVATE include ${PSXRECOMP_ROOT}/recompiler/include) + target_link_libraries(iso_reader_geometry_test PRIVATE chdr-static) + add_test(NAME iso_reader_geometry_test COMMAND iso_reader_geometry_test) + add_executable(iso_reader_sbi_test tests/test_iso_reader_sbi.cpp src/iso_reader.cpp diff --git a/runtime/include/cue_sheet.h b/runtime/include/cue_sheet.h index 7900c35d..79a2f3ec 100644 --- a/runtime/include/cue_sheet.h +++ b/runtime/include/cue_sheet.h @@ -45,6 +45,11 @@ struct CueTrackRef { // shifting every later disc LBA (and the TOC) accordingly. uint32_t pregap = 0; uint32_t postgap = 0; + // Bytes per stored sector declared by the TRACK mode: 2352 for AUDIO and + // every "/2352" mode, 2048 or 2336 for the cooked modes. 0 = the mode named + // no size (unknown). The cue is the authority for a file's geometry; the + // file length is not (dumps can carry a partial trailing sector). + uint32_t sector_size = 0; }; struct CueSheet { diff --git a/runtime/include/iso_reader.h b/runtime/include/iso_reader.h index ff75e9c3..7737d494 100644 --- a/runtime/include/iso_reader.h +++ b/runtime/include/iso_reader.h @@ -1,12 +1,12 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include /** * ISO/BIN/CUE Reader for PS1 CD-ROM images @@ -20,10 +20,10 @@ * - Read file contents by path */ -namespace PS1 { - -struct CHDState; - +namespace PS1 { + +struct CHDState; + /** * Information about a file entry in the ISO filesystem */ @@ -44,21 +44,21 @@ struct RootDirectoryInfo { /** * A CD track parsed from the .cue sheet (or the synthesized single data - * track for a bare .bin/.iso). Multi-track discs can carry a + * track for a bare .bin/.iso). Multi-track discs can carry a * Red Book CD-DA audio track after the data track; the CD model's TOC * commands (GetTN/GetTD) must report them. */ -struct CDTrack { +struct CDTrack { int number; // 1-based track number bool is_audio; // true = CD-DA audio (Red Book); false = data - uint32_t start_lba; // disc-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. - // Either way, plus every cue PREGAP/POSTGAP before it - // (gaps the dump does not store). - uint32_t pregap_lba; // disc-relative start of the track's pregap: its cue - // PREGAP if any, else INDEX 00, else INDEX 01 + // owning file's first disc sector plus its INDEX time. + // Either way, plus every cue PREGAP/POSTGAP before it + // (gaps the dump does not store). + uint32_t pregap_lba; // disc-relative start of the track's pregap: its cue + // PREGAP if any, else INDEX 00, else INDEX 01 }; /** @@ -70,23 +70,23 @@ struct CDTrack { struct BinSegment { std::string path; // resolved on-disk path std::ifstream file; // opened for the reader's lifetime - uint32_t start_lba; // disc sector of this file's first stored sector + uint32_t start_lba; // disc sector of this file's first stored sector uint32_t sector_count; // sectors stored in this file bool raw; // 2352-byte raw sectors (BIN) vs 2048 (ISO) -}; - -/** - * A contiguous run of disc sectors. A stored span maps onto sectors of one - * BinSegment; a virtual span is a cue PREGAP/POSTGAP that the dump does not - * store and that reads back as zero (silence), as the CHD path already does. - * Without PREGAP/POSTGAP lines each file is exactly one stored span, so the - * disc layout is the plain concatenation of the files. - */ -struct BinSpan { - uint32_t disc_start; // first disc-relative sector - uint32_t sector_count; - int segment; // index into segments_, or -1 for a virtual gap - uint32_t file_sector; // first sector within the segment's file +}; + +/** + * A contiguous run of disc sectors. A stored span maps onto sectors of one + * BinSegment; a virtual span is a cue PREGAP/POSTGAP that the dump does not + * store and that reads back as zero (silence), as the CHD path already does. + * Without PREGAP/POSTGAP lines each file is exactly one stored span, so the + * disc layout is the plain concatenation of the files. + */ +struct BinSpan { + uint32_t disc_start; // first disc-relative sector + uint32_t sector_count; + int segment; // index into segments_, or -1 for a virtual gap + uint32_t file_sector; // first sector within the segment's file }; class ISOReader { @@ -103,11 +103,23 @@ public: /** * Open an ISO/BIN file for reading - * @param filename Path to .iso, .bin, .img, .car, .cue, or .chd file + * @param filename Path to .iso, .bin, .img, .car, .cue, or .chd file * @return true if opened successfully, false otherwise */ bool Open(const std::string& filename); + /** + * Sector geometry of the first stored file behind an image, decided the + * same way Open() decides it: a .cue's TRACK mode is authoritative; a bare + * file is raw when sector 0 starts with the CD sync pattern. File length + * is never the evidence (raw dumps can be a multiple of 2048). + * @param path .cue or bare .bin/.iso/.img + * @param raw set to true for 2352-byte raw sectors, false for 2048 + * @return false when the image cannot be opened or its geometry is not + * mountable (conflicting or MODE2/2336 track modes) + */ + static bool FirstFileIsRaw(const std::string& path, bool* raw); + /** * Close the currently open file */ @@ -127,15 +139,15 @@ public: * @param buffer Buffer to read into (must be at least 2352 bytes) * @return true if the image stores raw sectors and read succeeded */ - bool ReadRawSector(uint32_t lba, uint8_t* buffer); - - /** - * Read generated or SBI-replacement subchannel Q for a disc LBA. - * SBI records intentionally carry invalid CRC data; valid is false for - * those sectors so the CD controller can retain its last valid Q packet. - */ - bool ReadSubChannelQ(uint32_t lba, uint8_t* buffer, bool* valid) const; - bool HasSubChannelReplacements() const { return !subq_replacements_.empty(); } + bool ReadRawSector(uint32_t lba, uint8_t* buffer); + + /** + * Read generated or SBI-replacement subchannel Q for a disc LBA. + * SBI records intentionally carry invalid CRC data; valid is false for + * those sectors so the CD controller can retain its last valid Q packet. + */ + bool ReadSubChannelQ(uint32_t lba, uint8_t* buffer, bool* valid) const; + bool HasSubChannelReplacements() const { return !subq_replacements_.empty(); } /** * Check if a file is currently open @@ -163,11 +175,11 @@ public: /** * CD-track TOC accessors (multi-track .cue support). * TrackCount() is >= 1 (a bare image synthesizes one data track). - * TrackStartLBA(n)/TrackPregapLBA(n)/TrackIsAudio(n) take a 1-based track number. + * TrackStartLBA(n)/TrackPregapLBA(n)/TrackIsAudio(n) take a 1-based track number. */ - int TrackCount() const; - uint32_t TrackStartLBA(int track) const; - uint32_t TrackPregapLBA(int track) const; + int TrackCount() const; + uint32_t TrackStartLBA(int track) const; + uint32_t TrackPregapLBA(int track) const; bool TrackIsAudio(int track) const; /** @@ -237,23 +249,23 @@ private: std::vector ListFilesByLBA(uint32_t lba, uint32_t dir_size); /** - * Helper: find the span containing a disc-relative LBA. - * @return span pointer, or nullptr when lba is past the disc end + * Helper: find the span containing a disc-relative LBA. + * @return span pointer, or nullptr when lba is past the disc end */ - const BinSpan* SpanForLBA(uint32_t lba) const; - - bool LoadSBICompanion(const std::string& image_path); + const BinSpan* SpanForLBA(uint32_t lba) const; + + bool LoadSBICompanion(const std::string& image_path); bool is_open_; std::string volume_id_; 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() - std::vector spans_; // disc layout over segments_ + cue gaps - uint32_t disc_sector_count_ = 0; // BIN/CUE: end of the last span - std::unique_ptr chd_; // present only for a directly mounted CHD - std::unordered_map> subq_replacements_; -}; + std::vector tracks_; // from the .cue TOC; >=1 entry after Open() + std::vector segments_; // cue FILE entries in disc order; >=1 after Open() + std::vector spans_; // disc layout over segments_ + cue gaps + uint32_t disc_sector_count_ = 0; // BIN/CUE: end of the last span + std::unique_ptr chd_; // present only for a directly mounted CHD + std::unordered_map> subq_replacements_; +}; } // namespace PS1 diff --git a/runtime/src/cue_sheet.cpp b/runtime/src/cue_sheet.cpp index 340608a4..82dedc35 100644 --- a/runtime/src/cue_sheet.cpp +++ b/runtime/src/cue_sheet.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -125,6 +126,7 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) { std::string line; int cur_track_num = -1; bool cur_track_audio = false; + uint32_t cur_sector_size = 0; uint32_t cur_index00 = 0; bool cur_has_index00 = false; uint32_t cur_pregap = 0; @@ -162,8 +164,19 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) { int tn = 0; char type_buf[32] = {0}; if (std::sscanf(upper.c_str(), " TRACK %d %31s", &tn, type_buf) == 2) { + const std::string type(type_buf); cur_track_num = tn; - cur_track_audio = (std::string(type_buf).find("AUDIO") != std::string::npos); + cur_track_audio = (type.find("AUDIO") != std::string::npos); + // "MODE2/2352" etc. name the stored sector size; AUDIO is always + // 2352. Anything else (CDG, malformed) leaves it unknown. + cur_sector_size = 0; + const size_t slash = type.find('/'); + if (cur_track_audio) { + cur_sector_size = 2352; + } else if (slash != std::string::npos) { + const unsigned long n = std::strtoul(type.c_str() + slash + 1, nullptr, 10); + if (n == 2048 || n == 2336 || n == 2352) cur_sector_size = (uint32_t)n; + } cur_index00 = 0; cur_has_index00 = false; cur_pregap = 0; @@ -200,6 +213,7 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) { t.index00 = cur_index00; t.has_index00 = cur_has_index00; t.pregap = cur_pregap; + t.sector_size = cur_sector_size; sheet.tracks.push_back(t); last_pushed_track = cur_track_num; cur_track_num = -1; diff --git a/runtime/src/iso_reader.cpp b/runtime/src/iso_reader.cpp index af866320..5c6c5cfb 100644 --- a/runtime/src/iso_reader.cpp +++ b/runtime/src/iso_reader.cpp @@ -7,19 +7,19 @@ #include #include #include - -namespace PS1 { - -// PS1 CD-ROM sector size (Mode 2, Form 1 user data) -constexpr size_t SECTOR_SIZE = 2048; - -// Full sector size including headers/subchannel (2352 bytes for raw BIN files) -constexpr size_t RAW_SECTOR_SIZE = 2352; - -// Offset to user data in raw sector (Mode 2, Form 1) -constexpr size_t RAW_DATA_OFFSET = 24; - -// Primary Volume Descriptor location + +namespace PS1 { + +// PS1 CD-ROM sector size (Mode 2, Form 1 user data) +constexpr size_t SECTOR_SIZE = 2048; + +// Full sector size including headers/subchannel (2352 bytes for raw BIN files) +constexpr size_t RAW_SECTOR_SIZE = 2352; + +// Offset to user data in raw sector (Mode 2, Form 1) +constexpr size_t RAW_DATA_OFFSET = 24; + +// Primary Volume Descriptor location constexpr uint32_t PVD_SECTOR = 16; constexpr uint32_t CD_FRAMES_PER_SECOND = 75; @@ -30,6 +30,49 @@ static bool valid_bcd(uint8_t value) { return (value & 0x0f) <= 9 && (value >> 4) <= 9; } +// Sector size the cue's TRACK modes declare for one FILE: 0 when no track of +// that file names a size, UINT32_MAX when its tracks disagree. +static uint32_t cue_declared_sector_size( + const std::vector& tracks, size_t file_index) { + uint32_t declared = 0; + for (const PSXRecompV4::CueTrackRef& t : tracks) { + if (t.file_index != file_index || t.sector_size == 0) continue; + if (declared != 0 && declared != t.sector_size) + return std::numeric_limits::max(); + declared = t.sector_size; + } + return declared; +} + +// Raw (2352) vs cooked (2048) for one stored file. The cue's TRACK mode is +// the authority. File length is not evidence: a raw dump can carry a partial +// trailing sector and still be a multiple of 2048, which a length test mounts +// as a cooked ISO so every read lands mid-sector (LBA 16 reads back zeros and +// the BIOS gives up with SystemError B/906). Without a cue mode, a raw sector +// 0 starts with the 12-byte CD sync pattern and an ISO's system area does not; +// length only decides when sector 0 is unreadable. Returns false for layouts +// this reader cannot mount (conflicting modes, MODE2/2336). +static bool stored_file_is_raw(std::ifstream& file, uint64_t size, + uint32_t declared, bool* raw) { + if (declared == std::numeric_limits::max() || declared == 2336) + return false; + if (declared != 0) { + *raw = declared == RAW_SECTOR_SIZE; + return true; + } + static const uint8_t kSync[12] = {0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00}; + uint8_t head[sizeof(kSync)] = {}; + file.clear(); + file.seekg(0, std::ios::beg); + file.read(reinterpret_cast(head), sizeof(head)); + const bool have_head = file.gcount() == (std::streamsize)sizeof(head); + file.clear(); + *raw = have_head ? std::memcmp(head, kSync, sizeof(kSync)) == 0 + : (size % RAW_SECTOR_SIZE) == 0; + return true; +} + static uint32_t bcd_to_binary(uint8_t value) { return (value >> 4) * 10u + (value & 0x0f); } @@ -143,20 +186,20 @@ static bool read_chd_raw(CHDState& chd, uint32_t lba, uint8_t* buffer, } ISOReader::ISOReader() - : is_open_(false) { - root_dir_.lba = 0; - root_dir_.size = 0; -} - -ISOReader::~ISOReader() { - Close(); -} - + : is_open_(false) { + root_dir_.lba = 0; + root_dir_.size = 0; +} + +ISOReader::~ISOReader() { + Close(); +} + bool ISOReader::Open(const std::string& filename) { - // Close any previously opened image (also resets tracks/segments) - Close(); - - // Check if file exists + // Close any previously opened image (also resets tracks/segments) + Close(); + + // Check if file exists if (!std::filesystem::exists(filename)) { return false; } @@ -270,69 +313,75 @@ bool ISOReader::Open(const std::string& filename) { return true; } - // 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. - using PendingTrack = PSXRecompV4::CueTrackRef; - std::vector pending_tracks; - - if (PSXRecompV4::path_has_extension_ci(std::filesystem::path(filename), ".cue")) { - // Parse the .cue via the shared parser: 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. - const PSXRecompV4::CueSheet sheet = - PSXRecompV4::parse_cue_sheet(std::filesystem::path(filename)); - - // A cue we cannot read, one that names no FILE, or one whose payload is - // not raw BINARY (WAVE/MP3 have no fixed sector geometry — mis-mapping - // the TOC would be worse than refusing the image) has nothing to mount. - if (!sheet.opened || sheet.files.empty() || sheet.has_non_binary_file()) { - return false; - } - - bin_files.reserve(sheet.files.size()); - for (const PSXRecompV4::CueFileRef& f : sheet.files) { - bin_files.push_back(f.path.string()); - } - pending_tracks = sheet.tracks; - } 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. - 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)); + // 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. + using PendingTrack = PSXRecompV4::CueTrackRef; + std::vector pending_tracks; + + if (PSXRecompV4::path_has_extension_ci(std::filesystem::path(filename), ".cue")) { + // Parse the .cue via the shared parser: 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. + const PSXRecompV4::CueSheet sheet = + PSXRecompV4::parse_cue_sheet(std::filesystem::path(filename)); + + // A cue we cannot read, one that names no FILE, or one whose payload is + // not raw BINARY (WAVE/MP3 have no fixed sector geometry — mis-mapping + // the TOC would be worse than refusing the image) has nothing to mount. + if (!sheet.opened || sheet.files.empty() || sheet.has_non_binary_file()) { + return false; + } + + bin_files.reserve(sheet.files.size()); + for (const PSXRecompV4::CueFileRef& f : sheet.files) { + bin_files.push_back(f.path.string()); + } + pending_tracks = sheet.tracks; + } 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. + 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); + if (!stored_file_is_raw(seg.file, size, + cue_declared_sector_size(pending_tracks, segments_.size()), + &seg.raw)) { + Close(); + return false; + } + // Whole sectors only; a partial trailing sector is not addressable. + seg.sector_count = static_cast(size / (seg.raw ? RAW_SECTOR_SIZE + : SECTOR_SIZE)); seg.start_lba = 0; // assigned by the layout pass below - segments_.push_back(std::move(seg)); - } - + segments_.push_back(std::move(seg)); + } + // Lay the files out on the disc and convert the pending cue tracks to // disc-relative LBAs. INDEX times are file-relative; a cue PREGAP (before // a track's INDEX 00/01) or POSTGAP (after its data) is a run of sectors @@ -392,29 +441,29 @@ bool ISOReader::Open(const std::string& filename) { } } push_stored(si, cursor, n); - } + } disc_sector_count_ = disc; - - // Synthesize a single data track for a bare .bin/.iso or a .cue with no - // parseable TRACK entries, so TrackCount() is always >= 1. - if (tracks_.empty()) { - CDTrack t; - t.number = 1; t.is_audio = false; t.start_lba = 0; t.pregap_lba = 0; - tracks_.push_back(t); - } - - // Store the resolved data-track path for callers - bin_path_ = segments_.front().path; - - is_open_ = true; - - // Parse the volume descriptor to extract filesystem metadata when - // present. Runtime CD-ROM access only needs sector reads, so keep the - // image mounted even if an ISO9660 header is missing or nonstandard. + + // Synthesize a single data track for a bare .bin/.iso or a .cue with no + // parseable TRACK entries, so TrackCount() is always >= 1. + if (tracks_.empty()) { + CDTrack t; + t.number = 1; t.is_audio = false; t.start_lba = 0; t.pregap_lba = 0; + tracks_.push_back(t); + } + + // Store the resolved data-track path for callers + bin_path_ = segments_.front().path; + + is_open_ = true; + + // Parse the volume descriptor to extract filesystem metadata when + // 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()) { - volume_id_.clear(); - root_dir_.lba = 0; - root_dir_.size = 0; + volume_id_.clear(); + root_dir_.lba = 0; + root_dir_.size = 0; } if (!LoadSBICompanion(filename)) { @@ -424,24 +473,43 @@ bool ISOReader::Open(const std::string& filename) { return true; } - + +bool ISOReader::FirstFileIsRaw(const std::string& path, bool* raw) { + if (!raw) return false; + std::string stored = path; + uint32_t declared = 0; + if (PSXRecompV4::path_has_extension_ci(std::filesystem::path(path), ".cue")) { + const PSXRecompV4::CueSheet sheet = + PSXRecompV4::parse_cue_sheet(std::filesystem::path(path)); + if (!sheet.opened || sheet.files.empty()) return false; + stored = sheet.files.front().path.string(); + declared = cue_declared_sector_size(sheet.tracks, 0); + } + std::ifstream file(stored, std::ios::binary); + if (!file.is_open()) return false; + file.seekg(0, std::ios::end); + const std::streampos size = file.tellg(); + if (size <= 0) return false; + return stored_file_is_raw(file, static_cast(size), declared, raw); +} + void ISOReader::Close() { chd_.reset(); - for (BinSegment& seg : segments_) { - if (seg.file.is_open()) { - seg.file.close(); - } - } - segments_.clear(); + for (BinSegment& seg : segments_) { + if (seg.file.is_open()) { + seg.file.close(); + } + } + segments_.clear(); spans_.clear(); disc_sector_count_ = 0; tracks_.clear(); subq_replacements_.clear(); - bin_path_.clear(); - volume_id_.clear(); - root_dir_.lba = 0; - root_dir_.size = 0; - is_open_ = false; + bin_path_.clear(); + volume_id_.clear(); + root_dir_.lba = 0; + root_dir_.size = 0; + is_open_ = false; } bool ISOReader::LoadSBICompanion(const std::string& image_path) { @@ -516,23 +584,23 @@ bool ISOReader::ReadSubChannelQ(uint32_t lba, uint8_t* buffer, bool* valid) cons *valid = true; return true; } - + const BinSpan* ISOReader::SpanForLBA(uint32_t lba) const { // Linear scan: images carry a handful of spans (one per track file, plus // one per cue gap). for (const BinSpan& span : spans_) { if (lba >= span.disc_start && lba - span.disc_start < span.sector_count) { return &span; - } - } - return nullptr; -} - + } + } + return nullptr; +} + bool ISOReader::ReadSector(uint32_t lba, uint8_t* buffer) { - if (!is_open_ || !buffer) { - return false; - } - + if (!is_open_ || !buffer) { + return false; + } + if (chd_) { uint8_t raw[RAW_SECTOR_SIZE]; CHDTrackMode mode; @@ -546,8 +614,8 @@ bool ISOReader::ReadSector(uint32_t lba, uint8_t* buffer) { const BinSpan* span = SpanForLBA(lba); if (!span) { - return false; - } + return false; + } if (span->segment < 0) { // Cue PREGAP/POSTGAP: not stored in the dump; reads back as zero. std::memset(buffer, 0, SECTOR_SIZE); @@ -555,55 +623,55 @@ bool ISOReader::ReadSector(uint32_t lba, uint8_t* buffer) { } BinSegment* seg = &segments_[(size_t)span->segment]; const uint32_t local_lba = span->file_sector + (lba - span->disc_start); - - // Clear any error flags from a previous failed read - seg->file.clear(); - - if (seg->raw) { - // Raw BIN format - read full sector, extract user data - std::streampos offset = - static_cast(local_lba) * RAW_SECTOR_SIZE + RAW_DATA_OFFSET; - seg->file.seekg(offset, std::ios::beg); - - if (!seg->file.good()) { - seg->file.clear(); - return false; - } - - seg->file.read(reinterpret_cast(buffer), SECTOR_SIZE); - std::streamsize bytes_read = seg->file.gcount(); - bool success = (bytes_read == SECTOR_SIZE); - seg->file.clear(); - return success; - } else { - // ISO format - sectors are already 2048 bytes - std::streampos offset = static_cast(local_lba) * SECTOR_SIZE; - seg->file.seekg(offset, std::ios::beg); - - if (!seg->file.good()) { - seg->file.clear(); - return false; - } - - seg->file.read(reinterpret_cast(buffer), SECTOR_SIZE); - std::streamsize bytes_read = seg->file.gcount(); - bool success = (bytes_read == SECTOR_SIZE); - seg->file.clear(); - return success; - } -} - + + // Clear any error flags from a previous failed read + seg->file.clear(); + + if (seg->raw) { + // Raw BIN format - read full sector, extract user data + std::streampos offset = + static_cast(local_lba) * RAW_SECTOR_SIZE + RAW_DATA_OFFSET; + seg->file.seekg(offset, std::ios::beg); + + if (!seg->file.good()) { + seg->file.clear(); + return false; + } + + seg->file.read(reinterpret_cast(buffer), SECTOR_SIZE); + std::streamsize bytes_read = seg->file.gcount(); + bool success = (bytes_read == SECTOR_SIZE); + seg->file.clear(); + return success; + } else { + // ISO format - sectors are already 2048 bytes + std::streampos offset = static_cast(local_lba) * SECTOR_SIZE; + seg->file.seekg(offset, std::ios::beg); + + if (!seg->file.good()) { + seg->file.clear(); + return false; + } + + seg->file.read(reinterpret_cast(buffer), SECTOR_SIZE); + std::streamsize bytes_read = seg->file.gcount(); + bool success = (bytes_read == SECTOR_SIZE); + seg->file.clear(); + return success; + } +} + bool ISOReader::ReadRawSector(uint32_t lba, uint8_t* buffer) { - if (!is_open_ || !buffer) { - return false; - } - + if (!is_open_ || !buffer) { + return false; + } + if (chd_) return read_chd_raw(*chd_, lba, buffer, nullptr); const BinSpan* span = SpanForLBA(lba); if (!span) { - return false; - } + return false; + } if (span->segment < 0) { // Cue PREGAP/POSTGAP: not stored in the dump; digital silence, the // same as the CHD path's virtual pregap. @@ -615,352 +683,352 @@ bool ISOReader::ReadRawSector(uint32_t lba, uint8_t* buffer) { return false; } const uint32_t local_lba = span->file_sector + (lba - span->disc_start); - - 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; - } - - seg->file.read(reinterpret_cast(buffer), RAW_SECTOR_SIZE); - std::streamsize bytes_read = seg->file.gcount(); - bool success = (bytes_read == RAW_SECTOR_SIZE); - seg->file.clear(); - return success; -} - -bool ISOReader::IsOpen() const { - return is_open_; -} - -std::string ISOReader::GetVolumeID() const { - return volume_id_; -} - -std::string ISOReader::GetBinPath() const { - return bin_path_; -} - + + 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; + } + + seg->file.read(reinterpret_cast(buffer), RAW_SECTOR_SIZE); + std::streamsize bytes_read = seg->file.gcount(); + bool success = (bytes_read == RAW_SECTOR_SIZE); + seg->file.clear(); + return success; +} + +bool ISOReader::IsOpen() const { + return is_open_; +} + +std::string ISOReader::GetVolumeID() const { + return volume_id_; +} + +std::string ISOReader::GetBinPath() const { + return bin_path_; +} + uint32_t ISOReader::GetSectorCount() { if (!is_open_) { return 0; } if (chd_) return chd_->disc_sector_count; return disc_sector_count_; -} - -int ISOReader::TrackCount() const { - return static_cast(tracks_.size()); -} - -uint32_t ISOReader::TrackStartLBA(int track) const { - for (const auto& t : tracks_) { - if (t.number == track) return t.start_lba; - } - return 0; -} - -uint32_t ISOReader::TrackPregapLBA(int track) const { - for (const auto& t : tracks_) { - if (t.number == track) return t.pregap_lba; - } - return 0; -} - -bool ISOReader::TrackIsAudio(int track) const { - for (const auto& t : tracks_) { - if (t.number == track) return t.is_audio; - } - return false; -} - -RootDirectoryInfo ISOReader::GetRootDirectory() const { - return root_dir_; -} - -uint32_t ISOReader::Read733(const uint8_t* data) const { - // Read little-endian half of both-endian 32-bit value - return data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); -} - -bool ISOReader::ParseVolumeDescriptor() { - // Read Primary Volume Descriptor from sector 16 - uint8_t pvd[SECTOR_SIZE]; - if (!ReadSector(PVD_SECTOR, pvd)) { - return false; - } - - // Verify ISO9660 signature: offset 1 should contain "CD001" - if (pvd[0] != 0x01 || std::memcmp(&pvd[1], "CD001", 5) != 0) { - return false; // Not a valid ISO9660 disc - } - - // Extract volume ID (offset 40, 32 bytes, space-padded ASCII) - volume_id_.clear(); - for (int i = 0; i < 32; i++) { - char c = pvd[40 + i]; - if (c != ' ' && c != '\0') { - volume_id_ += c; - } - } - - // Extract root directory record (offset 156, 34 bytes) - const uint8_t* root_record = &pvd[156]; - - // Root directory LBA is at offset 2 within the directory record (both-endian 32-bit) - root_dir_.lba = Read733(&root_record[2]); - - // Root directory size is at offset 10 within the directory record (both-endian 32-bit) - root_dir_.size = Read733(&root_record[10]); - - return true; -} - -bool ISOReader::ParseDirectoryRecord(const uint8_t* data, ISOFileEntry& entry) const { - // Check record length (offset 0) - uint8_t record_len = data[0]; - if (record_len == 0 || record_len < 33) { - return false; // Invalid or padding record - } - - // Extract LBA (offset 2, both-endian 32-bit) - entry.lba = Read733(&data[2]); - - // Extract file size (offset 10, both-endian 32-bit) - entry.size = Read733(&data[10]); - - // Extract file flags (offset 25) - uint8_t flags = data[25]; - entry.is_directory = (flags & 0x02) != 0; - - // Extract filename length (offset 32) - uint8_t name_len = data[32]; - if (name_len == 0) { - return false; // Invalid record - } - - // Extract filename (offset 33) - const char* name_ptr = reinterpret_cast(&data[33]); - - // Handle special directory entries - if (name_len == 1 && name_ptr[0] == '\x00') { - entry.name = "."; // Current directory - return true; - } - if (name_len == 1 && name_ptr[0] == '\x01') { - entry.name = ".."; // Parent directory - return true; - } - - // Parse regular filename, strip version suffix (";1") - entry.name.clear(); - for (uint8_t i = 0; i < name_len; i++) { - char c = name_ptr[i]; - if (c == ';') { - break; // Stop at version separator - } - entry.name += c; - } - - return true; -} - -std::vector ISOReader::ListFilesByLBA(uint32_t lba, uint32_t dir_size) { - std::vector results; - - if (!is_open_ || lba == 0 || dir_size == 0) { - return results; - } - - // Calculate number of sectors needed for directory data - uint32_t num_sectors = (dir_size + 2047) / 2048; - - // Allocate buffer for directory data - std::vector dir_data(num_sectors * 2048, 0); - - // Read all directory sectors - for (uint32_t i = 0; i < num_sectors; i++) { - if (!ReadSector(lba + i, &dir_data[i * 2048])) { - return results; // Error reading sector - } - } - - // Parse directory records - uint32_t offset = 0; - while (offset < dir_size) { - uint8_t record_len = dir_data[offset]; - - if (record_len == 0) { - // Skip to next sector boundary - uint32_t sector_offset = offset % 2048; - if (sector_offset != 0) { - offset += (2048 - sector_offset); - continue; - } - break; - } - - if (offset + record_len > dir_data.size()) break; - - ISOFileEntry entry; - if (ParseDirectoryRecord(&dir_data[offset], entry)) { - if (entry.name != "." && entry.name != "..") { - results.push_back(entry); - } - } - - offset += record_len; - } - - return results; -} - -std::vector ISOReader::ListFiles(const std::string& path) { - std::vector results; - - // Check if file is open - if (!is_open_) { - return results; - } - - if (path.empty()) { - // List root directory - RootDirectoryInfo root = GetRootDirectory(); - return ListFilesByLBA(root.lba, root.size); - } - - // Non-empty path: navigate to that subdirectory within the root - // Find the matching directory entry in root - RootDirectoryInfo root = GetRootDirectory(); - std::vector root_entries = ListFilesByLBA(root.lba, root.size); - - std::string path_upper = path; - std::transform(path_upper.begin(), path_upper.end(), path_upper.begin(), - [](unsigned char c) { return std::toupper(c); }); - - for (const auto& e : root_entries) { - if (!e.is_directory) continue; - std::string name_upper = e.name; - std::transform(name_upper.begin(), name_upper.end(), name_upper.begin(), - [](unsigned char c) { return std::toupper(c); }); - if (name_upper == path_upper) { - // Found the subdirectory — list its contents - return ListFilesByLBA(e.lba, e.size > 0 ? e.size : 2048); - } - } - - return results; // Directory not found -} - -bool ISOReader::FindFile(const std::string& path, ISOFileEntry& entry) { - // Check if file is open - if (!is_open_) { - return false; - } - - // Check if path contains a directory separator - size_t sep = path.find('/'); - if (sep == std::string::npos) { - sep = path.find('\\'); - } - - if (sep != std::string::npos) { - // Subdirectory path: "DIR/FILE" or "DIR\FILE" - std::string dir_name = path.substr(0, sep); - std::string file_name = path.substr(sep + 1); - - // List the subdirectory - std::vector sub_files = ListFiles(dir_name); - - std::string file_upper = file_name; - std::transform(file_upper.begin(), file_upper.end(), file_upper.begin(), - [](unsigned char c) { return std::toupper(c); }); - - for (const auto& f : sub_files) { - std::string name_upper = f.name; - std::transform(name_upper.begin(), name_upper.end(), name_upper.begin(), - [](unsigned char c) { return std::toupper(c); }); - if (name_upper == file_upper) { - entry = f; - return true; - } - } - return false; - } - - // Root-level file: search root directory - std::vector files = ListFiles(""); - - // Search for matching filename (case-insensitive comparison) - for (const auto& file : files) { - std::string file_upper = file.name; - std::string path_upper = path; - - std::transform(file_upper.begin(), file_upper.end(), file_upper.begin(), - [](unsigned char c) { return std::toupper(c); }); - std::transform(path_upper.begin(), path_upper.end(), path_upper.begin(), - [](unsigned char c) { return std::toupper(c); }); - - if (file_upper == path_upper) { - entry = file; - return true; - } - } - - // File not found - return false; -} - -size_t ISOReader::ReadFile(const std::string& path, uint8_t* buffer, size_t max_size) { - // Validate buffer pointer - if (!buffer) { - return 0; - } - - // Find the file - ISOFileEntry entry; - if (!FindFile(path, entry)) { - return 0; // File not found - } - - // Calculate how many bytes to read (min of file size and max_size) - size_t bytes_to_read = std::min(static_cast(entry.size), max_size); - - // Calculate number of sectors to read - uint32_t sectors_to_read = (bytes_to_read + SECTOR_SIZE - 1) / SECTOR_SIZE; - - // Read sectors sequentially - size_t bytes_read = 0; - for (uint32_t i = 0; i < sectors_to_read; i++) { - // Calculate how many bytes to read from this sector - size_t bytes_remaining = bytes_to_read - bytes_read; - size_t sector_bytes = std::min(bytes_remaining, SECTOR_SIZE); - - // Read sector into temporary buffer - uint8_t sector_buffer[SECTOR_SIZE]; - if (!ReadSector(entry.lba + i, sector_buffer)) { - return bytes_read; // Error - return what we've read so far - } - - // Copy data to output buffer - std::memcpy(buffer + bytes_read, sector_buffer, sector_bytes); - bytes_read += sector_bytes; - } - - return bytes_read; -} - -size_t ISOReader::GetFileSize(const std::string& path) { - // Find the file - ISOFileEntry entry; - if (!FindFile(path, entry)) { - return 0; // File not found - } - - // Return file size - return entry.size; -} - -} // namespace PS1 +} + +int ISOReader::TrackCount() const { + return static_cast(tracks_.size()); +} + +uint32_t ISOReader::TrackStartLBA(int track) const { + for (const auto& t : tracks_) { + if (t.number == track) return t.start_lba; + } + return 0; +} + +uint32_t ISOReader::TrackPregapLBA(int track) const { + for (const auto& t : tracks_) { + if (t.number == track) return t.pregap_lba; + } + return 0; +} + +bool ISOReader::TrackIsAudio(int track) const { + for (const auto& t : tracks_) { + if (t.number == track) return t.is_audio; + } + return false; +} + +RootDirectoryInfo ISOReader::GetRootDirectory() const { + return root_dir_; +} + +uint32_t ISOReader::Read733(const uint8_t* data) const { + // Read little-endian half of both-endian 32-bit value + return data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); +} + +bool ISOReader::ParseVolumeDescriptor() { + // Read Primary Volume Descriptor from sector 16 + uint8_t pvd[SECTOR_SIZE]; + if (!ReadSector(PVD_SECTOR, pvd)) { + return false; + } + + // Verify ISO9660 signature: offset 1 should contain "CD001" + if (pvd[0] != 0x01 || std::memcmp(&pvd[1], "CD001", 5) != 0) { + return false; // Not a valid ISO9660 disc + } + + // Extract volume ID (offset 40, 32 bytes, space-padded ASCII) + volume_id_.clear(); + for (int i = 0; i < 32; i++) { + char c = pvd[40 + i]; + if (c != ' ' && c != '\0') { + volume_id_ += c; + } + } + + // Extract root directory record (offset 156, 34 bytes) + const uint8_t* root_record = &pvd[156]; + + // Root directory LBA is at offset 2 within the directory record (both-endian 32-bit) + root_dir_.lba = Read733(&root_record[2]); + + // Root directory size is at offset 10 within the directory record (both-endian 32-bit) + root_dir_.size = Read733(&root_record[10]); + + return true; +} + +bool ISOReader::ParseDirectoryRecord(const uint8_t* data, ISOFileEntry& entry) const { + // Check record length (offset 0) + uint8_t record_len = data[0]; + if (record_len == 0 || record_len < 33) { + return false; // Invalid or padding record + } + + // Extract LBA (offset 2, both-endian 32-bit) + entry.lba = Read733(&data[2]); + + // Extract file size (offset 10, both-endian 32-bit) + entry.size = Read733(&data[10]); + + // Extract file flags (offset 25) + uint8_t flags = data[25]; + entry.is_directory = (flags & 0x02) != 0; + + // Extract filename length (offset 32) + uint8_t name_len = data[32]; + if (name_len == 0) { + return false; // Invalid record + } + + // Extract filename (offset 33) + const char* name_ptr = reinterpret_cast(&data[33]); + + // Handle special directory entries + if (name_len == 1 && name_ptr[0] == '\x00') { + entry.name = "."; // Current directory + return true; + } + if (name_len == 1 && name_ptr[0] == '\x01') { + entry.name = ".."; // Parent directory + return true; + } + + // Parse regular filename, strip version suffix (";1") + entry.name.clear(); + for (uint8_t i = 0; i < name_len; i++) { + char c = name_ptr[i]; + if (c == ';') { + break; // Stop at version separator + } + entry.name += c; + } + + return true; +} + +std::vector ISOReader::ListFilesByLBA(uint32_t lba, uint32_t dir_size) { + std::vector results; + + if (!is_open_ || lba == 0 || dir_size == 0) { + return results; + } + + // Calculate number of sectors needed for directory data + uint32_t num_sectors = (dir_size + 2047) / 2048; + + // Allocate buffer for directory data + std::vector dir_data(num_sectors * 2048, 0); + + // Read all directory sectors + for (uint32_t i = 0; i < num_sectors; i++) { + if (!ReadSector(lba + i, &dir_data[i * 2048])) { + return results; // Error reading sector + } + } + + // Parse directory records + uint32_t offset = 0; + while (offset < dir_size) { + uint8_t record_len = dir_data[offset]; + + if (record_len == 0) { + // Skip to next sector boundary + uint32_t sector_offset = offset % 2048; + if (sector_offset != 0) { + offset += (2048 - sector_offset); + continue; + } + break; + } + + if (offset + record_len > dir_data.size()) break; + + ISOFileEntry entry; + if (ParseDirectoryRecord(&dir_data[offset], entry)) { + if (entry.name != "." && entry.name != "..") { + results.push_back(entry); + } + } + + offset += record_len; + } + + return results; +} + +std::vector ISOReader::ListFiles(const std::string& path) { + std::vector results; + + // Check if file is open + if (!is_open_) { + return results; + } + + if (path.empty()) { + // List root directory + RootDirectoryInfo root = GetRootDirectory(); + return ListFilesByLBA(root.lba, root.size); + } + + // Non-empty path: navigate to that subdirectory within the root + // Find the matching directory entry in root + RootDirectoryInfo root = GetRootDirectory(); + std::vector root_entries = ListFilesByLBA(root.lba, root.size); + + std::string path_upper = path; + std::transform(path_upper.begin(), path_upper.end(), path_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + + for (const auto& e : root_entries) { + if (!e.is_directory) continue; + std::string name_upper = e.name; + std::transform(name_upper.begin(), name_upper.end(), name_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + if (name_upper == path_upper) { + // Found the subdirectory — list its contents + return ListFilesByLBA(e.lba, e.size > 0 ? e.size : 2048); + } + } + + return results; // Directory not found +} + +bool ISOReader::FindFile(const std::string& path, ISOFileEntry& entry) { + // Check if file is open + if (!is_open_) { + return false; + } + + // Check if path contains a directory separator + size_t sep = path.find('/'); + if (sep == std::string::npos) { + sep = path.find('\\'); + } + + if (sep != std::string::npos) { + // Subdirectory path: "DIR/FILE" or "DIR\FILE" + std::string dir_name = path.substr(0, sep); + std::string file_name = path.substr(sep + 1); + + // List the subdirectory + std::vector sub_files = ListFiles(dir_name); + + std::string file_upper = file_name; + std::transform(file_upper.begin(), file_upper.end(), file_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + + for (const auto& f : sub_files) { + std::string name_upper = f.name; + std::transform(name_upper.begin(), name_upper.end(), name_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + if (name_upper == file_upper) { + entry = f; + return true; + } + } + return false; + } + + // Root-level file: search root directory + std::vector files = ListFiles(""); + + // Search for matching filename (case-insensitive comparison) + for (const auto& file : files) { + std::string file_upper = file.name; + std::string path_upper = path; + + std::transform(file_upper.begin(), file_upper.end(), file_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + std::transform(path_upper.begin(), path_upper.end(), path_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + + if (file_upper == path_upper) { + entry = file; + return true; + } + } + + // File not found + return false; +} + +size_t ISOReader::ReadFile(const std::string& path, uint8_t* buffer, size_t max_size) { + // Validate buffer pointer + if (!buffer) { + return 0; + } + + // Find the file + ISOFileEntry entry; + if (!FindFile(path, entry)) { + return 0; // File not found + } + + // Calculate how many bytes to read (min of file size and max_size) + size_t bytes_to_read = std::min(static_cast(entry.size), max_size); + + // Calculate number of sectors to read + uint32_t sectors_to_read = (bytes_to_read + SECTOR_SIZE - 1) / SECTOR_SIZE; + + // Read sectors sequentially + size_t bytes_read = 0; + for (uint32_t i = 0; i < sectors_to_read; i++) { + // Calculate how many bytes to read from this sector + size_t bytes_remaining = bytes_to_read - bytes_read; + size_t sector_bytes = std::min(bytes_remaining, SECTOR_SIZE); + + // Read sector into temporary buffer + uint8_t sector_buffer[SECTOR_SIZE]; + if (!ReadSector(entry.lba + i, sector_buffer)) { + return bytes_read; // Error - return what we've read so far + } + + // Copy data to output buffer + std::memcpy(buffer + bytes_read, sector_buffer, sector_bytes); + bytes_read += sector_bytes; + } + + return bytes_read; +} + +size_t ISOReader::GetFileSize(const std::string& path) { + // Find the file + ISOFileEntry entry; + if (!FindFile(path, entry)) { + return 0; // File not found + } + + // Return file size + return entry.size; +} + +} // namespace PS1 diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 86913b04..343186b9 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -714,8 +714,14 @@ bool sha256_disc_range(const std::filesystem::path& image, std::array bytes{}; size_t remaining = size; uint64_t at = location; - const bool raw_source = file_size > 0 && - ((uint64_t)file_size % 2352u) == 0; + // Same geometry rule as the mount (cue TRACK mode, else sector-0 sync): + // a raw dump whose length happens to be a multiple of 2048 is still raw. + bool raw_source = false; + if (target == ModPatchTarget::DiscUser && + !PS1::ISOReader::FirstFileIsRaw(image.string(), &raw_source)) { + if (error) *error = "cannot determine stock image sector geometry: " + image.string(); + return false; + } while (remaining != 0) { uint64_t physical = at; size_t chunk = remaining; diff --git a/runtime/tests/test_iso_reader_geometry.cpp b/runtime/tests/test_iso_reader_geometry.cpp new file mode 100644 index 00000000..9b4d5995 --- /dev/null +++ b/runtime/tests/test_iso_reader_geometry.cpp @@ -0,0 +1,136 @@ +// Raw (2352) vs cooked (2048) sector geometry. The cue's TRACK mode decides; +// a bare file is raw when sector 0 starts with the CD sync pattern. File +// length is never the evidence: the Legend of Dragoon disc 1 dump is a raw +// MODE2/2352 image with 384 trailing bytes whose length is a multiple of 2048, +// and a length test mounted it as a cooked ISO (PVD read back as zeros, BIOS +// SystemError B/906). + +#include "iso_reader.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +static int g_failures = 0; +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + ++g_failures; \ + } \ + } while (0) + +static const uint8_t kSync[12] = {0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00}; + +// Raw MODE2 sectors: sync + header + 8-byte subheader, user data = marker i+1. +static void write_raw(const fs::path& path, uint32_t sectors, size_t trailing) { + std::ofstream out(path, std::ios::binary); + for (uint32_t i = 0; i < sectors; ++i) { + std::vector s(2352, 0); + std::memcpy(s.data(), kSync, sizeof(kSync)); + s[15] = 2; + std::memset(s.data() + 24, (int)(i + 1), 2048); + out.write(reinterpret_cast(s.data()), (std::streamsize)s.size()); + } + std::vector tail(trailing, 0xEE); + out.write(reinterpret_cast(tail.data()), (std::streamsize)tail.size()); +} + +// Cooked sectors; sector 0 is an all-zero system-area sector, others = i+1. +static void write_cooked(const fs::path& path, uint32_t sectors) { + std::ofstream out(path, std::ios::binary); + for (uint32_t i = 0; i < sectors; ++i) { + std::vector s(2048, i == 0 ? 0 : (uint8_t)(i + 1)); + out.write(reinterpret_cast(s.data()), (std::streamsize)s.size()); + } +} + +static void write_text(const fs::path& path, const std::string& text) { + std::ofstream out(path); + out << text; +} + +static void check_markers(const std::string& image, uint32_t sectors, bool first_is_zero) { + PS1::ISOReader r; + CHECK(r.Open(image)); + CHECK(r.GetSectorCount() == sectors); + uint8_t buf[2048]; + for (uint32_t i = 0; i < sectors; ++i) { + CHECK(r.ReadSector(i, buf)); + const uint8_t want = (i == 0 && first_is_zero) ? 0 : (uint8_t)(i + 1); + if (buf[0] != want || buf[2047] != want) { + std::printf(" %s sector %u: got %02x want %02x\n", image.c_str(), i, buf[0], want); + ++g_failures; + } + } + CHECK(!r.ReadSector(sectors, buf)); +} + +int main() { + const fs::path dir = fs::temp_directory_path() / "psxrecomp-iso-geometry-test"; + fs::remove_all(dir); + fs::create_directories(dir); + + // 20 raw sectors + 64 trailing bytes = 47104 = 23 * 2048: a length test + // calls this a cooked ISO. + const fs::path odd = dir / "odd.bin"; + write_raw(odd, 20, 64); + CHECK(fs::file_size(odd) % 2048 == 0); + CHECK(fs::file_size(odd) % 2352 != 0); + + // Cue declares MODE2/2352: raw. + write_text(dir / "odd.cue", "FILE \"odd.bin\" BINARY\n TRACK 01 MODE2/2352\n INDEX 01 00:00:00\n"); + check_markers((dir / "odd.cue").string(), 20, false); + bool raw = false; + CHECK(PS1::ISOReader::FirstFileIsRaw((dir / "odd.cue").string(), &raw) && raw); + + // Bare: sector-0 sync says raw. + check_markers(odd.string(), 20, false); + raw = false; + CHECK(PS1::ISOReader::FirstFileIsRaw(odd.string(), &raw) && raw); + + // A cooked ISO whose length is a multiple of BOTH 2048 and 2352 + // (147 * 2048 = 128 * 2352): no sync at sector 0, so cooked. + const fs::path both = dir / "both.iso"; + write_cooked(both, 147); + CHECK(fs::file_size(both) % 2352 == 0); + check_markers(both.string(), 147, true); + raw = true; + CHECK(PS1::ISOReader::FirstFileIsRaw(both.string(), &raw) && !raw); + + // Cue MODE1/2048 over a cooked file: cooked. + write_text(dir / "both.cue", "FILE \"both.iso\" BINARY\n TRACK 01 MODE1/2048\n INDEX 01 00:00:00\n"); + check_markers((dir / "both.cue").string(), 147, true); + + // Tracks of one FILE disagreeing on sector size: refuse. + write_text(dir / "conflict.cue", + "FILE \"odd.bin\" BINARY\n TRACK 01 MODE2/2352\n INDEX 01 00:00:00\n" + " TRACK 02 MODE1/2048\n INDEX 01 00:00:10\n"); + { + PS1::ISOReader r; + CHECK(!r.Open((dir / "conflict.cue").string())); + CHECK(!PS1::ISOReader::FirstFileIsRaw((dir / "conflict.cue").string(), &raw)); + } + + // MODE2/2336 has no sync/header; not mountable here: refuse, never misread. + write_text(dir / "m2336.cue", "FILE \"odd.bin\" BINARY\n TRACK 01 MODE2/2336\n INDEX 01 00:00:00\n"); + { + PS1::ISOReader r; + CHECK(!r.Open((dir / "m2336.cue").string())); + } + + fs::remove_all(dir); + if (g_failures) { + std::printf("iso_reader_geometry_test: %d failure(s)\n", g_failures); + return 1; + } + std::printf("iso_reader_geometry_test: PASS\n"); + return 0; +} diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index a0a6ec1c..977f3fe9 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -1165,6 +1165,9 @@ int main(int argc, char** argv) { check(!psx_mod_read_disc_file("S0",nullptr,0,&bytes),"directory rejected"); std::vector raw_iso(24*2352); for(unsigned i=0;i<24;++i) { + /* A raw sector starts with the CD sync pattern; geometry is read from + * it, never from the file length. */ + raw_iso[i*2352]=0; std::fill_n(raw_iso.begin()+i*2352+1,10,(uint8_t)0xFF); raw_iso[i*2352+11]=0; raw_iso[i*2352+15]=2; std::copy_n(iso.begin()+i*2048,2048,raw_iso.begin()+i*2352+24); }