Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/util/cue_parser.cpp
4212 views
1
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "cue_parser.h"
5
6
#include "common/error.h"
7
#include "common/log.h"
8
#include "common/string_util.h"
9
10
#include <cstdarg>
11
12
LOG_CHANNEL(CueParser);
13
14
namespace CueParser {
15
static bool TokenMatch(std::string_view s1, const char* token);
16
}
17
18
bool CueParser::TokenMatch(std::string_view s1, const char* token)
19
{
20
const size_t token_len = std::strlen(token);
21
if (s1.length() != token_len)
22
return false;
23
24
return (StringUtil::Strncasecmp(s1.data(), token, token_len) == 0);
25
}
26
27
CueParser::File::File() = default;
28
29
CueParser::File::~File() = default;
30
31
const CueParser::Track* CueParser::File::GetTrack(u32 n) const
32
{
33
for (const auto& it : m_tracks)
34
{
35
if (it.number == n)
36
return &it;
37
}
38
39
return nullptr;
40
}
41
42
CueParser::Track* CueParser::File::GetMutableTrack(u32 n)
43
{
44
for (auto& it : m_tracks)
45
{
46
if (it.number == n)
47
return &it;
48
}
49
50
return nullptr;
51
}
52
53
bool CueParser::File::Parse(std::FILE* fp, Error* error)
54
{
55
char line[1024];
56
u32 line_number = 1;
57
while (std::fgets(line, sizeof(line), fp))
58
{
59
if (!ParseLine(line, line_number, error))
60
return false;
61
62
line_number++;
63
}
64
65
if (!CompleteLastTrack(line_number, error))
66
return false;
67
68
if (!SetTrackLengths(line_number, error))
69
return false;
70
71
return true;
72
}
73
74
void CueParser::File::SetError(u32 line_number, Error* error, const char* format, ...)
75
{
76
std::va_list ap;
77
SmallString str;
78
va_start(ap, format);
79
str.vsprintf(format, ap);
80
va_end(ap);
81
82
ERROR_LOG("Cue parse error at line {}: {}", line_number, str.c_str());
83
Error::SetStringFmt(error, "Cue parse error at line {}: {}", line_number, str);
84
}
85
86
std::string_view CueParser::File::GetToken(const char*& line)
87
{
88
std::string_view ret;
89
90
const char* start = line;
91
while (StringUtil::IsWhitespace(*start) && *start != '\0')
92
start++;
93
94
if (*start == '\0')
95
return ret;
96
97
const char* end;
98
const bool quoted = *start == '\"';
99
if (quoted)
100
{
101
start++;
102
end = start;
103
while (*end != '\"' && *end != '\0')
104
end++;
105
106
if (*end != '\"')
107
return ret;
108
109
ret = std::string_view(start, static_cast<size_t>(end - start));
110
111
// eat closing "
112
end++;
113
}
114
else
115
{
116
end = start;
117
while (!StringUtil::IsWhitespace(*end) && *end != '\0')
118
end++;
119
120
ret = std::string_view(start, static_cast<size_t>(end - start));
121
}
122
123
line = end;
124
return ret;
125
}
126
127
std::optional<CueParser::MSF> CueParser::File::GetMSF(std::string_view token)
128
{
129
static const s32 max_values[] = {std::numeric_limits<s32>::max(), 60, 75};
130
131
u32 parts[3] = {};
132
u32 part = 0;
133
134
u32 start = 0;
135
for (;;)
136
{
137
while (start < token.length() && token[start] < '0' && token[start] <= '9')
138
start++;
139
140
if (start == token.length())
141
return std::nullopt;
142
143
u32 end = start;
144
while (end < token.length() && token[end] >= '0' && token[end] <= '9')
145
end++;
146
147
const std::optional<s32> value = StringUtil::FromChars<s32>(token.substr(start, end - start));
148
if (!value.has_value() || value.value() < 0 || value.value() > max_values[part])
149
return std::nullopt;
150
151
parts[part] = static_cast<u32>(value.value());
152
part++;
153
154
if (part == 3)
155
break;
156
157
while (end < token.length() && StringUtil::IsWhitespace(token[end]))
158
end++;
159
if (end == token.length() || token[end] != ':')
160
return std::nullopt;
161
162
start = end + 1;
163
}
164
165
MSF ret;
166
ret.minute = static_cast<u8>(parts[0]);
167
ret.second = static_cast<u8>(parts[1]);
168
ret.frame = static_cast<u8>(parts[2]);
169
return ret;
170
}
171
172
bool CueParser::File::ParseLine(const char* line, u32 line_number, Error* error)
173
{
174
const std::string_view command(GetToken(line));
175
if (command.empty())
176
return true;
177
178
if (TokenMatch(command, "REM"))
179
{
180
// comment, eat it
181
return true;
182
}
183
184
if (TokenMatch(command, "FILE"))
185
return HandleFileCommand(line, line_number, error);
186
else if (TokenMatch(command, "TRACK"))
187
return HandleTrackCommand(line, line_number, error);
188
else if (TokenMatch(command, "INDEX"))
189
return HandleIndexCommand(line, line_number, error);
190
else if (TokenMatch(command, "PREGAP"))
191
return HandlePregapCommand(line, line_number, error);
192
else if (TokenMatch(command, "FLAGS"))
193
return HandleFlagCommand(line, line_number, error);
194
195
if (TokenMatch(command, "POSTGAP"))
196
{
197
WARNING_LOG("Ignoring '{}' command", command);
198
return true;
199
}
200
201
// stuff we definitely ignore
202
if (TokenMatch(command, "CATALOG") || TokenMatch(command, "CDTEXTFILE") || TokenMatch(command, "ISRC") ||
203
TokenMatch(command, "TRACK_ISRC") || TokenMatch(command, "TITLE") || TokenMatch(command, "PERFORMER") ||
204
TokenMatch(command, "SONGWRITER") || TokenMatch(command, "COMPOSER") || TokenMatch(command, "ARRANGER") ||
205
TokenMatch(command, "MESSAGE") || TokenMatch(command, "DISC_ID") || TokenMatch(command, "GENRE") ||
206
TokenMatch(command, "TOC_INFO1") || TokenMatch(command, "TOC_INFO2") || TokenMatch(command, "UPC_EAN") ||
207
TokenMatch(command, "SIZE_INFO"))
208
{
209
return true;
210
}
211
212
SetError(line_number, error, "Invalid command '%*s'", static_cast<int>(command.size()), command.data());
213
return false;
214
}
215
216
bool CueParser::File::HandleFileCommand(const char* line, u32 line_number, Error* error)
217
{
218
const std::string_view filename(GetToken(line));
219
const std::string_view mode(GetToken(line));
220
221
if (filename.empty())
222
{
223
SetError(line_number, error, "Missing filename");
224
return false;
225
}
226
227
FileFormat format;
228
if (TokenMatch(mode, "BINARY"))
229
{
230
format = FileFormat::Binary;
231
}
232
else if (TokenMatch(mode, "WAVE"))
233
{
234
format = FileFormat::Wave;
235
}
236
else
237
{
238
SetError(line_number, error, "Only BINARY and WAVE modes are supported");
239
return false;
240
}
241
242
m_current_file = {std::string(filename), format};
243
DEBUG_LOG("File '{}'", filename);
244
return true;
245
}
246
247
bool CueParser::File::HandleTrackCommand(const char* line, u32 line_number, Error* error)
248
{
249
if (!CompleteLastTrack(line_number, error))
250
return false;
251
252
if (!m_current_file.has_value())
253
{
254
SetError(line_number, error, "Starting a track declaration without a file set");
255
return false;
256
}
257
258
const std::string_view track_number_str(GetToken(line));
259
if (track_number_str.empty())
260
{
261
SetError(line_number, error, "Missing track number");
262
return false;
263
}
264
265
const std::optional<s32> track_number = StringUtil::FromChars<s32>(track_number_str);
266
if (track_number.value_or(0) < MIN_TRACK_NUMBER || track_number.value_or(0) > MAX_TRACK_NUMBER)
267
{
268
SetError(line_number, error, "Invalid track number %d", track_number.value_or(0));
269
return false;
270
}
271
272
const std::string_view mode_str = GetToken(line);
273
TrackMode mode;
274
if (TokenMatch(mode_str, "AUDIO"))
275
mode = TrackMode::Audio;
276
else if (TokenMatch(mode_str, "MODE1/2048"))
277
mode = TrackMode::Mode1;
278
else if (TokenMatch(mode_str, "MODE1/2352"))
279
mode = TrackMode::Mode1Raw;
280
else if (TokenMatch(mode_str, "MODE2/2336"))
281
mode = TrackMode::Mode2;
282
else if (TokenMatch(mode_str, "MODE2/2048"))
283
mode = TrackMode::Mode2Form1;
284
else if (TokenMatch(mode_str, "MODE2/2342"))
285
mode = TrackMode::Mode2Form2;
286
else if (TokenMatch(mode_str, "MODE2/2332"))
287
mode = TrackMode::Mode2FormMix;
288
else if (TokenMatch(mode_str, "MODE2/2352"))
289
mode = TrackMode::Mode2Raw;
290
else
291
{
292
SetError(line_number, error, "Invalid mode: '%*s'", static_cast<int>(mode_str.length()), mode_str.data());
293
return false;
294
}
295
296
m_current_track = Track();
297
m_current_track->number = static_cast<u8>(track_number.value());
298
m_current_track->file = m_current_file->first;
299
m_current_track->file_format = m_current_file->second;
300
m_current_track->mode = mode;
301
return true;
302
}
303
304
bool CueParser::File::HandleIndexCommand(const char* line, u32 line_number, Error* error)
305
{
306
if (!m_current_track.has_value())
307
{
308
SetError(line_number, error, "Setting index without track");
309
return false;
310
}
311
312
const std::string_view index_number_str(GetToken(line));
313
if (index_number_str.empty())
314
{
315
SetError(line_number, error, "Missing index number");
316
return false;
317
}
318
319
const std::optional<s32> index_number = StringUtil::FromChars<s32>(index_number_str);
320
if (index_number.value_or(-1) < MIN_INDEX_NUMBER || index_number.value_or(-1) > MAX_INDEX_NUMBER)
321
{
322
SetError(line_number, error, "Invalid index number %d", index_number.value_or(-1));
323
return false;
324
}
325
326
if (m_current_track->GetIndex(static_cast<u32>(index_number.value())) != nullptr)
327
{
328
SetError(line_number, error, "Duplicate index %d", index_number.value());
329
return false;
330
}
331
332
const std::string_view msf_str(GetToken(line));
333
if (msf_str.empty())
334
{
335
SetError(line_number, error, "Missing index location");
336
return false;
337
}
338
339
const std::optional<MSF> msf(GetMSF(msf_str));
340
if (!msf.has_value())
341
{
342
SetError(line_number, error, "Invalid index location '%*s'", static_cast<int>(msf_str.size()), msf_str.data());
343
return false;
344
}
345
346
m_current_track->indices.emplace_back(static_cast<u32>(index_number.value()), msf.value());
347
return true;
348
}
349
350
bool CueParser::File::HandlePregapCommand(const char* line, u32 line_number, Error* error)
351
{
352
if (!m_current_track.has_value())
353
{
354
SetError(line_number, error, "Setting pregap without track");
355
return false;
356
}
357
358
if (m_current_track->zero_pregap.has_value())
359
{
360
SetError(line_number, error, "Pregap already specified for track %u", m_current_track->number);
361
return false;
362
}
363
364
const std::string_view msf_str(GetToken(line));
365
if (msf_str.empty())
366
{
367
SetError(line_number, error, "Missing pregap location");
368
return false;
369
}
370
371
const std::optional<MSF> msf(GetMSF(msf_str));
372
if (!msf.has_value())
373
{
374
SetError(line_number, error, "Invalid pregap location '%*s'", static_cast<int>(msf_str.size()), msf_str.data());
375
return false;
376
}
377
378
m_current_track->zero_pregap = msf;
379
return true;
380
}
381
382
bool CueParser::File::HandleFlagCommand(const char* line, u32 line_number, Error* error)
383
{
384
if (!m_current_track.has_value())
385
{
386
SetError(line_number, error, "Flags command outside of track");
387
return false;
388
}
389
390
for (;;)
391
{
392
const std::string_view token(GetToken(line));
393
if (token.empty())
394
break;
395
396
if (TokenMatch(token, "PRE"))
397
m_current_track->SetFlag(TrackFlag::PreEmphasis);
398
else if (TokenMatch(token, "DCP"))
399
m_current_track->SetFlag(TrackFlag::CopyPermitted);
400
else if (TokenMatch(token, "4CH"))
401
m_current_track->SetFlag(TrackFlag::FourChannelAudio);
402
else if (TokenMatch(token, "SCMS"))
403
m_current_track->SetFlag(TrackFlag::SerialCopyManagement);
404
else
405
WARNING_LOG("Unknown track flag '{}'", token);
406
}
407
408
return true;
409
}
410
411
bool CueParser::File::CompleteLastTrack(u32 line_number, Error* error)
412
{
413
if (!m_current_track.has_value())
414
return true;
415
416
const MSF* index1 = m_current_track->GetIndex(1);
417
if (!index1)
418
{
419
SetError(line_number, error, "Track %u is missing index 1", m_current_track->number);
420
return false;
421
}
422
423
// check indices
424
for (const auto& [index_number, index_msf] : m_current_track->indices)
425
{
426
if (index_number == 0)
427
continue;
428
429
const MSF* prev_index = m_current_track->GetIndex(index_number - 1);
430
if (prev_index && *prev_index > index_msf)
431
{
432
SetError(line_number, error, "Index %u is after index %u in track %u", index_number - 1, index_number,
433
m_current_track->number);
434
return false;
435
}
436
}
437
438
const MSF* index0 = m_current_track->GetIndex(0);
439
if (index0 && m_current_track->zero_pregap.has_value())
440
{
441
WARNING_LOG("Zero pregap and index 0 specified in track {}, ignoring zero pregap", m_current_track->number);
442
m_current_track->zero_pregap.reset();
443
}
444
445
m_current_track->start = *index1;
446
447
m_tracks.push_back(std::move(m_current_track.value()));
448
m_current_track.reset();
449
return true;
450
}
451
452
bool CueParser::File::SetTrackLengths(u32 line_number, Error* error)
453
{
454
for (const Track& track : m_tracks)
455
{
456
if (track.number > 1)
457
{
458
// set the length of the previous track based on this track's start, if they're the same file
459
Track* previous_track = GetMutableTrack(track.number - 1);
460
if (previous_track && previous_track->file == track.file)
461
{
462
if (previous_track->start > track.start)
463
{
464
SetError(line_number, error, "Track %u start greater than track %u start", previous_track->number,
465
track.number);
466
return false;
467
}
468
469
// Use index 0, otherwise index 1.
470
const MSF* start_index = track.GetIndex(0);
471
if (!start_index)
472
start_index = track.GetIndex(1);
473
474
previous_track->length = MSF::FromLBA(start_index->ToLBA() - previous_track->start.ToLBA());
475
}
476
}
477
}
478
479
return true;
480
}
481
482
const CueParser::MSF* CueParser::Track::GetIndex(u32 n) const
483
{
484
for (const auto& it : indices)
485
{
486
if (it.first == n)
487
return &it.second;
488
}
489
490
return nullptr;
491
}
492
493