cue_parser.cpp (12858B)
1 // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR 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_SetChannel(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 ⁢ 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 ⁢ 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::SetString(error, fmt::format("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 (std::isspace(*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 (!std::isspace(*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() && std::isspace(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 if (!TokenMatch(mode, "BINARY")) 228 { 229 SetError(line_number, error, "Only BINARY modes are supported"); 230 return false; 231 } 232 233 m_current_file = filename; 234 DEBUG_LOG("File '{}'", filename); 235 return true; 236 } 237 238 bool CueParser::File::HandleTrackCommand(const char* line, u32 line_number, Error* error) 239 { 240 if (!CompleteLastTrack(line_number, error)) 241 return false; 242 243 if (!m_current_file.has_value()) 244 { 245 SetError(line_number, error, "Starting a track declaration without a file set"); 246 return false; 247 } 248 249 const std::string_view track_number_str(GetToken(line)); 250 if (track_number_str.empty()) 251 { 252 SetError(line_number, error, "Missing track number"); 253 return false; 254 } 255 256 const std::optional<s32> track_number = StringUtil::FromChars<s32>(track_number_str); 257 if (track_number.value_or(0) < MIN_TRACK_NUMBER || track_number.value_or(0) > MAX_TRACK_NUMBER) 258 { 259 SetError(line_number, error, "Invalid track number %d", track_number.value_or(0)); 260 return false; 261 } 262 263 const std::string_view mode_str = GetToken(line); 264 TrackMode mode; 265 if (TokenMatch(mode_str, "AUDIO")) 266 mode = TrackMode::Audio; 267 else if (TokenMatch(mode_str, "MODE1/2048")) 268 mode = TrackMode::Mode1; 269 else if (TokenMatch(mode_str, "MODE1/2352")) 270 mode = TrackMode::Mode1Raw; 271 else if (TokenMatch(mode_str, "MODE2/2336")) 272 mode = TrackMode::Mode2; 273 else if (TokenMatch(mode_str, "MODE2/2048")) 274 mode = TrackMode::Mode2Form1; 275 else if (TokenMatch(mode_str, "MODE2/2342")) 276 mode = TrackMode::Mode2Form2; 277 else if (TokenMatch(mode_str, "MODE2/2332")) 278 mode = TrackMode::Mode2FormMix; 279 else if (TokenMatch(mode_str, "MODE2/2352")) 280 mode = TrackMode::Mode2Raw; 281 else 282 { 283 SetError(line_number, error, "Invalid mode: '%*s'", static_cast<int>(mode_str.length()), mode_str.data()); 284 return false; 285 } 286 287 m_current_track = Track(); 288 m_current_track->number = static_cast<u32>(track_number.value()); 289 m_current_track->file = m_current_file.value(); 290 m_current_track->mode = mode; 291 return true; 292 } 293 294 bool CueParser::File::HandleIndexCommand(const char* line, u32 line_number, Error* error) 295 { 296 if (!m_current_track.has_value()) 297 { 298 SetError(line_number, error, "Setting index without track"); 299 return false; 300 } 301 302 const std::string_view index_number_str(GetToken(line)); 303 if (index_number_str.empty()) 304 { 305 SetError(line_number, error, "Missing index number"); 306 return false; 307 } 308 309 const std::optional<s32> index_number = StringUtil::FromChars<s32>(index_number_str); 310 if (index_number.value_or(-1) < MIN_INDEX_NUMBER || index_number.value_or(-1) > MAX_INDEX_NUMBER) 311 { 312 SetError(line_number, error, "Invalid index number %d", index_number.value_or(-1)); 313 return false; 314 } 315 316 if (m_current_track->GetIndex(static_cast<u32>(index_number.value())) != nullptr) 317 { 318 SetError(line_number, error, "Duplicate index %d", index_number.value()); 319 return false; 320 } 321 322 const std::string_view msf_str(GetToken(line)); 323 if (msf_str.empty()) 324 { 325 SetError(line_number, error, "Missing index location"); 326 return false; 327 } 328 329 const std::optional<MSF> msf(GetMSF(msf_str)); 330 if (!msf.has_value()) 331 { 332 SetError(line_number, error, "Invalid index location '%*s'", static_cast<int>(msf_str.size()), msf_str.data()); 333 return false; 334 } 335 336 m_current_track->indices.emplace_back(static_cast<u32>(index_number.value()), msf.value()); 337 return true; 338 } 339 340 bool CueParser::File::HandlePregapCommand(const char* line, u32 line_number, Error* error) 341 { 342 if (!m_current_track.has_value()) 343 { 344 SetError(line_number, error, "Setting pregap without track"); 345 return false; 346 } 347 348 if (m_current_track->zero_pregap.has_value()) 349 { 350 SetError(line_number, error, "Pregap already specified for track %u", m_current_track->number); 351 return false; 352 } 353 354 const std::string_view msf_str(GetToken(line)); 355 if (msf_str.empty()) 356 { 357 SetError(line_number, error, "Missing pregap location"); 358 return false; 359 } 360 361 const std::optional<MSF> msf(GetMSF(msf_str)); 362 if (!msf.has_value()) 363 { 364 SetError(line_number, error, "Invalid pregap location '%*s'", static_cast<int>(msf_str.size()), msf_str.data()); 365 return false; 366 } 367 368 m_current_track->zero_pregap = msf; 369 return true; 370 } 371 372 bool CueParser::File::HandleFlagCommand(const char* line, u32 line_number, Error* error) 373 { 374 if (!m_current_track.has_value()) 375 { 376 SetError(line_number, error, "Flags command outside of track"); 377 return false; 378 } 379 380 for (;;) 381 { 382 const std::string_view token(GetToken(line)); 383 if (token.empty()) 384 break; 385 386 if (TokenMatch(token, "PRE")) 387 m_current_track->SetFlag(TrackFlag::PreEmphasis); 388 else if (TokenMatch(token, "DCP")) 389 m_current_track->SetFlag(TrackFlag::CopyPermitted); 390 else if (TokenMatch(token, "4CH")) 391 m_current_track->SetFlag(TrackFlag::FourChannelAudio); 392 else if (TokenMatch(token, "SCMS")) 393 m_current_track->SetFlag(TrackFlag::SerialCopyManagement); 394 else 395 WARNING_LOG("Unknown track flag '{}'", token); 396 } 397 398 return true; 399 } 400 401 bool CueParser::File::CompleteLastTrack(u32 line_number, Error* error) 402 { 403 if (!m_current_track.has_value()) 404 return true; 405 406 const MSF* index1 = m_current_track->GetIndex(1); 407 if (!index1) 408 { 409 SetError(line_number, error, "Track %u is missing index 1", m_current_track->number); 410 return false; 411 } 412 413 // check indices 414 for (const auto& [index_number, index_msf] : m_current_track->indices) 415 { 416 if (index_number == 0) 417 continue; 418 419 const MSF* prev_index = m_current_track->GetIndex(index_number - 1); 420 if (prev_index && *prev_index > index_msf) 421 { 422 SetError(line_number, error, "Index %u is after index %u in track %u", index_number - 1, index_number, 423 m_current_track->number); 424 return false; 425 } 426 } 427 428 const MSF* index0 = m_current_track->GetIndex(0); 429 if (index0 && m_current_track->zero_pregap.has_value()) 430 { 431 WARNING_LOG("Zero pregap and index 0 specified in track {}, ignoring zero pregap", m_current_track->number); 432 m_current_track->zero_pregap.reset(); 433 } 434 435 m_current_track->start = *index1; 436 437 m_tracks.push_back(std::move(m_current_track.value())); 438 m_current_track.reset(); 439 return true; 440 } 441 442 bool CueParser::File::SetTrackLengths(u32 line_number, Error* error) 443 { 444 for (const Track& track : m_tracks) 445 { 446 if (track.number > 1) 447 { 448 // set the length of the previous track based on this track's start, if they're the same file 449 Track* previous_track = GetMutableTrack(track.number - 1); 450 if (previous_track && previous_track->file == track.file) 451 { 452 if (previous_track->start > track.start) 453 { 454 SetError(line_number, error, "Track %u start greater than track %u start", previous_track->number, 455 track.number); 456 return false; 457 } 458 459 // Use index 0, otherwise index 1. 460 const MSF* start_index = track.GetIndex(0); 461 if (!start_index) 462 start_index = track.GetIndex(1); 463 464 previous_track->length = MSF::FromLBA(start_index->ToLBA() - previous_track->start.ToLBA()); 465 } 466 } 467 } 468 469 return true; 470 } 471 472 const CueParser::MSF* CueParser::Track::GetIndex(u32 n) const 473 { 474 for (const auto& it : indices) 475 { 476 if (it.first == n) 477 return &it.second; 478 } 479 480 return nullptr; 481 }