DSF: Support frame factory, ID3v2.3, allow trailing garbage

Additionally test coverage and formatting are improved.
This commit is contained in:
Urs Fleisch 2023-09-25 08:09:16 +02:00
parent 39d1d68237
commit eaf7ff8b94
4 changed files with 105 additions and 50 deletions

View File

@ -36,12 +36,18 @@ using namespace TagLib;
class DSF::File::FilePrivate
{
public:
FilePrivate() = default;
FilePrivate(ID3v2::FrameFactory *frameFactory)
: ID3v2FrameFactory(frameFactory ? frameFactory
: ID3v2::FrameFactory::instance())
{
}
~FilePrivate() = default;
FilePrivate(const FilePrivate &) = delete;
FilePrivate &operator=(const FilePrivate &) = delete;
const ID3v2::FrameFactory *ID3v2FrameFactory;
long long fileSize = 0;
long long metadataOffset = 0;
std::unique_ptr<Properties> properties;
@ -56,18 +62,20 @@ bool DSF::File::isSupported(IOStream *stream)
}
DSF::File::File(FileName file, bool readProperties,
AudioProperties::ReadStyle propertiesStyle) :
AudioProperties::ReadStyle propertiesStyle,
ID3v2::FrameFactory *frameFactory) :
TagLib::File(file),
d(std::make_unique<FilePrivate>())
d(std::make_unique<FilePrivate>(frameFactory))
{
if(isOpen())
read(propertiesStyle);
}
DSF::File::File(IOStream *stream, bool readProperties,
AudioProperties::ReadStyle propertiesStyle) :
AudioProperties::ReadStyle propertiesStyle,
ID3v2::FrameFactory *frameFactory) :
TagLib::File(stream),
d(std::make_unique<FilePrivate>())
d(std::make_unique<FilePrivate>(frameFactory))
{
if(isOpen())
read(propertiesStyle);
@ -96,6 +104,11 @@ DSF::Properties *DSF::File::audioProperties() const
}
bool DSF::File::save()
{
return save(ID3v2::v4);
}
bool DSF::File::save(ID3v2::Version version)
{
if(readOnly()) {
debug("DSF::File::save() - Cannot save to a read only file.");
@ -123,7 +136,7 @@ bool DSF::File::save()
truncate(newFileSize);
}
else {
ByteVector tagData = d->tag->render();
ByteVector tagData = d->tag->render(version);
long long newMetadataOffset = d->metadataOffset ? d->metadataOffset : d->fileSize;
long long newFileSize = newMetadataOffset + tagData.size();
@ -164,19 +177,19 @@ void DSF::File::read(AudioProperties::ReadStyle propertiesStyle)
return;
}
long long chunkSize = readBlock(8).toLongLong(false);
long long dsdHeaderSize = readBlock(8).toLongLong(false);
// Integrity check
if(chunkSize != 28) {
debug("DSF::File::read() -- File is corrupted, wrong chunk size");
if(dsdHeaderSize != 28) {
debug("DSF::File::read() -- File is corrupted, wrong DSD header size");
setValid(false);
return;
}
d->fileSize = readBlock(8).toLongLong(false);
// File is malformed or corrupted
if(d->fileSize != length()) {
// File is malformed or corrupted, allow trailing garbage
if(d->fileSize > length()) {
debug("DSF::File::read() -- File is corrupted wrong length");
setValid(false);
return;
@ -199,9 +212,14 @@ void DSF::File::read(AudioProperties::ReadStyle propertiesStyle)
return;
}
chunkSize = readBlock(8).toLongLong(false);
long long fmtHeaderSize = readBlock(8).toLongLong(false);
if(fmtHeaderSize != 52) {
debug("DSF::File::read() -- File is corrupted, wrong FMT header size");
setValid(false);
return;
}
d->properties = std::make_unique<Properties>(readBlock(chunkSize), propertiesStyle);
d->properties = std::make_unique<Properties>(readBlock(fmtHeaderSize), propertiesStyle);
// Skip the data chunk
@ -209,5 +227,6 @@ void DSF::File::read(AudioProperties::ReadStyle propertiesStyle)
if(d->metadataOffset == 0)
d->tag = std::make_unique<ID3v2::Tag>();
else
d->tag = std::make_unique<ID3v2::Tag>(this, d->metadataOffset);
d->tag = std::make_unique<ID3v2::Tag>(this, d->metadataOffset,
d->ID3v2FrameFactory);
}

View File

@ -45,10 +45,14 @@ namespace TagLib {
* \note In the current implementation, both \a readProperties and
* \a propertiesStyle are ignored. The audio properties are always
* read.
*
* If this file contains and ID3v2 tag the frames will be created using
* \a frameFactory (default if null).
*/
File(FileName file, bool readProperties = true,
AudioProperties::ReadStyle propertiesStyle =
AudioProperties::Average);
AudioProperties::Average,
ID3v2::FrameFactory *frameFactory = nullptr);
/*!
* Constructs a DSD stream file from \a stream.
@ -57,18 +61,25 @@ namespace TagLib {
* \a propertiesStyle are ignored. The audio properties are always
* read.
*
* If this file contains and ID3v2 tag the frames will be created using
* \a frameFactory (default if null).
*
* \note TagLib will *not* take ownership of the stream, the caller is
* responsible for deleting it after the File object.
*/
File(IOStream *stream, bool readProperties = true,
AudioProperties::ReadStyle propertiesStyle =
AudioProperties::Average);
AudioProperties::Average,
ID3v2::FrameFactory *frameFactory = nullptr);
/*!
* Destroys this instance of the File.
*/
~File() override;
/*!
* Returns the ID3v2 Tag for this file.
*/
ID3v2::Tag *tag() const override;
/*!
@ -91,19 +102,26 @@ namespace TagLib {
/*!
* Save the file.
*
*
* This returns true if the save was successful.
*/
bool save() override;
/*!
* Returns whether or not the given \a stream can be opened as a DSF
* file.
*
* \note This method is designed to do a quick check. The result may
* not necessarily be correct.
*/
static bool isSupported(IOStream *stream);
/*!
* Save the file.
*
* \a version specifies the ID3v2 version to be used for writing tags.
*/
bool save(ID3v2::Version version);
/*!
* Returns whether or not the given \a stream can be opened as a DSF
* file.
*
* \note This method is designed to do a quick check. The result may
* not necessarily be correct.
*/
static bool isSupported(IOStream *stream);
private:
void read(AudioProperties::ReadStyle propertiesStyle);

View File

@ -117,17 +117,18 @@ int DSF::Properties::blockSizePerChannel() const
void DSF::Properties::read(const ByteVector &data)
{
d->formatVersion = data.toUInt(0U,false);
d->formatID = data.toUInt(4U,false);
d->channelType = data.toUInt(8U,false);
d->channelNum = data.toUInt(12U,false);
d->samplingFrequency = data.toUInt(16U,false);
d->bitsPerSample = data.toUInt(20U,false);
d->sampleCount = data.toLongLong(24U,false);
d->blockSizePerChannel = data.toUInt(32U,false);
d->formatVersion = data.toUInt(0U,false);
d->formatID = data.toUInt(4U,false);
d->channelType = data.toUInt(8U,false);
d->channelNum = data.toUInt(12U,false);
d->samplingFrequency = data.toUInt(16U,false);
d->bitsPerSample = data.toUInt(20U,false);
d->sampleCount = data.toLongLong(24U,false);
d->blockSizePerChannel = data.toUInt(32U,false);
d->bitrate
= static_cast<unsigned int>((d->samplingFrequency * d->bitsPerSample * d->channelNum) / 1000.0 + 0.5);
d->length
= d->samplingFrequency > 0 ? static_cast<unsigned int>(d->sampleCount * 1000.0 / d->samplingFrequency + 0.5) : 0;
d->bitrate = static_cast<unsigned int>(
(d->samplingFrequency * d->bitsPerSample * d->channelNum) / 1000.0 + 0.5);
d->length = d->samplingFrequency > 0
? static_cast<unsigned int>(d->sampleCount * 1000.0 / d->samplingFrequency + 0.5)
: 0;
}

View File

@ -1,8 +1,11 @@
#include <string>
#include <stdio.h>
#include <tag.h>
#include <tbytevectorlist.h>
#include <dsffile.h>
#include <cstdio>
#include "tbytevectorlist.h"
#include "tpropertymap.h"
#include "tag.h"
#include "dsffile.h"
#include "plainfile.h"
#include <cppunit/extensions/HelperMacros.h>
#include "utils.h"
@ -22,7 +25,6 @@ public:
{
DSF::File f(TEST_FILE_PATH_C("empty10ms.dsf"));
CPPUNIT_ASSERT(f.audioProperties());
CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->length());
CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->lengthInSeconds());
CPPUNIT_ASSERT_EQUAL(10, f.audioProperties()->lengthInMilliseconds());
CPPUNIT_ASSERT_EQUAL(5645, f.audioProperties()->bitrate());
@ -41,15 +43,30 @@ public:
ScopedFileCopy copy("empty10ms", ".dsf");
string newname = copy.fileName();
DSF::File *f = new DSF::File(newname.c_str());
CPPUNIT_ASSERT_EQUAL(String(""), f->tag()->artist());
f->tag()->setArtist("The Artist");
f->save();
delete f;
{
DSF::File f(newname.c_str());
CPPUNIT_ASSERT(f.properties().isEmpty());
CPPUNIT_ASSERT_EQUAL(String(""), f.tag()->artist());
f.tag()->setArtist("The Artist");
PropertyMap properties = f.properties();
properties["ALBUMARTIST"] = StringList("Album Artist");
f.setProperties(properties);
f.save();
}
f = new DSF::File(newname.c_str());
CPPUNIT_ASSERT_EQUAL(String("The Artist"), f->tag()->artist());
delete f;
{
DSF::File f(newname.c_str());
CPPUNIT_ASSERT_EQUAL(String("The Artist"), f.tag()->artist());
CPPUNIT_ASSERT_EQUAL(String("The Artist"), f.properties()["ARTIST"].front());
CPPUNIT_ASSERT_EQUAL(String("Album Artist"), f.properties()["ALBUMARTIST"].front());
f.setProperties(PropertyMap());
f.save();
}
// Check if file without tags is same as original empty file
const ByteVector dsfData = PlainFile(TEST_FILE_PATH_C("empty10ms.dsf")).readAll();
const ByteVector fileData = PlainFile(newname.c_str()).readAll();
CPPUNIT_ASSERT(dsfData == fileData);
}
};