duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

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 &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::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 }