diff --git a/taglib/toolkit/tcharset.cpp b/taglib/toolkit/tcharset.cpp index 7228acc1..e5376ce7 100644 --- a/taglib/toolkit/tcharset.cpp +++ b/taglib/toolkit/tcharset.cpp @@ -73,9 +73,18 @@ ByteVector TagLib::Charset::encode(const String &s, Type type) if(type == Type::UTF8) return s.data(String::UTF8); - // Fast path for likely Latin-1 case - if(type == Type::Latin1) - return s.data(String::Latin1); + if(type == Type::Latin1) { + // String::data(Latin1) casts every code point to char, keeping only the + // low byte, so the '?' substitution the header promises is done here. + ByteVector data(s.size(), '?'); + char *p = data.data(); + for(const wchar_t c : s) { + if(c < 256) + *p = static_cast(c); + ++p; + } + return data; + } const auto *entry = detail::entryForCodePage(static_cast(type)); if(!entry) diff --git a/tests/test_id3v1.cpp b/tests/test_id3v1.cpp index e71d70bd..ee490f6b 100644 --- a/tests/test_id3v1.cpp +++ b/tests/test_id3v1.cpp @@ -28,6 +28,7 @@ #include "taglib_config.h" #include "tstring.h" +#include "tcharset.h" #include "mpegfile.h" #include "id3v1tag.h" #include "id3v1genres.h" @@ -64,6 +65,7 @@ class TestID3v1 : public CppUnit::TestFixture CPPUNIT_TEST(testStripWhiteSpace); CPPUNIT_TEST(testGenres); CPPUNIT_TEST(testRenamedGenres); + CPPUNIT_TEST(testRenderLatin1Substitutes); #ifdef TAGLIB_WITH_CHARSETTABLES CPPUNIT_TEST(testAssumedCodePageDefaultIsLatin1); CPPUNIT_TEST(testAssumedCodePage); @@ -116,6 +118,22 @@ public: CPPUNIT_ASSERT_EQUAL(129U, tag.genreNumber()); } + // Encoding into Latin1 has to substitute '?' for characters it cannot hold; + // truncating them to the low byte would write garbage into a file. + void testRenderLatin1Substitutes() + { + const ByteVector expected("A ? B", 5); + + CPPUNIT_ASSERT_EQUAL(expected, + Charset::encode(kCyrillicArtistText, Charset::Type::Latin1)); + + // A fresh tag has no code page stated and is Latin1, so this is what + // render() writes for the artist field at offset 3 + 30. + ID3v1::Tag tag; + tag.setArtist(kCyrillicArtistText); + CPPUNIT_ASSERT_EQUAL(expected, tag.render().mid(33, 5)); + } + #ifdef TAGLIB_WITH_CHARSETTABLES //! A temporary MP3 file which is automatically deleted when it goes out of scope. class ScopedMp3FileCopyWithId3v1Tag : public ScopedFileCopy