mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* 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<ISOFileEntry> 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<CDTrack> tracks_; // from the .cue TOC; >=1 entry after Open()
|
||||
std::vector<BinSegment> segments_; // cue FILE entries in disc order; >=1 after Open()
|
||||
std::vector<BinSpan> spans_; // disc layout over segments_ + cue gaps
|
||||
uint32_t disc_sector_count_ = 0; // BIN/CUE: end of the last span
|
||||
std::unique_ptr<CHDState> chd_; // present only for a directly mounted CHD
|
||||
std::unordered_map<uint32_t, std::array<uint8_t, 12>> subq_replacements_;
|
||||
};
|
||||
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()
|
||||
std::vector<BinSpan> spans_; // disc layout over segments_ + cue gaps
|
||||
uint32_t disc_sector_count_ = 0; // BIN/CUE: end of the last span
|
||||
std::unique_ptr<CHDState> chd_; // present only for a directly mounted CHD
|
||||
std::unordered_map<uint32_t, std::array<uint8_t, 12>> subq_replacements_;
|
||||
};
|
||||
|
||||
} // namespace PS1
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
@@ -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;
|
||||
|
||||
+588
-520
File diff suppressed because it is too large
Load Diff
@@ -714,8 +714,14 @@ bool sha256_disc_range(const std::filesystem::path& image,
|
||||
std::array<uint8_t, 2048> 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;
|
||||
|
||||
@@ -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 <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<uint8_t> 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<const char*>(s.data()), (std::streamsize)s.size());
|
||||
}
|
||||
std::vector<uint8_t> tail(trailing, 0xEE);
|
||||
out.write(reinterpret_cast<const char*>(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<uint8_t> s(2048, i == 0 ? 0 : (uint8_t)(i + 1));
|
||||
out.write(reinterpret_cast<const char*>(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;
|
||||
}
|
||||
@@ -1165,6 +1165,9 @@ int main(int argc, char** argv) {
|
||||
check(!psx_mod_read_disc_file("S0",nullptr,0,&bytes),"directory rejected");
|
||||
std::vector<uint8_t> 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user