diff --git a/taglib/ogg/xiphcomment.cpp b/taglib/ogg/xiphcomment.cpp index 9de81788..d825333e 100644 --- a/taglib/ogg/xiphcomment.cpp +++ b/taglib/ogg/xiphcomment.cpp @@ -26,17 +26,22 @@ #include #include +#include #include #include using namespace TagLib; + +typedef List PictureList; + class Ogg::XiphComment::XiphCommentPrivate { public: FieldListMap fieldListMap; String vendorID; String commentField; + PictureList pictureList; }; //////////////////////////////////////////////////////////////////////////////// @@ -56,6 +61,7 @@ Ogg::XiphComment::XiphComment(const ByteVector &data) : TagLib::Tag() Ogg::XiphComment::~XiphComment() { + removePictures(); delete d; } @@ -188,6 +194,8 @@ TagLib::uint Ogg::XiphComment::fieldCount() const for(; it != d->fieldListMap.end(); ++it) count += (*it).second.size(); + count += d->pictureList.size(); + return count; } @@ -296,6 +304,36 @@ bool Ogg::XiphComment::contains(const String &key) const return !d->fieldListMap[key.upper()].isEmpty(); } +void Ogg::XiphComment::removePicture(FLAC::Picture *picture, bool del) +{ + PictureList::Iterator it = d->pictureList.find(picture); + if(it != d->pictureList.end()) + d->pictureList.erase(it); + + if(del) + delete picture; +} + +void Ogg::XiphComment::removePictures() +{ + PictureList newList; + for(uint i = 0; i < d->pictureList.size(); i++) { + delete d->pictureList[i]; + } + d->pictureList = newList; +} + +void Ogg::XiphComment::addPicture(FLAC::Picture * picture) +{ + d->pictureList.append(picture); +} + + +List Ogg::XiphComment::pictureList() +{ + return d->pictureList; +} + ByteVector Ogg::XiphComment::render() const { return render(true); @@ -342,6 +380,13 @@ ByteVector Ogg::XiphComment::render(bool addFramingBit) const } } + for(PictureList::ConstIterator it = d->pictureList.begin(); it != d->pictureList.end(); ++it) { + ByteVector picture = (*it)->render().toBase64(); + data.append(ByteVector::fromUInt(picture.size() + 23, false)); + data.append("METADATA_BLOCK_PICTURE="); + data.append(picture); + } + // Append the "framing bit". if(addFramingBit) @@ -384,20 +429,85 @@ void Ogg::XiphComment::parse(const ByteVector &data) const uint commentLength = data.toUInt(pos, false); pos += 4; - String comment = String(data.mid(pos, commentLength), String::UTF8); + ByteVector entry = data.mid(pos, commentLength); + pos += commentLength; - if(pos > data.size()) { + + // Don't go past data end + if (pos > data.size()) break; + + // Handle Pictures separately + if(entry.startsWith("METADATA_BLOCK_PICTURE=")) { + + // We need base64 encoded data including padding + if((entry.size() - 23) > 3 && ((entry.size() - 23) % 4) == 0) { + + // Decode base64 picture data + ByteVector picturedata = ByteVector::fromBase64(entry.mid(23)); + if (picturedata.size()) { + + // Decode Flac Picture + FLAC::Picture * picture = new FLAC::Picture(); + if(picture->parse(picturedata)) { + + d->pictureList.append(picture); + + // continue to next field + continue; + } + else { + delete picture; + debug("Failed to decode FlacPicture block"); + } + } + else { + debug("Failed to decode base64 encoded data"); + } + } + else { + debug("Invalid base64 encoded data"); + } } - int commentSeparatorPosition = comment.find("="); - if(commentSeparatorPosition == -1) { - break; + // Handle old picture standard + if (entry.startsWith("COVERART=")) { + + if((entry.size() - 9) > 3 && ((entry.size() - 9) % 4) == 0) { + + // Decode base64 picture data + ByteVector picturedata = ByteVector::fromBase64(entry.mid(9)); + if (picturedata.size()) { + + // Assume it's some type of image file + FLAC::Picture * picture = new FLAC::Picture(); + picture->setData(picturedata); + picture->setMimeType("image/"); + picture->setType(FLAC::Picture::Other); + d->pictureList.append(picture); + + // continue to next field + continue; + } + else { + debug("Failed to decode base64 encoded data"); + } + } + else { + debug("Invalid base64 encoded data"); + } } - String key = comment.substr(0, commentSeparatorPosition); - String value = comment.substr(commentSeparatorPosition + 1); + // Check for field separator + int sep = entry.find('='); + if (sep < 1) { + debug("Discarding invalid comment field."); + continue; + } + // Parse key and value + String key = String(entry.mid(0, sep), String::UTF8); + String value = String(entry.mid(sep + 1), String::UTF8); addField(key, value, false); } } diff --git a/taglib/ogg/xiphcomment.h b/taglib/ogg/xiphcomment.h index 8914ec07..7f6d4e89 100644 --- a/taglib/ogg/xiphcomment.h +++ b/taglib/ogg/xiphcomment.h @@ -32,6 +32,7 @@ #include "tstring.h" #include "tstringlist.h" #include "tbytevector.h" +#include "flacpicture.h" #include "taglib_export.h" namespace TagLib { @@ -229,6 +230,31 @@ namespace TagLib { */ ByteVector render(bool addFramingBit) const; + + /*! + * Returns a list of pictures attached to the xiph comment. + */ + List pictureList(); + + /*! + * Removes an picture. If \a del is true the picture's memory + * will be freed; if it is false, it must be deleted by the user. + */ + void removePicture(FLAC::Picture *picture, bool del = true); + + /*! + * Remove all pictures. + */ + void removePictures(); + + /*! + * Add a new picture to the comment block. The comment block takes ownership of the + * picture and will handle freeing its memory. + * + * \note The file will be saved only after calling save(). + */ + void addPicture(FLAC::Picture *picture); + protected: /*! * Reads the tag from the file specified in the constructor and fills the diff --git a/taglib/toolkit/tbytevector.cpp b/taglib/toolkit/tbytevector.cpp index 909a2aae..fbdc8b7c 100644 --- a/taglib/toolkit/tbytevector.cpp +++ b/taglib/toolkit/tbytevector.cpp @@ -871,6 +871,123 @@ ByteVector ByteVector::toHex() const return encoded; } +ByteVector ByteVector::fromBase64(const ByteVector & input) +{ + static const unsigned char base64[256] = { + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x3e,0x80,0x80,0x80,0x3f, + 0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x3b,0x3c,0x3d,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e, + 0x0f,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x80,0x80,0x80,0x80,0x80, + 0x80,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f,0x30,0x31,0x32,0x33,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80 + }; + + uint len = input.size(); + + ByteVector output(len); + + const unsigned char * src = (const unsigned char*) input.data(); + unsigned char * dst = (unsigned char*) output.data(); + + while(4 <= len) { + + // Check invalid character + if(base64[src[0]] == 0x80) + break; + + // Check invalid character + if(base64[src[1]] == 0x80) + break; + + // Decode first byte + *dst++ = ((base64[src[0]] << 2) & 0xfc) | ((base64[src[1]] >> 4) & 0x03); + + if(src[2] != '=') { + + // Check invalid character + if(base64[src[2]] == 0x80) + break; + + // Decode second byte + *dst++ = ((base64[src[1]] & 0x0f) << 4) | ((base64[src[2]] >> 2) & 0x0f); + + if(src[3] != '=') { + + // Check invalid character + if(base64[src[3]] == 0x80) + break; + + // Decode third byte + *dst++ = ((base64[src[2]] & 0x03) << 6) | (base64[src[3]] & 0x3f); + } + else { + // assume end of data + len -= 4; + break; + } + } + else { + // assume end of data + len -= 4; + break; + } + src += 4; + len -= 4; + } + + // Only return output if we processed all bytes + if(len == 0) { + output.resize(dst - (unsigned char*) output.data()); + return output; + } + return ByteVector(); +} + +ByteVector ByteVector::toBase64() const +{ + static const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + if (size()) { + uint len = size(); + ByteVector output(4 * ((len - 1) / 3 + 1)); // note roundup + + const char * src = data(); + char * dst = output.data(); + while(3 <= len) { + *dst++ = alphabet[(src[0] >> 2) & 0x3f]; + *dst++ = alphabet[((src[0] & 0x03) << 4) | ((src[1] >> 4) & 0x0f)]; + *dst++ = alphabet[((src[1] & 0x0f) << 2) | ((src[2] >> 6) & 0x03)]; + *dst++ = alphabet[src[2] & 0x3f]; + src += 3; + len -= 3; + } + if(len) { + *dst++ = alphabet[(src[0] >> 2) & 0x3f]; + if(len>1) { + *dst++ = alphabet[((src[0] & 0x03) << 4) | ((src[1] >> 4) & 0x0f)]; + *dst++ = alphabet[((src[1] & 0x0f) << 2)]; + } + else { + *dst++ = alphabet[(src[0] & 0x03) << 4]; + *dst++ = '='; + } + *dst++ = '='; + } + return output; + } + return ByteVector(); +} + + //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/toolkit/tbytevector.h b/taglib/toolkit/tbytevector.h index e04d338d..b2847c9f 100644 --- a/taglib/toolkit/tbytevector.h +++ b/taglib/toolkit/tbytevector.h @@ -594,6 +594,16 @@ namespace TagLib { */ ByteVector toHex() const; + /*! + * Returns a base64 encoded copy of the byte vector + */ + ByteVector toBase64() const; + + /*! + * Decodes the base64 encoded byte vector. + */ + static ByteVector fromBase64(const ByteVector &); + protected: /* * If this ByteVector is being shared via implicit sharing, do a deep copy diff --git a/tests/test_bytevector.cpp b/tests/test_bytevector.cpp index b9e31f87..2848c6ce 100644 --- a/tests/test_bytevector.cpp +++ b/tests/test_bytevector.cpp @@ -44,6 +44,7 @@ class TestByteVector : public CppUnit::TestFixture CPPUNIT_TEST(testIterator); CPPUNIT_TEST(testResize); CPPUNIT_TEST(testAppend); + CPPUNIT_TEST(testBase64); CPPUNIT_TEST_SUITE_END(); public: @@ -402,6 +403,111 @@ public: CPPUNIT_ASSERT_EQUAL(ByteVector("taglib"), v2); } + void testBase64() + { + ByteVector sempty; + ByteVector t0("a"); // test 1 byte + ByteVector t1("any carnal pleasure."); + ByteVector t2("any carnal pleasure"); + ByteVector t3("any carnal pleasur"); + ByteVector s0("a"); // test 1 byte + ByteVector s1("any carnal pleasure."); + ByteVector s2("any carnal pleasure"); + ByteVector s3("any carnal pleasur"); + ByteVector eempty; + ByteVector e0("YQ=="); + ByteVector e1("YW55IGNhcm5hbCBwbGVhc3VyZS4="); + ByteVector e2("YW55IGNhcm5hbCBwbGVhc3VyZQ=="); + ByteVector e3("YW55IGNhcm5hbCBwbGVhc3Vy"); + + // Encode + CPPUNIT_ASSERT_EQUAL(eempty, sempty.toBase64()); + CPPUNIT_ASSERT_EQUAL(e0, s0.toBase64()); + CPPUNIT_ASSERT_EQUAL(e1, s1.toBase64()); + CPPUNIT_ASSERT_EQUAL(e2, s2.toBase64()); + CPPUNIT_ASSERT_EQUAL(e3, s3.toBase64()); + + // Decode + CPPUNIT_ASSERT_EQUAL(sempty, eempty.toBase64()); + CPPUNIT_ASSERT_EQUAL(s0, ByteVector::fromBase64(e0)); + CPPUNIT_ASSERT_EQUAL(s1, ByteVector::fromBase64(e1)); + CPPUNIT_ASSERT_EQUAL(s2, ByteVector::fromBase64(e2)); + CPPUNIT_ASSERT_EQUAL(s3, ByteVector::fromBase64(e3)); + + CPPUNIT_ASSERT_EQUAL(t0, ByteVector::fromBase64(s0.toBase64())); + CPPUNIT_ASSERT_EQUAL(t1, ByteVector::fromBase64(s1.toBase64())); + CPPUNIT_ASSERT_EQUAL(t2, ByteVector::fromBase64(s2.toBase64())); + CPPUNIT_ASSERT_EQUAL(t3, ByteVector::fromBase64(s3.toBase64())); + + ByteVector all((uint)256); + + // in order + { + for(int i = 0; i < 256; i++){ + all[i]=(unsigned char)i; + } + ByteVector b64 = all.toBase64(); + ByteVector original = ByteVector::fromBase64(b64); + CPPUNIT_ASSERT_EQUAL(all,original); + } + + // reverse + { + for(int i = 0; i < 256; i++){ + all[i]=(unsigned char)255-i; + } + ByteVector b64 = all.toBase64(); + ByteVector original = ByteVector::fromBase64(b64); + CPPUNIT_ASSERT_EQUAL(all,original); + } + + // all zeroes + { + for(int i = 0; i < 256; i++){ + all[i]=0; + } + ByteVector b64 = all.toBase64(); + ByteVector original = ByteVector::fromBase64(b64); + CPPUNIT_ASSERT_EQUAL(all,original); + } + + // all ones + { + for(int i = 0; i < 256; i++){ + all[i]=(unsigned char)0xff; + } + ByteVector b64 = all.toBase64(); + ByteVector original = ByteVector::fromBase64(b64); + CPPUNIT_ASSERT_EQUAL(all,original); + } + + // Missing end bytes + { + // No missing bytes + ByteVector m0("YW55IGNhcm5hbCBwbGVhc3VyZQ=="); + CPPUNIT_ASSERT_EQUAL(s2,ByteVector::fromBase64(m0)); + + // 1 missing byte + ByteVector m1("YW55IGNhcm5hbCBwbGVhc3VyZQ="); + CPPUNIT_ASSERT_EQUAL(sempty,ByteVector::fromBase64(m1)); + + // 2 missing bytes + ByteVector m2("YW55IGNhcm5hbCBwbGVhc3VyZQ"); + CPPUNIT_ASSERT_EQUAL(sempty,ByteVector::fromBase64(m2)); + + // 3 missing bytes + ByteVector m3("YW55IGNhcm5hbCBwbGVhc3VyZ"); + CPPUNIT_ASSERT_EQUAL(sempty,ByteVector::fromBase64(m3)); + } + + // Grok invalid characters + { + ByteVector invalid("abd\x00\x01\x02\x03\x04"); + CPPUNIT_ASSERT_EQUAL(sempty,ByteVector::fromBase64(invalid)); + } + + } + }; CPPUNIT_TEST_SUITE_REGISTRATION(TestByteVector); diff --git a/tests/test_ogg.cpp b/tests/test_ogg.cpp index 45620d61..4adf0c50 100644 --- a/tests/test_ogg.cpp +++ b/tests/test_ogg.cpp @@ -22,6 +22,7 @@ class TestOGG : public CppUnit::TestFixture CPPUNIT_TEST(testDictInterface2); CPPUNIT_TEST(testAudioProperties); CPPUNIT_TEST(testPageChecksum); + CPPUNIT_TEST(testPicture); CPPUNIT_TEST_SUITE_END(); public: @@ -172,6 +173,39 @@ public: } + void testPicture() + { + ScopedFileCopy copy("empty", ".ogg"); + string newname = copy.fileName(); + + Vorbis::File *f = new Vorbis::File(newname.c_str()); + FLAC::Picture *newpic = new FLAC::Picture(); + newpic->setType(FLAC::Picture::BackCover); + newpic->setWidth(5); + newpic->setHeight(6); + newpic->setColorDepth(16); + newpic->setNumColors(7); + newpic->setMimeType("image/jpeg"); + newpic->setDescription("new image"); + newpic->setData("JPEG data"); + f->tag()->addPicture(newpic); + f->save(); + delete f; + + f = new Vorbis::File(newname.c_str()); + List lst = f->tag()->pictureList(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), lst.size()); + CPPUNIT_ASSERT_EQUAL(int(5), lst[0]->width()); + CPPUNIT_ASSERT_EQUAL(int(6), lst[0]->height()); + CPPUNIT_ASSERT_EQUAL(int(16), lst[0]->colorDepth()); + CPPUNIT_ASSERT_EQUAL(int(7), lst[0]->numColors()); + CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), lst[0]->mimeType()); + CPPUNIT_ASSERT_EQUAL(String("new image"), lst[0]->description()); + CPPUNIT_ASSERT_EQUAL(ByteVector("JPEG data"), lst[0]->data()); + + delete f; + } + }; CPPUNIT_TEST_SUITE_REGISTRATION(TestOGG);