#include "./vorbiscommentfield.h" #include "./vorbiscommentids.h" #include "../flac/flacmetadata.h" #include "../ogg/oggiterator.h" #include "../diagnostics.h" #include "../exceptions.h" #include #include #include #include #include #include #include using namespace std; using namespace CppUtilities; namespace TagParser { /*! * \class TagParser::VorbisCommentField * \brief The VorbisCommentField class is used by VorbisComment to store the fields. */ /*! * \brief Constructs a new Vorbis comment field. */ VorbisCommentField::VorbisCommentField() { } /*! * \brief Constructs a new Vorbis comment with the specified \a id and \a value. */ VorbisCommentField::VorbisCommentField(const IdentifierType &id, const TagValue &value) : TagField(id, value) { } /*! * \brief Internal implementation for parsing. */ template void VorbisCommentField::internalParse(StreamType &stream, std::uint64_t &maxSize, Diagnostics &diag) { static const string context("parsing Vorbis comment field"); char buff[4]; if (maxSize < 4) { diag.emplace_back(DiagLevel::Critical, argsToString("Field expected at ", static_cast(stream.tellg()), '.'), context); throw TruncatedDataException(); } else { maxSize -= 4; } stream.read(buff, 4); if (const auto size = LE::toUInt32(buff)) { // read size if (size <= maxSize) { maxSize -= size; // read data auto data = make_unique(size); stream.read(data.get(), size); std::uint32_t idSize = 0; for (const char *i = data.get(), *end = data.get() + size; i != end && *i != '='; ++i, ++idSize) ; // extract id setId(string(data.get(), idSize)); if (!idSize) { // empty field ID diag.emplace_back( DiagLevel::Critical, argsToString("The field ID at ", static_cast(stream.tellg()), " is empty."), context); throw InvalidDataException(); } else if (id() == VorbisCommentIds::cover()) { // extract cover value try { auto decoded = decodeBase64(data.get() + idSize + 1, size - idSize - 1); stringstream bufferStream(ios_base::in | ios_base::out | ios_base::binary); bufferStream.exceptions(ios_base::failbit | ios_base::badbit); #if defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION) bufferStream.rdbuf()->pubsetbuf(reinterpret_cast(decoded.first.get()), decoded.second); #else bufferStream.write(reinterpret_cast(decoded.first.get()), decoded.second); #endif FlacMetaDataBlockPicture pictureBlock(value()); pictureBlock.parse(bufferStream, decoded.second); setTypeInfo(pictureBlock.pictureType()); } catch (const TruncatedDataException &) { diag.emplace_back(DiagLevel::Critical, "METADATA_BLOCK_PICTURE is truncated.", context); throw; } catch (const ConversionException &) { diag.emplace_back(DiagLevel::Critical, "Base64 coding of METADATA_BLOCK_PICTURE is invalid.", context); throw InvalidDataException(); } catch (const std::ios_base::failure &failure) { diag.emplace_back(DiagLevel::Critical, argsToString("An IO error occurred when reading the METADATA_BLOCK_PICTURE struct: ", failure.what()), context); throw Failure(); } } else if (id().size() + 1 < size) { const auto str = std::string_view(data.get() + idSize + 1, size - idSize - 1); if (id() == VorbisCommentIds::rating()) { try { // set rating as Popularity to preserve the scale information value().assignPopularity(Popularity{ .rating = stringToNumber(str), .scale = TagType::VorbisComment }); } catch (const ConversionException &) { // fallback to text value().assignText(str, TagTextEncoding::Utf8); diag.emplace_back(DiagLevel::Warning, argsToString("The rating is not a number."), context); } } else { // extract other values (as string) value().assignText(str, TagTextEncoding::Utf8); } } } else { diag.emplace_back(DiagLevel::Critical, argsToString("Field at ", static_cast(stream.tellg()), " is truncated."), context); throw TruncatedDataException(); } } } /*! * \brief Parses a field using the specified \a iterator. * * The currentCharacterOffset() of the iterator is expected to be * at the beginning of the field to be parsed. * * \throws Throws std::ios_base::failure when an IO error occurs. * \throws Throws TagParser::Failure or a derived exception when a parsing * error occurs. */ void VorbisCommentField::parse(OggIterator &iterator, Diagnostics &diag) { std::uint64_t maxSize = iterator.streamSize() - iterator.currentCharacterOffset(); internalParse(iterator, maxSize, diag); } /*! * \brief Parses a field using the specified \a iterator. * * The currentCharacterOffset() of the iterator is expected to be * at the beginning of the field to be parsed. * * \throws Throws std::ios_base::failure when an IO error occurs. * \throws Throws TagParser::Failure or a derived exception when a parsing * error occurs. */ void VorbisCommentField::parse(OggIterator &iterator, std::uint64_t &maxSize, Diagnostics &diag) { internalParse(iterator, maxSize, diag); } /*! * \brief Parses a field from the specified \a stream. * * The position of the current character in the input stream is expected to be * at the beginning of the field to be parsed. * * \throws Throws std::ios_base::failure when an IO error occurs. * \throws Throws TagParser::Failure or a derived exception when a parsing * error occurs. */ void VorbisCommentField::parse(istream &stream, std::uint64_t &maxSize, Diagnostics &diag) { internalParse(stream, maxSize, diag); } /*! * \brief Writes the field to a stream using the specified \a writer. * * \throws Throws std::ios_base::failure when an IO error occurs. * \throws Throws TagParser::Failure or a derived exception when a making * error occurs. * \returns Returns whether the field has been written. (Some fields might be skipped * when specific \a flags are set.) */ bool VorbisCommentField::make(BinaryWriter &writer, VorbisCommentFlags flags, Diagnostics &diag) { static const string context("making Vorbis comment field"); if (id().empty()) { diag.emplace_back(DiagLevel::Critical, "The field ID is empty.", context); } try { // try to convert value to string string valueString; if (id() == VorbisCommentIds::cover()) { if (flags & VorbisCommentFlags::NoCovers) { return false; } // make cover if (value().type() != TagDataType::Picture) { diag.emplace_back(DiagLevel::Critical, "Assigned value of cover field is not picture data.", context); throw InvalidDataException(); } try { FlacMetaDataBlockPicture pictureBlock(value()); pictureBlock.setPictureType(typeInfo()); const auto requiredSize = pictureBlock.requiredSize(); auto buffer = make_unique(requiredSize); stringstream bufferStream(ios_base::in | ios_base::out | ios_base::binary); bufferStream.exceptions(ios_base::failbit | ios_base::badbit); #if defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION) bufferStream.rdbuf()->pubsetbuf(buffer.get(), requiredSize); #endif pictureBlock.make(bufferStream); #if defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION) bufferStream.read(buffer.get(), static_cast(requiredSize)); #endif valueString = encodeBase64(reinterpret_cast(buffer.get()), requiredSize); } catch (const Failure &) { diag.emplace_back(DiagLevel::Critical, "Unable to make METADATA_BLOCK_PICTURE struct from the assigned value.", context); throw; } catch (const std::ios_base::failure &failure) { diag.emplace_back(DiagLevel::Critical, argsToString("An IO error occurred when writing the METADATA_BLOCK_PICTURE struct: ", failure.what()), context); throw Failure(); } } else if (value().type() == TagDataType::Popularity) { valueString = value().toScaledPopularity(TagType::VorbisComment).toString(); } else { // make normal string value valueString = value().toString(TagTextEncoding::Utf8); } const auto size(valueString.size() + id().size() + 1); if (size > numeric_limits::max()) { diag.emplace_back(DiagLevel::Critical, "Assigned value exceeds the maximum size.", context); throw InvalidDataException(); } writer.writeUInt32LE(static_cast(size)); writer.writeString(id()); writer.writeChar('='); writer.writeString(valueString); } catch (const ConversionException &) { diag.emplace_back(DiagLevel::Critical, "Assigned value can not be converted appropriately.", context); throw InvalidDataException(); } return true; } } // namespace TagParser