Merge branch 'master' into merge-master-to-taglib2

# Conflicts:
#	taglib/mp4/mp4atom.cpp
#	taglib/toolkit/tutils.h
#	tests/test_apetag.cpp
This commit is contained in:
Tsuda Kageyu 2016-11-09 11:05:53 +09:00
commit bd5688ae5f
14 changed files with 190 additions and 59 deletions

2
NEWS
View File

@ -1,7 +1,9 @@
============================
* Added support for WinRT.
* Added support for classical music tags of iTunes 12.5.
* Dropped support for Windows 9x and NT 4.0 or older.
* Fixed reading MP4 atoms with zero length.
* Several smaller bug fixes and performance improvements.
TagLib 1.11.1 (Oct 24, 2016)

26
README.md Normal file
View File

@ -0,0 +1,26 @@
# TagLib
[![Build Status](https://travis-ci.org/taglib/taglib.svg?branch=master)](https://travis-ci.org/taglib/taglib)
### TagLib Audio Meta-Data Library
http://taglib.org/
TagLib is a library for reading and editing the meta-data of several
popular audio formats. Currently it supports both ID3v1 and [ID3v2][]
for MP3 files, [Ogg Vorbis][] comments and ID3 tags and Vorbis comments
in [FLAC][], MPC, Speex, WavPack, TrueAudio, WAV, AIFF, MP4 and ASF
files.
TagLib is distributed under the [GNU Lesser General Public License][]
(LGPL) and [Mozilla Public License][] (MPL). Essentially that means that
it may be used in proprietary applications, but if changes are made to
TagLib they must be contributed back to the project. Please review the
licenses if you are considering using TagLib in your project.
[ID3v2]: http://www.id3.org
[Ogg Vorbis]: http://vorbis.com/
[FLAC]: https://xiph.org/flac/
[GNU Lesser General Public License]: http://www.gnu.org/licenses/lgpl.html
[Mozilla Public License]: http://www.mozilla.org/MPL/MPL-1.1.html

View File

@ -48,23 +48,23 @@ using namespace APE;
namespace
{
bool isKeyValid(const char *key, size_t length)
const unsigned int MinKeyLength = 2;
const unsigned int MaxKeyLength = 255;
bool isKeyValid(const ByteVector &key)
{
const char *invalidKeys[] = { "ID3", "TAG", "OGGS", "MP+", 0 };
if(length < 2 || length > 255)
return false;
// only allow printable ASCII including space (32..126)
for(const char *p = key; p < key + length; ++p) {
const int c = static_cast<unsigned char>(*p);
for(ByteVector::ConstIterator it = key.begin(); it != key.end(); ++it) {
const int c = static_cast<unsigned char>(*it);
if(c < 32 || c > 126)
return false;
}
for(size_t i = 0; invalidKeys[i] != 0; ++i) {
if(Utils::equalsIgnoreCase(key, invalidKeys[i]))
if(String(key).upper() == invalidKeys[i])
return false;
}
@ -390,11 +390,10 @@ PropertyMap APE::Tag::setProperties(const PropertyMap &origProps)
bool APE::Tag::checkKey(const String &key)
{
if(!key.isLatin1())
if(key.size() < MinKeyLength || key.size() > MaxKeyLength)
return false;
const std::string data = key.to8Bit(false);
return isKeyValid(data.c_str(), data.size());
return isKeyValid(key.data(String::UTF8));
}
APE::Footer *APE::Tag::footer() const
@ -513,7 +512,10 @@ void APE::Tag::parse(const ByteVector &data)
const size_t keyLength = nullPos - pos - 8;
const size_t valLegnth = data.toUInt32LE(pos);
if(isKeyValid(&data[pos + 8], keyLength)){
if(keyLength >= MinKeyLength
&& keyLength <= MaxKeyLength
&& isKeyValid(data.mid(pos + 8, keyLength)))
{
APE::Item item;
item.parse(data.mid(pos));

View File

@ -53,8 +53,14 @@ MP4::Atom::Atom(File *file)
length = header.toUInt32BE(0);
if(length == 1)
if(length == 0) {
// The last atom which extends to the end of the file.
length = file->length() - offset;
}
else if(length == 1) {
// The atom has a 64-bit length.
length = file->readBlock(8).toInt64BE(0);
}
if(length < 8) {
debug("MP4: Invalid atom size");

View File

@ -72,10 +72,10 @@ MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms) :
parseIntPair(atom);
}
else if(atom->name == "cpil" || atom->name == "pgap" || atom->name == "pcst" ||
atom->name == "hdvd") {
atom->name == "hdvd" || atom->name == "shwm") {
parseBool(atom);
}
else if(atom->name == "tmpo") {
else if(atom->name == "tmpo" || atom->name == "\251mvi" || atom->name == "\251mvc") {
parseInt(atom);
}
else if(atom->name == "tvsn" || atom->name == "tves" || atom->name == "cnID" ||
@ -473,10 +473,11 @@ MP4::Tag::save()
else if(name == "disk") {
data.append(renderIntPairNoTrailing(name.data(String::Latin1), it->second));
}
else if(name == "cpil" || name == "pgap" || name == "pcst" || name == "hdvd") {
else if(name == "cpil" || name == "pgap" || name == "pcst" || name == "hdvd" ||
name == "shwm") {
data.append(renderBool(name.data(String::Latin1), it->second));
}
else if(name == "tmpo") {
else if(name == "tmpo" || name == "\251mvi" || name == "\251mvc") {
data.append(renderInt(name.data(String::Latin1), it->second));
}
else if(name == "tvsn" || name == "tves" || name == "cnID" ||
@ -924,6 +925,11 @@ namespace
{ "sonm", "TITLESORT" },
{ "soco", "COMPOSERSORT" },
{ "sosn", "SHOWSORT" },
{ "shwm", "SHOWWORKMOVEMENT" },
{ "\251wrk", "WORK" },
{ "\251mvn", "MOVEMENTNAME" },
{ "\251mvi", "MOVEMENTNUMBER" },
{ "\251mvc", "MOVEMENTCOUNT" },
{ "----:com.apple.iTunes:MusicBrainz Track Id", "MUSICBRAINZ_TRACKID" },
{ "----:com.apple.iTunes:MusicBrainz Artist Id", "MUSICBRAINZ_ARTISTID" },
{ "----:com.apple.iTunes:MusicBrainz Album Id", "MUSICBRAINZ_ALBUMID" },
@ -977,10 +983,10 @@ PropertyMap MP4::Tag::properties() const
}
props[key] = value;
}
else if(key == "BPM") {
else if(key == "BPM" || key == "MOVEMENTNUMBER" || key == "MOVEMENTCOUNT") {
props[key] = String::number(it->second.toInt());
}
else if(key == "COMPILATION") {
else if(key == "COMPILATION" || key == "SHOWWORKMOVEMENT") {
props[key] = String::number(it->second.toBool());
}
else {
@ -1032,11 +1038,11 @@ PropertyMap MP4::Tag::setProperties(const PropertyMap &props)
d->items[name] = MP4::Item(first, second);
}
}
else if(it->first == "BPM" && !it->second.isEmpty()) {
else if((it->first == "BPM" || it->first == "MOVEMENTNUMBER" || it->first == "MOVEMENTCOUNT") && !it->second.isEmpty()) {
int value = it->second.front().toInt();
d->items[name] = MP4::Item(value);
}
else if(it->first == "COMPILATION" && !it->second.isEmpty()) {
else if((it->first == "COMPILATION" || it->first == "SHOWWORKMOVEMENT") && !it->second.isEmpty()) {
bool value = (it->second.front().toInt() != 0);
d->items[name] = MP4::Item(value);
}

View File

@ -37,7 +37,11 @@ class ChapterFrame::ChapterFramePrivate
{
public:
ChapterFramePrivate() :
tagHeader(0)
tagHeader(0),
startTime(0),
endTime(0),
startOffset(0),
endOffset(0)
{
embeddedFrameList.setAutoDelete(true);
}

View File

@ -111,8 +111,8 @@ Frame *Frame::createTextualFrame(const String &key, const StringList &values) //
// check if the key is contained in the key<=>frameID mapping
ByteVector frameID = keyToFrameID(key);
if(!frameID.isEmpty()) {
// Apple proprietary WFED (Podcast URL) is in fact a text frame.
if(frameID[0] == 'T' || frameID == "WFED"){ // text frame
// Apple proprietary WFED (Podcast URL), MVNM (Movement Name), MVIN (Movement Number) are in fact text frames.
if(frameID[0] == 'T' || frameID == "WFED" || frameID == "MVNM" || frameID == "MVIN"){ // text frame
TextIdentificationFrame *frame = new TextIdentificationFrame(frameID, String::UTF8);
frame->setText(values);
return frame;
@ -386,6 +386,8 @@ namespace
{ "TDES", "PODCASTDESC" },
{ "TGID", "PODCASTID" },
{ "WFED", "PODCASTURL" },
{ "MVNM", "MOVEMENTNAME" },
{ "MVIN", "MOVEMENTNUMBER" },
};
const size_t frameTranslationSize = sizeof(frameTranslation) / sizeof(frameTranslation[0]);
@ -468,8 +470,8 @@ PropertyMap Frame::asProperties() const
// workaround until this function is virtual
if(id == "TXXX")
return dynamic_cast< const UserTextIdentificationFrame* >(this)->asProperties();
// Apple proprietary WFED (Podcast URL) is in fact a text frame.
else if(id[0] == 'T' || id == "WFED")
// Apple proprietary WFED (Podcast URL), MVNM (Movement Name), MVIN (Movement Number) are in fact text frames.
else if(id[0] == 'T' || id == "WFED" || id == "MVNM" || id == "MVIN")
return dynamic_cast< const TextIdentificationFrame* >(this)->asProperties();
else if(id == "WXXX")
return dynamic_cast< const UserUrlLinkFrame* >(this)->asProperties();

View File

@ -186,8 +186,8 @@ Frame *FrameFactory::createFrame(const ByteVector &origData, const Header *tagHe
// Text Identification (frames 4.2)
// Apple proprietary WFED (Podcast URL) is in fact a text frame.
if(frameID.startsWith("T") || frameID == "WFED") {
// Apple proprietary WFED (Podcast URL), MVNM (Movement Name), MVIN (Movement Number) are in fact text frames.
if(frameID.startsWith("T") || frameID == "WFED" || frameID == "MVNM" || frameID == "MVIN") {
TextIdentificationFrame *f = frameID != "TXXX"
? new TextIdentificationFrame(data, header)
@ -444,6 +444,8 @@ namespace
{ "TDS", "TDES" },
{ "TID", "TGID" },
{ "WFD", "WFED" },
{ "MVN", "MVNM" },
{ "MVI", "MVIN" },
};
const size_t frameConversion2Size = sizeof(frameConversion2) / sizeof(frameConversion2[0]);

View File

@ -349,18 +349,7 @@ void FileStream::seek(long long offset, Position p)
#ifdef _WIN32
DWORD whence;
switch(p) {
case Beginning:
whence = FILE_BEGIN;
break;
case Current:
whence = FILE_CURRENT;
break;
case End:
whence = FILE_END;
break;
default:
if(p != Beginning && p != Current && p != End) {
debug("FileStream::seek() -- Invalid Position value.");
return;
}
@ -368,7 +357,7 @@ void FileStream::seek(long long offset, Position p)
LARGE_INTEGER liOffset;
liOffset.QuadPart = offset;
if(!SetFilePointerEx(d->file, liOffset, NULL, whence)) {
if(!SetFilePointerEx(d->file, liOffset, NULL, static_cast<DWORD>(p))) {
debug("FileStream::seek() -- Failed to set the file pointer.");
}

View File

@ -222,25 +222,8 @@ namespace TagLib
}
/*!
* Returns whether the two strings s1 and s2 are equal, ignoring the case of
* the characters.
*
* We took the trouble to define this one here, since there are some
* incompatible variations of case insensitive strcmp().
* Returns the integer byte order of the system.
*/
inline bool equalsIgnoreCase(const char *s1, const char *s2)
{
while(*s1 != '\0' && *s2 != '\0' && ::tolower(*s1) == ::tolower(*s2)) {
s1++;
s2++;
}
return (*s1 == '\0' && *s2 == '\0');
}
/*!
* Returns the integer byte order of the system.
*/
inline ByteOrder systemByteOrder()
{
union {

Binary file not shown.

View File

@ -114,18 +114,21 @@ public:
PropertyMap properties;
properties["A"] = String("invalid key: one character");
properties["MP+"] = String("invalid key: forbidden string");
properties[L"\x1234\x3456"] = String("invalid key: Unicode");
properties["A B~C"] = String("valid key: space and tilde");
properties["ARTIST"] = String("valid key: normal one");
APE::Tag tag;
PropertyMap unsuccessful = tag.setProperties(properties);
CPPUNIT_ASSERT_EQUAL((size_t)2, unsuccessful.size());
CPPUNIT_ASSERT_EQUAL((size_t)3, unsuccessful.size());
CPPUNIT_ASSERT(unsuccessful.contains("A"));
CPPUNIT_ASSERT(unsuccessful.contains("MP+"));
CPPUNIT_ASSERT(unsuccessful.contains(L"\x1234\x3456"));
CPPUNIT_ASSERT_EQUAL((size_t)2, tag.itemListMap().size());
tag.addValue("VALID KEY", "Test Value 1");
tag.addValue("INVALID KEY \x7f", "Test Value 2");
tag.addValue(L"INVALID KEY \x1234\x3456", "Test Value 3");
CPPUNIT_ASSERT_EQUAL((size_t)3, tag.itemListMap().size());
}

View File

@ -111,6 +111,7 @@ class TestID3v2 : public CppUnit::TestFixture
CPPUNIT_TEST(testW000);
CPPUNIT_TEST(testPropertyInterface);
CPPUNIT_TEST(testPropertyInterface2);
CPPUNIT_TEST(testPropertiesMovement);
CPPUNIT_TEST(testDeleteFrame);
CPPUNIT_TEST(testSaveAndStripID3v1ShouldNotAddFrameFromID3v1ToId3v2);
CPPUNIT_TEST(testParseChapterFrame);
@ -932,6 +933,48 @@ public:
CPPUNIT_ASSERT_EQUAL(frame6, ID3v2::UniqueFileIdentifierFrame::findByOwner(&tag, "http://musicbrainz.org"));
}
void testPropertiesMovement()
{
ID3v2::Tag tag;
ID3v2::TextIdentificationFrame *frameMvnm = new ID3v2::TextIdentificationFrame("MVNM");
frameMvnm->setText("Movement Name");
tag.addFrame(frameMvnm);
ID3v2::TextIdentificationFrame *frameMvin = new ID3v2::TextIdentificationFrame("MVIN");
frameMvin->setText("2/3");
tag.addFrame(frameMvin);
PropertyMap properties = tag.properties();
CPPUNIT_ASSERT(properties.contains("MOVEMENTNAME"));
CPPUNIT_ASSERT(properties.contains("MOVEMENTNUMBER"));
CPPUNIT_ASSERT_EQUAL(String("Movement Name"), properties["MOVEMENTNAME"].front());
CPPUNIT_ASSERT_EQUAL(String("2/3"), properties["MOVEMENTNUMBER"].front());
ByteVector frameDataMvnm("MVNM"
"\x00\x00\x00\x0e"
"\x00\x00"
"\x00"
"Movement Name", 24);
CPPUNIT_ASSERT_EQUAL(frameDataMvnm, frameMvnm->render());
ByteVector frameDataMvin("MVIN"
"\x00\x00\x00\x04"
"\x00\x00"
"\x00"
"2/3", 14);
CPPUNIT_ASSERT_EQUAL(frameDataMvin, frameMvin->render());
ID3v2::Header header;
ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance();
ID3v2::TextIdentificationFrame *parsedFrameMvnm =
dynamic_cast<ID3v2::TextIdentificationFrame *>(factory->createFrame(frameDataMvnm, &header));
ID3v2::TextIdentificationFrame *parsedFrameMvin =
dynamic_cast<ID3v2::TextIdentificationFrame *>(factory->createFrame(frameDataMvin, &header));
CPPUNIT_ASSERT(parsedFrameMvnm);
CPPUNIT_ASSERT(parsedFrameMvin);
CPPUNIT_ASSERT_EQUAL(String("Movement Name"), parsedFrameMvnm->toString());
CPPUNIT_ASSERT_EQUAL(String("2/3"), parsedFrameMvin->toString());
}
void testDeleteFrame()
{
ScopedFileCopy copy("rare_frames", ".mp3");

View File

@ -55,8 +55,10 @@ class TestMP4 : public CppUnit::TestFixture
CPPUNIT_TEST(testCovrWrite);
CPPUNIT_TEST(testCovrRead2);
CPPUNIT_TEST(testProperties);
CPPUNIT_TEST(testPropertiesMovement);
CPPUNIT_TEST(testFuzzedFile);
CPPUNIT_TEST(testRepeatedSave);
CPPUNIT_TEST(testWithZeroLengthAtom);
CPPUNIT_TEST_SUITE_END();
public:
@ -378,6 +380,58 @@ public:
f.setProperties(tags);
}
void testPropertiesMovement()
{
MP4::File f(TEST_FILE_PATH_C("has-tags.m4a"));
PropertyMap tags = f.properties();
tags["WORK"] = StringList("Foo");
tags["MOVEMENTNAME"] = StringList("Bar");
tags["MOVEMENTNUMBER"] = StringList("2");
tags["MOVEMENTCOUNT"] = StringList("3");
tags["SHOWWORKMOVEMENT"] = StringList("1");
f.setProperties(tags);
tags = f.properties();
CPPUNIT_ASSERT(f.tag()->contains("\251wrk"));
CPPUNIT_ASSERT_EQUAL(StringList("Foo"), f.tag()->item("\251wrk").toStringList());
CPPUNIT_ASSERT_EQUAL(StringList("Foo"), tags["WORK"]);
CPPUNIT_ASSERT(f.tag()->contains("\251mvn"));
CPPUNIT_ASSERT_EQUAL(StringList("Bar"), f.tag()->item("\251mvn").toStringList());
CPPUNIT_ASSERT_EQUAL(StringList("Bar"), tags["MOVEMENTNAME"]);
CPPUNIT_ASSERT(f.tag()->contains("\251mvi"));
CPPUNIT_ASSERT_EQUAL(2, f.tag()->item("\251mvi").toInt());
CPPUNIT_ASSERT_EQUAL(StringList("2"), tags["MOVEMENTNUMBER"]);
CPPUNIT_ASSERT(f.tag()->contains("\251mvc"));
CPPUNIT_ASSERT_EQUAL(3, f.tag()->item("\251mvc").toInt());
CPPUNIT_ASSERT_EQUAL(StringList("3"), tags["MOVEMENTCOUNT"]);
CPPUNIT_ASSERT(f.tag()->contains("shwm"));
CPPUNIT_ASSERT_EQUAL(true, f.tag()->item("shwm").toBool());
CPPUNIT_ASSERT_EQUAL(StringList("1"), tags["SHOWWORKMOVEMENT"]);
tags["SHOWWORKMOVEMENT"] = StringList("0");
f.setProperties(tags);
tags = f.properties();
CPPUNIT_ASSERT(f.tag()->contains("shwm"));
CPPUNIT_ASSERT_EQUAL(false, f.tag()->item("shwm").toBool());
CPPUNIT_ASSERT_EQUAL(StringList("0"), tags["SHOWWORKMOVEMENT"]);
tags["WORK"] = StringList();
tags["MOVEMENTNAME"] = StringList();
tags["MOVEMENTNUMBER"] = StringList();
tags["MOVEMENTCOUNT"] = StringList();
tags["SHOWWORKMOVEMENT"] = StringList();
f.setProperties(tags);
}
void testFuzzedFile()
{
MP4::File f(TEST_FILE_PATH_C("infloop.m4a"));
@ -395,6 +449,15 @@ public:
CPPUNIT_ASSERT_EQUAL(2862LL, f.find("0123456789"));
CPPUNIT_ASSERT_EQUAL(-1LL, f.find("0123456789", 2863));
}
void testWithZeroLengthAtom()
{
MP4::File f(TEST_FILE_PATH_C("zero-length-mdat.m4a"));
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT_EQUAL(1115, f.audioProperties()->lengthInMilliseconds());
CPPUNIT_ASSERT_EQUAL(22050, f.audioProperties()->sampleRate());
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestMP4);