Improve handling of Unicode and BOM in ID3v2

* Convert UTF-8 text fields automatically to UTF-16
  when version is ID3v2.3 or below
  -> so conversion from ID3v2.4 to ID3v2.3 or below
     doesn't require manual conversion of possibly
     UTF-8 encoded fields
* Don't use magic numbers for ID3v2 encoding byte
* Don't warn about UTF-16 Big Endian with BOM
This commit is contained in:
Martchus 2017-05-20 23:34:45 +02:00
parent 2ed7ea800d
commit e925e15533
7 changed files with 213 additions and 90 deletions

View File

@ -142,6 +142,7 @@ set(SRC_FILES
)
set(TEST_HEADER_FILES
tests/overall.h
tests/helper.h
)
set(TEST_SRC_FILES
tests/cppunit.cpp

View File

@ -20,6 +20,16 @@ using namespace IoUtilities;
namespace Media {
namespace Id3v2TextEncodingBytes {
enum Id3v2TextEncodingByte : byte
{
Ascii,
Utf16WithBom,
Utf16BigEndianWithoutBom,
Utf8
};
}
/*!
* \class Media::Id3v2Frame
* \brief The Id3v2Frame class is used by Id3v2Tag to store the fields.
@ -422,7 +432,13 @@ Id3v2FrameMaker::Id3v2FrameMaker(Id3v2Frame &frame, const byte version) :
m_frame.makeString(m_data, m_decompressedSize, ConversionUtilities::numberToString(m_frame.value().toStandardGenreIndex()), TagTextEncoding::Latin1);
} else {
// any other text frame
m_frame.makeString(m_data, m_decompressedSize, m_frame.value().toString(), m_frame.value().dataEncoding()); // the same as a normal text frame
if(version <= 3 && m_frame.value().dataEncoding() == TagTextEncoding::Utf8) {
// UTF-8 is only supported by ID3v2.4, so convert back to UTF-16
m_frame.makeString(m_data, m_decompressedSize, m_frame.value().toString(TagTextEncoding::Utf16LittleEndian), TagTextEncoding::Utf16LittleEndian);
} else {
// just keep encoding of the assigned value
m_frame.makeString(m_data, m_decompressedSize, m_frame.value().toString(), m_frame.value().dataEncoding());
}
}
} else if(version >= 3 && m_frameId == Id3v2FrameIds::lCover) {
@ -438,13 +454,11 @@ Id3v2FrameMaker::Id3v2FrameMaker(Id3v2Frame &frame, const byte version) :
|| ((version >= 3 && m_frameId == Id3v2FrameIds::lUnsynchronizedLyrics)
|| (version < 3 && m_frameId == Id3v2FrameIds::sUnsynchronizedLyrics))) {
// the comment frame or the unsynchronized lyrics frame
m_frame.makeComment(m_data, m_decompressedSize, m_frame.value());
m_frame.makeCommentConsideringVersion(m_data, m_decompressedSize, m_frame.value(), version);
} else {
// an unknown frame
// create buffer
m_data = make_unique<char[]>(m_decompressedSize = m_frame.value().dataSize());
// just write the data
copy(m_frame.value().dataPointer(), m_frame.value().dataPointer() + m_decompressedSize, m_data.get());
}
} catch(const ConversionException &) {
@ -534,13 +548,13 @@ void Id3v2FrameMaker::make(BinaryWriter &writer)
TagTextEncoding Id3v2Frame::parseTextEncodingByte(byte textEncodingByte)
{
switch(textEncodingByte) {
case 0: // Ascii
case Id3v2TextEncodingBytes::Ascii:
return TagTextEncoding::Latin1;
case 1: // Utf 16 with bom
case Id3v2TextEncodingBytes::Utf16WithBom:
return TagTextEncoding::Utf16LittleEndian;
case 2: // Utf 16 without bom
case Id3v2TextEncodingBytes::Utf16BigEndianWithoutBom:
return TagTextEncoding::Utf16BigEndian;
case 3: // Utf 8
case Id3v2TextEncodingBytes::Utf8:
return TagTextEncoding::Utf8;
default:
addNotification(NotificationType::Warning, "The charset of the frame is invalid. Latin-1 will be used.", "parsing encoding of frame " + frameIdString());
@ -555,13 +569,13 @@ byte Id3v2Frame::makeTextEncodingByte(TagTextEncoding textEncoding)
{
switch(textEncoding) {
case TagTextEncoding::Latin1:
return 0;
return Id3v2TextEncodingBytes::Ascii;
case TagTextEncoding::Utf8:
return 3;
return Id3v2TextEncodingBytes::Utf8;
case TagTextEncoding::Utf16LittleEndian:
return 1;
return Id3v2TextEncodingBytes::Utf16WithBom;
case TagTextEncoding::Utf16BigEndian:
return 2;
return Id3v2TextEncodingBytes::Utf16WithBom;
default:
return 0;
}
@ -585,20 +599,43 @@ tuple<const char *, size_t, const char *> Id3v2Frame::parseSubstring(const char
{
tuple<const char *, size_t, const char *> res(buffer, 0, buffer + bufferSize);
switch(encoding) {
case TagTextEncoding::Unspecified:
case TagTextEncoding::Latin1:
case TagTextEncoding::Utf8: {
if((bufferSize >= 3) && (ConversionUtilities::BE::toUInt24(buffer) == 0x00EFBBBF)) {
if(encoding == TagTextEncoding::Latin1) {
addNotification(NotificationType::Critical, "Denoted character set is Latin-1 but an UTF-8 BOM is present - assuming UTF-8.", "parsing frame " + frameIdString());
encoding = TagTextEncoding::Utf8;
}
get<0>(res) += 3;
}
const char *pos = get<0>(res);
for(; *pos != 0x00; ++pos) {
if(pos < get<2>(res)) {
++get<1>(res);
} else {
if(addWarnings) {
addNotification(NotificationType::Warning, "String in frame is not terminated proberly.", "parsing termination of frame " + frameIdString());
}
break;
}
}
get<2>(res) = pos + 1;
break;
}
case TagTextEncoding::Utf16BigEndian:
case TagTextEncoding::Utf16LittleEndian: {
if(bufferSize >= 2) {
if(ConversionUtilities::LE::toUInt16(buffer) == 0xFEFF) {
if(encoding != TagTextEncoding::Utf16LittleEndian) {
addNotification(NotificationType::Critical, "Denoted character set doesn't match present BOM - assuming UTF-16 Little Endian.", "parsing frame " + frameIdString());
switch(ConversionUtilities::LE::toUInt16(buffer)) {
case 0xFEFF:
if(encoding == TagTextEncoding::Utf16BigEndian) {
addNotification(NotificationType::Critical, "Denoted character set is UTF-16 Big Endian but UTF-16 Little Endian BOM is present - assuming UTF-16 LE.", "parsing frame " + frameIdString());
encoding = TagTextEncoding::Utf16LittleEndian;
}
get<0>(res) += 2;
} else if(ConversionUtilities::BE::toUInt16(buffer) == 0xFEFF) {
if(encoding != TagTextEncoding::Utf16BigEndian) {
addNotification(NotificationType::Critical, "Denoted character set doesn't match present BOM - assuming UTF-16 Big Endian.", "parsing frame " + frameIdString());
encoding = TagTextEncoding::Utf16BigEndian;
}
break;
case 0xFFFE:
encoding = TagTextEncoding::Utf16BigEndian;
get<0>(res) += 2;
}
}
@ -613,29 +650,7 @@ tuple<const char *, size_t, const char *> Id3v2Frame::parseSubstring(const char
break;
}
}
get<2>(res) = reinterpret_cast<const char *>(++pos);
break;
}
default: {
if((bufferSize >= 3) && (ConversionUtilities::BE::toUInt24(buffer) == 0x00EFBBBF)) {
get<0>(res) += 3;
if(encoding != TagTextEncoding::Utf8) {
addNotification(NotificationType::Critical, "Denoted character set doesn't match present BOM - assuming UTF-8.", "parsing frame " + frameIdString());
encoding = TagTextEncoding::Utf8;
}
}
const char *pos = get<0>(res);
for(; *pos != 0x00; ++pos) {
if(pos < get<2>(res)) {
++get<1>(res);
} else {
if(addWarnings) {
addNotification(NotificationType::Warning, "String in frame is not terminated proberly.", "parsing termination of frame " + frameIdString());
}
break;
}
}
get<2>(res) = ++pos;
get<2>(res) = reinterpret_cast<const char *>(pos + 1);
break;
}
}
@ -725,7 +740,6 @@ void Id3v2Frame::parseLegacyPicture(const char *buffer, std::size_t maxSize, Tag
}
const char *end = buffer + maxSize;
auto dataEncoding = parseTextEncodingByte(*buffer); // the first byte stores the encoding
//auto imageFormat = parseSubstring(buffer + 1, 3, TagTextEncoding::Latin1);
typeInfo = static_cast<unsigned char>(*(buffer + 4));
auto substr = parseSubstring(buffer + 5, end - 5 - buffer, dataEncoding, true);
tagValue.setDescription(string(get<0>(substr), get<1>(substr)), dataEncoding);
@ -872,15 +886,25 @@ size_t Id3v2Frame::makeBom(char *buffer, TagTextEncoding encoding)
*/
void Id3v2Frame::makeLegacyPicture(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo)
{
// calculate needed buffer size and create buffer
const TagTextEncoding descriptionEncoding = picture.descriptionEncoding();
const uint32 dataSize = picture.dataSize();
string::size_type descriptionLength = picture.description().find(descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? "\0\0" : "\0");
if(descriptionLength == string::npos) {
descriptionLength = picture.description().length();
// determine description
TagTextEncoding descriptionEncoding = picture.descriptionEncoding();
StringData convertedDescription;
string::size_type descriptionSize = picture.description().find(descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? "\0\0" : "\0");
if(descriptionEncoding == TagTextEncoding::Utf8) {
// UTF-8 is only supported by ID3v2.4, so convert back to UTF-16
descriptionEncoding = TagTextEncoding::Utf16LittleEndian;
convertedDescription = convertUtf8ToUtf16LE(picture.description().data(), picture.description().size());
descriptionSize = convertedDescription.second;
} else {
descriptionSize = picture.description().find(descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? "\0\0" : "\0");
if(descriptionSize == string::npos) {
descriptionSize = picture.description().size();
}
}
buffer = make_unique<char[]>(bufferSize = 1 + 3 + 1 + descriptionLength + (descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? 4 : 1) + dataSize);
// note: encoding byte + image format + picture type byte + description length + 1 or 2 null bytes (depends on encoding) + data size
// calculate needed buffer size and create buffer
const uint32 dataSize = picture.dataSize();
buffer = make_unique<char[]>(bufferSize = 1 + 3 + 1 + descriptionSize + (descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? 4 : 1) + dataSize);
// note: encoding byte + image format + picture type byte + description size + 1 or 2 null bytes (depends on encoding) + data size
char *offset = buffer.get();
// write encoding byte
*offset = makeTextEncodingByte(descriptionEncoding);
@ -902,8 +926,12 @@ void Id3v2Frame::makeLegacyPicture(unique_ptr<char[]> &buffer, uint32 &bufferSiz
*(offset += 3) = typeInfo;
// write description
offset += makeBom(offset + 1, descriptionEncoding);
picture.description().copy(++offset, descriptionLength);
*(offset += descriptionLength) = 0x00; // terminate description and increase data offset
if(convertedDescription.first) {
copy(convertedDescription.first.get(), convertedDescription.first.get() + descriptionSize, ++offset);
} else {
picture.description().copy(++offset, descriptionSize);
}
*(offset += descriptionSize) = 0x00; // terminate description and increase data offset
if(descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian) {
*(++offset) = 0x00;
}
@ -916,31 +944,46 @@ void Id3v2Frame::makeLegacyPicture(unique_ptr<char[]> &buffer, uint32 &bufferSiz
*/
void Id3v2Frame::makePicture(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo)
{
// determine description
TagTextEncoding descriptionEncoding = picture.descriptionEncoding();
StringData convertedDescription;
string::size_type descriptionSize = picture.description().find(descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? "\0\0" : "\0");
if(descriptionEncoding == TagTextEncoding::Utf8) {
// UTF-8 is only supported by ID3v2.4, so convert back to UTF-16
descriptionEncoding = TagTextEncoding::Utf16LittleEndian;
convertedDescription = convertUtf8ToUtf16LE(picture.description().data(), picture.description().size());
descriptionSize = convertedDescription.second;
} else {
descriptionSize = picture.description().find(descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? "\0\0" : "\0");
if(descriptionSize == string::npos) {
descriptionSize = picture.description().size();
}
}
// determine mime-type
string::size_type mimeTypeSize = picture.mimeType().find('\0');
if(mimeTypeSize == string::npos) {
mimeTypeSize = picture.mimeType().length();
}
// calculate needed buffer size and create buffer
const TagTextEncoding descriptionEncoding = picture.descriptionEncoding();
const uint32 dataSize = picture.dataSize();
string::size_type mimeTypeLength = picture.mimeType().find('\0');
if(mimeTypeLength == string::npos) {
mimeTypeLength = picture.mimeType().length();
}
string::size_type descriptionLength = picture.description().find(descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? "\0\0" : "\0");
if(descriptionLength == string::npos) {
descriptionLength = picture.description().length();
}
buffer = make_unique<char[]>(bufferSize = 1 + mimeTypeLength + 1 + 1 + descriptionLength + (descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? 4 : 1) + dataSize);
// note: encoding byte + mime type length + 0 byte + picture type byte + description length + 1 or 4 null bytes (depends on encoding) + data size
buffer = make_unique<char[]>(bufferSize = 1 + mimeTypeSize + 1 + 1 + descriptionSize + (descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? 4 : 1) + dataSize);
// note: encoding byte + mime type size + 0 byte + picture type byte + description size + 1 or 4 null bytes (depends on encoding) + data size
char *offset = buffer.get();
// write encoding byte
*offset = makeTextEncodingByte(descriptionEncoding);
// write mime type
picture.mimeType().copy(++offset, mimeTypeLength);
*(offset += mimeTypeLength) = 0x00; // terminate mime type
picture.mimeType().copy(++offset, mimeTypeSize);
*(offset += mimeTypeSize) = 0x00; // terminate mime type
// write picture type
*(++offset) = typeInfo;
// write description
offset += makeBom(offset + 1, descriptionEncoding);
picture.description().copy(++offset, descriptionLength);
*(offset += descriptionLength) = 0x00; // terminate description and increase data offset
if(convertedDescription.first) {
copy(convertedDescription.first.get(), convertedDescription.first.get() + descriptionSize, ++offset);
} else {
picture.description().copy(++offset, descriptionSize);
}
*(offset += descriptionSize) = 0x00; // terminate description and increase data offset
if(descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian) {
*(++offset) = 0x00;
}
@ -952,6 +995,14 @@ void Id3v2Frame::makePicture(unique_ptr<char[]> &buffer, uint32 &bufferSize, con
* \brief Writes the specified comment to the specified buffer.
*/
void Id3v2Frame::makeComment(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &comment)
{
makeCommentConsideringVersion(buffer, bufferSize, comment, 3);
}
/*!
* \brief Writes the specified comment to the specified buffer.
*/
void Id3v2Frame::makeCommentConsideringVersion(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &comment, byte version)
{
static const string context("making comment frame");
// check type and other values are valid
@ -965,14 +1016,23 @@ void Id3v2Frame::makeComment(unique_ptr<char[]> &buffer, uint32 &bufferSize, con
addNotification(NotificationType::Critical, "The language must be 3 bytes long (ISO-639-2).", context);
throw InvalidDataException();
}
// calculate needed buffer size and create buffer
string::size_type descriptionLength = comment.description().find(encoding == TagTextEncoding::Utf16BigEndian || encoding == TagTextEncoding::Utf16LittleEndian ? "\0\0" : "\0");
if(descriptionLength == string::npos) {
descriptionLength = comment.description().length();
StringData convertedDescription;
string::size_type descriptionSize;
if(version < 4 && encoding == TagTextEncoding::Utf8) {
// UTF-8 is only supported by ID3v2.4, so convert back to UTF-16
encoding = TagTextEncoding::Utf16LittleEndian;
convertedDescription = convertUtf8ToUtf16LE(comment.description().data(), comment.description().size());
descriptionSize = convertedDescription.second;
} else {
descriptionSize = comment.description().find(encoding == TagTextEncoding::Utf16BigEndian || encoding == TagTextEncoding::Utf16LittleEndian ? "\0\0" : "\0");
if(descriptionSize == string::npos) {
descriptionSize = comment.description().size();
}
}
const auto data = comment.toString();
buffer = make_unique<char[]>(bufferSize = 1 + 3 + descriptionLength + data.size() + (encoding == TagTextEncoding::Utf16BigEndian || encoding == TagTextEncoding::Utf16LittleEndian ? 6 : 1) + data.size());
// note: encoding byte + language + description length + actual data size + BOMs and termination
// calculate needed buffer size and create buffer
const auto data = comment.toString(encoding);
buffer = make_unique<char[]>(bufferSize = 1 + 3 + descriptionSize + data.size() + (encoding == TagTextEncoding::Utf16BigEndian || encoding == TagTextEncoding::Utf16LittleEndian ? 6 : 1) + data.size());
// note: encoding byte + language + description size + actual data size + BOMs and termination
char *offset = buffer.get();
// write encoding
*offset = makeTextEncodingByte(encoding);
@ -982,8 +1042,12 @@ void Id3v2Frame::makeComment(unique_ptr<char[]> &buffer, uint32 &bufferSize, con
}
// write description
offset += makeBom(offset + 1, encoding);
comment.description().copy(++offset, descriptionLength);
offset += descriptionLength;
if(convertedDescription.first) {
copy(convertedDescription.first.get(), convertedDescription.first.get() + descriptionSize, ++offset);
} else {
comment.description().copy(++offset, descriptionSize);
}
offset += descriptionSize;
*offset = 0x00; // terminate description and increase data offset
if(encoding == TagTextEncoding::Utf16BigEndian || encoding == TagTextEncoding::Utf16LittleEndian) {
*(++offset) = 0x00;

View File

@ -100,6 +100,7 @@ public:
class TAG_PARSER_EXPORT Id3v2Frame : public TagField<Id3v2Frame>, public StatusProvider
{
friend class TagField<Id3v2Frame>;
friend class Id3v2FrameMaker; // FIXME: remove when making methods public in next minor release
public:
Id3v2Frame();
@ -157,7 +158,9 @@ protected:
void cleared();
private:
std::size_t makeBom(char *buffer, TagTextEncoding encoding); // FIXME: add to public API in next minor release
// FIXME: add to public API in next minor release
void makeCommentConsideringVersion(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &comment, byte version);
std::size_t makeBom(char *buffer, TagTextEncoding encoding);
uint16 m_flag;
byte m_group;

View File

@ -84,6 +84,7 @@ bool TagValue::operator==(const TagValue &other) const
switch(m_type) {
case TagDataType::Text:
if(m_size != other.m_size && m_encoding != other.m_encoding) {
// don't consider differently encoded text values equal
return false;
}
return strncmp(m_ptr.get(), other.m_ptr.get(), m_size) == 0;

38
tests/helper.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef TAGPARSER_TEST_HELPER
#define TAGPARSER_TEST_HELPER
#include "../tagvalue.h"
#include <ostream>
/*!
* \brief Prints a TagTextEncoding to enable CPPUNIT_ASSERT_EQUAL for tag values.
*/
std::ostream &operator <<(std::ostream &os, const Media::TagTextEncoding &encoding)
{
using namespace Media;
switch(encoding) {
case TagTextEncoding::Unspecified:
return os << "unspecified";
case TagTextEncoding::Latin1:
return os << "Latin-1";
case TagTextEncoding::Utf8:
return os << "UTF-8";
case TagTextEncoding::Utf16LittleEndian:
return os << "UTF-16 LE";
case TagTextEncoding::Utf16BigEndian:
return os << "UTF-16 BE";
}
return os;
}
/*!
* \brief Prints a TagValue UTF-8 encoded to enable CPPUNIT_ASSERT_EQUAL for tag values.
*/
std::ostream &operator <<(std::ostream &os, const Media::TagValue &tagValue)
{
os << tagValue.toString(Media::TagTextEncoding::Utf8) << " (encoding: " << tagValue.dataEncoding() << ")";
return os;
}
#endif // TAGPARSER_TEST_HELPER

View File

@ -9,6 +9,7 @@ void OverallTests::setUp()
{
m_testTitle.assignText("some title", TagTextEncoding::Utf8);
m_testComment.assignText("some cómment", TagTextEncoding::Utf8);
m_testComment.setDescription("some descriptión", TagTextEncoding::Utf8);
m_testAlbum.assignText("some album", TagTextEncoding::Utf8);
m_testPartNumber.assignInteger(41);
m_testTotalParts.assignInteger(61);
@ -17,8 +18,10 @@ void OverallTests::setUp()
void OverallTests::tearDown()
{
if(!m_nestedTagsMkvPath.empty()) {
remove(m_nestedTagsMkvPath.data());
for(const string &file : {m_nestedTagsMkvPath, m_rawFlacPath, m_flacInOggPath}) {
if(!file.empty()) {
remove(file.data());
}
}
}

View File

@ -1,3 +1,4 @@
#include "./helper.h"
#include "./overall.h"
#include "../abstracttrack.h"
@ -91,16 +92,28 @@ void OverallTests::checkMp3TestMetaData()
}
// check common test meta data
for(Tag *tag : initializer_list<Tag *>{id3v1Tag, id3v2Tag}) {
if(tag) {
CPPUNIT_ASSERT(tag->value(KnownField::Title) == m_testTitle);
CPPUNIT_ASSERT(tag->value(KnownField::Comment) == m_testComment);
CPPUNIT_ASSERT(tag->value(KnownField::Album) == m_testAlbum);
CPPUNIT_ASSERT(tag->value(KnownField::Artist) == m_preservedMetaData.front());
// TODO: check more fields
m_preservedMetaData.pop();
}
if(id3v1Tag) {
CPPUNIT_ASSERT_EQUAL(m_testTitle, id3v1Tag->value(KnownField::Title));
CPPUNIT_ASSERT_EQUAL(m_testComment.toString(), id3v1Tag->value(KnownField::Comment).toString()); // ignore encoding here
CPPUNIT_ASSERT_EQUAL(m_testAlbum, id3v1Tag->value(KnownField::Album));
CPPUNIT_ASSERT_EQUAL(m_preservedMetaData.front(), id3v1Tag->value(KnownField::Artist));
m_preservedMetaData.pop();
}
if(id3v2Tag) {
const TagValue &titleValue = id3v2Tag->value(KnownField::Title);
CPPUNIT_ASSERT_MESSAGE("not attempted to use UTF-8 in ID3v2.3", titleValue.dataEncoding() == TagTextEncoding::Utf16LittleEndian);
CPPUNIT_ASSERT_EQUAL(m_testTitle.toString(), titleValue.toString(TagTextEncoding::Utf8));
const TagValue &commentValue = id3v2Tag->value(KnownField::Comment);
CPPUNIT_ASSERT_MESSAGE("not attempted to use UTF-8 in ID3v2.3", commentValue.dataEncoding() == TagTextEncoding::Utf16LittleEndian);
CPPUNIT_ASSERT_MESSAGE("not attempted to use UTF-8 in ID3v2.3", commentValue.descriptionEncoding() == TagTextEncoding::Utf16LittleEndian);
CPPUNIT_ASSERT_EQUAL(m_testComment.toString(), commentValue.toString(TagTextEncoding::Utf8));
CPPUNIT_ASSERT_EQUAL_MESSAGE("description is also converted to UTF-16", "s\0o\0m\0e\0 \0d\0e\0s\0c\0r\0i\0p\0t\0i\0\xf3\0n\0"s, commentValue.description());
CPPUNIT_ASSERT_EQUAL(m_testAlbum.toString(TagTextEncoding::Utf8), id3v2Tag->value(KnownField::Album).toString(TagTextEncoding::Utf8));
CPPUNIT_ASSERT_EQUAL(m_preservedMetaData.front(), id3v2Tag->value(KnownField::Artist));
// TODO: check more fields
m_preservedMetaData.pop();
}
// test ID3v1 specific test meta data
if(id3v1Tag) {
CPPUNIT_ASSERT(id3v1Tag->value(KnownField::TrackPosition).toPositionInSet().position() == m_testPosition.toPositionInSet().position());