RIFF/WAV: Default to latin1, change charset when CSET chunk is present (#1451)

- WAV files being read evaluate the charset properly (overridable
  via option by caller)
- WAV files being saved always use UTF-8 (they add a CSET chunk)

following the specification at
https://www.robotplanet.dk/audio/wav_meta_data/riff_mci.pdf#page=25

The code page charset can also be used for ID3v1 tags, both for
parsing and rendering.

For users in space restricted environments, there
is a build flag WITH_CHARSETTABLES to exclude the character tables.

---------

Co-authored-by: Urs Fleisch <[email protected]>
This commit is contained in:
MinnieTheMoocher
2026-10-28 17:49:28 +01:00
committed by GitHub
co-authored by Urs Fleisch
parent bdfeb82cbe
commit ff3b53670a
16 changed files with 3731 additions and 38 deletions
+386
View File
@@ -26,22 +26,54 @@
#include <string>
#include <cstdio>
#include "taglib_config.h"
#include "tstring.h"
#include "mpegfile.h"
#include "id3v1tag.h"
#include "id3v1genres.h"
#include <cppunit/extensions/HelperMacros.h>
#include "plainfile.h"
#include "utils.h"
using namespace std;
using namespace TagLib;
namespace
{
// The bytes of a Windows code page an ID3v1 field cannot tell from ISO 8859-1
// on disk, and the text they stand for. 0x80 is a Euro sign in every ANSI
// code page in use and a control character in ISO 8859-1.
const ByteVector kAnsiArtist("A \x80 B", 5);
const String kAnsiArtistText(L"A \x20AC B");
// A Cyrillic byte, which ISO 8859-1 cannot represent at all and which is once
// again a control character there.
const ByteVector kCyrillicArtist("A \xC0 B", 5);
const String kCyrillicArtistText(L"A \x0410 B");
// The same text in UTF-8, as software that writes UTF-8 into ID3v1 produces
// it, plus the ISO 8859-1 reading of those same bytes.
const ByteVector kUtf8Artist("Caf\xC3\xA9", 5);
const String kUtf8ArtistText(L"Caf\xE9");
const String kUtf8ArtistLatin1(L"Caf\xC3\xA9");
}
class TestID3v1 : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(TestID3v1);
CPPUNIT_TEST(testStripWhiteSpace);
CPPUNIT_TEST(testGenres);
CPPUNIT_TEST(testRenamedGenres);
#ifdef TAGLIB_WITH_CHARSETTABLES
CPPUNIT_TEST(testAssumedCodePageDefaultIsLatin1);
CPPUNIT_TEST(testAssumedCodePage);
CPPUNIT_TEST(testConstructorCodePage);
CPPUNIT_TEST(testUnsupportedCodePage);
CPPUNIT_TEST(testCommentUsesCodePage);
CPPUNIT_TEST(testUserHandlerBeatsCodePage);
CPPUNIT_TEST(testMp3ReadsWithIso88591);
CPPUNIT_TEST(testMp3ReadsWithWindows1252);
#endif
CPPUNIT_TEST_SUITE_END();
public:
@@ -84,6 +116,360 @@ public:
CPPUNIT_ASSERT_EQUAL(129U, tag.genreNumber());
}
#ifdef TAGLIB_WITH_CHARSETTABLES
//! A temporary MP3 file which is automatically deleted when it goes out of scope.
class ScopedMp3FileCopyWithId3v1Tag : public ScopedFileCopy
{
public:
static constexpr unsigned int ARTIST_OFFSET = 3 + 30;
static constexpr unsigned int COMMENT_OFFSET = 3 + 30 + 30 + 30 + 4;
//! Construct MP3 file with an ID3v1 tag which has one raw \a value at
//! \a offset, which points by default to the artist field.
explicit ScopedMp3FileCopyWithId3v1Tag(const ByteVector &value,
unsigned int offset = ARTIST_OFFSET)
: ScopedFileCopy("xing", ".mp3")
{
ByteVector tag = ID3v1::Tag::fileIdentifier();
// Leave empty fields up to the given offset.
tag.resize(offset, 0);
tag.append(value);
tag.resize(128, 0);
// 0xff in the genre byte is the ID3v1 value for "no genre".
tag[127] = static_cast<char>(0xff);
std::ofstream file(fileName().c_str(), std::ios::in | std::ios::out | std::ios::binary);
// The tag goes where a file already ending in one would carry it: the last
// 128 bytes. The source file is longer than that, so a shorter tag would
// not be found at all.
file.seekp(-128, std::ios::end);
file.write(tag.data(), tag.size());
}
//! The file offset of the ID3v1 tag.
offset_t tagOffset() const
{
std::ifstream file(fileName().c_str(), std::ios::binary | std::ios::ate);
return static_cast<offset_t>(file.tellg()) - 128;
}
//! Get the raw field value from the ID3v1 tag starting at \a offset to
//! the next zero byte.
ByteVector getRawValue(unsigned int offset = ARTIST_OFFSET)
{
std::ifstream file(fileName().c_str(), std::ios::binary | std::ios::ate);
file.seekg(-128, std::ios::end);
ByteVector tag(128U, 0);
file.read(tag.data(), tag.size());
return extractRawValue(tag, offset);
}
static ByteVector extractRawValue(const ByteVector &tag, unsigned int offset = ARTIST_OFFSET)
{
if(tag.size() != 128)
return {};
const char *str = tag.data();
const char *end = std::find(str + offset, str + 128, '\0');
return tag.mid(offset, static_cast<unsigned int>(end - str - offset));
}
};
// Puts back the code page a tag is assumed to have when none is stated, so
// that a test stating another one does not carry it into the next test.
class RestoreAssumedCodePage
{
public:
RestoreAssumedCodePage() :
m_previous(ID3v1::Tag::assumedCodePage())
{
}
~RestoreAssumedCodePage()
{
ID3v1::Tag::setAssumedCodePage(m_previous);
}
private:
const unsigned int m_previous;
};
// A handler the user installed is an explicit override, so it outranks any
// code page, once again because installing one is a deliberate act.
class FixedStringHandler : public ID3v1::StringHandler
{
public:
String parse(const ByteVector &) const override { return String(L"from the handler"); }
};
// The ID3v1 specification says ISO 8859-1, and that is what an untouched
// library assumes: the bytes of a file that holds another encoding are read
// as the Latin-1 characters they also happen to be.
void testAssumedCodePageDefaultIsLatin1()
{
CPPUNIT_ASSERT_EQUAL(0u, ID3v1::Tag::assumedCodePage());
ScopedMp3FileCopyWithId3v1Tag copy(kUtf8Artist);
const string name = copy.fileName();
MPEG::File f(name.c_str());
CPPUNIT_ASSERT(f.ID3v1Tag(false));
CPPUNIT_ASSERT_EQUAL(kUtf8ArtistLatin1, f.ID3v1Tag(false)->artist());
}
// Software that writes UTF-8 or an ANSI code page without any way of saying
// so is what the option is for. Stated once, it is what every tag read
// afterwards is decoded with.
void testAssumedCodePage()
{
const RestoreAssumedCodePage restore;
ID3v1::Tag::setAssumedCodePage(65001);
CPPUNIT_ASSERT_EQUAL(65001u, ID3v1::Tag::assumedCodePage());
{
ScopedMp3FileCopyWithId3v1Tag copy(kUtf8Artist);
const string name = copy.fileName();
MPEG::File f(name.c_str());
CPPUNIT_ASSERT_EQUAL(kUtf8ArtistText, f.ID3v1Tag(false)->artist());
f.save();
CPPUNIT_ASSERT_EQUAL(kUtf8Artist, copy.getRawValue());
}
ID3v1::Tag::setAssumedCodePage(1252);
CPPUNIT_ASSERT_EQUAL(1252u, ID3v1::Tag::assumedCodePage());
{
ScopedMp3FileCopyWithId3v1Tag copy(kAnsiArtist);
const string name = copy.fileName();
MPEG::File f(name.c_str());
CPPUNIT_ASSERT_EQUAL(kAnsiArtistText, f.ID3v1Tag(false)->artist());
f.save();
CPPUNIT_ASSERT_EQUAL(kAnsiArtist, copy.getRawValue());
}
ID3v1::Tag::setAssumedCodePage(1251);
CPPUNIT_ASSERT_EQUAL(1251u, ID3v1::Tag::assumedCodePage());
{
ScopedMp3FileCopyWithId3v1Tag copy(kCyrillicArtist);
const string name = copy.fileName();
{
MPEG::File f(name.c_str());
CPPUNIT_ASSERT_EQUAL(kCyrillicArtistText, f.ID3v1Tag(false)->artist());
f.save();
CPPUNIT_ASSERT_EQUAL(kCyrillicArtist, copy.getRawValue());
}
{
MPEG::File f(name.c_str());
ID3v1::Tag *tag = f.ID3v1Tag(false);
CPPUNIT_ASSERT_EQUAL(kCyrillicArtistText, tag->artist());
// The character 03B1 is a greek alpha, which is not represented in 1251
tag->setArtist((L"A \x0410\x03B1 B"));
CPPUNIT_ASSERT(f.save());
}
{
MPEG::File f(name.c_str());
CPPUNIT_ASSERT_EQUAL(String(L"A \x0410? B"), f.ID3v1Tag(false)->artist());
}
}
// Stating 0 again is going back to what the specification says.
ID3v1::Tag::setAssumedCodePage(0);
CPPUNIT_ASSERT_EQUAL(0u, ID3v1::Tag::assumedCodePage());
{
ScopedMp3FileCopyWithId3v1Tag copy(kUtf8Artist);
const string name = copy.fileName();
MPEG::File f(name.c_str());
CPPUNIT_ASSERT_EQUAL(kUtf8ArtistLatin1, f.ID3v1Tag(false)->artist());
f.save();
CPPUNIT_ASSERT_EQUAL(kUtf8Artist, copy.getRawValue());
}
}
// A tag can be given its own code page as well, for callers that read files
// from more than one source and cannot state a single process wide default.
void testConstructorCodePage()
{
const RestoreAssumedCodePage restore;
// Even with another assumption in place, the code page handed to the
// constructor is the one that decides.
ID3v1::Tag::setAssumedCodePage(65001);
{
ScopedMp3FileCopyWithId3v1Tag copy(kAnsiArtist);
const string name = copy.fileName();
PlainFile file(name.c_str());
CPPUNIT_ASSERT(file.isValid());
ID3v1::Tag tag(&file, copy.tagOffset(), 1252);
CPPUNIT_ASSERT_EQUAL(kAnsiArtistText, tag.artist());
CPPUNIT_ASSERT_EQUAL(kAnsiArtist,
ScopedMp3FileCopyWithId3v1Tag::extractRawValue(tag.render()));
}
{
ScopedMp3FileCopyWithId3v1Tag copy(kCyrillicArtist);
const string name = copy.fileName();
PlainFile file(name.c_str());
ID3v1::Tag tag(&file, copy.tagOffset(), 1251);
CPPUNIT_ASSERT_EQUAL(kCyrillicArtistText, tag.artist());
CPPUNIT_ASSERT_EQUAL(kCyrillicArtist,
ScopedMp3FileCopyWithId3v1Tag::extractRawValue(tag.render()));
}
// Code page 0 is the "not stated" value, which means the assumption.
{
ScopedMp3FileCopyWithId3v1Tag copy(kUtf8Artist);
const string name = copy.fileName();
PlainFile file(name.c_str());
ID3v1::Tag tag(&file, copy.tagOffset(), 0);
CPPUNIT_ASSERT_EQUAL(kUtf8ArtistText, tag.artist());
CPPUNIT_ASSERT_EQUAL(kUtf8Artist,
ScopedMp3FileCopyWithId3v1Tag::extractRawValue(tag.render()));
}
}
// A code page this library cannot decode falls back to what is assumed: the
// number is a hint about how the text was written, and dropping the whole tag
// over a hint that could not be followed would lose the fields as well.
void testUnsupportedCodePage()
{
const RestoreAssumedCodePage restore;
// A code page this library has no table for, here a Japanese one, is
// refused as the assumption and the previous one stays.
ID3v1::Tag::setAssumedCodePage(1252);
ID3v1::Tag::setAssumedCodePage(932);
CPPUNIT_ASSERT_EQUAL(1252u, ID3v1::Tag::assumedCodePage());
{
ScopedMp3FileCopyWithId3v1Tag copy(kAnsiArtist);
const string name = copy.fileName();
PlainFile file(name.c_str());
// Handing the same unsupported number to the constructor falls back to
// the assumption rather than refusing to read the tag.
ID3v1::Tag tag(&file, copy.tagOffset(), 932);
CPPUNIT_ASSERT_EQUAL(kAnsiArtistText, tag.artist());
CPPUNIT_ASSERT_EQUAL(kAnsiArtist,
ScopedMp3FileCopyWithId3v1Tag::extractRawValue(tag.render()));
}
}
// The charset covers the whole tag, the ID3v1.0 comment included. Its 30
// bytes are a single field there, so it goes through the same decoding even
// though ID3v1.1 would have cut the track number out of it.
void testCommentUsesCodePage()
{
const RestoreAssumedCodePage restore;
ID3v1::Tag::setAssumedCodePage(1252);
ScopedMp3FileCopyWithId3v1Tag copy(kAnsiArtist,
ScopedMp3FileCopyWithId3v1Tag::COMMENT_OFFSET);
const string name = copy.fileName();
MPEG::File f(name.c_str());
CPPUNIT_ASSERT(f.ID3v1Tag(false));
CPPUNIT_ASSERT_EQUAL(kAnsiArtistText, f.ID3v1Tag(false)->comment());
CPPUNIT_ASSERT_EQUAL(kAnsiArtist, copy.getRawValue(
ScopedMp3FileCopyWithId3v1Tag::COMMENT_OFFSET));
}
// A real MP3 carrying an ID3v1 tag whose bytes are only meaningful as ISO
// 8859-1, which is what the ID3v1 specification prescribes and what a caller
// states explicitly. The bytes are one of Latin-1's own characters, so the
// reading is the text it spells; a code page that reassigns this range would
// not produce it.
void testMp3ReadsWithIso88591()
{
const RestoreAssumedCodePage restore;
// Keep any assumption out of it: this tag is read as ISO 8859-1 because
// the code page says so, not because the default happens to agree.
ID3v1::Tag::setAssumedCodePage(65001);
const ByteVector latin1Artist("Caf\xE9", 4);
const String latin1ArtistText(L"Caf\xE9");
ScopedMp3FileCopyWithId3v1Tag copy(latin1Artist);
const string name = copy.fileName();
PlainFile file(name.c_str());
CPPUNIT_ASSERT(file.isValid());
ID3v1::Tag tag(&file, copy.tagOffset(), 28591);
CPPUNIT_ASSERT_EQUAL(latin1ArtistText, tag.artist());
}
// The same MP3 shape read as Windows-1252 instead. The bytes below are a
// Euro sign and a pair of curly quotes in 1252, and C1 controls in ISO
// 8859-1, so the two readings differ and only one of them is right.
void testMp3ReadsWithWindows1252()
{
const RestoreAssumedCodePage restore;
// An assumption the tag contradicts, so the reading has to come from the
// code page handed to the constructor.
ID3v1::Tag::setAssumedCodePage(28591);
const ByteVector windows1252Artist("A \x80 \x93q" "\x94", 7);
const String windows1252ArtistText(L"A \x20AC \x201Cq\x201D");
ScopedMp3FileCopyWithId3v1Tag copy(windows1252Artist);
const string name = copy.fileName();
PlainFile file(name.c_str());
CPPUNIT_ASSERT(file.isValid());
ID3v1::Tag tag(&file, copy.tagOffset(), 1252);
CPPUNIT_ASSERT_EQUAL(windows1252ArtistText, tag.artist());
}
// An installed handler is used for every tag, whatever code page is stated,
// and removing it hands the tags back to that code page.
void testUserHandlerBeatsCodePage()
{
const RestoreAssumedCodePage restore;
class RestoreHandler
{
public:
~RestoreHandler() { ID3v1::Tag::setStringHandler(nullptr); }
} restoreHandler;
ID3v1::Tag::setAssumedCodePage(1252);
{
ScopedMp3FileCopyWithId3v1Tag copy(kAnsiArtist);
const string name = copy.fileName();
MPEG::File f(name.c_str());
CPPUNIT_ASSERT_EQUAL(kAnsiArtistText, f.ID3v1Tag(false)->artist());
}
const FixedStringHandler handler;
ID3v1::Tag::setStringHandler(&handler);
{
ScopedMp3FileCopyWithId3v1Tag copy(kAnsiArtist);
const string name = copy.fileName();
MPEG::File f(name.c_str());
CPPUNIT_ASSERT_EQUAL(String(L"from the handler"), f.ID3v1Tag(false)->artist());
}
// Also for a tag whose code page was stated explicitly.
{
ScopedMp3FileCopyWithId3v1Tag copy(kAnsiArtist);
const string name = copy.fileName();
PlainFile file(name.c_str());
ID3v1::Tag tag(&file, copy.tagOffset(), 1252);
CPPUNIT_ASSERT_EQUAL(String(L"from the handler"), tag.artist());
}
ID3v1::Tag::setStringHandler(nullptr);
{
ScopedMp3FileCopyWithId3v1Tag copy(kAnsiArtist);
const string name = copy.fileName();
MPEG::File f(name.c_str());
CPPUNIT_ASSERT_EQUAL(kAnsiArtistText, f.ID3v1Tag(false)->artist());
}
}
#endif // TAGLIB_WITH_CHARSETTABLES
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestID3v1);
+836 -1
View File
@@ -23,9 +23,12 @@
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include <initializer_list>
#include <string>
#include <cstdio>
#include "taglib_config.h"
#include "tcharset.h"
#include "id3v2tag.h"
#include "infotag.h"
#include "tbytevectorlist.h"
@@ -40,6 +43,253 @@
using namespace std;
using namespace TagLib;
#ifdef TAGLIB_WITH_CHARSETTABLES
namespace
{
// The bytes of a chunk: its id, its size, its payload, and the pad byte that
// brings an odd sized payload to an even length.
ByteVector riffChunk(const char *id, const ByteVector &payload)
{
ByteVector chunk = ByteVector(id, 4) + ByteVector::fromUInt(payload.size(), false) + payload;
if(payload.size() % 2)
chunk.append(static_cast<char>(0));
return chunk;
}
// The payload of a conforming CSET chunk, which the RIFF specification lays
// out as wCodePage, wCountryCode, wLanguageCode and wDialect, one WORD each.
// Only the code page matters here, so the other three are the zero that means
// "no opinion".
ByteVector riffCsetPayload(unsigned int codePage)
{
return ByteVector::fromShort(static_cast<short>(codePage), false) + ByteVector(6, '\0');
}
// The same layout, with the country, language and dialect the file was written
// for filled in as well. These are what a reader that took the code page for a
// 32 bit number mixed into its result.
ByteVector riffCsetPayloadWithLanguage(unsigned int codePage)
{
return ByteVector::fromShort(static_cast<short>(codePage), false) +
ByteVector::fromShort(1, false) + ByteVector::fromShort(9, false) +
ByteVector::fromShort(1, false);
}
// One field of an INFO list, as the reader will find it: an id and the text
// in whatever encoding the file declares, without the NUL that ends it.
struct InfoField
{
const char *id;
ByteVector text;
};
// A WAV file with an INFO tag holding \a fields, and a CSET chunk whose raw
// payload is \a cset. An empty \a cset means no CSET chunk at all, which is
// what a file that declares no charset looks like. The CSET is written in
// front of the LIST it describes, unless \a csetBehindList is set, which is
// where a file TagLib did not write tends to put it.
ByteVector riffWithInfoCset(std::initializer_list<InfoField> fields,
const ByteVector &cset, bool csetBehindList = false)
{
ByteVector list("INFO", 4);
for(const auto &field : fields)
list.append(riffChunk(field.id, field.text + ByteVector(1, '\0')));
// fmt: PCM, one channel, 22050 Hz, 8 bits, 22050 bytes per second, and the
// 8 bytes of audio the data chunk below has to hold for the file to parse.
ByteVector body =
riffChunk("fmt ", ByteVector("\x01\x00\x01\x00\x3E\x56\x00\x00"
"\x3E\x56\x00\x00\x01\x00\x08\x00", 16));
body.append(riffChunk("data", ByteVector(8, static_cast<char>(0x80))));
if(!cset.isEmpty() && !csetBehindList)
body.append(riffChunk("CSET", cset));
body.append(riffChunk("LIST", list));
if(!cset.isEmpty() && csetBehindList)
body.append(riffChunk("CSET", cset));
return ByteVector("RIFF", 4) + ByteVector::fromUInt(body.size() + 4, false) +
ByteVector("WAVE", 4) + body;
}
// The same file, with the charset declared by \a codePage in the conforming
// 8 byte CSET layout, or with no CSET chunk at all when \a codePage is 0.
ByteVector riffWithInfo(std::initializer_list<InfoField> fields,
unsigned int codePage = 0, bool csetBehindList = false)
{
return riffWithInfoCset(fields, codePage ? riffCsetPayload(codePage) : ByteVector(),
csetBehindList);
}
// An INFO tag with no CSET chunk, which is what a reader has to fall back to
// Latin-1 on.
ByteVector latin1Riff()
{
return riffWithInfo({{"IART", ByteVector("A \xAE B", 5)},
{"IPRD", ByteVector("A \xAE B", 5)},
{"ICMT", ByteVector("Caf\xE9", 4)},
{"ICRD", ByteVector("2024-01-01", 10)}});
}
// An INFO tag that declares UTF-8, with the CSET behind the LIST it describes.
ByteVector utf8Riff()
{
return riffWithInfo({{"IPRD", ByteVector("Caf\xC3\xA9", 5)},
{"ICRD", ByteVector("2024-03-03", 10)}},
65001, true);
}
// A WAV file whose INFO text is in \a codePage, holding samples that a
// different table would decode differently: 0x80 is the Euro sign in 1252
// and U+0080 in Latin-1, 0x93/0x94 are curly quotes in 1252 and C1 controls
// in Latin-1, 0xc0..0xc2 are Cyrillic capitals in 1251 and accented Latin
// letters in Latin-1. So the expected strings below only come out right
// when the page the CSET declares is the table actually used.
ByteVector windows1252Riff()
{
return riffWithInfo({{"IART", ByteVector("A \x80 B", 5)},
{"IPRD", ByteVector("Caf\xE9 \x80", 6)},
{"ICMT", ByteVector("\x93quoted\x94", 10)}},
1252);
}
// The same file, with the country, language and dialect of the CSET chunk
// filled in instead of left at their zero defaults.
ByteVector windows1252WithLanguageRiff()
{
return riffWithInfoCset({{"IART", ByteVector("A \x80 B", 5)},
{"IPRD", ByteVector("Caf\xE9 \x80", 6)},
{"ICMT", ByteVector("\x93quoted\x94", 10)}},
riffCsetPayloadWithLanguage(1252));
}
ByteVector windows1251Riff()
{
return riffWithInfo({{"IART", ByteVector("A \xC0 B", 5)},
{"IPRD", ByteVector("Caf\xE9", 4)},
{"ICMT", ByteVector("\xC0\xC1\xC2", 3)}},
1251);
}
// The same text in a file whose CSET chunk is truncated: its payload holds a
// single byte, where wCodePage alone needs 2, so it cannot hold the number the
// chunk exists to declare and the file is malformed.
ByteVector truncatedCsetRiff()
{
return riffWithInfoCset({{"IART", ByteVector("A \x80 B", 5)}},
ByteVector("\xA4", 1));
}
// The same text in a file whose CSET holds the code page and nothing else: the
// three WORD fields behind wCodePage are what a conforming writer fills in, and
// a file that leaves them out entirely still says all there is to say.
ByteVector codePageOnlyCsetRiff()
{
return riffWithInfoCset({{"IART", ByteVector("A \x80 B", 5)}},
ByteVector::fromShort(1252, false));
}
// A file whose CSET declares a code page this library has no table for, here a
// Japanese one. Its text is written in the RIFF default ISO 8859-1, so it reads
// back as itself: the number is a hint, and a hint that cannot be followed must
// not cost the tag its fields.
ByteVector unsupportedCsetRiff()
{
return riffWithInfo({{"IART", ByteVector("A \xAE B", 5)},
{"ICMT", ByteVector("Caf\xE9", 4)}},
932);
}
// A file whose INFO text is UTF-8 and which declares no charset at all. The
// specification cannot recognize this combination, because a file holding
// ISO 8859-1 and one holding UTF-8 look the same without a CSET chunk; only
// the reader's application knows which of them it wrote.
ByteVector utf8WithoutCsetRiff()
{
return riffWithInfo({{"IART", ByteVector("Caf\xC3\xA9", 5)}}, 0);
}
// Puts back the code page an INFO tag assumes when a file declares none, so
// that a test stating another one does not carry it into the next test.
class RestoreAssumedCodePage
{
public:
RestoreAssumedCodePage() :
m_previous(RIFF::Info::Tag::assumedCodePage())
{
}
~RestoreAssumedCodePage()
{
RIFF::Info::Tag::setAssumedCodePage(m_previous);
}
private:
const unsigned int m_previous;
};
// Validates the raw chunk layout of the RIFF file in \a data against the
// format itself instead of against TagLib, so that a file TagLib has just
// written is checked independently of how TagLib would read it back. Returns
// the data of the first top level chunk called \a name.
ByteVector checkedRawChunk(const ByteVector &data, const char *name)
{
const ByteVector wanted(name);
CPPUNIT_ASSERT(data.size() >= 12u);
CPPUNIT_ASSERT(data.startsWith("RIFF"));
// The size in the header covers everything after the size field itself,
// so the file must be exactly that long: no gap and no trailing junk.
const offset_t declaredEnd = static_cast<offset_t>(data.toUInt(4, false)) + 8;
CPPUNIT_ASSERT_EQUAL(declaredEnd, static_cast<offset_t>(data.size()));
ByteVector found;
offset_t offset = 12;
while(offset + 8 <= declaredEnd) {
const ByteVector chunkHeader = data.mid(static_cast<unsigned int>(offset), 8);
CPPUNIT_ASSERT_EQUAL(8u, chunkHeader.size());
const unsigned int size = chunkHeader.toUInt(4, false);
const ByteVector payload = data.mid(static_cast<unsigned int>(offset + 8), size);
CPPUNIT_ASSERT_EQUAL(size, payload.size());
if(found.isEmpty() && chunkHeader.startsWith(wanted))
found = payload;
// Chunks are padded to an even length, so an odd size is followed by a
// pad byte which is not counted in the size.
offset += 8 + size + (size % 2);
}
// The chunks must tile the file exactly, up to and including its end.
CPPUNIT_ASSERT_EQUAL(declaredEnd, offset);
return found;
}
// The CSET chunk of a saved file must declare UTF-8 and nothing else, in the
// full 8 byte layout of four WORD fields: wCodePage is UTF-8's 65001, and the
// country, language and dialect behind it are the zero that means the default.
void assertRawCsetDeclaresUtf8(const ByteVector &data)
{
const ByteVector cset = checkedRawChunk(data, "CSET");
CPPUNIT_ASSERT_EQUAL(8u, cset.size());
CPPUNIT_ASSERT_EQUAL(65001u,
static_cast<unsigned int>(
static_cast<unsigned short>(cset.toShort(0, false))));
CPPUNIT_ASSERT_EQUAL(ByteVector(6, '\0'), cset.mid(2, 6));
}
// Tags no single byte charset could have represented, as the strings and as
// the UTF-8 bytes they have to end up as on disk.
const String kNewTitle(L"Title \x4F60\x597D \x20AC");
const ByteVector kNewTitleUtf8("Title \xE4\xBD\xA0\xE5\xA5\xBD \xE2\x82\xAC", 16);
const String kNewComment(L"\x4F60\x597D");
const ByteVector kNewCommentUtf8("\xE4\xBD\xA0\xE5\xA5\xBD", 6);
}
#endif // TAGLIB_WITH_CHARSETTABLES
class TestWAV : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(TestWAV);
@@ -69,6 +319,30 @@ class TestWAV : public CppUnit::TestFixture
CPPUNIT_TEST(testBEXTTagWithOtherTags);
CPPUNIT_TEST(testiXMLTag);
CPPUNIT_TEST(testiXMLTagWithOtherTags);
#ifdef TAGLIB_WITH_CHARSETTABLES
CPPUNIT_TEST(testInfoLatin1Default);
CPPUNIT_TEST(testInfoWindows1252);
CPPUNIT_TEST(testInfoUtf8);
CPPUNIT_TEST(testInfoLatin1SaveAsUtf8);
CPPUNIT_TEST(testInfoWindows1252SaveAsUtf8);
CPPUNIT_TEST(testInfoUtf8SaveAsUtf8);
CPPUNIT_TEST(testInfoSaveWithoutFieldsWritesNoCset);
CPPUNIT_TEST(testInfoSaveWithoutModificationPreservesTag);
CPPUNIT_TEST(testInfoStripRemovesCset);
CPPUNIT_TEST(testInfoTruncatedCsetIsFormatError);
CPPUNIT_TEST(testInfoCsetWithLanguageFields);
CPPUNIT_TEST(testInfoCodePageOnlyCset);
CPPUNIT_TEST(testInfoUnsupportedCsetKeepsFields);
CPPUNIT_TEST(testCharsetDecodeStopsAtNul);
CPPUNIT_TEST(testCharsetEncodeIsUtf8Only);
CPPUNIT_TEST(testInfoUserHandlerBeatsCodePage);
CPPUNIT_TEST(testInfoCodePageReturnsWithoutUserHandler);
CPPUNIT_TEST(testInfoAssumedCodePageDefaultIsLatin1);
CPPUNIT_TEST(testInfoAssumedCodePageAcceptsUtf8);
CPPUNIT_TEST(testInfoAssumedCodePageWindows1252);
CPPUNIT_TEST(testInfoAssumedCodePageOnlyWhenUndeclared);
CPPUNIT_TEST(testInfoAssumedCodePageRejectsUnsupported);
#endif
CPPUNIT_TEST_SUITE_END();
public:
@@ -236,6 +510,565 @@ public:
}
}
#ifdef TAGLIB_WITH_CHARSETTABLES
void testInfoLatin1Default()
{
ByteVectorStream stream(latin1Riff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"A \xAE B"), f.InfoTag()->album());
CPPUNIT_ASSERT_EQUAL(String(L"A \xAE B"), f.InfoTag()->artist());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9"), f.InfoTag()->comment());
CPPUNIT_ASSERT_EQUAL(static_cast<unsigned int>(2024), f.InfoTag()->year());
}
void testInfoWindows1252()
{
ByteVectorStream stream(windows1252Riff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9 \x20AC"), f.InfoTag()->album());
CPPUNIT_ASSERT_EQUAL(String(L"A \x20AC B"), f.InfoTag()->artist());
CPPUNIT_ASSERT_EQUAL(String(L"\x201Cquoted\x201D"), f.InfoTag()->comment());
}
void testInfoUtf8()
{
ByteVectorStream stream(utf8Riff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9"), f.InfoTag()->album());
CPPUNIT_ASSERT_EQUAL(static_cast<unsigned int>(2024), f.InfoTag()->year());
}
// Each of the three files is put through the same two steps. First it must
// parse, with the charset its own CSET chunk declares. Then its tags are
// changed and written back, which rewrites the INFO chunk as UTF-8 and
// declares that in a CSET chunk, and the result must be read back correctly.
void testInfoLatin1SaveAsUtf8()
{
// Step 1: the file has no CSET chunk, so it is Latin-1 by default.
{
ByteVectorStream stream(latin1Riff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"A \xAE B"), f.InfoTag()->album());
CPPUNIT_ASSERT_EQUAL(String(L"A \xAE B"), f.InfoTag()->artist());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9"), f.InfoTag()->comment());
CPPUNIT_ASSERT_EQUAL(2024u, f.InfoTag()->year());
}
// Step 2: change the tags and write it out.
ByteVectorStream stream(latin1Riff());
{
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
f.InfoTag()->setTitle(kNewTitle);
f.InfoTag()->setComment(kNewComment);
CPPUNIT_ASSERT(f.save());
CPPUNIT_ASSERT(f.hasInfoTag());
}
const ByteVector saved = *stream.data();
// What was written must be a well formed RIFF file that declares UTF-8.
assertRawCsetDeclaresUtf8(saved);
const ByteVector list = checkedRawChunk(saved, "LIST");
CPPUNIT_ASSERT(list.startsWith("INFO"));
CPPUNIT_ASSERT(list.find(kNewTitleUtf8) >= 0);
CPPUNIT_ASSERT(list.find(kNewCommentUtf8) >= 0);
// Reading it back must give the changed tags, plus the fields that were left
// alone unchanged by the Latin-1 to UTF-8 rewrite.
{
ByteVectorStream savedStream(saved);
RIFF::WAV::File f(&savedStream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(kNewTitle, f.InfoTag()->title());
CPPUNIT_ASSERT_EQUAL(kNewComment, f.InfoTag()->comment());
CPPUNIT_ASSERT_EQUAL(String(L"A \xAE B"), f.InfoTag()->album());
CPPUNIT_ASSERT_EQUAL(String(L"A \xAE B"), f.InfoTag()->artist());
CPPUNIT_ASSERT_EQUAL(2024u, f.InfoTag()->year());
}
}
void testInfoWindows1252SaveAsUtf8()
{
// Step 1: the file declares code page 1252 in its CSET chunk.
{
ByteVectorStream stream(windows1252Riff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9 \x20AC"), f.InfoTag()->album());
}
// Step 2: change the tags and write it out.
ByteVectorStream stream(windows1252Riff());
{
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
f.InfoTag()->setTitle(kNewTitle);
f.InfoTag()->setComment(kNewComment);
CPPUNIT_ASSERT(f.save());
}
const ByteVector saved = *stream.data();
// It used to declare code page 1252 and now declares UTF-8.
assertRawCsetDeclaresUtf8(saved);
const ByteVector list = checkedRawChunk(saved, "LIST");
CPPUNIT_ASSERT(list.find(kNewTitleUtf8) >= 0);
CPPUNIT_ASSERT(list.find(kNewCommentUtf8) >= 0);
{
ByteVectorStream savedStream(saved);
RIFF::WAV::File f(&savedStream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(kNewTitle, f.InfoTag()->title());
CPPUNIT_ASSERT_EQUAL(kNewComment, f.InfoTag()->comment());
// The album was never touched and survived the rewrite unchanged.
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9 \x20AC"), f.InfoTag()->album());
}
}
void testInfoUtf8SaveAsUtf8()
{
// Step 1: the file already declares UTF-8, and its CSET chunk comes
// after the LIST chunk it applies to.
{
ByteVectorStream stream(utf8Riff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9"), f.InfoTag()->album());
CPPUNIT_ASSERT_EQUAL(2024u, f.InfoTag()->year());
}
// Step 2: change the tags and write it out.
ByteVectorStream stream(utf8Riff());
{
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
f.InfoTag()->setTitle(kNewTitle);
f.InfoTag()->setComment(kNewComment);
CPPUNIT_ASSERT(f.save());
}
const ByteVector saved = *stream.data();
assertRawCsetDeclaresUtf8(saved);
const ByteVector list = checkedRawChunk(saved, "LIST");
CPPUNIT_ASSERT(list.find(kNewTitleUtf8) >= 0);
CPPUNIT_ASSERT(list.find(kNewCommentUtf8) >= 0);
{
ByteVectorStream savedStream(saved);
RIFF::WAV::File f(&savedStream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(kNewTitle, f.InfoTag()->title());
CPPUNIT_ASSERT_EQUAL(kNewComment, f.InfoTag()->comment());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9"), f.InfoTag()->album());
CPPUNIT_ASSERT_EQUAL(2024u, f.InfoTag()->year());
}
}
void testInfoSaveWithoutFieldsWritesNoCset()
{
ByteVectorStream stream(latin1Riff());
{
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
for(const auto &[id, value] : f.InfoTag()->fieldListMap())
f.InfoTag()->removeField(id);
CPPUNIT_ASSERT(f.save());
// Nothing is left to describe, so no CSET chunk is written either.
CPPUNIT_ASSERT(!f.hasInfoTag());
}
const ByteVector saved = *stream.data();
CPPUNIT_ASSERT(checkedRawChunk(saved, "CSET").isEmpty());
CPPUNIT_ASSERT(checkedRawChunk(saved, "LIST").isEmpty());
}
// Saving a file that already has an INFO tag must keep that tag. It comes
// back in UTF-8 with a CSET chunk saying so, whatever it was read as.
void testInfoSaveWithoutModificationPreservesTag()
{
struct {
ByteVector file;
const wchar_t *album;
} cases[] = {
{latin1Riff(), L"A \xAE B"},
{windows1251Riff(), L"Caf\x0439"},
{windows1252Riff(), L"Caf\xE9 \x20AC"},
{utf8Riff(), L"Caf\xE9"}
};
for(const auto &c : cases) {
// Save without touching a single field.
ByteVectorStream stream(c.file);
{
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT(f.save());
}
// The tag survived, and the file now declares UTF-8 for it.
const ByteVector saved = *stream.data();
assertRawCsetDeclaresUtf8(saved);
CPPUNIT_ASSERT(checkedRawChunk(saved, "LIST").startsWith("INFO"));
ByteVectorStream savedStream(saved);
RIFF::WAV::File f(&savedStream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(c.album), f.InfoTag()->album());
}
}
// Stripping the INFO tag takes the CSET chunk with it. A CSET that outlived
// its LIST would describe a charset for text the file no longer has.
void testInfoStripRemovesCset()
{
const ByteVector files[] = {latin1Riff(), utf8Riff(), windows1251Riff(), windows1252Riff()};
for(const ByteVector &file : files) {
ByteVectorStream stream(file);
{
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
f.strip(RIFF::WAV::File::Info);
CPPUNIT_ASSERT(f.InfoTag()->isEmpty());
CPPUNIT_ASSERT(f.save(RIFF::WAV::File::Info));
CPPUNIT_ASSERT(!f.hasInfoTag());
}
const ByteVector saved = *stream.data();
CPPUNIT_ASSERT(checkedRawChunk(saved, "LIST").isEmpty());
CPPUNIT_ASSERT(checkedRawChunk(saved, "CSET").isEmpty());
}
}
// A CSET chunk too short to hold its own wCodePage is a file format error.
// Two bytes are the whole of wCodePage, so there is nothing shorter to read,
// and the chunk is ignored instead of guessed at: the INFO text keeps the RIFF
// default of Latin-1, under which 0x80 is U+0080 and not the Euro sign that the
// page the chunk tried to declare would have made of it.
void testInfoTruncatedCsetIsFormatError()
{
// Step 1: the chunk is ignored, and the text it failed to declare the
// charset of decodes as the Latin-1 default.
{
ByteVectorStream stream(truncatedCsetRiff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"A \x80 B"), f.InfoTag()->artist());
}
// Step 2: the malformed chunk never declared a charset, so saving replaces
// it with the one valid CSET chunk TagLib writes, instead of keeping it.
ByteVectorStream stream(truncatedCsetRiff());
{
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.save());
}
assertRawCsetDeclaresUtf8(*stream.data());
}
// The code page of a CSET chunk is wCodePage, one WORD of the four the layout
// has. The country, language and dialect behind it are not part of that number:
// a file that fills them in declares the page its first WORD holds, and has to
// be read with that page.
void testInfoCsetWithLanguageFields()
{
{
ByteVectorStream stream(windows1252WithLanguageRiff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"A \x20AC B"), f.InfoTag()->artist());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9 \x20AC"), f.InfoTag()->album());
CPPUNIT_ASSERT_EQUAL(String(L"\x201Cquoted\x201D"), f.InfoTag()->comment());
}
// Saving rewrites the chunk the way TagLib writes it: the code page, and the
// three fields behind it back at their zero defaults.
ByteVectorStream stream(windows1252WithLanguageRiff());
{
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.save());
}
assertRawCsetDeclaresUtf8(*stream.data());
}
// wCodePage on its own is all the chunk has to say. A file that leaves the
// three fields behind it out entirely still declares a charset.
void testInfoCodePageOnlyCset()
{
ByteVectorStream stream(codePageOnlyCsetRiff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"A \x20AC B"), f.InfoTag()->artist());
}
// A code page this library has no table for is a hint it cannot follow, not a
// reason to throw the tag away. The text keeps the RIFF default of ISO 8859-1,
// so every field is still read, and a save still writes it back.
void testInfoUnsupportedCsetKeepsFields()
{
{
ByteVectorStream stream(unsupportedCsetRiff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"A \xAE B"), f.InfoTag()->artist());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9"), f.InfoTag()->comment());
}
ByteVectorStream stream(unsupportedCsetRiff());
{
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
f.InfoTag()->setTitle(kNewTitle);
CPPUNIT_ASSERT(f.save());
}
// The field was read as ISO 8859-1 and is written back as UTF-8, so the
// character it held is in the saved LIST as its two UTF-8 bytes.
const ByteVector saved = *stream.data();
assertRawCsetDeclaresUtf8(saved);
CPPUNIT_ASSERT(checkedRawChunk(saved, "LIST").find(ByteVector("A \xC2\xAE B", 6)) >= 0);
ByteVectorStream savedStream(saved);
RIFF::WAV::File f(&savedStream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT_EQUAL(kNewTitle, f.InfoTag()->title());
CPPUNIT_ASSERT_EQUAL(String(L"A \xAE B"), f.InfoTag()->artist());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9"), f.InfoTag()->comment());
}
// Byte 0x00 is the one byte of a code page's repertoire that cannot be part
// of a field's text, because decoding stops at the first NUL: that is how a
// string ends. It has to end the string and not turn into a character.
void testCharsetDecodeStopsAtNul()
{
const ByteVector data("\x41\x00\x42", 3);
CPPUNIT_ASSERT_EQUAL(String(L"A"), Charset::decode(data, Charset::Type::Windows1251));
CPPUNIT_ASSERT_EQUAL(String(), Charset::decode(ByteVector("\x00\x41", 2), Charset::Type::Windows1251));
}
// A string is decoded into TagLib's internal UTF-16 representation, which is
// not a byte encoding at all, so there is no way to encode back into a legacy
// code page. That is precisely what keeps text a code page cannot hold from
// being quietly replaced.
void testCharsetEncodeIsUtf8Only()
{
CPPUNIT_ASSERT_EQUAL(Charset::Type::UTF8, Charset::typeForCodePage(65001).value());
// Encoding is UTF-8 and nothing else, so text no single byte code page could
// ever represent survives instead of turning into '?'.
CPPUNIT_ASSERT_EQUAL(ByteVector("\xE4\xBD\xA0", 3), Charset::encode(String(L"\x4F60"), Charset::Type::UTF8));
CPPUNIT_ASSERT_EQUAL(kNewTitleUtf8, Charset::encode(kNewTitle, Charset::Type::UTF8));
// And it is a plain UTF-8 round trip, so nothing is lost on the way through.
const String sample(L"\x0416\x0490\x05D0\x0625\x0E01\x20A7");
CPPUNIT_ASSERT_EQUAL(sample, Charset::decode(Charset::encode(sample, Charset::Type::UTF8), Charset::Type::UTF8));
}
// A handler the user installed is an explicit override, so it outranks the
// code page the file declares. Otherwise setStringHandler() would do nothing
// at all for every file TagLib itself writes, since those all carry a CSET.
class FixedStringHandler : public RIFF::Info::StringHandler
{
public:
String parse(const ByteVector &) const override { return String(L"from the handler"); }
ByteVector render(const String &) const override { return ByteVector("from the handler"); }
};
void testInfoUserHandlerBeatsCodePage()
{
class Restore
{
public:
~Restore() { RIFF::Info::Tag::setStringHandler(nullptr); }
} restore;
// The file declares 1252, and the default path reads it with that table.
{
ByteVectorStream stream(windows1252Riff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT(f.InfoTag()->artist() != String(L"from the handler"));
}
const FixedStringHandler handler;
RIFF::Info::Tag::setStringHandler(&handler);
// With a handler installed it is used even though the file declares 1252,
// on the way in ...
{
ByteVectorStream stream(windows1252Riff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"from the handler"), f.InfoTag()->artist());
}
// ... and on the way out, so the handler decides the bytes as well.
ByteVectorStream stream(windows1252Riff());
{
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"from the handler"), f.InfoTag()->artist());
CPPUNIT_ASSERT(f.save());
}
CPPUNIT_ASSERT(checkedRawChunk(*stream.data(), "LIST")
.find(ByteVector("from the handler")) >= 0);
}
// Removing the handler again must put the file's own code page back in charge.
void testInfoCodePageReturnsWithoutUserHandler()
{
ByteVectorStream stream(windows1251Riff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT(f.InfoTag()->artist() != String(L"from the handler"));
CPPUNIT_ASSERT_EQUAL(String(L"A \x0410 B"), f.InfoTag()->artist());
}
// The specification makes ISO 8859-1 the charset of a file that declares
// none, and that is what an untouched library assumes: the UTF-8 bytes of a
// file that never said what they are read as the Latin-1 characters they
// also happen to be.
void testInfoAssumedCodePageDefaultIsLatin1()
{
CPPUNIT_ASSERT_EQUAL(0u, RIFF::Info::Tag::assumedCodePage());
ByteVectorStream stream(utf8WithoutCsetRiff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xC3\xA9"), f.InfoTag()->artist());
}
// Software that writes UTF-8 without declaring it is what the option is for.
// Stated once, it is what every file that declares nothing is read with.
void testInfoAssumedCodePageAcceptsUtf8()
{
const RestoreAssumedCodePage restore;
RIFF::Info::Tag::setAssumedCodePage(65001);
CPPUNIT_ASSERT_EQUAL(65001u, RIFF::Info::Tag::assumedCodePage());
{
ByteVectorStream stream(utf8WithoutCsetRiff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9"), f.InfoTag()->artist());
}
// An INFO list read without its file, as in Tag(const ByteVector &data),
// declares nothing either, so it is read the same way.
const ByteVector list = ByteVector("INFO", 4) +
riffChunk("IART", ByteVector("Caf\xC3\xA9", 5) + ByteVector(1, '\0'));
RIFF::Info::Tag tag(list);
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9"), tag.artist());
// Stating 0 again is going back to what the specification says.
RIFF::Info::Tag::setAssumedCodePage(0);
CPPUNIT_ASSERT_EQUAL(0u, RIFF::Info::Tag::assumedCodePage());
ByteVectorStream stream(utf8WithoutCsetRiff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xC3\xA9"), f.InfoTag()->artist());
}
// The other real world answer is an ANSI code page: such a file keeps its
// typographic characters in 0x80-0x9f, where ISO 8859-1 has control
// characters that no amount of faithful decoding turns into text.
void testInfoAssumedCodePageWindows1252()
{
const RestoreAssumedCodePage restore;
RIFF::Info::Tag::setAssumedCodePage(1252);
CPPUNIT_ASSERT_EQUAL(1252u, RIFF::Info::Tag::assumedCodePage());
ByteVectorStream stream(riffWithInfo({{"IART", ByteVector("A \x80 B", 5)}}));
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT_EQUAL(String(L"A \x20AC B"), f.InfoTag()->artist());
}
// The option is for files that declare nothing. A file declaring a code page
// keeps it, whether or not this library has a table for it, and a CSET whose
// wCodePage is zero is the specification's way of declaring nothing, so that
// is where the assumption applies.
void testInfoAssumedCodePageOnlyWhenUndeclared()
{
const RestoreAssumedCodePage restore;
RIFF::Info::Tag::setAssumedCodePage(65001);
{
ByteVectorStream stream(windows1252Riff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9 \x20AC"), f.InfoTag()->album());
CPPUNIT_ASSERT_EQUAL(String(L"A \x20AC B"), f.InfoTag()->artist());
}
{
ByteVectorStream stream(unsupportedCsetRiff());
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT_EQUAL(String(L"A \xAE B"), f.InfoTag()->artist());
}
{
ByteVectorStream stream(riffWithInfoCset(
{{"IART", ByteVector("Caf\xC3\xA9", 5)}}, ByteVector(8, '\0')));
RIFF::WAV::File f(&stream);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT_EQUAL(String(L"Caf\xE9"), f.InfoTag()->artist());
}
}
// A code page this library cannot decode would quietly do nothing if it were
// accepted as the assumption, so it is refused and the previous one stays.
void testInfoAssumedCodePageRejectsUnsupported()
{
const RestoreAssumedCodePage restore;
RIFF::Info::Tag::setAssumedCodePage(1252);
CPPUNIT_ASSERT_EQUAL(1252u, RIFF::Info::Tag::assumedCodePage());
RIFF::Info::Tag::setAssumedCodePage(932);
CPPUNIT_ASSERT_EQUAL(1252u, RIFF::Info::Tag::assumedCodePage());
}
#endif // TAGLIB_WITH_CHARSETTABLES
void testStripTags()
{
ScopedFileCopy copy("empty", ".wav");
@@ -291,7 +1124,9 @@ public:
CPPUNIT_ASSERT_EQUAL(String("Title1"), f.InfoTag()->title());
f.save();
CPPUNIT_ASSERT_EQUAL(static_cast<offset_t>(15898), f.length());
// Saving the INFO tag also writes the 16 byte CSET chunk (8 byte header
// plus the 8 byte payload) which declares that the tag is UTF-8.
CPPUNIT_ASSERT_EQUAL(static_cast<offset_t>(15914), f.length());
CPPUNIT_ASSERT_EQUAL(static_cast<offset_t>(-1), f.find("Title2"));
}