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