Begin implementing the rest of the File API

This adds:

- ID3v2 versions (v3, v4): working
- Ability to create missing tags (working for ID3v2, not for DIIN)
- Ability to strip tags (incomplete)
- Some tests for these features, and other basic functionality
  (incomplete)

There are still a number of major issues in the code, so I'm pushing
this out to a feature branch for now.  Specifically, the structure of
the code does not closely follow the format specification (as in other
implementations) and is not internally documented, so it is very
difficult to follow.

Specifically chunk type names should follow more closely those in the
spec.  Right now there is only one chunk type (whereas the spec has
several), and the notion / implementation of child chunks seems
incomplete.

Addtionally, before this is merged back to master a number of formatting
changes are needed:

- Spacing as done in the rest of TagLib
- Remove usage of NULL
- Usage of TagLib's containers instead of STL containers
- Use of std::array rather than C arrays where fixed length containers
  are needed
- More complete test coverage

CC #878
This commit is contained in:
Scott Wheeler 2019-09-12 06:46:23 +02:00
parent f1b40de66b
commit 088e063bbb
3 changed files with 291 additions and 116 deletions

View File

@ -35,16 +35,28 @@
using namespace TagLib;
struct Chunk64
{
ByteVector name;
unsigned long long offset;
unsigned long long size;
char padding;
};
namespace
{
struct Chunk64
{
ByteVector name;
unsigned long long offset;
unsigned long long size;
char padding;
};
typedef std::vector<Chunk64> ChunkList;
int chunkIndex(const ChunkList &chunks, const ByteVector &id)
{
for(int i = 0; i < chunks.size(); i++) {
if(chunks[i].name == id)
return i;
}
return -1;
}
enum {
ID3v2Index = 0,
DIINIndex = 1
@ -81,8 +93,8 @@ public:
ByteVector type;
unsigned long long size;
ByteVector format;
std::vector<Chunk64> chunks;
std::vector<Chunk64> childChunks[2];
ChunkList chunks;
ChunkList childChunks[2];
int childChunkIndex[2];
bool isID3InPropChunk; // Two possibilities can be found: ID3V2 chunk inside PROP chunk or at root level
int duplicateID3V2chunkIndex; // 2 ID3 chunks are present. This is then the index of the one in
@ -142,9 +154,9 @@ TagLib::Tag *DSDIFF::File::tag() const
return &d->tag;
}
ID3v2::Tag *DSDIFF::File::ID3v2Tag() const
ID3v2::Tag *DSDIFF::File::ID3v2Tag(bool create) const
{
return d->tag.access<ID3v2::Tag>(ID3v2Index, false);
return d->tag.access<ID3v2::Tag>(ID3v2Index, create);
}
bool DSDIFF::File::hasID3v2Tag() const
@ -152,9 +164,9 @@ bool DSDIFF::File::hasID3v2Tag() const
return d->hasID3v2;
}
DSDIFF::DIIN::Tag *DSDIFF::File::DIINTag() const
DSDIFF::DIIN::Tag *DSDIFF::File::DIINTag(bool create) const
{
return d->tag.access<DSDIFF::DIIN::Tag>(DIINIndex, false);
return d->tag.access<DSDIFF::DIIN::Tag>(DIINIndex, create);
}
bool DSDIFF::File::hasDIINTag() const
@ -190,6 +202,11 @@ DSDIFF::Properties *DSDIFF::File::audioProperties() const
}
bool DSDIFF::File::save()
{
return save(AllTags);
}
bool DSDIFF::File::save(TagTypes tags, StripTags strip, ID3v2::Version version)
{
if(readOnly()) {
debug("DSDIFF::File::save() -- File is read only.");
@ -201,35 +218,41 @@ bool DSDIFF::File::save()
return false;
}
if(strip == StripOthers || strip == StripAll)
File::strip(static_cast<TagTypes>(AllTags & ~tags));
// First: save ID3V2 chunk
ID3v2::Tag *id3v2Tag = d->tag.access<ID3v2::Tag>(ID3v2Index, false);
if(d->isID3InPropChunk) {
if(id3v2Tag != NULL && !id3v2Tag->isEmpty()) {
setChildChunkData(d->id3v2TagChunkID, id3v2Tag->render(), PROPChunk);
d->hasID3v2 = true;
if(tags & ID3v2 && id3v2Tag) {
if(d->isID3InPropChunk) {
if(id3v2Tag != NULL && !id3v2Tag->isEmpty()) {
setChildChunkData(d->id3v2TagChunkID, id3v2Tag->render(version), PROPChunk);
d->hasID3v2 = true;
}
else {
// Empty tag: remove it
setChildChunkData(d->id3v2TagChunkID, ByteVector(), PROPChunk);
d->hasID3v2 = false;
}
}
else {
// Empty tag: remove it
setChildChunkData(d->id3v2TagChunkID, ByteVector(), PROPChunk);
d->hasID3v2 = false;
}
}
else {
if(id3v2Tag != NULL && !id3v2Tag->isEmpty()) {
setRootChunkData(d->id3v2TagChunkID, id3v2Tag->render());
d->hasID3v2 = true;
}
else {
// Empty tag: remove it
setRootChunkData(d->id3v2TagChunkID, ByteVector());
d->hasID3v2 = false;
if(id3v2Tag != NULL && !id3v2Tag->isEmpty()) {
setRootChunkData(d->id3v2TagChunkID, id3v2Tag->render(version));
d->hasID3v2 = true;
}
else {
// Empty tag: remove it
setRootChunkData(d->id3v2TagChunkID, ByteVector());
d->hasID3v2 = false;
}
}
}
// Second: save the DIIN chunk
if(d->hasDiin) {
DSDIFF::DIIN::Tag *diinTag = d->tag.access<DSDIFF::DIIN::Tag>(DIINIndex, false);
DSDIFF::DIIN::Tag *diinTag = d->tag.access<DSDIFF::DIIN::Tag>(DIINIndex, false);
if(tags & DIIN && diinTag) {
if(!diinTag->title().isEmpty()) {
ByteVector diinTitle;
diinTitle.append(ByteVector::fromUInt(diinTag->title().size(), d->endianness == BigEndian));
@ -258,46 +281,77 @@ bool DSDIFF::File::save()
return true;
}
void DSDIFF::File::strip(TagTypes tags)
{
if(tags & ID3v2) {
removeRootChunk("ID3 ");
removeRootChunk("id3 ");
d->hasID3v2 = false;
d->tag.set(ID3v2Index, new ID3v2::Tag);
/// TODO: needs to also account for ID3v2 tags under the PROP chunk
}
if(tags & DIIN) {
removeRootChunk("DITI");
removeRootChunk("DIAR");
d->hasDiin = false;
d->tag.set(DIINIndex, new DIIN::Tag);
}
}
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////
void DSDIFF::File::setRootChunkData(unsigned int i, const ByteVector &data)
void DSDIFF::File::removeRootChunk(unsigned int i)
{
if(data.isEmpty()) {
// Null data: remove chunk
// Update global size
unsigned long long removedChunkTotalSize = d->chunks[i].size + d->chunks[i].padding + 12;
d->size -= removedChunkTotalSize;
unsigned long long chunkSize = d->chunks[i].size + d->chunks[i].padding + 12;
d->size -= chunkSize;
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
removeBlock(d->chunks[i].offset - 12, removedChunkTotalSize);
removeBlock(d->chunks[i].offset - 12, chunkSize);
// Update the internal offsets
for(unsigned long r = i + 1; r < d->chunks.size(); r++)
d->chunks[r].offset = d->chunks[r - 1].offset + 12
+ d->chunks[r - 1].size + d->chunks[r - 1].padding;
d->chunks.erase(d->chunks.begin() + i);
}
void DSDIFF::File::removeRootChunk(const ByteVector &id)
{
int i = chunkIndex(d->chunks, id);
if(i >= 0)
removeRootChunk(i);
}
void DSDIFF::File::setRootChunkData(unsigned int i, const ByteVector &data)
{
if(data.isEmpty()) {
removeRootChunk(i);
return;
}
else {
// Non null data: update chunk
// First we update the global size
d->size += ((data.size() + 1) & ~1) - (d->chunks[i].size + d->chunks[i].padding);
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
// Now update the specific chunk
writeChunk(d->chunks[i].name,
data,
d->chunks[i].offset - 12,
d->chunks[i].size + d->chunks[i].padding + 12);
// Non null data: update chunk
// First we update the global size
d->size += ((data.size() + 1) & ~1) - (d->chunks[i].size + d->chunks[i].padding);
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
d->chunks[i].size = data.size();
d->chunks[i].padding = (data.size() & 0x01) ? 1 : 0;
// Now update the specific chunk
writeChunk(d->chunks[i].name,
data,
d->chunks[i].offset - 12,
d->chunks[i].size + d->chunks[i].padding + 12);
// Finally update the internal offsets
updateRootChunksStructure(i + 1);
}
d->chunks[i].size = data.size();
d->chunks[i].padding = (data.size() & 0x01) ? 1 : 0;
// Finally update the internal offsets
updateRootChunksStructure(i + 1);
}
void DSDIFF::File::setRootChunkData(const ByteVector &name, const ByteVector &data)
@ -307,15 +361,15 @@ void DSDIFF::File::setRootChunkData(const ByteVector &name, const ByteVector &da
return;
}
for(unsigned int i = 0; i < d->chunks.size(); i++) {
if(d->chunks[i].name == name) {
setRootChunkData(i, data);
return;
}
int i = chunkIndex(d->chunks, name);
if(i >= 0) {
setRootChunkData(i, data);
return;
}
// Couldn't find an existing chunk, so let's create a new one.
unsigned int i = d->chunks.size() - 1;
i = d->chunks.size() - 1;
unsigned long offset = d->chunks[i].offset + d->chunks[i].size + d->chunks[i].padding;
// First we update the global size
@ -338,15 +392,12 @@ void DSDIFF::File::setRootChunkData(const ByteVector &name, const ByteVector &da
d->chunks.push_back(chunk);
}
void DSDIFF::File::setChildChunkData(unsigned int i,
const ByteVector &data,
unsigned int childChunkNum)
void DSDIFF::File::removeChildChunk(unsigned int i, unsigned int childChunkNum)
{
std::vector<Chunk64> &childChunks = d->childChunks[childChunkNum];
ChunkList &childChunks = d->childChunks[childChunkNum];
if(data.isEmpty()) {
// Null data: remove chunk
// Update global size
unsigned long long removedChunkTotalSize = childChunks[i].size + childChunks[i].padding + 12;
d->size -= removedChunkTotalSize;
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
@ -374,44 +425,54 @@ void DSDIFF::File::setChildChunkData(unsigned int i,
+ d->chunks[i - 1].size + d->chunks[i - 1].padding;
childChunks.erase(childChunks.begin() + i);
}
void DSDIFF::File::setChildChunkData(unsigned int i,
const ByteVector &data,
unsigned int childChunkNum)
{
ChunkList &childChunks = d->childChunks[childChunkNum];
if(data.isEmpty()) {
removeChildChunk(i, childChunkNum);
return;
}
else {
// Non null data: update chunk
// First we update the global size
d->size += ((data.size() + 1) & ~1) - (childChunks[i].size + childChunks[i].padding);
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
// And the PROP chunk size
d->chunks[d->childChunkIndex[childChunkNum]].size += ((data.size() + 1) & ~1)
- (childChunks[i].size + childChunks[i].padding);
insert(ByteVector::fromLongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
d->endianness == BigEndian),
d->chunks[d->childChunkIndex[childChunkNum]].offset - 8, 8);
// Now update the specific chunk
writeChunk(childChunks[i].name,
data,
childChunks[i].offset - 12,
childChunks[i].size + childChunks[i].padding + 12);
// Non null data: update chunk
// First we update the global size
d->size += ((data.size() + 1) & ~1) - (childChunks[i].size + childChunks[i].padding);
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
// And the PROP chunk size
d->chunks[d->childChunkIndex[childChunkNum]].size += ((data.size() + 1) & ~1)
- (childChunks[i].size + childChunks[i].padding);
insert(ByteVector::fromLongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
d->endianness == BigEndian),
d->chunks[d->childChunkIndex[childChunkNum]].offset - 8, 8);
childChunks[i].size = data.size();
childChunks[i].padding = (data.size() & 0x01) ? 1 : 0;
// Now update the specific chunk
writeChunk(childChunks[i].name,
data,
childChunks[i].offset - 12,
childChunks[i].size + childChunks[i].padding + 12);
// Now update the internal offsets
// For child Chunks
for(i++; i < childChunks.size(); i++)
childChunks[i].offset = childChunks[i - 1].offset + 12
+ childChunks[i - 1].size + childChunks[i - 1].padding;
childChunks[i].size = data.size();
childChunks[i].padding = (data.size() & 0x01) ? 1 : 0;
// And for root chunks
updateRootChunksStructure(d->childChunkIndex[childChunkNum] + 1);
}
// Now update the internal offsets
// For child Chunks
for(i++; i < childChunks.size(); i++)
childChunks[i].offset = childChunks[i - 1].offset + 12
+ childChunks[i - 1].size + childChunks[i - 1].padding;
// And for root chunks
updateRootChunksStructure(d->childChunkIndex[childChunkNum] + 1);
}
void DSDIFF::File::setChildChunkData(const ByteVector &name,
const ByteVector &data,
unsigned int childChunkNum)
{
std::vector<Chunk64> &childChunks = d->childChunks[childChunkNum];
ChunkList &childChunks = d->childChunks[childChunkNum];
if(childChunks.size() == 0) {
debug("DSDIFF::File::setPropChunkData - No valid chunks found.");
@ -485,7 +546,7 @@ void DSDIFF::File::updateRootChunksStructure(unsigned int startingChunk)
// Update childchunks structure as well
if(d->childChunkIndex[PROPChunk] >= static_cast<int>(startingChunk)) {
std::vector<Chunk64> &childChunksToUpdate = d->childChunks[PROPChunk];
ChunkList &childChunksToUpdate = d->childChunks[PROPChunk];
if(childChunksToUpdate.size() > 0) {
childChunksToUpdate[0].offset = d->chunks[d->childChunkIndex[PROPChunk]].offset + 12;
for(unsigned int i = 1; i < childChunksToUpdate.size(); i++)
@ -494,7 +555,7 @@ void DSDIFF::File::updateRootChunksStructure(unsigned int startingChunk)
}
}
if(d->childChunkIndex[DIINChunk] >= static_cast<int>(startingChunk)) {
std::vector<Chunk64> &childChunksToUpdate = d->childChunks[DIINChunk];
ChunkList &childChunksToUpdate = d->childChunks[DIINChunk];
if(childChunksToUpdate.size() > 0) {
childChunksToUpdate[0].offset = d->chunks[d->childChunkIndex[DIINChunk]].offset + 12;
for(unsigned int i = 1; i < childChunksToUpdate.size(); i++)

View File

@ -61,6 +61,22 @@ namespace TagLib {
class TAGLIB_EXPORT File : public TagLib::File
{
public:
/*!
* This set of flags is used for various operations and is suitable for
* being OR-ed together.
*/
enum TagTypes {
//! Empty set. Matches no tag types.
NoTags = 0x0000,
//! Matches DIIN tags.
DIIN = 0x0002,
//! Matches ID3v1 tags.
ID3v2 = 0x0002,
//! Matches all tag types.
AllTags = 0xffff
};
/*!
* Constructs an DSDIFF file from \a file. If \a readProperties is true
* the file's audio properties will also be read.
@ -114,13 +130,13 @@ namespace TagLib {
*
* \see hasID3v2Tag()
*/
virtual ID3v2::Tag *ID3v2Tag() const;
ID3v2::Tag *ID3v2Tag(bool create = false) const;
/*!
* Returns the DSDIFF DIIN Tag for this file
*
*/
DSDIFF::DIIN::Tag *DIINTag() const;
DSDIFF::DIIN::Tag *DIINTag(bool create = false) const;
/*!
* Implements the unified property interface -- export function.
@ -160,15 +176,21 @@ namespace TagLib {
virtual bool save();
/*!
* Save the file. This will attempt to save all of the tag types that are
* specified by OR-ing together TagTypes values. The save() method above
* uses AllTags. This returns true if saving was successful.
*
* This strips all tags not included in the mask, but does not modify them
* in memory, so later calls to save() which make use of these tags will
* remain valid. This also strips empty tags.
* Save the file. If \a strip is specified, it is possible to choose if
* tags not specified in \a tags should be stripped from the file or
* retained. With \a version, it is possible to specify whether ID3v2.4
* or ID3v2.3 should be used.
*/
bool save(int tags);
bool save(TagTypes tags, StripTags strip = StripOthers, ID3v2::Version version = ID3v2::v4);
/*!
* This will strip the tags that match the OR-ed together TagTypes from the
* file. By default it strips all tags. It returns true if the tags are
* successfully stripped.
*
* \note This will update the file immediately.
*/
void strip(TagTypes tags = AllTags);
/*!
* Returns whether or not the file on disk actually has an ID3v2 tag.
@ -179,7 +201,7 @@ namespace TagLib {
/*!
* Returns whether or not the file on disk actually has the DSDIFF
* Title & Artist tag.
* title and artist tags.
*
* \see DIINTag()
*/
@ -204,6 +226,10 @@ namespace TagLib {
File(const File &);
File &operator=(const File &);
void removeRootChunk(const ByteVector &id);
void removeRootChunk(unsigned int chunk);
void removeChildChunk(unsigned int i, unsigned int chunk);
/*!
* Sets the data for the the specified chunk at root level to \a data.
*

View File

@ -15,6 +15,8 @@ class TestDSDIFF : public CppUnit::TestFixture
CPPUNIT_TEST(testProperties);
CPPUNIT_TEST(testTags);
CPPUNIT_TEST(testSaveID3v2);
CPPUNIT_TEST(testSaveID3v23);
CPPUNIT_TEST(testStrip);
CPPUNIT_TEST(testRepeatedSave);
CPPUNIT_TEST_SUITE_END();
@ -49,16 +51,16 @@ public:
CPPUNIT_ASSERT_EQUAL(String("The Artist"), f->tag()->artist());
delete f;
}
void testSaveID3v2()
{
ScopedFileCopy copy("empty10ms", ".dff");
string newname = copy.fileName();
{
DSDIFF::File f(newname.c_str());
CPPUNIT_ASSERT(!f.hasID3v2Tag());
f.tag()->setTitle(L"TitleXXX");
f.save();
CPPUNIT_ASSERT(f.hasID3v2Tag());
@ -67,7 +69,7 @@ public:
DSDIFF::File f(newname.c_str());
CPPUNIT_ASSERT(f.hasID3v2Tag());
CPPUNIT_ASSERT_EQUAL(String(L"TitleXXX"), f.tag()->title());
f.tag()->setTitle("");
f.save();
CPPUNIT_ASSERT(!f.hasID3v2Tag());
@ -85,12 +87,100 @@ public:
CPPUNIT_ASSERT_EQUAL(String(L"TitleXXX"), f.tag()->title());
}
}
void testSaveID3v23()
{
ScopedFileCopy copy("empty10ms", ".dff");
string newname = copy.fileName();
String xxx = ByteVector(254, 'X');
{
DSDIFF::File f(newname.c_str());
CPPUNIT_ASSERT_EQUAL(false, f.hasID3v2Tag());
f.tag()->setTitle(xxx);
f.tag()->setArtist("Artist A");
f.save(DSDIFF::File::AllTags, File::StripOthers, ID3v2::v3);
CPPUNIT_ASSERT_EQUAL(true, f.hasID3v2Tag());
}
{
DSDIFF::File f2(newname.c_str());
CPPUNIT_ASSERT_EQUAL((unsigned int)3, f2.ID3v2Tag()->header()->majorVersion());
CPPUNIT_ASSERT_EQUAL(String("Artist A"), f2.tag()->artist());
CPPUNIT_ASSERT_EQUAL(xxx, f2.tag()->title());
}
}
void testStrip()
{
{
ScopedFileCopy copy("empty10ms", ".dff");
{
DSDIFF::File f(copy.fileName().c_str());
f.tag()->setArtist("X");
f.save();
}
{
DSDIFF::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(f.hasID3v2Tag());
CPPUNIT_ASSERT(f.hasDIINTag());
f.strip();
}
{
DSDIFF::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(!f.hasID3v2Tag());
CPPUNIT_ASSERT(!f.hasDIINTag());
}
}
{
ScopedFileCopy copy("empty10ms", ".dff");
{
DSDIFF::File f(copy.fileName().c_str());
f.ID3v2Tag(true);
f.DIINTag(true);
f.tag()->setArtist("X");
f.save();
}
{
DSDIFF::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(f.hasID3v2Tag());
CPPUNIT_ASSERT(f.hasDIINTag());
f.strip(DSDIFF::File::ID3v2);
}
{
DSDIFF::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(!f.hasID3v2Tag());
CPPUNIT_ASSERT(f.hasDIINTag());
}
}
{
ScopedFileCopy copy("empty10ms", ".dff");
{
DSDIFF::File f(copy.fileName().c_str());
f.tag()->setArtist("X");
f.save();
}
{
DSDIFF::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(f.hasID3v2Tag());
CPPUNIT_ASSERT(f.hasDIINTag());
f.strip(DSDIFF::File::DIIN);
}
{
DSDIFF::File f(copy.fileName().c_str());
CPPUNIT_ASSERT(f.hasID3v2Tag());
CPPUNIT_ASSERT(!f.hasDIINTag());
}
}
}
void testRepeatedSave()
{
ScopedFileCopy copy("empty10ms", ".dff");
string newname = copy.fileName();
{
DSDIFF::File f(newname.c_str());
CPPUNIT_ASSERT_EQUAL(String(""), f.tag()->title());
@ -109,8 +199,6 @@ public:
CPPUNIT_ASSERT_EQUAL(String("NEW TITLE 2"), f.tag()->title());
}
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestDSDIFF);