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 }