diff --git a/id3/id3v2frame.cpp b/id3/id3v2frame.cpp index 00b0111..9d8aaeb 100644 --- a/id3/id3v2frame.cpp +++ b/id3/id3v2frame.cpp @@ -121,6 +121,23 @@ u16string wideStringFromSubstring(tuple subs return res; } +/*! + * \brief Reads the play counter from the specified range. + */ +static std::uint64_t readPlayCounter(const char *begin, const char *end, const std::string &context, Diagnostics &diag) +{ + auto res = std::uint64_t(); + auto pos = end - 1; + if (end - begin > 8) { + diag.emplace_back(DiagLevel::Critical, "Play counter is bigger than eight bytes and therefore not supported.", context); + return res; + } + for (auto shift = 0; pos >= begin; shift += 8, --pos) { + res += static_cast(static_cast(*pos)) << shift; + } + return res; +} + /*! * \brief Parses a frame from the stream read using the specified \a reader. * @@ -363,6 +380,24 @@ void Id3v2Frame::parse(BinaryReader &reader, std::uint32_t version, std::uint32_ // parse comment frame or unsynchronized lyrics frame (these two frame types have the same structure) parseComment(buffer.get(), m_dataSize, value(), diag); + } else if (((version >= 3 && id() == Id3v2FrameIds::lRating) || (version < 3 && id() == Id3v2FrameIds::sRating))) { + // parse popularimeter frame + auto popularity = Popularity(); + auto userEncoding = TagTextEncoding::Latin1; + auto substr = parseSubstring(buffer.get(), m_dataSize, userEncoding, true, diag); + auto end = buffer.get() + m_dataSize; + if (std::get<1>(substr)) { + popularity.user.assign(std::get<0>(substr), std::get<1>(substr)); + } + auto ratingPos = std::get<2>(substr); + if (ratingPos >= end) { + diag.emplace_back(DiagLevel::Critical, "Popularimeter frame is incomplete (rating is missing).", context); + throw TruncatedDataException(); + } + popularity.rating = static_cast(*ratingPos); + popularity.playCounter = readPlayCounter(ratingPos + 1, end, context, diag); + value().assignPopularity(popularity); + } else { // parse unknown/unsupported frame value().assignData(buffer.get(), m_dataSize, TagDataType::Undefined); @@ -430,6 +465,28 @@ std::string Id3v2Frame::ignoreAdditionalValuesDiagMsg() const return argsToString("Additional values ", DiagMessage::formatList(TagValue::toStrings(m_additionalValues)), " are supposed to be ignored."); } +/*! + * \brief Computes the size required to serialize the specified \a playCounter value. + */ +static std::uint32_t computePlayCounterSize(std::uint64_t playCounter) +{ + auto res = 4u; + for (playCounter >>= 32; playCounter; playCounter >>= 8, ++res) + ; // additional bytes for play counter into account when it is > 0xFFFFFFFF + return res; +} + +/*! + * \brief Writes the specified \a playCounter with the specified \a playCounterSize to the buffer specified + * by the \a last address to write the data to. + */ +static void writePlayCounter(char *last, std::uint32_t playCounterSize, std::uint64_t playCounter) +{ + for (; playCounter || playCounterSize; playCounter >>= 8, --playCounterSize, --last) { + *last = static_cast(playCounter & 0xFF); + } +} + /*! * \class TagParser::Id3v2FrameMaker * \brief The Id3v2FrameMaker class helps making ID3v2 frames. @@ -640,6 +697,36 @@ Id3v2FrameMaker::Id3v2FrameMaker(Id3v2Frame &frame, std::uint8_t version, Diagno // make comment frame or the unsynchronized lyrics frame m_frame.makeComment(m_data, m_decompressedSize, *values.front(), version, diag); + } else if (((version >= 3 && m_frameId == Id3v2FrameIds::lRating) || (version < 3 && m_frameId == Id3v2FrameIds::sRating))) { + // make popularimeter frame + auto popularity = Popularity(); + try { + popularity = values.front()->toPopularity(); + } catch (const ConversionException &) { + diag.emplace_back(DiagLevel::Warning, + argsToString( + "The popularity \"", values.front()->toDisplayString(), "\" is not of the expected form, eg. \"user|rating|counter\"."), + context); + } + // -> clamp rating + if (popularity.rating > 0xFF) { + popularity.rating = 0xFF; + diag.emplace_back(DiagLevel::Warning, argsToString("The rating has been clamped to 255."), context); + } else if (popularity.rating < 0x00) { + popularity.rating = 0x00; + diag.emplace_back(DiagLevel::Warning, argsToString("The rating has been clamped to 0."), context); + } + // -> compute size: user name length + termination + rating byte + m_decompressedSize = static_cast(popularity.user.size() + 2); + const auto playCounterSize = computePlayCounterSize(popularity.playCounter); + m_decompressedSize += playCounterSize; + // -> copy data into buffer + m_data = make_unique(m_decompressedSize); + auto pos = popularity.user.size() + 1; + std::memcpy(m_data.get(), popularity.user.data(), pos); + m_data[pos] = static_cast(popularity.rating); + writePlayCounter(m_data.get() + pos + playCounterSize, playCounterSize, popularity.playCounter); + } else { // make unknown frame const auto &value(*values.front()); @@ -648,7 +735,7 @@ Id3v2FrameMaker::Id3v2FrameMaker(Id3v2Frame &frame, std::uint8_t version, Diagno throw InvalidDataException(); } m_data = make_unique(m_decompressedSize = static_cast(value.dataSize())); - copy(value.dataPointer(), value.dataPointer() + m_decompressedSize, m_data.get()); + std::memcpy(m_data.get(), value.dataPointer(), m_decompressedSize); } } catch (const ConversionException &) { try { diff --git a/tagvalue.cpp b/tagvalue.cpp index 043634c..8c2c774 100644 --- a/tagvalue.cpp +++ b/tagvalue.cpp @@ -332,6 +332,30 @@ void TagValue::clearMetadata() m_type = TagDataType::Undefined; } +/*! + * \brief Returns a "display string" for the specified value. + * \returns + * - Returns the just the type if no displayable string can be made of it, eg. "picture", otherwise + * returns the string representation. + * - Returns "invalid …" if a conversion error returned when making the string representation but + * never throws a ConversionException (unlike toString()). + */ +std::string TagParser::TagValue::toDisplayString() const +{ + switch (m_type) { + case TagDataType::Undefined: + case TagDataType::Binary: + case TagDataType::Picture: + return std::string(tagDataTypeString(m_type)); + default: + try { + return toString(TagTextEncoding::Utf8); + } catch (const ConversionException &e) { + return argsToString("invalid ", tagDataTypeString(m_type), ':', ' ', e.what()); + } + } +} + /*! * \brief Converts the value of the current TagValue object to its equivalent * integer representation. @@ -1050,11 +1074,12 @@ const TagValue &TagValue::empty() } /*! - * \brief Returns the popularity as string in the format "user|rating|play-counter". + * \brief Returns the popularity as string in the format "user|rating|play-counter" or an empty + * string if the popularity isEmpty(). */ std::string Popularity::toString() const { - return user % '|' % numberToString(rating) % '|' + playCounter; + return isEmpty() ? std::string() : user % '|' % numberToString(rating) % '|' + playCounter; } /*! diff --git a/tagvalue.h b/tagvalue.h index cdca161..5795c91 100644 --- a/tagvalue.h +++ b/tagvalue.h @@ -76,6 +76,10 @@ struct TAG_PARSER_EXPORT Popularity { /// \brief Play counter specific to the user. std::uint64_t playCounter = 0; + bool isEmpty() const + { + return user.empty() && rating != 0.0 && !playCounter; + } std::string toString() const; static Popularity fromString(std::string_view str); bool operator==(const Popularity &other) const @@ -149,6 +153,7 @@ public: void clearDataAndMetadata(); TagDataType type() const; std::string toString(TagTextEncoding encoding = TagTextEncoding::Unspecified) const; + std::string toDisplayString() const; void toString(std::string &result, TagTextEncoding encoding = TagTextEncoding::Unspecified) const; std::u16string toWString(TagTextEncoding encoding = TagTextEncoding::Unspecified) const; void toWString(std::u16string &result, TagTextEncoding encoding = TagTextEncoding::Unspecified) const;