CDImage: Support reading non-mode2/raw sectors

Transparently convert them to mode2/raw.

Also detect non-raw iso/bin files and flag appropriately.
pull/3774/head
Stenzek 4 days ago
parent 9c79341cf4
commit b89e1bfeb4
No known key found for this signature in database

@ -6,6 +6,7 @@
add_executable(util-tests
animated_image_tests.cpp
cd_image_tests.cpp
elf_parser_tests.cpp
cue_parser_tests.cpp
image_tests.cpp

@ -0,0 +1,217 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "util/cd_image.h"
#include "common/bcdutils.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/path.h"
#include "common/types.h"
#include "fmt/core.h"
#include <gtest/gtest.h>
#include <array>
#include <cstdio>
#include <cstring>
#include <span>
#include <string>
namespace {
// TODO: Extract this out, use FileSystem temporary file
class TempFile
{
public:
TempFile(const char* name, const char* extension)
: m_path(::Path::Combine(FileSystem::GetWorkingDirectory(), fmt::format("{}_{}.{}", name, s_counter++, extension)))
{
}
~TempFile()
{
if (!m_path.empty())
FileSystem::DeleteFile(m_path.c_str());
}
const std::string& GetPath() const { return m_path; }
bool Write(std::span<const u8> data)
{
std::FILE* fp = FileSystem::OpenCFile(m_path.c_str(), "wb");
if (!fp)
return false;
const bool result = (data.empty() || std::fwrite(data.data(), data.size(), 1, fp) == 1);
std::fclose(fp);
return result;
}
bool WriteString(std::string_view data)
{
return Write(std::span<const u8>(reinterpret_cast<const u8*>(data.data()), data.size()));
}
private:
static inline u32 s_counter = 0;
std::string m_path;
};
std::array<u8, CDImage::RAW_SECTOR_SIZE> MakePatternSector()
{
std::array<u8, CDImage::RAW_SECTOR_SIZE> sector = {};
for (u32 i = 0; i < sector.size(); i++)
sector[i] = static_cast<u8>(i * 3 + 7);
return sector;
}
void ExpectSyncAndHeader(const std::array<u8, CDImage::RAW_SECTOR_SIZE>& sector, u32 mode, u32 lba = 0)
{
const CDImage::Position position = CDImage::Position::FromLBA(lba);
EXPECT_EQ(std::memcmp(sector.data(), CDImage::SECTOR_SYNC_DATA.data(), CDImage::SECTOR_SYNC_DATA.size()), 0);
EXPECT_EQ(sector[12], BinaryToBCD(position.minute));
EXPECT_EQ(sector[13], BinaryToBCD(position.second));
EXPECT_EQ(sector[14], BinaryToBCD(position.frame));
EXPECT_EQ(sector[15], mode);
}
} // namespace
TEST(CDImage, ConvertMode1ToRaw)
{
std::array<u8, CDImage::RAW_SECTOR_SIZE> sector = MakePatternSector();
const std::array<u8, CDImage::DATA_SECTOR_SIZE> payload = [&sector] {
std::array<u8, CDImage::DATA_SECTOR_SIZE> ret;
std::memcpy(ret.data(), sector.data(), ret.size());
return ret;
}();
CDImage::ConvertSectorToRaw(sector.data(), 0, CDImage::TrackMode::Mode1);
ExpectSyncAndHeader(sector, 0x01);
EXPECT_EQ(std::memcmp(&sector[16], payload.data(), payload.size()), 0);
EXPECT_EQ(std::memcmp(&sector[2068], std::array<u8, 8>{}.data(), 8), 0);
}
TEST(CDImage, ConvertMode2ToRaw)
{
std::array<u8, CDImage::RAW_SECTOR_SIZE> sector = MakePatternSector();
const std::array<u8, CDImage::MODE2_DATA_SECTOR_SIZE> payload = [&sector] {
std::array<u8, CDImage::MODE2_DATA_SECTOR_SIZE> ret;
std::memcpy(ret.data(), sector.data(), ret.size());
return ret;
}();
CDImage::ConvertSectorToRaw(sector.data(), 0, CDImage::TrackMode::Mode2);
ExpectSyncAndHeader(sector, 0x02);
EXPECT_EQ(std::memcmp(&sector[16], payload.data(), payload.size()), 0);
}
TEST(CDImage, ConvertMode2FormsToRaw)
{
std::array<u8, CDImage::RAW_SECTOR_SIZE> form1 = MakePatternSector();
std::array<u8, CDImage::DATA_SECTOR_SIZE> form1_payload;
std::memcpy(form1_payload.data(), form1.data(), form1_payload.size());
CDImage::ConvertSectorToRaw(form1.data(), 0, CDImage::TrackMode::Mode2Form1);
ExpectSyncAndHeader(form1, 0x02);
EXPECT_EQ(form1[18], 0x08);
EXPECT_EQ(form1[22], 0x08);
EXPECT_EQ(std::memcmp(&form1[24], form1_payload.data(), form1_payload.size()), 0);
std::array<u8, CDImage::RAW_SECTOR_SIZE> form2 = MakePatternSector();
std::array<u8, 2324> form2_payload;
std::memcpy(form2_payload.data(), form2.data(), form2_payload.size());
CDImage::ConvertSectorToRaw(form2.data(), 0, CDImage::TrackMode::Mode2Form2);
ExpectSyncAndHeader(form2, 0x02);
EXPECT_EQ(form2[18], 0x28);
EXPECT_EQ(form2[22], 0x28);
EXPECT_EQ(std::memcmp(&form2[24], form2_payload.data(), form2_payload.size()), 0);
}
TEST(CDImage, ConvertMode2FormMixToRaw)
{
std::array<u8, CDImage::RAW_SECTOR_SIZE> sector = MakePatternSector();
sector[2] = 0x20;
sector[6] = 0x20;
std::array<u8, 2332> payload;
std::memcpy(payload.data(), sector.data(), payload.size());
CDImage::ConvertSectorToRaw(sector.data(), 0, CDImage::TrackMode::Mode2FormMix);
ExpectSyncAndHeader(sector, 0x02);
EXPECT_EQ(std::memcmp(&sector[16], payload.data(), payload.size()), 0);
}
TEST(CDImage, CueMode1_2048ReadsAsRaw)
{
TempFile bin("duckstation_cd_image_mode1", "bin");
std::array<u8, CDImage::DATA_SECTOR_SIZE> payload = {};
for (u32 i = 0; i < payload.size(); i++)
payload[i] = static_cast<u8>(i);
ASSERT_TRUE(bin.Write(payload));
TempFile cue("duckstation_cd_image_mode1", "cue");
const std::string cue_data =
fmt::format("FILE \"{}\" BINARY\nTRACK 01 MODE1/2048\nINDEX 01 00:00:00\n", Path::GetFileName(bin.GetPath()));
ASSERT_TRUE(cue.WriteString(cue_data));
Error error;
std::unique_ptr<CDImage> image = CDImage::Open(cue.GetPath().c_str(), false, &error);
ASSERT_TRUE(image) << error.GetDescription();
std::array<u8, CDImage::RAW_SECTOR_SIZE> sector;
ASSERT_TRUE(image->ReadRawSector(sector.data(), nullptr));
ExpectSyncAndHeader(sector, 0x01, 2 * CDImage::FRAMES_PER_SECOND);
EXPECT_EQ(std::memcmp(&sector[16], payload.data(), payload.size()), 0);
}
TEST(CDImage, Iso2048DetectedAsMode1)
{
TempFile iso("duckstation_cd_image_mode1", "iso");
std::array<u8, CDImage::DATA_SECTOR_SIZE> payload = {};
for (u32 i = 0; i < payload.size(); i++)
payload[i] = static_cast<u8>(i + 1);
ASSERT_TRUE(iso.Write(payload));
Error error;
std::unique_ptr<CDImage> image = CDImage::Open(iso.GetPath().c_str(), false, &error);
ASSERT_TRUE(image) << error.GetDescription();
EXPECT_EQ(image->GetTrackMode(1), CDImage::TrackMode::Mode1);
std::array<u8, CDImage::RAW_SECTOR_SIZE> sector;
ASSERT_TRUE(image->ReadRawSector(sector.data(), nullptr));
ExpectSyncAndHeader(sector, 0x01, 2 * CDImage::FRAMES_PER_SECOND);
EXPECT_EQ(std::memcmp(&sector[16], payload.data(), payload.size()), 0);
}
TEST(CDImage, IsoRawDetectedAsRaw)
{
TempFile iso("duckstation_cd_image_raw", "iso");
std::array<u8, CDImage::RAW_SECTOR_SIZE> raw = MakePatternSector();
std::memcpy(raw.data(), CDImage::SECTOR_SYNC_DATA.data(), CDImage::SECTOR_SYNC_DATA.size());
raw[12] = 0x00;
raw[13] = 0x00;
raw[14] = 0x00;
raw[15] = 0x02;
ASSERT_TRUE(iso.Write(raw));
Error error;
std::unique_ptr<CDImage> image = CDImage::Open(iso.GetPath().c_str(), false, &error);
ASSERT_TRUE(image) << error.GetDescription();
EXPECT_EQ(image->GetTrackMode(1), CDImage::TrackMode::Mode2Raw);
std::array<u8, CDImage::RAW_SECTOR_SIZE> sector;
ASSERT_TRUE(image->ReadRawSector(sector.data(), nullptr));
EXPECT_EQ(std::memcmp(sector.data(), raw.data(), raw.size()), 0);
}

@ -4,6 +4,7 @@
<ItemGroup>
<ClCompile Include="..\..\dep\googletest\src\gtest_main.cc" />
<ClCompile Include="animated_image_tests.cpp" />
<ClCompile Include="cd_image_tests.cpp" />
<ClCompile Include="cue_parser_tests.cpp" />
<ClCompile Include="elf_parser_tests.cpp" />
<ClCompile Include="image_tests.cpp" />

@ -9,5 +9,6 @@
<ClCompile Include="stub_host.cpp" />
<ClCompile Include="ini_settings_interface_tests.cpp" />
<ClCompile Include="object_archive_tests.cpp" />
<ClCompile Include="cd_image_tests.cpp" />
</ItemGroup>
</Project>

@ -13,12 +13,17 @@
#include "common/path.h"
#include "common/string_util.h"
#include "libchdr/cdrom.h"
#include <fmt/format.h>
#include <array>
LOG_CHANNEL(CDImage);
const std::array<u8, CDImage::SECTOR_SYNC_SIZE> CDImage::SECTOR_SYNC_DATA = {0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00};
CDImage::CDImage() = default;
CDImage::~CDImage() = default;
@ -148,6 +153,89 @@ const char* CDImage::GetTrackModeDisplayName(TrackMode mode)
return Host::TranslateToCString("CDImage", track_mode_strings[static_cast<size_t>(mode)], "TrackMode");
}
void CDImage::FillRawSectorSyncAndHeader(u8* sector, LBA lba, u8 sector_mode)
{
std::memcpy(sector, SECTOR_SYNC_DATA.data(), SECTOR_SYNC_DATA.size());
const Position position = Position::FromLBA(lba);
sector[12] = BinaryToBCD(position.minute);
sector[13] = BinaryToBCD(position.second);
sector[14] = BinaryToBCD(position.frame);
sector[15] = sector_mode;
}
void CDImage::ConvertSectorToRaw(void* buffer, u32 lba, TrackMode mode)
{
u8* const sector = reinterpret_cast<u8*>(buffer);
switch (mode)
{
case TrackMode::Mode1:
{
std::memmove(sector + 16, sector, DATA_SECTOR_SIZE);
FillRawSectorSyncAndHeader(sector, lba, 0x01);
edc_set(&sector[2064], edc_compute(sector, 2064));
std::memset(&sector[2068], 0, 8);
ecc_generate(sector);
return;
}
case TrackMode::Mode2:
{
std::memmove(sector + 16, sector, MODE2_DATA_SECTOR_SIZE);
FillRawSectorSyncAndHeader(sector, lba, 0x02);
return;
}
case TrackMode::Mode2Form1:
{
std::memmove(sector + 24, sector, DATA_SECTOR_SIZE);
FillRawSectorSyncAndHeader(sector, lba, 0x02);
std::memset(&sector[16], 0, 8); // subheader
sector[18] = 0x08; // data
sector[22] = 0x08;
edc_set(&sector[2072], edc_compute(&sector[16], 2056));
ecc_generate(sector);
return;
}
case TrackMode::Mode2Form2:
{
std::memmove(sector + 24, sector, 2324);
FillRawSectorSyncAndHeader(sector, lba, 0x02);
std::memset(&sector[16], 0, 8); // subheader
sector[18] = 0x28; // data, form 2
sector[22] = 0x28;
edc_set(&sector[2348], edc_compute(&sector[16], 2332));
return;
}
case TrackMode::Mode2FormMix:
{
std::memmove(sector + 16, sector, 2332);
FillRawSectorSyncAndHeader(sector, lba, 0x02);
// Bit 5 of the submode byte selects Form 2. The subheader is duplicated; prefer the first copy.
if ((sector[16 + 2] & 0x20) != 0)
{
edc_set(&sector[2348], edc_compute(&sector[16], 2332));
}
else
{
edc_set(&sector[2072], edc_compute(&sector[16], 2056));
ecc_generate(sector);
}
return;
}
case TrackMode::Audio:
case TrackMode::Mode1Raw:
case TrackMode::Mode2Raw:
default:
return;
}
}
CDImage::LBA CDImage::GetTrackStartPosition(u8 track) const
{
Assert(track > 0 && track <= m_tracks.size());
@ -276,13 +364,15 @@ bool CDImage::ReadRawSector(void* buffer, SubChannelQ* subq)
{
if (m_current_index->file_sector_size > 0)
{
// TODO: This is where we'd reconstruct the header for other mode tracks.
if (!ReadSectorFromIndex(buffer, *m_current_index, m_position_in_index))
{
ERROR_LOG("Read of LBA {} failed", m_position_on_disc);
Seek(m_position_on_disc);
return false;
}
// Fix up the sector header and sync data if necessary.
ConvertSectorToRaw(buffer, m_current_index->start_lba_on_disc + m_position_in_index, m_current_index->mode);
}
else
{

@ -31,8 +31,8 @@ public:
SECTOR_HEADER_SIZE = 4,
MODE1_HEADER_SIZE = 4,
MODE2_HEADER_SIZE = 12,
MODE2_DATA_SECTOR_SIZE = 2336, // header + edc
FRAMES_PER_SECOND = 75, // "sectors", or "timecode frames" (not "channel frames")
MODE2_DATA_SECTOR_SIZE = 2336,
FRAMES_PER_SECOND = 75, // "sectors", or "timecode frames" (not "channel frames")
SECONDS_PER_MINUTE = 60,
FRAMES_PER_MINUTE = FRAMES_PER_SECOND * SECONDS_PER_MINUTE,
SUBCHANNEL_BYTES_PER_FRAME = 12,
@ -177,6 +177,9 @@ public:
bool is_pregap;
};
// Sector sync bit pattern, used for raw sector generation.
static const std::array<u8, SECTOR_SYNC_SIZE> SECTOR_SYNC_DATA;
// Helper functions.
static u32 GetBytesPerSector(TrackMode mode);
static void DeinterleaveSubcode(const u8* subcode_in, u8* subcode_out);
@ -193,6 +196,9 @@ public:
/// Returns a readable string for the given track mode.
static const char* GetTrackModeDisplayName(TrackMode mode);
/// Converts a stored sector in the specified mode to a 2352-byte raw sector.
static void ConvertSectorToRaw(void* buffer, u32 lba, TrackMode mode);
// Opening disc image.
static std::unique_ptr<CDImage> Open(const char* path, bool allow_patches, Error* error);
static std::unique_ptr<CDImage> OpenBinImage(const char* path, Error* error);
@ -305,6 +311,9 @@ protected:
std::vector<Index> m_indices;
private:
// Helper function for filling in raw sector headers.
static void FillRawSectorSyncAndHeader(u8* sector, LBA lba, u8 sector_mode);
// Position on disc.
LBA m_position_on_disc = 0;

@ -21,6 +21,7 @@
#include <algorithm>
#include <cinttypes>
#include <map>
#include <optional>
LOG_CHANNEL(CDImage);
@ -150,6 +151,8 @@ protected:
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
private:
std::optional<TrackMode> DetectSingleFileTrackMode(TrackFileInterface* fi, const char* path, Error* error);
std::vector<std::unique_ptr<TrackFileInterface>> m_files;
};
@ -818,13 +821,18 @@ bool CDImageCueSheet::OpenAndParseSingleFile(const char* path, Error* error)
if (!fi)
return false;
const u32 track_sector_size = RAW_SECTOR_SIZE;
const std::optional<TrackMode> detected_mode = DetectSingleFileTrackMode(fi.get(), path, error);
if (!detected_mode.has_value())
return false;
INFO_LOG("Detected track mode {} for '{}'", GetTrackModeDisplayName(detected_mode.value()), Path::GetFileName(path));
const u32 track_sector_size = GetBytesPerSector(detected_mode.value());
m_lba_count = Truncate32(fi->GetSize() / track_sector_size);
m_files.push_back(std::move(fi));
SubChannelQ::Control control = {};
TrackMode mode = TrackMode::Mode2Raw;
control.data = mode != TrackMode::Audio;
control.data = (detected_mode.value() != TrackMode::Audio);
// Two seconds default pregap.
const u32 pregap_frames = 2 * FRAMES_PER_SECOND;
@ -835,7 +843,7 @@ bool CDImageCueSheet::OpenAndParseSingleFile(const char* path, Error* error)
pregap_index.length = pregap_frames;
pregap_index.track_number = 1;
pregap_index.index_number = 0;
pregap_index.mode = mode;
pregap_index.mode = detected_mode.value();
pregap_index.submode = CDImage::SubchannelMode::None;
pregap_index.control.bits = control.bits;
pregap_index.is_pregap = true;
@ -851,20 +859,53 @@ bool CDImageCueSheet::OpenAndParseSingleFile(const char* path, Error* error)
data_index.index_number = 1;
data_index.start_lba_in_track = 0;
data_index.length = m_lba_count;
data_index.mode = mode;
data_index.mode = detected_mode.value();
data_index.submode = CDImage::SubchannelMode::None;
data_index.control.bits = control.bits;
m_indices.push_back(data_index);
// Assume a single track.
m_tracks.push_back(Track{static_cast<u32>(1), data_index.start_lba_on_disc, static_cast<u32>(0),
m_lba_count + pregap_frames, mode, SubchannelMode::None, control});
m_lba_count + pregap_frames, detected_mode.value(), SubchannelMode::None, control});
AddLeadOutIndex();
return Seek(1, Position{0, 0, 0});
}
std::optional<CDImage::TrackMode> CDImageCueSheet::DetectSingleFileTrackMode(TrackFileInterface* fi, const char* path,
Error* error)
{
const u64 file_size = fi->GetSize();
if (file_size == 0)
{
Error::SetStringFmt(error, "File '{}' is empty", Path::GetFileName(path));
return std::nullopt;
}
std::array<u8, 16> sector_header;
if (file_size < sector_header.size() || !fi->Read(sector_header.data(), 0, sector_header.size(), error))
{
Error::SetStringFmt(error, "Unable to determine sector format for '{}'", Path::GetFileName(path));
return std::nullopt;
}
// Check for a sector header.
if (std::memcmp(sector_header.data(), SECTOR_SYNC_DATA.data(), SECTOR_SYNC_DATA.size()) == 0)
{
// Check for mode 1/mode 2.
if ((sector_header[15] == 0x01 || sector_header[15] == 0x02) && (file_size % RAW_SECTOR_SIZE) == 0)
return (sector_header[15] == 0x01) ? TrackMode::Mode1Raw : TrackMode::Mode2Raw;
}
// If it's aligned to 2352 and missing a sector header, it's probably audio.
if ((file_size % RAW_SECTOR_SIZE) == 0)
return TrackMode::Audio;
// Otherwise assume mode 1.
return TrackMode::Mode1;
}
bool CDImageCueSheet::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index)
{
DebugAssert(index.file_index < m_files.size());

Loading…
Cancel
Save