diff --git a/taglib/CMakeLists.txt b/taglib/CMakeLists.txt index 793c45e6..da23f731 100644 --- a/taglib/CMakeLists.txt +++ b/taglib/CMakeLists.txt @@ -175,6 +175,7 @@ if(WITH_RIFF) set(tag_HDRS ${tag_HDRS} riff/rifffile.h riff/aiff/aifffile.h + riff/aiff/aiffnativetag.h riff/aiff/aiffproperties.h riff/wav/wavfile.h riff/wav/wavproperties.h @@ -412,6 +413,7 @@ if(WITH_RIFF) set(aiff_SRCS riff/aiff/aifffile.cpp + riff/aiff/aiffnativetag.cpp riff/aiff/aiffproperties.cpp ) diff --git a/taglib/riff/aiff/aifffile.cpp b/taglib/riff/aiff/aifffile.cpp index 25e8f856..b0dabfa0 100644 --- a/taglib/riff/aiff/aifffile.cpp +++ b/taglib/riff/aiff/aifffile.cpp @@ -25,6 +25,7 @@ #include "aifffile.h" +#include "aiffnativetag.h" #include "tdebug.h" #include "tpropertymap.h" #include "tagutils.h" @@ -36,7 +37,8 @@ class RIFF::AIFF::File::FilePrivate public: FilePrivate(const ID3v2::FrameFactory *frameFactory) : ID3v2FrameFactory(frameFactory ? frameFactory - : ID3v2::FrameFactory::instance()) + : ID3v2::FrameFactory::instance()), + nativeTag(std::make_unique()) { } @@ -45,8 +47,10 @@ public: const ID3v2::FrameFactory *ID3v2FrameFactory; std::unique_ptr properties; std::unique_ptr tag; + std::unique_ptr nativeTag; bool hasID3v2 { false }; + bool hasNative { false }; }; //////////////////////////////////////////////////////////////////////////////// @@ -90,6 +94,11 @@ ID3v2::Tag *RIFF::AIFF::File::tag() const return d->tag.get(); } +RIFF::AIFF::NativeTag *RIFF::AIFF::File::nativeTag() const +{ + return d->nativeTag.get(); +} + PropertyMap RIFF::AIFF::File::properties() const { return d->tag->properties(); @@ -138,6 +147,16 @@ bool RIFF::AIFF::File::save(ID3v2::Version version) d->hasID3v2 = true; } + d->hasNative = false; + for(const auto &[id, payload] : d->nativeTag->render()) { + if(payload.isEmpty()) + removeChunk(id); + else { + setChunkData(id, payload); + d->hasNative = true; + } + } + return true; } @@ -146,12 +165,19 @@ bool RIFF::AIFF::File::hasID3v2Tag() const return d->hasID3v2; } +bool RIFF::AIFF::File::hasNativeTag() const +{ + return d->hasNative; +} + //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void RIFF::AIFF::File::read(bool readProperties) { + NativeTag::FieldDataMap nativeFields; + for(unsigned int i = 0; i < chunkCount(); ++i) { if(const ByteVector name = chunkName(i); name == "ID3 " || name == "id3 ") { if(!d->tag) { @@ -163,11 +189,20 @@ void RIFF::AIFF::File::read(bool readProperties) debug("RIFF::AIFF::File::read() - Duplicate ID3v2 tag found."); } } + else if(NativeTag::isNativeField(name)) { + if(nativeFields.contains(name)) + debug("RIFF::AIFF::File::read() - Duplicate native field found."); + else + nativeFields[name] = chunkData(i); + } } if(!d->tag) d->tag = std::make_unique(nullptr, 0, d->ID3v2FrameFactory); + d->hasNative = !nativeFields.isEmpty(); + d->nativeTag = std::make_unique(nativeFields); + if(readProperties) d->properties = std::make_unique(this, Properties::Average); } diff --git a/taglib/riff/aiff/aifffile.h b/taglib/riff/aiff/aifffile.h index d50cf04e..23cce80c 100644 --- a/taglib/riff/aiff/aifffile.h +++ b/taglib/riff/aiff/aifffile.h @@ -40,11 +40,15 @@ namespace TagLib { * This is an implementation of AIFF metadata. * * This supports an ID3v2 tag as well as reading stream from the ID3 RIFF - * chunk as well as properties from the file. + * chunk as well as properties from the file, and the native text fields + * (NAME, AUTH, "(c) ", ANNO) an AIFF file carried before ID3v2 existed, + * which are reached through nativeTag(). */ namespace AIFF { + class NativeTag; + //! An implementation of TagLib::File with AIFF specific methods /*! @@ -127,6 +131,13 @@ namespace TagLib { /*! * Saves the file. + * + * The ID3v2 tag is written back to its chunk, and each + * native field that holds text is written to its own chunk, encoded + * with the charset the native tag was read with, while a native field + * that holds none loses its chunk. + * + * \see nativeTag() */ bool save() override; @@ -150,6 +161,29 @@ namespace TagLib { */ static bool isSupported(IOStream *stream); + /*! + * Returns the native text tag for this file: the NAME, AUTH, "(c) " + * and ANNO chunks an AIFF file held before ID3v2 existed. + * + * \note This always returns a valid pointer regardless of whether or + * not the file on disk has native text chunks. Use hasNativeTag() to + * check if the file on disk actually has them. + * + * \note This is a separate tag next to the ID3v2 one tag() returns; + * neither covers the other's fields. + * + * \see hasNativeTag() + */ + NativeTag *nativeTag() const; + + /*! + * Returns whether or not the file on disk actually has native text + * chunks. + * + * \see nativeTag() + */ + bool hasNativeTag() const; + private: void read(bool readProperties); diff --git a/taglib/riff/aiff/aiffnativetag.cpp b/taglib/riff/aiff/aiffnativetag.cpp new file mode 100644 index 00000000..74431ff8 --- /dev/null +++ b/taglib/riff/aiff/aiffnativetag.cpp @@ -0,0 +1,296 @@ +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "aiffnativetag.h" + +#include +#include +#include + +#include "tcharset.h" +#include "tbytevector.h" +#include "tdebug.h" +#include "tutils.h" +#include "tpropertymap.h" + +using namespace TagLib; +using namespace RIFF::AIFF; + +namespace +{ + //! The code page NativeTag::setAssumedCodePage() states. + //! + //! Used when a tag is read from a file, which declares none anywhere. + //! 0, the initial value, states none either and means Mac OS Roman, the + //! charset Macintosh software wrote these chunks in; any other number is + //! the one the caller stated itself. + unsigned int codePageAssumed = 0; + + //! The charset the assumed code page selects, decided when a tag is read. + Charset::Type assumedCharset() + { + // No code page stated is Mac OS Roman: these chunks were written by + // Macintosh software. + const unsigned int codePage = codePageAssumed != 0 ? codePageAssumed : 10000; + + if(const auto charset = Charset::typeForCodePage(codePage)) + return *charset; + + // setAssumedCodePage() only ever stores a page this build can convert, so + // the number reaching here with no table is the initial 0 of a build + // without the charset tables: its ASCII, the specification's own text, + // reads the same as ISO 8859-1. + return Charset::Type::Latin1; + } + + //! The four native text chunks, which is also the field list of the class. + const std::array nativeFieldIds { + ByteVector("NAME"), ByteVector("AUTH"), ByteVector("(c) "), ByteVector("ANNO") + }; + + //! Reads the text \a data holds. + String decodeField(const ByteVector &data, Charset::Type charset) + { + // The chunk is text from end to end, so nothing is skipped here; decode() + // itself stops at a NUL, which is how a writer that terminated its text + // like a C string comes out without one. + return Charset::decode(data, charset); + } + + const Map propertyKeyForId = { + {"NAME", "TITLE"}, + {"AUTH", "ARTIST"}, + {"(c) ", "COPYRIGHT"}, + {"ANNO", "COMMENT"} + }; +} // namespace + +class RIFF::AIFF::NativeTag::NativeTagPrivate +{ +public: + Map fieldListMap; + + //! The charset the tag's code page selects, resolved once when the tag is + //! read, and the one its text is written back with. + Charset::Type charset { Charset::Type::Latin1 }; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +RIFF::AIFF::NativeTag::NativeTag() : + NativeTag(FieldDataMap()) +{ +} + +RIFF::AIFF::NativeTag::NativeTag(const FieldDataMap &fields) : + d(std::make_unique()) +{ + d->charset = assumedCharset(); + + for(const auto &[id, payload] : fields) { + if(isNativeField(id)) { + if(String text = decodeField(payload, d->charset); !text.isEmpty()) + d->fieldListMap[id] = text; + } + } +} + +RIFF::AIFF::NativeTag::~NativeTag() = default; + +String RIFF::AIFF::NativeTag::title() const +{ + return fieldText("NAME"); +} + +String RIFF::AIFF::NativeTag::artist() const +{ + return fieldText("AUTH"); +} + +String RIFF::AIFF::NativeTag::album() const +{ + return String(); +} + +String RIFF::AIFF::NativeTag::comment() const +{ + return fieldText("ANNO"); +} + +String RIFF::AIFF::NativeTag::genre() const +{ + return String(); +} + +unsigned int RIFF::AIFF::NativeTag::year() const +{ + return 0; +} + +unsigned int RIFF::AIFF::NativeTag::track() const +{ + return 0; +} + +void RIFF::AIFF::NativeTag::setTitle(const String &s) +{ + setFieldText("NAME", s); +} + +void RIFF::AIFF::NativeTag::setArtist(const String &s) +{ + setFieldText("AUTH", s); +} + +void RIFF::AIFF::NativeTag::setAlbum(const String &) +{ +} + +void RIFF::AIFF::NativeTag::setComment(const String &s) +{ + setFieldText("ANNO", s); +} + +void RIFF::AIFF::NativeTag::setGenre(const String &) +{ +} + +void RIFF::AIFF::NativeTag::setYear(unsigned int) +{ +} + +void RIFF::AIFF::NativeTag::setTrack(unsigned int) +{ +} + +bool RIFF::AIFF::NativeTag::isEmpty() const +{ + return d->fieldListMap.isEmpty(); +} + +PropertyMap RIFF::AIFF::NativeTag::properties() const +{ + PropertyMap props; + for(const auto &[id, val] : std::as_const(d->fieldListMap)) { + if(const String key = propertyKeyForId.value(id); !key.isEmpty()) + props[key].append(val); + } + return props; +} + +void RIFF::AIFF::NativeTag::removeUnsupportedProperties(const StringList &props) +{ + // properties() reports unsupported data for none of its keys - all four + // chunks have one - so anything here is an id a caller passed itself. + for(const auto &id : props) + d->fieldListMap.erase(id.data(String::Latin1)); +} + +PropertyMap RIFF::AIFF::NativeTag::setProperties(const PropertyMap &props) +{ + static const Map idForPropertyKey = [] { + Map map; + for(const auto &[id, key] : propertyKeyForId) + map[key] = id; + return map; + }(); + + const PropertyMap origProps = properties(); + for(const auto &[key, _] : origProps) { + if(!props.contains(key) || props.value(key).isEmpty()) + d->fieldListMap.erase(idForPropertyKey.value(key)); + } + + PropertyMap ignoredProps; + for(const auto &[key, val] : props) { + if(ByteVector id = idForPropertyKey.value(key); + !id.isEmpty() && !val.isEmpty()) { + d->fieldListMap[id] = val.front(); + } + else { + ignoredProps.insert(key, val); + } + } + return ignoredProps; +} + +bool RIFF::AIFF::NativeTag::isNativeField(const ByteVector &id) +{ + return std::find(nativeFieldIds.begin(), nativeFieldIds.end(), id) != + nativeFieldIds.end(); +} + +String RIFF::AIFF::NativeTag::fieldText(const ByteVector &id) const +{ + if(d->fieldListMap.contains(id)) + return d->fieldListMap[id]; + return String(); +} + +void RIFF::AIFF::NativeTag::setFieldText(const ByteVector &id, const String &s) +{ + if(!isNativeField(id)) { + debug("RIFF::AIFF::NativeTag::setFieldText() - Not a native AIFF field, ignored."); + return; + } + + if(!s.isEmpty()) + d->fieldListMap[id] = s; + else + removeField(id); +} + +void RIFF::AIFF::NativeTag::removeField(const ByteVector &id) +{ + if(d->fieldListMap.contains(id)) + d->fieldListMap.erase(id); +} + +RIFF::AIFF::NativeTag::FieldDataMap RIFF::AIFF::NativeTag::render() const +{ + FieldDataMap chunks; + + for(const auto &id : nativeFieldIds) { + chunks[id] = d->fieldListMap.contains(id) + ? Charset::encode(d->fieldListMap[id], d->charset) + : ByteVector(); + } + + return chunks; +} + +unsigned int RIFF::AIFF::NativeTag::assumedCodePage() +{ + return codePageAssumed; +} + +void RIFF::AIFF::NativeTag::setAssumedCodePage(unsigned int codePage) +{ + if(codePage != 0 && !Charset::typeForCodePage(codePage)) { + debug(Utils::formatString( + "RIFF::AIFF::NativeTag::setAssumedCodePage() - Unsupported code page %u, ignored.", + codePage)); + return; + } + + codePageAssumed = codePage; +} diff --git a/taglib/riff/aiff/aiffnativetag.h b/taglib/riff/aiff/aiffnativetag.h new file mode 100644 index 00000000..2c0666f5 --- /dev/null +++ b/taglib/riff/aiff/aiffnativetag.h @@ -0,0 +1,193 @@ +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_AIFFNATIVETAG_H +#define TAGLIB_AIFFNATIVETAG_H + +#include "tmap.h" +#include "tstring.h" +#include "tbytevector.h" +#include "taglib_export.h" +#include "tag.h" + +namespace TagLib::RIFF::AIFF { + + //! The historic native text fields of an AIFF file. + + /*! + * Before ID3v2 existed, an AIFF file carried its metadata in four text + * chunks of its own: NAME (the title), AUTH (the artist), "(c) " (the + * copyright) and ANNO (the comment). The text fills the chunk from end + * to end. A file may hold any subset of them, and this class is the + * subset a file held, plus whatever the caller sets on it. + * + * The AIFF specification gives these chunks no charset: they are bytes + * on a Macintosh filesystem, and the software that wrote them wrote Mac + * OS Roman, which is therefore what this class assumes. Because the + * file cannot say otherwise, the charset is what both reading and + * writing use - the same situation as ID3v1, and the same solution: the + * code page is resolved once when the tag is read and kept. A caller + * whose files hold another charset says so with setAssumedCodePage() + * before reading them, and a tag read and written under one code page + * keeps its text byte for byte. + * + * album(), genre(), year() and track() have no native field behind them: + * the getters are empty and the setters do nothing. + */ + class TAGLIB_EXPORT NativeTag : public Tag + { + public: + /*! + * The raw chunks a NativeTag is read from: a chunk id in the four this + * class understands, mapped to the contents of that chunk, which is + * its text from end to end. Ids this class does not know are ignored. + */ + using FieldDataMap = Map; + + /*! + * Constructs an empty native tag, whose text is read and written in + * the code page assumedCodePage() names. + * + * \see assumedCodePage() + */ + NativeTag(); + + /*! + * Constructs a native tag from the chunks \a fields, decoded with the + * code page assumedCodePage() names. + * + * The code page is decided here and kept: render() writes the text + * back in the same charset it was read with, which is what a file + * that declares no charset anywhere needs. A tag whose files hold + * another charset is read by naming that charset before this is + * constructed, so that it is the one kept. + * + * \see assumedCodePage() + * \see setAssumedCodePage() + */ + explicit NativeTag(const FieldDataMap &fields); + + ~NativeTag() override; + + NativeTag(const NativeTag &) = delete; + NativeTag &operator=(const NativeTag &) = delete; + + // Reimplementations + + String title() const override; + String artist() const override; + String album() const override; + String comment() const override; + String genre() const override; + unsigned int year() const override; + unsigned int track() const override; + + void setTitle(const String &s) override; + void setArtist(const String &s) override; + void setAlbum(const String &s) override; + void setComment(const String &s) override; + void setGenre(const String &s) override; + void setYear(unsigned int i) override; + void setTrack(unsigned int i) override; + + bool isEmpty() const override; + + PropertyMap properties() const override; + void removeUnsupportedProperties(const StringList &props) override; + PropertyMap setProperties(const PropertyMap &props) override; + + /*! + * Whether \a id is one of the four native text chunks this class + * reads and writes: "NAME", "AUTH", "(c) " or "ANNO". + */ + static bool isNativeField(const ByteVector &id); + + /*! + * Returns the value of the field with the id \a id, which has to be + * one of "NAME", "AUTH", "(c) " or "ANNO"; any other id is not a + * native field of this class and yields an empty string. + */ + String fieldText(const ByteVector &id) const; + + /*! + * Sets the value of the field with the id \a id to \a s, where \a id + * has to be one of "NAME", "AUTH", "(c) " or "ANNO". An empty \a s + * removes the field, and with it its chunk. Another id is a chunk + * this class does not write and is ignored with a debug message. + */ + void setFieldText(const ByteVector &id, const String &s); + + /*! + * Removes the field with the id \a id. + */ + void removeField(const ByteVector &id); + + /*! + * Renders every native field back to binary data, ready to be written + * to disk: each of the four chunk ids mapped to the text of its + * chunk, encoded with the code page this tag was read with. An empty + * payload is a field that holds no text, whose chunk has to go. + * + * RIFF::AIFF::File::save() writes these payloads to the file, which is + * where the assumed charset reaches the disk: a tag read and written + * under one code page keeps its text byte for byte. + */ + FieldDataMap render() const; + + /*! + * Returns the code page assumed for a native tag read from a file, + * which is 0 - no code page stated, and therefore Mac OS Roman - until + * setAssumedCodePage() states another one. + * + * \see setAssumedCodePage() + */ + static unsigned int assumedCodePage(); + + /*! + * Sets the code page \a codePage assumed for a native tag read from a + * file: the text of every chunk is decoded with it, and a tag read + * under it writes its text back with it too. + * + * \a codePage is a Windows code page number, the same numbering a CSET + * chunk of a RIFF file uses: 65001 assumes UTF-8 and 28591 assumes + * ISO 8859-1, while 0, the value to go back to, states no code page + * and therefore assumes Mac OS Roman, which is what a Macintosh + * writer's bytes are. This is where the caller says which of them its + * files hold, because an AIFF file has no place to say it itself. + * + * \note This is a process wide setting and applies to the tags read + * after the call. The charset is decided when a tag is constructed, so + * a tag already read keeps the one it was read with. + * + * \note A code page this library cannot convert is refused: the call + * then has no effect and a debug message is emitted. + * + * \see assumedCodePage() + */ + static void setAssumedCodePage(unsigned int codePage); + + private: + class NativeTagPrivate; + TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE + std::unique_ptr d; + }; +} // namespace TagLib::RIFF::AIFF + +#endif diff --git a/taglib/toolkit/charsettables-generator.py b/taglib/toolkit/charsettables-generator.py index 82500aa2..3311e943 100755 --- a/taglib/toolkit/charsettables-generator.py +++ b/taglib/toolkit/charsettables-generator.py @@ -89,6 +89,10 @@ PAGES = [ (1147, "EBCDIC1147", "IBM297 EBCDIC France with Euro", "CP1147"), (1148, "EBCDIC1148", "IBM500 EBCDIC International with Euro", "CP1148"), (1149, "EBCDIC1149", "IBM871 EBCDIC Icelandic with Euro", "CP1149"), + + # Not a Windows ANSI page, but a Microsoft code page number all the same: + # 10000 is what Windows calls Mac OS Roman, the classic Macintosh charset. + (10000, "MacRoman", "Mac OS Roman", "MACINTOSH"), ] def via_iconv(name): diff --git a/taglib/toolkit/charsettables.cpp b/taglib/toolkit/charsettables.cpp index cfaf7f9c..720998f6 100644 --- a/taglib/toolkit/charsettables.cpp +++ b/taglib/toolkit/charsettables.cpp @@ -839,6 +839,26 @@ namespace TagLib::Charset::detail { 0x0111, 0x00F1, 0x0323, 0x00F3, 0x00F4, 0x01A1, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x01B0, 0x20AB, 0x00FF, }; + // 10000 Mac OS Roman + constexpr unsigned short MacRomanTable[256] = { + 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, + 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F, + 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F, + 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, + 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, + 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F, + 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, + 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F, + 0x00C4, 0x00C5, 0x00C7, 0x00C9, 0x00D1, 0x00D6, 0x00DC, 0x00E1, 0x00E0, 0x00E2, 0x00E4, 0x00E3, 0x00E5, 0x00E7, 0x00E9, 0x00E8, + 0x00EA, 0x00EB, 0x00ED, 0x00EC, 0x00EE, 0x00EF, 0x00F1, 0x00F3, 0x00F2, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x00F9, 0x00FB, 0x00FC, + 0x2020, 0x00B0, 0x00A2, 0x00A3, 0x00A7, 0x2022, 0x00B6, 0x00DF, 0x00AE, 0x00A9, 0x2122, 0x00B4, 0x00A8, 0x2260, 0x00C6, 0x00D8, + 0x221E, 0x00B1, 0x2264, 0x2265, 0x00A5, 0x00B5, 0x2202, 0x2211, 0x220F, 0x03C0, 0x222B, 0x00AA, 0x00BA, 0x03A9, 0x00E6, 0x00F8, + 0x00BF, 0x00A1, 0x00AC, 0x221A, 0x0192, 0x2248, 0x0394, 0x00AB, 0x00BB, 0x2026, 0x00A0, 0x00C0, 0x00C3, 0x00D5, 0x0152, 0x0153, + 0x2013, 0x2014, 0x201C, 0x201D, 0x2018, 0x2019, 0x00F7, 0x25CA, 0x00FF, 0x0178, 0x2044, 0x20AC, 0x2039, 0x203A, 0xFB01, 0xFB02, + 0x2021, 0x00B7, 0x201A, 0x201E, 0x2030, 0x00C2, 0x00CA, 0x00C1, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF, 0x00CC, 0x00D3, 0x00D4, + 0xE01E, 0x00D2, 0x00DA, 0x00DB, 0x00D9, 0x0131, 0x02C6, 0x02DC, 0x00AF, 0x02D8, 0x02D9, 0x02DA, 0x00B8, 0x02DD, 0x02DB, 0x02C7, + }; + // 20127 US-ASCII constexpr unsigned short ASCIITable[256] = { 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, @@ -1400,6 +1420,7 @@ namespace TagLib::Charset::detail { { Type::Windows1256, 1256, Windows1256Table }, { Type::Windows1257, 1257, Windows1257Table }, { Type::Windows1258, 1258, Windows1258Table }, + { Type::MacRoman, 10000, MacRomanTable }, { Type::ASCII, 20127, ASCIITable }, { Type::EBCDIC273, 20273, EBCDIC273Table }, { Type::EBCDIC277, 20277, EBCDIC277Table }, @@ -1430,7 +1451,7 @@ namespace TagLib::Charset::detail { // Every Type but UTF8 has exactly one row. static_assert( - std::size(codePageTable) == 66, + std::size(codePageTable) == 67, "the code page table and Charset::Type disagree"); // A Type and the CSET code page number name the same row, so the diff --git a/taglib/toolkit/tcharset.h b/taglib/toolkit/tcharset.h index 6eeab68e..fca5ff2e 100644 --- a/taglib/toolkit/tcharset.h +++ b/taglib/toolkit/tcharset.h @@ -57,73 +57,74 @@ namespace TagLib { */ enum class Type : unsigned int { - EBCDIC037 = 37, //!< IBM EBCDIC US-Canada - OEM437 = 437, //!< IBM437 OEM US - EBCDIC500 = 500, //!< IBM EBCDIC International - OEM708 = 708, //!< ASMO-708 Arabic - OEM737 = 737, //!< IBM737 OEM Greek - OEM775 = 775, //!< IBM775 OEM Baltic - OEM850 = 850, //!< IBM850 OEM Latin 1 - OEM852 = 852, //!< IBM852 OEM Latin 2 - OEM855 = 855, //!< IBM855 OEM Cyrillic - OEM857 = 857, //!< IBM857 OEM Turkish - OEM858 = 858, //!< IBM858 OEM Latin 1 with Euro - OEM860 = 860, //!< IBM860 OEM Portuguese - OEM861 = 861, //!< IBM861 OEM Icelandic - OEM862 = 862, //!< DOS-862 OEM Hebrew - OEM863 = 863, //!< IBM863 OEM French Canadian - OEM864 = 864, //!< IBM864 OEM Arabic - OEM865 = 865, //!< IBM865 OEM Nordic - OEM866 = 866, //!< IBM866 OEM Russian - OEM869 = 869, //!< IBM869 OEM Modern Greek - Thai874 = 874, //!< Thai (Windows-874) - EBCDIC1047 = 1047, //!< IBM1047 EBCDIC Latin 1/Open System - EBCDIC1140 = 1140, //!< IBM037 EBCDIC US-Canada with Euro - EBCDIC1141 = 1141, //!< IBM273 EBCDIC Germany with Euro - EBCDIC1142 = 1142, //!< IBM277 EBCDIC Denmark-Norway with Euro - EBCDIC1143 = 1143, //!< IBM278 EBCDIC Finland-Sweden with Euro - EBCDIC1144 = 1144, //!< IBM280 EBCDIC Italy with Euro - EBCDIC1145 = 1145, //!< IBM284 EBCDIC Latin America-Spain with Euro - EBCDIC1146 = 1146, //!< IBM285 EBCDIC UK with Euro - EBCDIC1147 = 1147, //!< IBM297 EBCDIC France with Euro - EBCDIC1148 = 1148, //!< IBM500 EBCDIC International with Euro - EBCDIC1149 = 1149, //!< IBM871 EBCDIC Icelandic with Euro - Windows1250= 1250, //!< Windows-1250 Central European - Windows1251= 1251, //!< Windows-1251 Cyrillic - Windows1252= 1252, //!< Windows-1252 Western European - Windows1253= 1253, //!< Windows-1253 Greek - Windows1254= 1254, //!< Windows-1254 Turkish - Windows1255= 1255, //!< Windows-1255 Hebrew - Windows1256= 1256, //!< Windows-1256 Arabic - Windows1257= 1257, //!< Windows-1257 Baltic - Windows1258= 1258, //!< Windows-1258 Vietnamese - ASCII = 20127, //!< US-ASCII - EBCDIC273 = 20273, //!< IBM273 EBCDIC Germany - EBCDIC277 = 20277, //!< IBM277 EBCDIC Denmark-Norway - EBCDIC278 = 20278, //!< IBM278 EBCDIC Finland-Sweden - EBCDIC280 = 20280, //!< IBM280 EBCDIC Italy - EBCDIC284 = 20284, //!< IBM284 EBCDIC Latin America-Spain - EBCDIC285 = 20285, //!< IBM285 EBCDIC United Kingdom - EBCDIC290 = 20290, //!< IBM290 EBCDIC Japanese Katakana Extended - EBCDIC297 = 20297, //!< IBM297 EBCDIC France - EBCDIC420 = 20420, //!< IBM420 EBCDIC Arabic - EBCDIC423 = 20423, //!< IBM423 EBCDIC Greek - KOI8R = 20866, //!< KOI8-R - EBCDIC871 = 20871, //!< IBM871 EBCDIC Icelandic - EBCDIC1025 = 21025, //!< IBM1025 EBCDIC Cyrillic Serbian-Bulgarian - KOI8U = 21866, //!< KOI8-U - Latin1 = 28591, //!< ISO 8859-1 - Latin2 = 28592, //!< ISO 8859-2 - Latin3 = 28593, //!< ISO 8859-3 - Latin4 = 28594, //!< ISO 8859-4 - Latin5 = 28595, //!< ISO 8859-5 - Latin6 = 28596, //!< ISO 8859-6 - Latin7 = 28597, //!< ISO 8859-7 - Latin8 = 28598, //!< ISO 8859-8 Hebrew (logical) - Latin9 = 28599, //!< ISO 8859-9 - Latin13 = 28603, //!< ISO 8859-13 - Latin15 = 28605, //!< ISO 8859-15 - UTF8 = 65001 //!< UTF-8 + EBCDIC037 = 37, //!< IBM EBCDIC US-Canada + OEM437 = 437, //!< IBM437 OEM US + EBCDIC500 = 500, //!< IBM EBCDIC International + OEM708 = 708, //!< ASMO-708 Arabic + OEM737 = 737, //!< IBM737 OEM Greek + OEM775 = 775, //!< IBM775 OEM Baltic + OEM850 = 850, //!< IBM850 OEM Latin 1 + OEM852 = 852, //!< IBM852 OEM Latin 2 + OEM855 = 855, //!< IBM855 OEM Cyrillic + OEM857 = 857, //!< IBM857 OEM Turkish + OEM858 = 858, //!< IBM858 OEM Latin 1 with Euro + OEM860 = 860, //!< IBM860 OEM Portuguese + OEM861 = 861, //!< IBM861 OEM Icelandic + OEM862 = 862, //!< DOS-862 OEM Hebrew + OEM863 = 863, //!< IBM863 OEM French Canadian + OEM864 = 864, //!< IBM864 OEM Arabic + OEM865 = 865, //!< IBM865 OEM Nordic + OEM866 = 866, //!< IBM866 OEM Russian + OEM869 = 869, //!< IBM869 OEM Modern Greek + Thai874 = 874, //!< Thai (Windows-874) + EBCDIC1047 = 1047, //!< IBM1047 EBCDIC Latin 1/Open System + EBCDIC1140 = 1140, //!< IBM037 EBCDIC US-Canada with Euro + EBCDIC1141 = 1141, //!< IBM273 EBCDIC Germany with Euro + EBCDIC1142 = 1142, //!< IBM277 EBCDIC Denmark-Norway with Euro + EBCDIC1143 = 1143, //!< IBM278 EBCDIC Finland-Sweden with Euro + EBCDIC1144 = 1144, //!< IBM280 EBCDIC Italy with Euro + EBCDIC1145 = 1145, //!< IBM284 EBCDIC Latin America-Spain with Euro + EBCDIC1146 = 1146, //!< IBM285 EBCDIC UK with Euro + EBCDIC1147 = 1147, //!< IBM297 EBCDIC France with Euro + EBCDIC1148 = 1148, //!< IBM500 EBCDIC International with Euro + EBCDIC1149 = 1149, //!< IBM871 EBCDIC Icelandic with Euro + Windows1250 = 1250, //!< Windows-1250 Central European + Windows1251 = 1251, //!< Windows-1251 Cyrillic + Windows1252 = 1252, //!< Windows-1252 Western European + Windows1253 = 1253, //!< Windows-1253 Greek + Windows1254 = 1254, //!< Windows-1254 Turkish + Windows1255 = 1255, //!< Windows-1255 Hebrew + Windows1256 = 1256, //!< Windows-1256 Arabic + Windows1257 = 1257, //!< Windows-1257 Baltic + Windows1258 = 1258, //!< Windows-1258 Vietnamese + MacRoman = 10000, //!< Mac OS Roman + ASCII = 20127, //!< US-ASCII + EBCDIC273 = 20273, //!< IBM273 EBCDIC Germany + EBCDIC277 = 20277, //!< IBM277 EBCDIC Denmark-Norway + EBCDIC278 = 20278, //!< IBM278 EBCDIC Finland-Sweden + EBCDIC280 = 20280, //!< IBM280 EBCDIC Italy + EBCDIC284 = 20284, //!< IBM284 EBCDIC Latin America-Spain + EBCDIC285 = 20285, //!< IBM285 EBCDIC United Kingdom + EBCDIC290 = 20290, //!< IBM290 EBCDIC Japanese Katakana Extended + EBCDIC297 = 20297, //!< IBM297 EBCDIC France + EBCDIC420 = 20420, //!< IBM420 EBCDIC Arabic + EBCDIC423 = 20423, //!< IBM423 EBCDIC Greek + KOI8R = 20866, //!< KOI8-R + EBCDIC871 = 20871, //!< IBM871 EBCDIC Icelandic + EBCDIC1025 = 21025, //!< IBM1025 EBCDIC Cyrillic Serbian-Bulgarian + KOI8U = 21866, //!< KOI8-U + Latin1 = 28591, //!< ISO 8859-1 + Latin2 = 28592, //!< ISO 8859-2 + Latin3 = 28593, //!< ISO 8859-3 + Latin4 = 28594, //!< ISO 8859-4 + Latin5 = 28595, //!< ISO 8859-5 + Latin6 = 28596, //!< ISO 8859-6 + Latin7 = 28597, //!< ISO 8859-7 + Latin8 = 28598, //!< ISO 8859-8 Hebrew (logical) + Latin9 = 28599, //!< ISO 8859-9 + Latin13 = 28603, //!< ISO 8859-13 + Latin15 = 28605, //!< ISO 8859-15 + UTF8 = 65001 //!< UTF-8 }; /*! diff --git a/tests/test_aiff.cpp b/tests/test_aiff.cpp index d719bb0f..ce02c802 100644 --- a/tests/test_aiff.cpp +++ b/tests/test_aiff.cpp @@ -28,13 +28,104 @@ #include "tbytevectorlist.h" #include "tag.h" +#include "taglib_config.h" #include "aifffile.h" +#include "aiffnativetag.h" +#include "tbytevectorstream.h" +#include "tpropertymap.h" #include #include "utils.h" using namespace std; using namespace TagLib; +#ifdef TAGLIB_WITH_CHARSETTABLES +namespace +{ + // Puts back the code page a native tag assumes, so that a test stating + // another one does not carry it into the next test. + class RestoreAssumedCodePage + { + public: + RestoreAssumedCodePage() : + m_previous(RIFF::AIFF::NativeTag::assumedCodePage()) + { + } + + ~RestoreAssumedCodePage() + { + RIFF::AIFF::NativeTag::setAssumedCodePage(m_previous); + } + + private: + const unsigned int m_previous; + }; + + // One chunk of an AIFF file: its id, its size, its payload, and the pad + // byte that brings an odd sized payload to an even length. + ByteVector aiffChunk(const char *id, const ByteVector &payload) + { + ByteVector chunk = ByteVector(id, 4) + ByteVector::fromUInt(payload.size()) + + payload; + if(payload.size() % 2) + chunk.append(static_cast(0)); + return chunk; + } + + // A minimal AIFF file: a COMM chunk describing one silent channel, and the + // NAME chunk holding \a name unless it is empty. + ByteVector aiffWithName(const ByteVector &name) + { + ByteVector body = aiffChunk("COMM", + ByteVector("\x00\x01\x00\x00\x00\x00\x00\x10" + "\x40\x0E\xAC\x44\x00\x00\x00\x00" + "\x00\x00", 18)); + if(!name.isEmpty()) + body.append(aiffChunk("NAME", name)); + + return ByteVector("FORM") + ByteVector::fromUInt(body.size() + 4) + + ByteVector("AIFF") + body; + } + + // The payload of the first chunk called \a name in the AIFF file \a data, + // checked against the format itself instead of against TagLib: the FORM + // header's size has to cover exactly the file, and the chunks have to tile + // it, so a file TagLib has just written is validated independently of how + // TagLib would read it back. + ByteVector checkedRawChunk(const ByteVector &data, const char *name) + { + const ByteVector wanted(name); + + CPPUNIT_ASSERT(data.size() >= 12u); + CPPUNIT_ASSERT(data.startsWith("FORM")); + + const offset_t declaredEnd = static_cast(data.toUInt(4u)) + 8; + CPPUNIT_ASSERT_EQUAL(declaredEnd, static_cast(data.size())); + + ByteVector found; + offset_t offset = 12; + while(offset + 8 <= declaredEnd) { + const ByteVector header = data.mid(static_cast(offset), 8); + CPPUNIT_ASSERT_EQUAL(8u, header.size()); + + const unsigned int size = header.toUInt(4u); + const ByteVector payload = data.mid(static_cast(offset + 8), size); + CPPUNIT_ASSERT_EQUAL(size, payload.size()); + + if(found.isEmpty() && header.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); + } + CPPUNIT_ASSERT_EQUAL(declaredEnd, offset); + + return found; + } +} // namespace +#endif // TAGLIB_WITH_CHARSETTABLES + class TestAIFF : public CppUnit::TestFixture { CPPUNIT_TEST_SUITE(TestAIFF); @@ -43,6 +134,13 @@ class TestAIFF : public CppUnit::TestFixture CPPUNIT_TEST(testSaveID3v2); CPPUNIT_TEST(testSaveID3v23); CPPUNIT_TEST(testDuplicateID3v2); + CPPUNIT_TEST(testNativeFields); + CPPUNIT_TEST(testSaveNativeFields); +#ifdef TAGLIB_WITH_CHARSETTABLES + CPPUNIT_TEST(testNativeMacRomanRead); + CPPUNIT_TEST(testNativeMacRomanWrite); + CPPUNIT_TEST(testNativeCodePageOverride); +#endif // TAGLIB_WITH_CHARSETTABLES CPPUNIT_TEST(testFuzzedFile1); CPPUNIT_TEST(testFuzzedFile2); CPPUNIT_TEST_SUITE_END(); @@ -146,6 +244,211 @@ public: CPPUNIT_ASSERT_EQUAL(static_cast(-1), f.find("Title2")); } + void testNativeFields() + { + // The four text chunks of a file that predates ID3v2. + RIFF::AIFF::File f(TEST_FILE_PATH_C("alaw.aifc")); + CPPUNIT_ASSERT(f.hasNativeTag()); + CPPUNIT_ASSERT(!f.nativeTag()->isEmpty()); + CPPUNIT_ASSERT_EQUAL(String("woodblock"), f.nativeTag()->title()); + CPPUNIT_ASSERT_EQUAL(String("Prosonus"), f.nativeTag()->artist()); + CPPUNIT_ASSERT_EQUAL(String("Copyright 1991, Prosonus"), + f.nativeTag()->fieldText("(c) ")); + CPPUNIT_ASSERT(f.nativeTag()->comment().isEmpty()); + + // The native tag has no field behind these. + CPPUNIT_ASSERT(f.nativeTag()->album().isEmpty()); + CPPUNIT_ASSERT_EQUAL(0u, f.nativeTag()->year()); + f.nativeTag()->setAlbum("Album"); + f.nativeTag()->setYear(2026); + CPPUNIT_ASSERT(f.nativeTag()->album().isEmpty()); + CPPUNIT_ASSERT_EQUAL(0u, f.nativeTag()->year()); + + // The four chunks with their property keys, and nothing unsupported. + const PropertyMap props = f.nativeTag()->properties(); + CPPUNIT_ASSERT_EQUAL(3u, props.size()); + CPPUNIT_ASSERT_EQUAL(String("woodblock"), props.value("TITLE").front()); + CPPUNIT_ASSERT_EQUAL(String("Prosonus"), props.value("ARTIST").front()); + CPPUNIT_ASSERT_EQUAL(String("Copyright 1991, Prosonus"), + props.value("COPYRIGHT").front()); + + // A file with no native chunks holds an empty tag and says so. + RIFF::AIFF::File empty(TEST_FILE_PATH_C("empty.aiff")); + CPPUNIT_ASSERT(!empty.hasNativeTag()); + CPPUNIT_ASSERT(empty.nativeTag()->isEmpty()); + + // Only the four text chunks are fields of this class: COMM is the audio + // layout, so neither reading nor writing it as a field does anything. + RIFF::AIFF::NativeTag::FieldDataMap fields; + fields["NAME"] = ByteVector("Title"); + fields["COMM"] = ByteVector("not a field"); + RIFF::AIFF::NativeTag tag(fields); + CPPUNIT_ASSERT_EQUAL(String("Title"), tag.title()); + CPPUNIT_ASSERT(tag.fieldText("COMM").isEmpty()); + tag.setFieldText("COMM", "ignored"); + CPPUNIT_ASSERT(tag.fieldText("COMM").isEmpty()); + CPPUNIT_ASSERT_EQUAL(1u, tag.properties().size()); + } + + void testSaveNativeFields() + { + ScopedFileCopy copy("empty", ".aiff"); + const string newname = copy.fileName(); + + { + RIFF::AIFF::File f(newname.c_str()); + CPPUNIT_ASSERT(!f.hasNativeTag()); + + f.nativeTag()->setTitle("TitleXXX"); + f.nativeTag()->setArtist("Artist XXX"); + f.nativeTag()->setFieldText("(c) ", "Copyright XXX"); + f.nativeTag()->setComment("Comment XXX"); + CPPUNIT_ASSERT(f.save()); + CPPUNIT_ASSERT(f.hasNativeTag()); + + // The native tag is its own tag: writing it writes no ID3v2 chunk. + CPPUNIT_ASSERT(!f.hasID3v2Tag()); + } + { + RIFF::AIFF::File f(newname.c_str()); + CPPUNIT_ASSERT(f.hasNativeTag()); + CPPUNIT_ASSERT_EQUAL(String("TitleXXX"), f.nativeTag()->title()); + CPPUNIT_ASSERT_EQUAL(String("Artist XXX"), f.nativeTag()->artist()); + CPPUNIT_ASSERT_EQUAL(String("Copyright XXX"), + f.nativeTag()->fieldText("(c) ")); + CPPUNIT_ASSERT_EQUAL(String("Comment XXX"), f.nativeTag()->comment()); + + // A field that loses its text loses its chunk; the others stay. + f.nativeTag()->setTitle(""); + CPPUNIT_ASSERT(f.save()); + } + { + RIFF::AIFF::File f(newname.c_str()); + CPPUNIT_ASSERT(f.hasNativeTag()); + CPPUNIT_ASSERT(f.nativeTag()->title().isEmpty()); + CPPUNIT_ASSERT_EQUAL(String("Artist XXX"), f.nativeTag()->artist()); + } + { + RIFF::AIFF::File f(newname.c_str()); + f.nativeTag()->setArtist(""); + f.nativeTag()->setFieldText("(c) ", ""); + f.nativeTag()->setComment(""); + CPPUNIT_ASSERT(f.save()); + CPPUNIT_ASSERT(!f.hasNativeTag()); + } + { + RIFF::AIFF::File f(newname.c_str()); + CPPUNIT_ASSERT(!f.hasNativeTag()); + CPPUNIT_ASSERT(f.nativeTag()->isEmpty()); + } + + // Saving a file whose native chunks one never touched keeps them. + ScopedFileCopy aifcCopy("alaw", ".aifc"); + { + RIFF::AIFF::File f(aifcCopy.fileName().c_str()); + CPPUNIT_ASSERT(f.save()); + } + { + RIFF::AIFF::File f(aifcCopy.fileName().c_str()); + CPPUNIT_ASSERT(f.hasNativeTag()); + CPPUNIT_ASSERT_EQUAL(String("woodblock"), f.nativeTag()->title()); + CPPUNIT_ASSERT_EQUAL(String("Prosonus"), f.nativeTag()->artist()); + } + } + +#ifdef TAGLIB_WITH_CHARSETTABLES + void testNativeMacRomanRead() + { + RestoreAssumedCodePage restore; + + // No code page stated is what a native tag assumes when the caller has + // said nothing, which is what this build starts with: the tag then reads + // Mac OS Roman. + CPPUNIT_ASSERT_EQUAL(0u, RIFF::AIFF::NativeTag::assumedCodePage()); + { + ByteVectorStream stream(aiffWithName(ByteVector("Caf\x8E", 4))); + RIFF::AIFF::File f(&stream, false); + CPPUNIT_ASSERT_EQUAL(String(L"Caf\u00E9"), f.nativeTag()->title()); + } + + // Stating the Mac OS Roman code page itself reads the very same byte. + RIFF::AIFF::NativeTag::setAssumedCodePage(10000); + { + ByteVectorStream stream(aiffWithName(ByteVector("Caf\x8E", 4))); + RIFF::AIFF::File f(&stream, false); + CPPUNIT_ASSERT_EQUAL(String(L"Caf\u00E9"), f.nativeTag()->title()); + } + + // The caller saying its files are ISO 8859-1 reads that byte as that + // charset's character instead. + RIFF::AIFF::NativeTag::setAssumedCodePage(28591); + { + ByteVectorStream stream(aiffWithName(ByteVector("Caf\x8E", 4))); + RIFF::AIFF::File f(&stream, false); + CPPUNIT_ASSERT_EQUAL(String(L"Caf\u008E"), f.nativeTag()->title()); + } + + // Going back to no code page stated is going back to Mac OS Roman. + RIFF::AIFF::NativeTag::setAssumedCodePage(0); + CPPUNIT_ASSERT_EQUAL(0u, RIFF::AIFF::NativeTag::assumedCodePage()); + { + ByteVectorStream stream(aiffWithName(ByteVector("Caf\x8E", 4))); + RIFF::AIFF::File f(&stream, false); + CPPUNIT_ASSERT_EQUAL(String(L"Caf\u00E9"), f.nativeTag()->title()); + } + } + + void testNativeMacRomanWrite() + { + RestoreAssumedCodePage restore; + + ByteVectorStream stream(aiffWithName(ByteVector())); + { + RIFF::AIFF::File f(&stream, false); + CPPUNIT_ASSERT(!f.hasNativeTag()); + + f.nativeTag()->setTitle(L"Caf\u00E9"); + CPPUNIT_ASSERT(f.save()); + CPPUNIT_ASSERT(f.hasNativeTag()); + } + + // The bytes the assumed charset writes are Mac OS Roman, not ISO 8859-1. + CPPUNIT_ASSERT_EQUAL(ByteVector("Caf\x8E", 4), + checkedRawChunk(*stream.data(), "NAME")); + + // A tag read and written under one code page keeps its text as it was. + ByteVectorStream reopened(*stream.data()); + RIFF::AIFF::File f(&reopened, false); + CPPUNIT_ASSERT_EQUAL(String(L"Caf\u00E9"), f.nativeTag()->title()); + } + + void testNativeCodePageOverride() + { + RestoreAssumedCodePage restore; + + // The caller says its files are UTF-8, so that is what both the writing + // and the reading of the tag does. + RIFF::AIFF::NativeTag::setAssumedCodePage(65001); + ByteVectorStream stream(aiffWithName(ByteVector())); + { + RIFF::AIFF::File f(&stream, false); + f.nativeTag()->setTitle(L"Caf\u00E9"); + CPPUNIT_ASSERT(f.save()); + } + CPPUNIT_ASSERT_EQUAL(ByteVector("Caf\xC3\xA9", 5), + checkedRawChunk(*stream.data(), "NAME")); + + ByteVectorStream reopened(*stream.data()); + RIFF::AIFF::File f(&reopened, false); + CPPUNIT_ASSERT_EQUAL(String(L"Caf\u00E9"), f.nativeTag()->title()); + + // A code page this library cannot convert is refused, so the one in + // effect stays. + RIFF::AIFF::NativeTag::setAssumedCodePage(999999); + CPPUNIT_ASSERT_EQUAL(65001u, RIFF::AIFF::NativeTag::assumedCodePage()); + } +#endif // TAGLIB_WITH_CHARSETTABLES + void testFuzzedFile1() { RIFF::AIFF::File f(TEST_FILE_PATH_C("segfault.aif"));