Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/util/cd_image_ccd.cpp
10595 views
1
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "cd_image.h"
5
6
#include "common/assert.h"
7
#include "common/error.h"
8
#include "common/file_system.h"
9
#include "common/heterogeneous_containers.h"
10
#include "common/log.h"
11
#include "common/path.h"
12
#include "common/string_util.h"
13
14
#include <algorithm>
15
#include <cerrno>
16
#include <cstring>
17
#include <map>
18
19
LOG_CHANNEL(CDImage);
20
21
namespace {
22
23
class CDImageCCD : public CDImage
24
{
25
public:
26
CDImageCCD();
27
~CDImageCCD() override;
28
29
bool OpenAndParse(const char* filename, Error* error);
30
31
s64 GetSizeOnDisk() const override;
32
33
bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override;
34
bool HasSubchannelData() const override;
35
36
protected:
37
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
38
39
private:
40
static constexpr SubchannelMode SUBCHANNEL_MODE = SubchannelMode::Raw;
41
static constexpr u32 IMG_SECTOR_SIZE = RAW_SECTOR_SIZE;
42
43
std::FILE* m_img_file = nullptr;
44
std::FILE* m_sub_file = nullptr;
45
s64 m_img_file_position = 0;
46
};
47
48
} // namespace
49
50
CDImageCCD::CDImageCCD() = default;
51
52
CDImageCCD::~CDImageCCD()
53
{
54
if (m_sub_file)
55
std::fclose(m_sub_file);
56
if (m_img_file)
57
std::fclose(m_img_file);
58
}
59
60
bool CDImageCCD::OpenAndParse(const char* filename, Error* error)
61
{
62
// Read the CCD file as text.
63
std::optional<std::string> ccd_data = FileSystem::ReadFileToString(filename, error);
64
if (!ccd_data.has_value())
65
{
66
Error::AddPrefixFmt(error, "Failed to open ccd '{}': ", Path::GetFileName(filename));
67
return false;
68
}
69
70
// Open the IMG file (raw 2352-byte sector data).
71
std::string img_filename(Path::ReplaceExtension(filename, "img"));
72
m_img_file = FileSystem::OpenSharedCFile(img_filename.c_str(), "rb", FileSystem::FileShareMode::DenyWrite, error);
73
if (!m_img_file)
74
{
75
Error::AddPrefixFmt(error, "Failed to open img file '{}': ", Path::GetFileName(img_filename));
76
return false;
77
}
78
79
// Open the SUB file (96-byte raw interleaved subchannel data per sector).
80
std::string sub_filename(Path::ReplaceExtension(filename, "sub"));
81
m_sub_file = FileSystem::OpenSharedCFile(sub_filename.c_str(), "rb", FileSystem::FileShareMode::DenyWrite, error);
82
if (!m_sub_file)
83
{
84
Error::AddPrefixFmt(error, "Failed to open sub file '{}': ", Path::GetFileName(sub_filename));
85
return false;
86
}
87
88
// Parse CCD INI-style file into sections.
89
StringMap<StringMap<std::string>> sections;
90
std::string current_section;
91
92
const std::vector<std::string_view> lines = StringUtil::SplitString(ccd_data.value(), '\n', true);
93
for (const std::string_view& line : lines)
94
{
95
const std::string_view stripped = StringUtil::StripWhitespace(line);
96
if (stripped.empty() || stripped[0] == ';')
97
continue;
98
99
if (stripped.front() == '[' && stripped.back() == ']')
100
{
101
current_section = std::string(stripped.substr(1, stripped.size() - 2));
102
continue;
103
}
104
105
std::string_view key, value;
106
if (StringUtil::ParseAssignmentString(stripped, &key, &value))
107
sections[current_section][std::string(key)] = std::string(value);
108
}
109
110
// Verify CloneCD header.
111
if (sections.find("CloneCD") == sections.end())
112
{
113
ERROR_LOG("Missing [CloneCD] header in '{}'", Path::GetFileName(filename));
114
Error::SetStringFmt(error, "Missing [CloneCD] header in '{}'", Path::GetFileName(filename));
115
return false;
116
}
117
118
// Get disc info.
119
if (sections.find("Disc") == sections.end())
120
{
121
ERROR_LOG("Missing [Disc] section in '{}'", Path::GetFileName(filename));
122
Error::SetStringFmt(error, "Missing [Disc] section in '{}'", Path::GetFileName(filename));
123
return false;
124
}
125
126
// Helper to read integer values from sections, supporting both decimal and hex (0x) prefixes.
127
const auto get_int_value = [&sections](std::string_view section, std::string_view key) -> std::optional<s32> {
128
auto section_it = sections.find(section);
129
if (section_it == sections.end())
130
return std::nullopt;
131
auto key_it = section_it->second.find(key);
132
if (key_it == section_it->second.end())
133
return std::nullopt;
134
return StringUtil::FromCharsWithOptionalBase<s32>(key_it->second);
135
};
136
137
const std::optional<s32> toc_entries = get_int_value("Disc", "TocEntries");
138
if (!toc_entries.has_value() || toc_entries.value() < 3)
139
{
140
ERROR_LOG("Invalid or missing TocEntries in '{}'", Path::GetFileName(filename));
141
Error::SetStringFmt(error, "Invalid or missing TocEntries in '{}'", Path::GetFileName(filename));
142
return false;
143
}
144
145
// Parse TOC entries to get track control flags and lead-out LBA.
146
LBA leadout_lba = 0;
147
148
struct TocEntry
149
{
150
u8 point;
151
u8 adr;
152
u8 control;
153
s32 plba;
154
};
155
std::map<u32, TocEntry> track_toc_entries; // keyed by track number
156
157
for (s32 i = 0; i < toc_entries.value(); i++)
158
{
159
const std::string section_name = "Entry " + std::to_string(i);
160
const std::optional<s32> point = get_int_value(section_name, "Point");
161
if (!point.has_value())
162
continue;
163
164
const std::optional<s32> adr = get_int_value(section_name, "ADR");
165
const std::optional<s32> control = get_int_value(section_name, "Control");
166
const std::optional<s32> plba = get_int_value(section_name, "PLBA");
167
168
const u8 point_val = static_cast<u8>(point.value());
169
if (point_val == 0xA2 && plba.has_value())
170
{
171
// Lead-out position.
172
leadout_lba = static_cast<LBA>(plba.value());
173
}
174
else if (point_val >= 1 && point_val <= 99)
175
{
176
// Track entry.
177
TocEntry entry;
178
entry.point = point_val;
179
entry.adr = adr.has_value() ? static_cast<u8>(adr.value()) : 0x01;
180
entry.control = control.has_value() ? static_cast<u8>(control.value()) : 0x00;
181
entry.plba = plba.has_value() ? plba.value() : 0;
182
track_toc_entries[point_val] = entry;
183
}
184
}
185
186
if (track_toc_entries.empty())
187
{
188
ERROR_LOG("File '{}' contains no track entries", Path::GetFileName(filename));
189
Error::SetStringFmt(error, "File '{}' contains no track entries", Path::GetFileName(filename));
190
return false;
191
}
192
193
// If lead-out was not found, derive from IMG file size.
194
if (leadout_lba == 0)
195
{
196
const s64 img_size = FileSystem::FSize64(m_img_file);
197
if (img_size > 0)
198
{
199
leadout_lba = static_cast<LBA>(img_size / RAW_SECTOR_SIZE);
200
}
201
else
202
{
203
ERROR_LOG("Could not determine lead-out position in '{}'", Path::GetFileName(filename));
204
Error::SetStringFmt(error, "Could not determine lead-out position in '{}'", Path::GetFileName(filename));
205
return false;
206
}
207
}
208
209
// Build per-track info using [TRACK N] sections if available, falling back to [Entry N] data.
210
struct TrackInfo
211
{
212
u32 track_number;
213
TrackMode mode;
214
s32 index0; // -1 if not present
215
s32 index1;
216
u8 control;
217
};
218
std::vector<TrackInfo> parsed_tracks;
219
220
for (const auto& [track_num, toc_entry] : track_toc_entries)
221
{
222
const std::string track_section = "TRACK " + std::to_string(track_num);
223
224
TrackInfo info;
225
info.track_number = track_num;
226
info.control = toc_entry.control;
227
228
// Determine track mode from [TRACK N] section or from the Control field.
229
const std::optional<s32> mode_val = get_int_value(track_section, "MODE");
230
if (mode_val.has_value())
231
{
232
switch (mode_val.value())
233
{
234
case 0:
235
info.mode = TrackMode::Audio;
236
break;
237
case 1:
238
info.mode = TrackMode::Mode1Raw;
239
break;
240
case 2:
241
default:
242
info.mode = TrackMode::Mode2Raw;
243
break;
244
}
245
}
246
else
247
{
248
info.mode = (toc_entry.control & 0x04) ? TrackMode::Mode2Raw : TrackMode::Audio;
249
}
250
251
// Read index positions from [TRACK N] section, or fall back to PLBA.
252
const std::optional<s32> idx0 = get_int_value(track_section, "INDEX 0");
253
const std::optional<s32> idx1 = get_int_value(track_section, "INDEX 1");
254
255
info.index0 = idx0.has_value() ? idx0.value() : -1;
256
info.index1 = idx1.has_value() ? idx1.value() : toc_entry.plba;
257
258
parsed_tracks.push_back(info);
259
}
260
261
// Sort by track number.
262
std::sort(parsed_tracks.begin(), parsed_tracks.end(),
263
[](const TrackInfo& a, const TrackInfo& b) { return a.track_number < b.track_number; });
264
265
// Should have track 1, and no missing tracks.
266
if (parsed_tracks.empty() || parsed_tracks[0].track_number != 1)
267
{
268
ERROR_LOG("File '{}' must contain a track 1", Path::GetFileName(filename));
269
Error::SetStringFmt(error, "File '{}' must contain a track 1", Path::GetFileName(filename));
270
return false;
271
}
272
for (size_t i = 1; i < parsed_tracks.size(); i++)
273
{
274
if (parsed_tracks[i].track_number != parsed_tracks[i - 1].track_number + 1)
275
{
276
ERROR_LOG("File '{}' has missing track number {}", Path::GetFileName(filename),
277
parsed_tracks[i - 1].track_number + 1);
278
Error::SetStringFmt(error, "File '{}' has missing track number {}", Path::GetFileName(filename),
279
parsed_tracks[i - 1].track_number + 1);
280
return false;
281
}
282
}
283
284
// Track 1 pregap offset.
285
const LBA plba_offset = (parsed_tracks[0].index0 >= 0) ? 0 : 150;
286
287
// Build CDImage tracks and indices.
288
for (size_t i = 0; i < parsed_tracks.size(); i++)
289
{
290
const TrackInfo& ti = parsed_tracks[i];
291
const LBA track_index0 = static_cast<LBA>((ti.index0 >= 0) ? ti.index0 : ti.index1);
292
const LBA track_index1 = static_cast<LBA>(ti.index1);
293
294
// Determine where the next track (or lead-out) begins.
295
LBA next_track_index0, next_track_index1;
296
if (i + 1 < parsed_tracks.size())
297
{
298
const TrackInfo& next = parsed_tracks[i + 1];
299
next_track_index0 = static_cast<LBA>((next.index0 >= 0) ? next.index0 : next.index1);
300
next_track_index1 = static_cast<LBA>(next.index1);
301
}
302
else
303
{
304
next_track_index0 = leadout_lba;
305
next_track_index1 = leadout_lba;
306
}
307
308
if (track_index1 < track_index0 || next_track_index0 <= track_index1 || next_track_index0 > next_track_index1)
309
{
310
ERROR_LOG("Track {} has invalid length (start {}/{}, next {}/{})", ti.track_number, track_index0, track_index1,
311
next_track_index0, next_track_index1);
312
Error::SetStringFmt(error, "Track {} has invalid length", ti.track_number);
313
return false;
314
}
315
316
SubChannelQ::Control control{};
317
control.data = ti.mode != TrackMode::Audio;
318
319
// Track length is the distance from this track's start to the next track's start (or lead-out).
320
// I'm not sure if this is correct, it's needed for Rayman (Japan) where track 2 is the only track with a pregap.
321
u32 toc_track_length = next_track_index0 - track_index0;
322
323
// Handle pregap (index 0).
324
if (track_index1 > track_index0)
325
{
326
// Pregap is present in the IMG file.
327
const LBA pregap_length = track_index1 - track_index0;
328
329
Index pregap_index = {};
330
pregap_index.start_lba_on_disc = static_cast<LBA>(ti.index0) + plba_offset;
331
pregap_index.start_lba_in_track = static_cast<LBA>(-static_cast<s32>(pregap_length));
332
pregap_index.length = pregap_length;
333
pregap_index.track_number = ti.track_number;
334
pregap_index.index_number = 0;
335
pregap_index.file_index = 0;
336
pregap_index.file_sector_size = IMG_SECTOR_SIZE;
337
pregap_index.file_offset = static_cast<u64>(ti.index0) * IMG_SECTOR_SIZE;
338
pregap_index.mode = ti.mode;
339
pregap_index.submode = SUBCHANNEL_MODE;
340
pregap_index.control.bits = control.bits;
341
pregap_index.is_pregap = true;
342
m_indices.push_back(pregap_index);
343
}
344
else if (ti.track_number == 1 && plba_offset > 0)
345
{
346
Index pregap_index = {};
347
pregap_index.start_lba_on_disc = 0;
348
pregap_index.start_lba_in_track = static_cast<LBA>(-static_cast<s32>(plba_offset));
349
pregap_index.length = plba_offset;
350
pregap_index.track_number = ti.track_number;
351
pregap_index.index_number = 0;
352
pregap_index.mode = ti.mode;
353
pregap_index.submode = CDImage::SubchannelMode::None;
354
pregap_index.control.bits = control.bits;
355
pregap_index.is_pregap = true;
356
m_indices.push_back(pregap_index);
357
toc_track_length += plba_offset;
358
}
359
360
// Add the track.
361
m_tracks.push_back(Track{ti.track_number, track_index1 + plba_offset, static_cast<u32>(m_indices.size()),
362
toc_track_length, ti.mode, SubchannelMode::None, control});
363
364
// Add data index (index 1).
365
Index data_index = {};
366
data_index.start_lba_on_disc = track_index1 + plba_offset;
367
data_index.start_lba_in_track = 0;
368
data_index.track_number = ti.track_number;
369
data_index.index_number = 1;
370
data_index.file_index = 0;
371
data_index.file_sector_size = IMG_SECTOR_SIZE;
372
data_index.file_offset = static_cast<u64>(track_index1) * IMG_SECTOR_SIZE;
373
data_index.mode = ti.mode;
374
data_index.submode = SUBCHANNEL_MODE;
375
data_index.control.bits = control.bits;
376
data_index.is_pregap = false;
377
data_index.length = next_track_index0 - track_index1;
378
m_indices.push_back(data_index);
379
}
380
381
if (m_tracks.empty())
382
{
383
ERROR_LOG("File '{}' contains no tracks", Path::GetFileName(filename));
384
Error::SetStringFmt(error, "File '{}' contains no tracks", Path::GetFileName(filename));
385
return false;
386
}
387
388
m_lba_count = m_tracks.back().start_lba + m_tracks.back().length;
389
AddLeadOutIndex();
390
391
m_filename = filename;
392
393
return Seek(1, Position{0, 0, 0});
394
}
395
396
bool CDImageCCD::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index)
397
{
398
const s64 file_position = static_cast<s64>(index.file_offset + (static_cast<u64>(lba_in_index) * IMG_SECTOR_SIZE));
399
if (m_img_file_position != file_position)
400
{
401
if (FileSystem::FSeek64(m_img_file, file_position, SEEK_SET) != 0)
402
return false;
403
404
m_img_file_position = file_position;
405
}
406
407
if (std::fread(buffer, IMG_SECTOR_SIZE, 1, m_img_file) != 1)
408
{
409
FileSystem::FSeek64(m_img_file, m_img_file_position, SEEK_SET);
410
return false;
411
}
412
413
m_img_file_position += IMG_SECTOR_SIZE;
414
return true;
415
}
416
417
bool CDImageCCD::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index)
418
{
419
// For virtual pregaps (not in file), fall back to generated subchannel Q.
420
if (index.is_pregap && index.file_sector_size == 0)
421
return CDImage::ReadSubChannelQ(subq, index, lba_in_index);
422
423
// Q subchannel is the second 12-byte block (P, Q, R, S, T, U, V, W).
424
static constexpr u64 q_offset = SUBCHANNEL_BYTES_PER_FRAME;
425
426
// Have to wrangle this because of the two second implicit pregap.
427
const s64 sub_offset = static_cast<s64>(((index.file_offset / IMG_SECTOR_SIZE) * ALL_SUBCODE_SIZE) +
428
(static_cast<u64>(lba_in_index) * ALL_SUBCODE_SIZE) + q_offset);
429
430
// Since we're only reading partially, the position's never going to match for sequential. Always seek.
431
if (FileSystem::FSeek64(m_sub_file, static_cast<s64>(sub_offset), SEEK_SET) != 0 ||
432
std::fread(subq->data.data(), SUBCHANNEL_BYTES_PER_FRAME, 1, m_sub_file) != 1)
433
{
434
WARNING_LOG("Failed to read subq for sector {}", index.start_lba_on_disc + lba_in_index);
435
return CDImage::ReadSubChannelQ(subq, index, lba_in_index);
436
}
437
438
return true;
439
}
440
441
bool CDImageCCD::HasSubchannelData() const
442
{
443
return true;
444
}
445
446
s64 CDImageCCD::GetSizeOnDisk() const
447
{
448
s64 size = std::max<s64>(FileSystem::FSize64(m_img_file), 0);
449
if (m_sub_file)
450
size += std::max<s64>(FileSystem::FSize64(m_sub_file), 0);
451
452
return size;
453
}
454
455
std::unique_ptr<CDImage> CDImage::OpenCCDImage(const char* path, Error* error)
456
{
457
std::unique_ptr<CDImageCCD> image = std::make_unique<CDImageCCD>();
458
if (!image->OpenAndParse(path, error))
459
return {};
460
461
return image;
462
}
463
464