tagparser/vorbis/vorbiscommentfield.cpp

244 lines
10 KiB
C++

#include "./vorbiscommentfield.h"
#include "./vorbiscommentids.h"
#include "../flac/flacmetadata.h"
#include "../ogg/oggiterator.h"
#include "../diagnostics.h"
#include "../exceptions.h"
#include <c++utilities/conversion/binaryconversion.h>
#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/io/binaryreader.h>
#include <c++utilities/io/binarywriter.h>
#include <iostream>
#include <memory>
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<VorbisCommentField>(id, value)
{
}
/*!
* \brief Internal implementation for parsing.
*/
template <class StreamType> 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<std::streamoff>(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<char[]>(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<std::streamoff>(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<char *>(decoded.first.get()), decoded.second);
#else
bufferStream.write(reinterpret_cast<const char *>(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<double>(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<std::streamoff>(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<char[]>(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<std::streamsize>(requiredSize));
#endif
valueString = encodeBase64(reinterpret_cast<std::uint8_t *>(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<std::uint32_t>::max()) {
diag.emplace_back(DiagLevel::Critical, "Assigned value exceeds the maximum size.", context);
throw InvalidDataException();
}
writer.writeUInt32LE(static_cast<std::uint32_t>(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