Warning, file /multimedia/kid3/src/core/tags/framenotice.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /**
0002  * \file framenotice.cpp
0003  * Warning about tag frame.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 19 May 2017
0008  *
0009  * Copyright (C) 2017-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "framenotice.h"
0028 #include <QCoreApplication>
0029 #include <QSet>
0030 #include <QRegularExpression>
0031 #include "frame.h"
0032 
0033 namespace {
0034 
0035 using CheckFunction = bool (*)(const QString&);
0036 
0037 bool beginsWithYearAndSpace(const QString& str)
0038 {
0039   if (str.length() < 5 || str.at(4) != QLatin1Char(' '))
0040     return false;
0041 
0042   for (int i = 0; i < 4; ++i) {
0043     if (!str.at(i).isDigit()) {
0044       return false;
0045     }
0046   }
0047   return true;
0048 }
0049 
0050 bool isDayMonth(const QString& str)
0051 {
0052   if (str.length() != 4)
0053     return false;
0054 
0055 #if QT_VERSION >= 0x060000
0056   int day = str.left(2).toInt();
0057   int month = str.mid(2).toInt();
0058 #else
0059   int day = str.leftRef(2).toInt();
0060   int month = str.midRef(2).toInt();
0061 #endif
0062   return !(day < 1 || day > 31 || month < 1 || month > 12);
0063 }
0064 
0065 bool isHourMinute(const QString& str)
0066 {
0067   if (str.length() != 4)
0068     return false;
0069 
0070 #if QT_VERSION >= 0x060000
0071   int hour = str.left(2).toInt();
0072   int minute = str.mid(2).toInt();
0073 #else
0074   int hour = str.leftRef(2).toInt();
0075   int minute = str.midRef(2).toInt();
0076 #endif
0077   return !(hour < 0 || hour > 23 || minute < 0 || minute > 59);
0078 }
0079 
0080 bool isNumeric(const QString& str)
0081 {
0082   bool ok;
0083   str.toInt(&ok);
0084   return ok;
0085 }
0086 
0087 bool isYear(const QString& str)
0088 {
0089   return str.length() == 4 && isNumeric(str);
0090 }
0091 
0092 bool isNumberTotal(const QString& str)
0093 {
0094   if (const int slashPos = str.indexOf(QLatin1Char('/')); slashPos != -1) {
0095     return isNumeric(str.left(slashPos)) && isNumeric(str.mid(slashPos + 1));
0096   }
0097   return isNumeric(str);
0098 }
0099 
0100 bool isIsrc(const QString& str)
0101 {
0102   if (str.length() != 12)
0103     return false;
0104 
0105   for (int i = 0; i < 5; ++i) {
0106     if (!str.at(i).isLetterOrNumber()) {
0107       return false;
0108     }
0109   }
0110   for (int i = 5; i < 12; ++i) {
0111     if (!str.at(i).isDigit()) {
0112       return false;
0113     }
0114   }
0115   return true;
0116 }
0117 
0118 bool isIsoDateTime(const QString& str)
0119 {
0120   return FrameNotice::isoDateTimeRexExp().match(str).hasMatch();
0121 }
0122 
0123 bool isMusicalKey(const QString& str)
0124 {
0125   const int len = str.length();
0126   if (len < 1 || len > 3)
0127     return false;
0128 
0129   // Although not in the ID3v2 standard, allow commonly used Camelot wheel
0130   // values 1A-12A, 1B-12B http://www.mixedinkey.com/harmonic-mixing-guide/
0131   if (const QChar lastChar = str.at(len - 1);
0132       lastChar == QLatin1Char('A') || lastChar == QLatin1Char('B')) {
0133     bool ok;
0134 #if QT_VERSION >= 0x060000
0135     int nr = str.left(len - 1).toInt(&ok);
0136 #else
0137     int nr = str.leftRef(len - 1).toInt(&ok);
0138 #endif
0139     if (ok && nr >= 1 && nr <= 12) {
0140       return true;
0141     }
0142   }
0143 
0144   const QString allowedChars(QLatin1String("ABCDEFGb#mo"));
0145   for (int i = 0; i < len; ++i) {
0146     if (!allowedChars.contains(str.at(i))) {
0147       return false;
0148     }
0149   }
0150   return true;
0151 }
0152 
0153 bool isLanguageCode(const QString& str)
0154 {
0155   if (str.length() != 3)
0156     return false;
0157 
0158   if (str == QLatin1String("XXX"))
0159     return true;
0160 
0161   for (int i = 0; i < 3; ++i) {
0162     if (const QChar ch = str.at(i); !ch.isLetter() || !ch.isLower()) {
0163       return false;
0164     }
0165   }
0166   return true;
0167 }
0168 
0169 bool isStringList(const QString& str)
0170 {
0171   return str.contains(QLatin1Char('|'));
0172 }
0173 
0174 }
0175 
0176 /**
0177  * Get translated description of notice.
0178  * @return description, empty if none.
0179  */
0180 QString FrameNotice::getDescription() const
0181 {
0182   static const char* const descriptions[] = {
0183     "",
0184     QT_TRANSLATE_NOOP("@default", "Truncated"),
0185     QT_TRANSLATE_NOOP("@default", "Size is too large"),
0186     QT_TRANSLATE_NOOP("@default", "Must be unique"),
0187     QT_TRANSLATE_NOOP("@default", "New line is forbidden"),
0188     QT_TRANSLATE_NOOP("@default", "Carriage return is forbidden"),
0189     QT_TRANSLATE_NOOP("@default", "Owner must be non-empty"),
0190     QT_TRANSLATE_NOOP("@default", "Must be numeric"),
0191     QT_TRANSLATE_NOOP("@default", "Must be numeric or number/total"),
0192     QT_TRANSLATE_NOOP("@default", "Format is DDMM"),
0193     QT_TRANSLATE_NOOP("@default", "Format is HHMM"),
0194     QT_TRANSLATE_NOOP("@default", "Format is YYYY"),
0195     QT_TRANSLATE_NOOP("@default", "Must begin with a year and a space character"),
0196     QT_TRANSLATE_NOOP("@default", "Must be ISO 8601 date/time"),
0197     QT_TRANSLATE_NOOP("@default", "Must be musical key, 3 characters, A-G, b, #, m, o\n"
0198                                   "or Camelot wheel value 1A-12A, 1B-12B"),
0199     QT_TRANSLATE_NOOP("@default", "Must have ISO 639-2 language code, 3 lowercase characters"),
0200     QT_TRANSLATE_NOOP("@default", "Must be ISRC code, 12 characters"),
0201     QT_TRANSLATE_NOOP("@default", "Must be list of strings separated by '|'"),
0202     QT_TRANSLATE_NOOP("@default", "Has excess white space"),
0203   };
0204   Q_STATIC_ASSERT(std::size(descriptions) == NumWarnings);
0205   return m_warning < NumWarnings
0206       ? QCoreApplication::translate("@default", descriptions[m_warning])
0207       : QString();
0208 }
0209 
0210 /**
0211  * Get regular expression to validate an ISO 8601 date/time.
0212  * @return regular expression matching ISO date/time and periods.
0213  */
0214 const QRegularExpression& FrameNotice::isoDateTimeRexExp()
0215 {
0216   static const QRegularExpression isoDateRe(QLatin1String(
0217     // This is a simplified regular expression from
0218     // http://www.pelagodesign.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/
0219     // relaxed to allow appending any string after a slash, so that ISO 8601
0220     // periods of time can by entered while still providing sufficient validation.
0221     "^(\\d{4})(-((0[1-9]|1[0-2])(-([12]\\d|0[1-9]|3[01]))?)(T((([01]\\d|2[0-3])"
0222     "(:[0-5]\\d)?|24:00))?(:[0-5]\\d([\\.,]\\d+)?)?"
0223     "([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?(/.*)?)?$"
0224   ));
0225   return isoDateRe;
0226 }
0227 
0228 /**
0229  * Check if a picture frame exceeds a given size.
0230  * TooLarge notice is set in @a frame, if its size is larger than @a maxSize.
0231  * @param frame picture frame to check
0232  * @param maxSize maximum size of picture data in bytes
0233  * @return true if size too large.
0234  */
0235 bool FrameNotice::addPictureTooLargeNotice(Frame& frame, int maxSize)
0236 {
0237   if (QVariant data = Frame::getField(frame, Frame::ID_Data); !data.isNull()) {
0238     if (data.toByteArray().size() > maxSize) {
0239       frame.setMarked(FrameNotice::TooLarge);
0240       return true;
0241     }
0242   }
0243   return false;
0244 }
0245 
0246 /**
0247  * Check if frames violate the ID3v2 standard.
0248  * Violating frames are marked with the corresponding notice.
0249  * @param frames frames to check
0250  * @return true if a violation is detected.
0251  */
0252 bool FrameNotice::addId3StandardViolationNotice(FrameCollection& frames)
0253 {
0254   static const struct {
0255     const char* id;
0256     Warning warning;
0257   } idWarning[] = {
0258     {"TBPM", Numeric},
0259     {"TDLY", Numeric},
0260     {"TLEN", Numeric},
0261     {"TSIZ", Numeric},
0262     {"TCOP", YearSpace},
0263     {"TPRO", YearSpace},
0264     {"TDAT", DayMonth},
0265     {"TIME", HourMinute},
0266     {"TORY", Year},
0267     {"TYER", Year},
0268     {"TPOS", NrTotal},
0269     {"TRCK", NrTotal},
0270     {"TSRC", IsrcCode},
0271     {"TDEN", IsoDate},
0272     {"TDOR", IsoDate},
0273     {"TDRC", IsoDate},
0274     {"TDRL", IsoDate},
0275     {"TDTG", IsoDate},
0276     {"TKEY", MusicalKey},
0277     {"TLAN", LanguageCode},
0278     {"IPLS", StringList},
0279     {"TMCL", StringList},
0280     {"TIPL", StringList},
0281   };
0282   static const struct {
0283     Warning warning;
0284     CheckFunction func;
0285   } warningFunc[] = {
0286     {Numeric, isNumeric},
0287     {YearSpace, beginsWithYearAndSpace},
0288     {DayMonth, isDayMonth},
0289     {HourMinute, isHourMinute},
0290     {Year, isYear},
0291     {NrTotal, isNumberTotal},
0292     {IsrcCode, isIsrc},
0293     {IsoDate, isIsoDateTime},
0294     {MusicalKey, isMusicalKey},
0295     {LanguageCode, isLanguageCode},
0296     {StringList, isStringList},
0297   };
0298   static const struct {
0299     const char* id;
0300     Frame::FieldId field;
0301   } uniqIdField[] = {
0302     {"UFID", Frame::ID_Owner},
0303     {"TXXX", Frame::ID_Description},
0304     {"WXXX", Frame::ID_Description},
0305     {"IPLS", Frame::ID_NoField},
0306     {"USLT", Frame::ID_Language}, // and Frame::ID_Description
0307     {"SYLT", Frame::ID_Language}, // and Frame::ID_Description
0308     {"COMM", Frame::ID_Language}, // and Frame::ID_Description
0309     {"USER", Frame::ID_Language},
0310     {"APIC", Frame::ID_PictureType}, // and Frame::ID_Description
0311     {"GEOB", Frame::ID_Description},
0312     {"PCNT", Frame::ID_NoField},
0313     {"POPM", Frame::ID_Email},
0314     {"RBUF", Frame::ID_NoField},
0315     {"AENC", Frame::ID_Owner},
0316     {"LINK", Frame::ID_Id}, // and Frame::ID_Url, Frame::ID_Text
0317     {"POSS", Frame::ID_NoField},
0318     {"OWNE", Frame::ID_NoField},
0319     {"COMR", Frame::ID_Data},
0320     {"ENCR", Frame::ID_Owner},
0321     {"GRID", Frame::ID_Owner},
0322     {"PRIV", Frame::ID_Owner},
0323   };
0324   static QMap<QString, Warning> warnings;
0325   static QMap<Warning, CheckFunction> checks;
0326   static QMap<QString, Frame::FieldId> uniques;
0327   if (warnings.isEmpty()) {
0328     for (const auto& [id, warning] : idWarning) {
0329       warnings[QString::fromLatin1(id)] = warning;
0330     }
0331     for (const auto& [warning, func] : warningFunc) {
0332       checks[warning] = func;
0333     }
0334     for (const auto& [id, field] : uniqIdField) {
0335       uniques[QString::fromLatin1(id)] = field;
0336     }
0337   }
0338 
0339   QSet<QString> uniqueIds;
0340   bool marked = false;
0341   for (auto it = frames.begin(); it != frames.end(); ++it) {
0342     auto& frame = const_cast<Frame&>(*it);
0343     QString name = frame.getInternalName();
0344     QString id = name.left(4);
0345     QString uniqueId;
0346 
0347     // Check for uniqueness.
0348     static constexpr Frame::FieldId NOT_UNIQUE = Frame::ID_Subframe;
0349     if (Frame::FieldId fieldId = uniques.value(id, NOT_UNIQUE);
0350         fieldId != NOT_UNIQUE) {
0351       uniqueId = id;
0352       if (fieldId != Frame::ID_NoField) {
0353         uniqueId += frame.getFieldValue(fieldId).toString();
0354         if (fieldId == Frame::ID_Language || fieldId == Frame::ID_PictureType) {
0355           uniqueId += frame.getFieldValue(Frame::ID_Description).toString();
0356         } else if (fieldId == Frame::ID_Id) {
0357           uniqueId += frame.getFieldValue(Frame::ID_Url).toString();
0358           uniqueId += frame.getFieldValue(Frame::ID_Text).toString();
0359         }
0360       }
0361     } else if (id.startsWith(QLatin1Char('T')) ||
0362                (id.startsWith(QLatin1Char('W')) &&
0363                 id != QLatin1String("WCOM") && id != QLatin1String("WOAR"))) {
0364       uniqueId = id;
0365     }
0366     if (!uniqueId.isEmpty()) {
0367       if (uniqueIds.contains(uniqueId)) {
0368         frame.setMarked(Unique);
0369         marked = true;
0370         continue;
0371       }
0372       uniqueIds.insert(uniqueId);
0373     }
0374 
0375     // Check value formats.
0376     QString value = frame.getValue();
0377 
0378     if (Warning warning = warnings.value(id, None); warning != None) {
0379       if (CheckFunction check = checks.value(warning); check && !check(value)) {
0380         frame.setMarked(warning);
0381         marked = true;
0382         continue;
0383       }
0384     }
0385 
0386     // If nothing else is said newline character is forbidden.
0387     // Allowed in full text strings: USLT, SYLT, USER, COMM.
0388     if (value.contains(QLatin1Char('\n'))) {
0389       if (id != QLatin1String("COMM") && id != QLatin1String("USLT") &&
0390           id != QLatin1String("SYLT") && id != QLatin1String("USER")) {
0391         frame.setMarked(NlForbidden);
0392         marked = true;
0393         continue;
0394       }
0395       // A newline is represented, when allowed, with $0A only.
0396       if (value.contains(QLatin1String("\r\n")) &&
0397           frame.getFieldValue(Frame::ID_TextEnc).toInt() == Frame::TE_ISO8859_1)
0398       {
0399         frame.setMarked(CrForbidden);
0400         marked = true;
0401         continue;
0402       }
0403     }
0404     if (value.startsWith(QLatin1Char(' ')) ||
0405         value.endsWith(QLatin1Char(' '))) {
0406       frame.setMarked(ExcessSpace);
0407       marked = true;
0408       continue;
0409     }
0410     // 'Owner identifier' must be non-empty.
0411     if (id == QLatin1String("UFID") &&
0412         frame.getFieldValue(Frame::ID_Owner).toString().isEmpty()) {
0413       frame.setMarked(OwnerEmpty);
0414       marked = true;
0415     // USLT, SYLT, COMM, USER: The language should be represented in lower case.
0416     // If the language is not known the string "XXX" should be used.
0417     // USER is omitted because it is not supported by TagLib and would give
0418     // false positives.
0419     } else if ((id == QLatin1String("COMM") || id == QLatin1String("USLT") ||
0420                 id == QLatin1String("SYLT")) &&
0421                !isLanguageCode(frame.getFieldValue(Frame::ID_Language)
0422                                .toString())) {
0423       frame.setMarked(LanguageCode);
0424       marked = true;
0425     }
0426   }
0427   return marked;
0428 }