1 Commits

Author SHA1 Message Date
Urs Fleisch
208964e0bb Version 2.2.1 2026-02-27 06:27:25 +01:00
75 changed files with 236 additions and 5711 deletions

View File

@@ -1,27 +1,7 @@
TagLib 2.3 (May 10, 2026)
=========================
* MP4: Support for chapters (Nero and QuickTime).
* WAV: Support for BEXT and iXML chunks.
* FLAC: Support for BEXT and iXML application blocks.
* Opus: New audio property `outputGain()`.
* Speed up Matroska reading by using seek head for element lookup.
* Speed up Matroska writing by offering multiple write style modes.
* More tolerant handling of files with oversized RIFF chunks, zero size ID3v2
frames and Matroska chapters without edition.
* Avoid wrong content-based detection as MPEG files.
* Fix bitrate calculations for MPEG ADTS and MP4 ESDS.
* Fix data race with multi-threaded use of `MP4::ItemFactory`.
* Fix unbounded recursion in EBML/Matroska `MasterElement` and MP4 atoms.
* Limit number of MP4 atoms at top level.
* Fix writing too many offsets when updating MP4 stco/co64 atoms.
* Fix k bounds in Shorten Rice-Golomb coding.
TagLib 2.2.1 (Mar 7, 2026)
==========================
* Support edition, chapter and attachment UIDs in Matroska simple tags.
* Avoid duplicates in Matroska complex property keys.
TagLib 2.2 (Feb 18, 2026)
=========================

View File

@@ -92,8 +92,8 @@ endif()
# Minor version: increase it if you add ABI compatible features.
# Patch version: increase it for bug fix releases.
set(TAGLIB_SOVERSION_MAJOR 2)
set(TAGLIB_SOVERSION_MINOR 3)
set(TAGLIB_SOVERSION_PATCH 0)
set(TAGLIB_SOVERSION_MINOR 2)
set(TAGLIB_SOVERSION_PATCH 1)
include(ConfigureChecks.cmake)

View File

@@ -117,3 +117,4 @@ int main(int argc, char *argv[])
}
return 0;
}

View File

@@ -196,10 +196,6 @@ if(WITH_MP4)
mp4/mp4coverart.h
mp4/mp4stem.h
mp4/mp4itemfactory.h
mp4/mp4chapter.h
mp4/mp4chapterholder.h
mp4/mp4nerochapterlist.h
mp4/mp4qtchapterlist.h
)
endif()
if(WITH_MOD)
@@ -244,7 +240,6 @@ if(WITH_MATROSKA)
matroska/matroskaproperties.h
matroska/matroskasimpletag.h
matroska/matroskatag.h
matroska/matroskawritestyle.h
)
set(tag_PRIVATE_HDRS ${tag_PRIVATE_HDRS}
matroska/ebml/ebmlbinaryelement.h
@@ -377,9 +372,6 @@ if(WITH_MP4)
mp4/mp4coverart.cpp
mp4/mp4stem.cpp
mp4/mp4itemfactory.cpp
mp4/mp4chapter.cpp
mp4/mp4nerochapterlist.cpp
mp4/mp4qtchapterlist.cpp
)
endif()

View File

@@ -172,7 +172,7 @@ namespace TagLib {
void setReadOnly(bool readOnly);
/*!
* Returns \c true if the item is read-only.
* Return \c true if the item is read-only.
*/
bool isReadOnly() const;

View File

@@ -312,7 +312,7 @@ void DSDIFF::File::removeRootChunk(unsigned int i)
unsigned long long chunkSize = d->chunks[i].size + d->chunks[i].padding + 12;
d->size -= chunkSize;
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
removeBlock(d->chunks[i].offset - 12, chunkSize);
@@ -346,7 +346,7 @@ void DSDIFF::File::setRootChunkData(unsigned int i, const ByteVector &data)
// First we update the global size
d->size += ((data.size() + 1) & ~1) - (d->chunks[i].size + d->chunks[i].padding);
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
// Now update the specific chunk
@@ -383,7 +383,7 @@ void DSDIFF::File::setRootChunkData(const ByteVector &name, const ByteVector &da
// First we update the global size
d->size += (offset & 1) + ((data.size() + 1) & ~1) + 12;
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
// Now add the chunk to the file
const unsigned long long fileLength = length();
@@ -410,12 +410,12 @@ void DSDIFF::File::removeChildChunk(unsigned int i, unsigned int childChunkNum)
unsigned long long removedChunkTotalSize = childChunks[i].size + childChunks[i].padding + 12;
d->size -= removedChunkTotalSize;
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
// Update child chunk size
d->chunks[d->childChunkIndex[childChunkNum]].size -= removedChunkTotalSize;
insert(ByteVector::fromULongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
insert(ByteVector::fromLongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
d->endianness == BigEndian),
d->chunks[d->childChunkIndex[childChunkNum]].offset - 8, 8);
// Remove the chunk
@@ -462,13 +462,13 @@ void DSDIFF::File::setChildChunkData(unsigned int i,
d->size += ((data.size() + 1) & ~1) - (childChunks[i].size + childChunks[i].padding);
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
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::fromULongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
insert(ByteVector::fromLongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
d->endianness == BigEndian),
d->chunks[d->childChunkIndex[childChunkNum]].offset - 8, 8);
@@ -538,13 +538,13 @@ void DSDIFF::File::setChildChunkData(const ByteVector &name,
// First we update the global size
d->size += (offset & 1) + ((data.size() + 1) & ~1) + 12;
insert(ByteVector::fromULongLong(d->size, d->endianness == BigEndian), 4, 8);
insert(ByteVector::fromLongLong(d->size, d->endianness == BigEndian), 4, 8);
// And the child chunk size
d->chunks[d->childChunkIndex[childChunkNum]].size += (offset & 1)
+ ((data.size() + 1) & ~1) + 12;
insert(ByteVector::fromULongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
insert(ByteVector::fromLongLong(d->chunks[d->childChunkIndex[childChunkNum]].size,
d->endianness == BigEndian),
d->chunks[d->childChunkIndex[childChunkNum]].offset - 8, 8);
@@ -606,14 +606,14 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
bool bigEndian = d->endianness == BigEndian;
d->type = readBlock(4);
d->size = readBlock(8).toULongLong(bigEndian);
d->size = readBlock(8).toLongLong(bigEndian);
d->format = readBlock(4);
// + 12: chunk header at least, fix for additional junk bytes
while(tell() + 12 <= length()) {
ByteVector chunkName = readBlock(4);
unsigned long long chunkSize = readBlock(8).toULongLong(bigEndian);
unsigned long long chunkSize = readBlock(8).toLongLong(bigEndian);
if(!isValidChunkID(chunkName)) {
debug("DSDIFF::File::read() -- Chunk '" + chunkName + "' has invalid ID");
@@ -666,14 +666,14 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
}
else if(d->chunks[i].name == "DST ") {
// Now decode the chunks inside the DST chunk to read the DST Frame Information one
unsigned long long dstChunkEnd = d->chunks[i].offset + d->chunks[i].size;
long long dstChunkEnd = d->chunks[i].offset + d->chunks[i].size;
seek(d->chunks[i].offset);
audioDataSizeinBytes = d->chunks[i].size;
while(static_cast<unsigned long long>(tell()) + 12 <= dstChunkEnd) {
while(tell() + 12 <= dstChunkEnd) {
ByteVector dstChunkName = readBlock(4);
unsigned long long dstChunkSize = readBlock(8).toULongLong(bigEndian);
long long dstChunkSize = readBlock(8).toLongLong(bigEndian);
if(!isValidChunkID(dstChunkName)) {
debug("DSDIFF::File::read() -- DST Chunk '" + dstChunkName + "' has invalid ID");
@@ -681,7 +681,7 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
break;
}
if(tell() + dstChunkSize > dstChunkEnd) {
if(static_cast<long long>(tell()) + dstChunkSize > dstChunkEnd) {
debug("DSDIFF::File::read() -- DST Chunk '" + dstChunkName
+ "' has invalid size (larger than the DST chunk)");
setValid(false);
@@ -708,14 +708,14 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
}
}
else if(d->chunks[i].name == "PROP") {
d->childChunkIndex[PROPChunk] = static_cast<int>(i);
d->childChunkIndex[PROPChunk] = i;
// Now decodes the chunks inside the PROP chunk
unsigned long long propChunkEnd = d->chunks[i].offset + d->chunks[i].size;
long long propChunkEnd = d->chunks[i].offset + d->chunks[i].size;
// +4 to remove the 'SND ' marker at beginning of 'PROP' chunk
seek(d->chunks[i].offset + 4);
while(static_cast<unsigned long long>(tell()) + 12 <= propChunkEnd) {
while(tell() + 12 <= propChunkEnd) {
ByteVector propChunkName = readBlock(4);
unsigned long long propChunkSize = readBlock(8).toULongLong(bigEndian);
long long propChunkSize = readBlock(8).toLongLong(bigEndian);
if(!isValidChunkID(propChunkName)) {
debug("DSDIFF::File::read() -- PROP Chunk '" + propChunkName + "' has invalid ID");
@@ -723,7 +723,7 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
break;
}
if(tell() + propChunkSize > propChunkEnd) {
if(static_cast<long long>(tell()) + propChunkSize > propChunkEnd) {
debug("DSDIFF::File::read() -- PROP Chunk '" + propChunkName
+ "' has invalid size (larger than the PROP chunk)");
setValid(false);
@@ -751,17 +751,17 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
}
}
else if(d->chunks[i].name == "DIIN") {
d->childChunkIndex[DIINChunk] = static_cast<int>(i);
d->childChunkIndex[DIINChunk] = i;
d->hasDiin = true;
// Now decode the chunks inside the DIIN chunk
unsigned long long diinChunkEnd = d->chunks[i].offset + d->chunks[i].size;
long long diinChunkEnd = d->chunks[i].offset + d->chunks[i].size;
seek(d->chunks[i].offset);
while(static_cast<unsigned long long>(tell()) + 12 <= diinChunkEnd) {
while(tell() + 12 <= diinChunkEnd) {
ByteVector diinChunkName = readBlock(4);
unsigned long long diinChunkSize = readBlock(8).toULongLong(bigEndian);
long long diinChunkSize = readBlock(8).toLongLong(bigEndian);
if(!isValidChunkID(diinChunkName)) {
debug("DSDIFF::File::read() -- DIIN Chunk '" + diinChunkName + "' has invalid ID");
@@ -769,7 +769,7 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
break;
}
if(tell() + diinChunkSize > diinChunkEnd) {
if(static_cast<long long>(tell()) + diinChunkSize > diinChunkEnd) {
debug("DSDIFF::File::read() -- DIIN Chunk '" + diinChunkName
+ "' has invalid size (larger than the DIIN chunk)");
setValid(false);
@@ -825,7 +825,7 @@ void DSDIFF::File::read(bool readProperties, Properties::ReadStyle propertiesSty
if(d->childChunks[PROPChunk][i].name == "ID3 " ||
d->childChunks[PROPChunk][i].name == "id3 ") {
if(d->hasID3v2) {
d->duplicateID3V2chunkIndex = static_cast<int>(i);
d->duplicateID3V2chunkIndex = i;
// ID3V2 tag has already been found at root level
continue;
}
@@ -913,7 +913,7 @@ void DSDIFF::File::writeChunk(const ByteVector &name, const ByteVector &data,
combined.append(ByteVector(leadingPadding, '\x00'));
combined.append(name);
combined.append(ByteVector::fromULongLong(data.size(), d->endianness == BigEndian));
combined.append(ByteVector::fromLongLong(data.size(), d->endianness == BigEndian));
combined.append(data);
if((data.size() & 0x01) != 0)
combined.append('\x00');

View File

@@ -46,8 +46,8 @@ public:
FilePrivate &operator=(const FilePrivate &) = delete;
const ID3v2::FrameFactory *ID3v2FrameFactory;
unsigned long long fileSize = 0;
unsigned long long metadataOffset = 0;
long long fileSize = 0;
long long metadataOffset = 0;
std::unique_ptr<Properties> properties;
std::unique_ptr<ID3v2::Tag> tag;
};
@@ -116,17 +116,17 @@ bool DSF::File::save(ID3v2::Version version)
// Three things must be updated: the file size, the tag data, and the metadata offset
if(d->tag->isEmpty()) {
unsigned long long newFileSize = d->metadataOffset ? d->metadataOffset : d->fileSize;
long long newFileSize = d->metadataOffset ? d->metadataOffset : d->fileSize;
// Update the file size
if(d->fileSize != newFileSize) {
insert(ByteVector::fromULongLong(newFileSize, false), 12, 8);
insert(ByteVector::fromLongLong(newFileSize, false), 12, 8);
d->fileSize = newFileSize;
}
// Update the metadata offset to 0 since there is no longer a tag
if(d->metadataOffset) {
insert(ByteVector::fromULongLong(0ULL, false), 20, 8);
insert(ByteVector::fromLongLong(0ULL, false), 20, 8);
d->metadataOffset = 0;
}
@@ -136,19 +136,19 @@ bool DSF::File::save(ID3v2::Version version)
else {
ByteVector tagData = d->tag->render(version);
unsigned long long newMetadataOffset = d->metadataOffset ? d->metadataOffset : d->fileSize;
unsigned long long newFileSize = newMetadataOffset + tagData.size();
unsigned long long oldTagSize = d->fileSize - newMetadataOffset;
long long newMetadataOffset = d->metadataOffset ? d->metadataOffset : d->fileSize;
long long newFileSize = newMetadataOffset + tagData.size();
long long oldTagSize = d->fileSize - newMetadataOffset;
// Update the file size
if(d->fileSize != newFileSize) {
insert(ByteVector::fromULongLong(newFileSize, false), 12, 8);
insert(ByteVector::fromLongLong(newFileSize, false), 12, 8);
d->fileSize = newFileSize;
}
// Update the metadata offset
if(d->metadataOffset != newMetadataOffset) {
insert(ByteVector::fromULongLong(newMetadataOffset, false), 20, 8);
insert(ByteVector::fromLongLong(newMetadataOffset, false), 20, 8);
d->metadataOffset = newMetadataOffset;
}
@@ -175,7 +175,7 @@ void DSF::File::read(AudioProperties::ReadStyle propertiesStyle)
return;
}
unsigned long long dsdHeaderSize = readBlock(8).toULongLong(false);
long long dsdHeaderSize = readBlock(8).toLongLong(false);
// Integrity check
if(dsdHeaderSize != 28) {
@@ -184,16 +184,16 @@ void DSF::File::read(AudioProperties::ReadStyle propertiesStyle)
return;
}
d->fileSize = readBlock(8).toULongLong(false);
d->fileSize = readBlock(8).toLongLong(false);
// File is malformed or corrupted, allow trailing garbage
if(d->fileSize > static_cast<unsigned long long>(length())) {
if(d->fileSize > length()) {
debug("DSF::File::read() -- File is corrupted wrong length");
setValid(false);
return;
}
d->metadataOffset = readBlock(8).toULongLong(false);
d->metadataOffset = readBlock(8).toLongLong(false);
// File is malformed or corrupted
if(d->metadataOffset > d->fileSize) {
@@ -210,7 +210,7 @@ void DSF::File::read(AudioProperties::ReadStyle propertiesStyle)
return;
}
unsigned long long fmtHeaderSize = readBlock(8).toULongLong(false);
long long fmtHeaderSize = readBlock(8).toLongLong(false);
if(fmtHeaderSize != 52) {
debug("DSF::File::read() -- File is corrupted, wrong FMT header size");
setValid(false);

View File

@@ -225,7 +225,7 @@ namespace
#endif
#ifdef TAGLIB_WITH_MATROSKA
else if(ext == "MKA" || ext == "MKV" || ext == "WEBM")
file = new Matroska::File(stream, readAudioProperties, audioPropertiesStyle);
file = new Matroska::File(stream, readAudioProperties);
#endif
// if file is not valid, leave it to content-based detection.
@@ -246,7 +246,8 @@ namespace
{
File *file = nullptr;
if(false);
if(MPEG::File::isSupported(stream))
file = new MPEG::File(stream, readAudioProperties, audioPropertiesStyle);
#ifdef TAGLIB_WITH_VORBIS
else if(Ogg::Vorbis::File::isSupported(stream))
file = new Ogg::Vorbis::File(stream, readAudioProperties, audioPropertiesStyle);
@@ -299,8 +300,6 @@ namespace
else if(Matroska::File::isSupported(stream))
file = new Matroska::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
else if(MPEG::File::isSupported(stream))
file = new MPEG::File(stream, readAudioProperties, audioPropertiesStyle);
// isSupported() only does a quick check, so double check the file here.

View File

@@ -70,15 +70,11 @@ public:
std::unique_ptr<Properties> properties;
ByteVector xiphCommentData;
String iXMLData;
ByteVector bextData;
List<FLAC::MetadataBlock *> blocks;
offset_t flacStart { 0 };
offset_t streamStart { 0 };
bool scanned { false };
bool hasiXML { false };
bool hasBEXT { false };
};
////////////////////////////////////////////////////////////////////////////////
@@ -245,60 +241,6 @@ bool FLAC::File::save()
d->xiphCommentData = xiphComment()->render(false);
// Drop any APPLICATION blocks we recognize as iXML or bext from the block
// list. Recognized blocks were normally extracted to d->iXMLData /
// d->bextData during scan() and never added here, but this also catches
// entries inserted after scan() (defensive).
for(auto it = d->blocks.begin(); it != d->blocks.end();) {
if((*it)->code() == MetadataBlock::Application) {
const ByteVector blockData = (*it)->render();
if(blockData.size() >= 4) {
const ByteVector appId = blockData.mid(0, 4);
ByteVector innerId;
if(appId == "riff" && blockData.size() >= 12)
innerId = blockData.mid(4, 4);
else if(appId == "iXML" || appId == "bext")
innerId = appId;
if(innerId == "iXML" || innerId == "bext") {
delete *it;
it = d->blocks.erase(it);
continue;
}
}
}
++it;
}
// Append fresh APPLICATION/"riff" blocks for iXML and bext if non-empty.
// Per FLAC foreign-metadata convention the payload is a RIFF chunk:
// <4 byte FOURCC><4 byte LE size><data>.
if(!d->iXMLData.isEmpty()) {
const ByteVector xml = d->iXMLData.data(String::UTF8);
ByteVector payload;
payload.append("riff");
payload.append("iXML");
payload.append(ByteVector::fromUInt(xml.size(), false));
payload.append(xml);
d->blocks.append(new UnknownMetadataBlock(MetadataBlock::Application, payload));
d->hasiXML = true;
}
else {
d->hasiXML = false;
}
if(!d->bextData.isEmpty()) {
ByteVector payload;
payload.append("riff");
payload.append("bext");
payload.append(ByteVector::fromUInt(d->bextData.size(), false));
payload.append(d->bextData);
d->blocks.append(new UnknownMetadataBlock(MetadataBlock::Application, payload));
d->hasBEXT = true;
}
else {
d->hasBEXT = false;
}
// Replace metadata blocks
MetadataBlock *commentBlock =
@@ -491,26 +433,6 @@ void FLAC::File::removePictures()
}
}
String FLAC::File::iXMLData() const
{
return d->iXMLData;
}
void FLAC::File::setiXMLData(const String &data)
{
d->iXMLData = data;
}
ByteVector FLAC::File::BEXTData() const
{
return d->bextData;
}
void FLAC::File::setBEXTData(const ByteVector &data)
{
d->bextData = data;
}
void FLAC::File::strip(int tags)
{
if(tags & ID3v1)
@@ -540,16 +462,6 @@ bool FLAC::File::hasID3v2Tag() const
return d->ID3v2Location >= 0;
}
bool FLAC::File::hasiXMLData() const
{
return d->hasiXML;
}
bool FLAC::File::hasBEXTData() const
{
return d->hasBEXT;
}
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////
@@ -701,53 +613,6 @@ void FLAC::File::scan()
else if(blockType == MetadataBlock::Padding) {
// Skip all padding blocks.
}
else if(blockType == MetadataBlock::Application && data.size() >= 4) {
// APPLICATION block (RFC 9639 § 8.4):
// <4 bytes> big-endian application ID (ASCII FOURCC in practice)
// <n bytes> application-defined data
//
// We recognize two conventions for carrying RIFF iXML / bext metadata:
// 1. App ID "riff" — IANA-registered FLAC foreign-metadata wrapper.
// Payload is a RIFF chunk: <4 byte FOURCC><4 byte LE size><data>.
// 2. App ID "iXML" or "bext" — direct, used by some third-party tools
// (e.g. Sequoia). Payload is the chunk data verbatim.
//
// Other application IDs (and "riff" wrapping FOURCCs we don't recognize)
// fall through to UnknownMetadataBlock so they round-trip unchanged.
const ByteVector appId = data.mid(0, 4);
ByteVector innerId;
ByteVector innerData;
if(appId == "riff" && data.size() >= 12) {
innerId = data.mid(4, 4);
const unsigned int innerSize = data.toUInt(8U, false);
innerData = data.mid(12, innerSize);
}
else if(appId == "iXML" || appId == "bext") {
innerId = appId;
innerData = data.mid(4);
}
if(innerId == "iXML") {
if(!d->hasiXML) {
d->hasiXML = true;
d->iXMLData = String(innerData, String::UTF8);
}
else
debug("FLAC::File::scan() -- multiple iXML blocks found, discarding");
}
else if(innerId == "bext") {
if(!d->hasBEXT) {
d->hasBEXT = true;
d->bextData = innerData;
}
else
debug("FLAC::File::scan() -- multiple BEXT blocks found, discarding");
}
else {
block = new UnknownMetadataBlock(blockType, data);
}
}
else {
block = new UnknownMetadataBlock(blockType, data);
}

View File

@@ -296,52 +296,6 @@ namespace TagLib {
*/
void addPicture(Picture *picture);
/*!
* Returns the raw iXML data as a String. Empty if no iXML metadata
* is present. Read from an APPLICATION metadata block (RFC 9639 § 8.4)
* carrying either the FLAC foreign-metadata application ID "riff"
* (with an iXML RIFF chunk as payload) or the direct application ID
* "iXML" used by some third-party tools.
*
* \see setiXMLData()
* \see hasiXMLData()
*/
String iXMLData() const;
/*!
* Sets the iXML data. Pass an empty string to remove the iXML
* APPLICATION block on save. On save, the data is written using the
* FLAC foreign-metadata convention: an APPLICATION block with
* application ID "riff" wrapping an iXML RIFF chunk.
*
* \see iXMLData()
* \see hasiXMLData()
*/
void setiXMLData(const String &data);
/*!
* Returns the raw BEXT (Broadcast Audio Extension) data as a
* ByteVector. Empty if no BEXT metadata is present. Read from an
* APPLICATION metadata block (RFC 9639 § 8.4) carrying either the FLAC
* foreign-metadata application ID "riff" (with a bext RIFF chunk as
* payload) or the direct application ID "bext".
*
* \see setBEXTData()
* \see hasBEXTData()
*/
ByteVector BEXTData() const;
/*!
* Sets the BEXT data. Pass an empty ByteVector to remove the BEXT
* APPLICATION block on save. On save, the data is written using the
* FLAC foreign-metadata convention: an APPLICATION block with
* application ID "riff" wrapping a bext RIFF chunk.
*
* \see BEXTData()
* \see hasBEXTData()
*/
void setBEXTData(const ByteVector &data);
/*!
* This will remove the tags that match the OR-ed together TagTypes from
* the file. By default it removes all tags.
@@ -378,22 +332,6 @@ namespace TagLib {
*/
bool hasID3v2Tag() const;
/*!
* Returns whether or not the file on disk actually has iXML data
* stored in an APPLICATION metadata block.
*
* \see iXMLData()
*/
bool hasiXMLData() const;
/*!
* Returns whether or not the file on disk actually has BEXT data
* stored in an APPLICATION metadata block.
*
* \see BEXTData()
*/
bool hasBEXTData() const;
/*!
* Returns whether or not the given \a stream can be opened as a FLAC
* file.

View File

@@ -87,7 +87,7 @@ namespace TagLib {
int bitsPerSample() const;
/*!
* Returns the number of sample frames.
* Return the number of sample frames.
*/
unsigned long long sampleFrames() const;

View File

@@ -21,7 +21,6 @@
#include "ebmlmasterelement.h"
#include "ebmlvoidelement.h"
#include "ebmlutils.h"
#include "tdebug.h"
#include "tfile.h"
using namespace TagLib;
@@ -98,34 +97,18 @@ void EBML::MasterElement::setMinRenderSize(offset_t minimumSize)
minRenderSize = minimumSize;
}
bool EBML::MasterElement::read(File &file, int depth)
bool EBML::MasterElement::read(File &file)
{
static constexpr int MAX_EBML_DEPTH = 64;
if(depth > MAX_EBML_DEPTH) {
debug("EBML: Maximum nesting depth exceeded");
return false;
}
const offset_t maxOffset = file.tell() + dataSize;
std::unique_ptr<Element> element;
while((element = findNextElement(file, maxOffset))) {
if(auto master = dynamic_cast<MasterElement *>(element.get())) {
if(!master->read(file, depth + 1))
return false;
}
else {
if(!element->read(file))
return false;
}
if(!element->read(file))
return false;
elements.push_back(std::move(element));
}
return file.tell() == maxOffset;
}
bool EBML::MasterElement::read(File &file)
{
return read(file, 0);
}
ByteVector EBML::MasterElement::render()
{
ByteVector buffer = renderId();

View File

@@ -55,8 +55,6 @@ namespace TagLib
void setMinRenderSize(offset_t minimumSize);
protected:
bool read(File &file, int depth);
offset_t offset;
offset_t padding = 0;
offset_t minRenderSize = 0;

View File

@@ -46,7 +46,7 @@ std::unique_ptr<Matroska::Attachments> EBML::MkAttachments::parse() const
{
auto attachments = std::make_unique<Matroska::Attachments>();
attachments->setOffset(offset);
attachments->setSize(getSize() + padding);
attachments->setSize(getSize());
for(const auto &element : elements) {
if(element->getId() != Id::MkAttachedFile)

View File

@@ -25,76 +25,12 @@
#include "ebmlmkchapters.h"
#include "ebmlstringelement.h"
#include <atomic>
#include "ebmluintelement.h"
#include "matroskachapters.h"
#include "matroskachapteredition.h"
using namespace TagLib;
namespace {
// Counter for synthesising a unique ChapterUID when the source file omits
// the MkChapterUID element (out-of-spec but produced by some muxers, e.g.
// older FFmpeg or audiobook generators). MKVToolNix / MediaInfo / FFmpeg
// all tolerate UID-less chapters; without this synth, the call sites in
// MkChapters::parse() filter such chapters out via the `chapter.uid()`
// check and they are silently lost.
//
// High bit set as a marker; counter starts at 1 and increments per atom
// so each parsed UID-less chapter gets a distinct value within the
// process lifetime. Real ChapterUIDs are random 64-bit values from
// muxers; the (1ULL << 63) | n encoding makes collision practically
// impossible while keeping the distinction local to TagLib.
static std::atomic<Matroska::Chapter::UID> synthesisedChapterUidCounter{1};
Matroska::Chapter parseChapterAtom(
const std::unique_ptr<EBML::Element> &atomElement)
{
Matroska::Chapter::UID chapterUid = 0;
Matroska::Chapter::Time chapterTimeStart = 0;
Matroska::Chapter::Time chapterTimeEnd = 0;
List<Matroska::Chapter::Display> chapterDisplays;
bool chapterHidden = false;
const auto chapterAtom = EBML::element_cast<EBML::Element::Id::MkChapterAtom>(atomElement);
for(const auto &chapterChild : *chapterAtom) {
if(const EBML::Element::Id cid = chapterChild->getId(); cid == EBML::Element::Id::MkChapterUID)
chapterUid = EBML::element_cast<EBML::Element::Id::MkChapterUID>(chapterChild)->getValue();
else if(cid == EBML::Element::Id::MkChapterTimeStart)
chapterTimeStart = EBML::element_cast<EBML::Element::Id::MkChapterTimeStart>(chapterChild)->getValue();
else if(cid == EBML::Element::Id::MkChapterTimeEnd)
chapterTimeEnd = EBML::element_cast<EBML::Element::Id::MkChapterTimeEnd>(chapterChild)->getValue();
else if(cid == EBML::Element::Id::MkChapterFlagHidden)
chapterHidden = EBML::element_cast<EBML::Element::Id::MkChapterFlagHidden>(chapterChild)->getValue() != 0;
else if (cid == EBML::Element::Id::MkChapterDisplay) {
const auto display = EBML::element_cast<EBML::Element::Id::MkChapterDisplay>(chapterChild);
String displayString;
String displayLanguage;
for(const auto &displayChild : *display) {
if (const EBML::Element::Id did = displayChild->getId(); did == EBML::Element::Id::MkChapString)
displayString = EBML::element_cast<EBML::Element::Id::MkChapString>(displayChild)->getValue();
else if(did == EBML::Element::Id::MkChapLanguage)
displayLanguage = EBML::element_cast<EBML::Element::Id::MkChapLanguage>(displayChild)->getValue();
}
if(!displayString.isEmpty()) {
chapterDisplays.append(Matroska::Chapter::Display(displayString, displayLanguage));
}
}
}
// Spec-noncompliant: ChapterAtom missing ChapterUID. Synthesise a
// process-unique UID so the chapter survives downstream `chapter.uid()`
// filters. See synthesisedChapterUidCounter comment above.
if(chapterUid == 0)
chapterUid = (1ULL << 63) |
synthesisedChapterUidCounter.fetch_add(1, std::memory_order_relaxed);
return Matroska::Chapter(chapterTimeStart, chapterTimeEnd, chapterDisplays,
chapterUid, chapterHidden);
}
} // namespae
EBML::MkChapters::MkChapters(int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkChapters, sizeLength, dataSize, offset)
{
@@ -105,7 +41,7 @@ EBML::MkChapters::MkChapters(Id, int sizeLength, offset_t dataSize, offset_t off
{
}
EBML::MkChapters::MkChapters() :
EBML::MkChapters::MkChapters():
MasterElement(Id::MkChapters, 0, 0, 0)
{
}
@@ -114,22 +50,9 @@ std::unique_ptr<Matroska::Chapters> EBML::MkChapters::parse() const
{
auto chapters = std::make_unique<Matroska::Chapters>();
chapters->setOffset(offset);
chapters->setSize(getSize() + padding);
// Collect any orphan ChapterAtom elements not wrapped in an EditionEntry.
// The Matroska spec requires ChapterAtom to be inside an EditionEntry, but
// some muxers produce files with ChapterAtom directly under Chapters.
// MKVToolNix and FFmpeg handle this case by treating the orphan atoms as
// belonging to an implicit default edition.
List<Matroska::Chapter> orphanChapters;
chapters->setSize(getSize());
for(const auto &element : elements) {
if(element->getId() == Id::MkChapterAtom) {
if(auto chapter = parseChapterAtom(element); chapter.uid()) {
orphanChapters.append(chapter);
}
continue;
}
if(element->getId() != Id::MkEditionEntry)
continue;
@@ -146,8 +69,39 @@ std::unique_ptr<Matroska::Chapters> EBML::MkChapters::parse() const
else if(id == Id::MkEditionFlagOrdered)
editionIsOrdered = element_cast<Id::MkEditionFlagOrdered>(editionChild)->getValue() != 0;
else if(id == Id::MkChapterAtom) {
if(auto chapter = parseChapterAtom(editionChild); chapter.uid()) {
editionChapters.append(chapter);
Matroska::Chapter::UID chapterUid = 0;
Matroska::Chapter::Time chapterTimeStart = 0;
Matroska::Chapter::Time chapterTimeEnd = 0;
List<Matroska::Chapter::Display> chapterDisplays;
bool chapterHidden = false;
const auto chapterAtom = element_cast<Id::MkChapterAtom>(editionChild);
for(const auto &chapterChild : *chapterAtom) {
if(const Id cid = chapterChild->getId(); cid == Id::MkChapterUID)
chapterUid = element_cast<Id::MkChapterUID>(chapterChild)->getValue();
else if(cid == Id::MkChapterTimeStart)
chapterTimeStart = element_cast<Id::MkChapterTimeStart>(chapterChild)->getValue();
else if(cid == Id::MkChapterTimeEnd)
chapterTimeEnd = element_cast<Id::MkChapterTimeEnd>(chapterChild)->getValue();
else if(cid == Id::MkChapterFlagHidden)
chapterHidden = element_cast<Id::MkChapterFlagHidden>(chapterChild)->getValue() != 0;
else if(cid == Id::MkChapterDisplay) {
const auto display = element_cast<Id::MkChapterDisplay>(chapterChild);
String displayString;
String displayLanguage;
for(const auto &displayChild : *display) {
if(const Id did = displayChild->getId(); did == Id::MkChapString)
displayString = element_cast<Id::MkChapString>(displayChild)->getValue();
else if(did == Id::MkChapLanguage)
displayLanguage = element_cast<Id::MkChapLanguage>(displayChild)->getValue();
}
if(!displayString.isEmpty()) {
chapterDisplays.append(Matroska::Chapter::Display(displayString, displayLanguage));
}
}
}
if(chapterUid) {
editionChapters.append(Matroska::Chapter(
chapterTimeStart, chapterTimeEnd, chapterDisplays, chapterUid, chapterHidden));
}
}
}
@@ -156,13 +110,5 @@ std::unique_ptr<Matroska::Chapters> EBML::MkChapters::parse() const
editionChapters, editionIsDefault, editionIsOrdered, editionUid));
}
}
// If orphan chapters were found, wrap them in an implicit default edition
// so they are not silently lost.
if (!orphanChapters.isEmpty()) {
chapters->addChapterEdition(Matroska::ChapterEdition(
orphanChapters, true, false, 0));
}
return chapters;
}

View File

@@ -19,9 +19,6 @@
***************************************************************************/
#include "ebmlmksegment.h"
#include <algorithm>
#include "ebmlutils.h"
#include "matroskafile.h"
#include "matroskatag.h"
@@ -33,32 +30,6 @@
using namespace TagLib;
namespace {
template <EBML::Element::Id Id, typename ElementType>
std::unique_ptr<ElementType> readElementAt(File &file,
offset_t offset,
offset_t maxOffset)
{
if(offset < 0 || offset >= maxOffset) {
return nullptr;
}
file.seek(offset);
auto element = EBML::Element::factory(file);
if(!element || element->getId() != Id) {
return nullptr;
}
auto typed = EBML::element_cast<Id>(std::move(element));
if(!typed || !typed->read(file)) {
return nullptr;
}
return typed;
}
} // namespace
EBML::MkSegment::MkSegment(int sizeLength, offset_t dataSize, offset_t offset):
MasterElement(Id::MkSegment, sizeLength, dataSize, offset)
{
@@ -78,178 +49,56 @@ offset_t EBML::MkSegment::segmentDataOffset() const
bool EBML::MkSegment::read(File &file)
{
return readLimited(file, dataSize);
}
bool EBML::MkSegment::readLimited(File &file, offset_t scanLimit)
{
const offset_t filePos = file.tell();
const offset_t maxOffset = filePos + dataSize;
const offset_t maxScanOffset = filePos + std::min(scanLimit, dataSize);
// When scanLimit is less than dataSize, the caller has requested a
// fast/limited scan (e.g. AudioProperties::Fast). In that case and if the
// file has been opened in read-only mode, we skip parsing the Cues element,
// which can be tens of MB on large files, causing severe slowdowns over
// network filesystems, and do not have to be updated in read-only mode.
const bool skipCues = file.readOnly() && scanLimit < dataSize;
MasterElement *pendingPaddingTarget = nullptr;
offset_t accumulatedPadding = 0;
const offset_t maxOffset = file.tell() + dataSize;
std::unique_ptr<Element> element;
while((element = findNextElement(file, maxScanOffset))) {
int i = 0;
int seekHeadIndex = -1;
while((element = findNextElement(file, maxOffset))) {
if(const Id id = element->getId(); id == Id::MkSeekHead) {
seekHeadIndex = i;
seekHead = element_cast<Id::MkSeekHead>(std::move(element));
if(!seekHead->read(file))
return false;
// We have a seek head, let's use it for faster access to the other elements
if(const auto elementAfterSeekHead = findNextElement(file, maxScanOffset);
elementAfterSeekHead && elementAfterSeekHead->getId() == Id::VoidElement)
seekHead->setPadding(elementAfterSeekHead->getSize());
const offset_t segDataOffset = segmentDataOffset();
const auto matroskaSeekHead = parseSeekHead();
const auto accumulateVoidPadding = [&](MasterElement *target) {
offset_t accPadding = 0;
while(const auto next = findNextElement(file, maxOffset)) {
if(next->getId() != Id::VoidElement)
break;
accPadding += next->getSize();
next->skipData(file);
}
if(accPadding > 0)
target->setPadding(accPadding);
};
// Build a work list of seek entries. Some muxers (e.g. MakeMKV,
// mkvmerge) write a small primary SeekHead at the start of the segment
// that only references a secondary SeekHead at the end of the file,
// which in turn lists Info / Tracks / Tags / Chapters / Attachments.
// Follow such MkSeekHead -> MkSeekHead chains so the real entries are
// not silently dropped.
List<std::pair<unsigned int, offset_t>> entries =
matroskaSeekHead->entryList();
// Guard against pathological / circular chains.
int chainedSeekHeadsFollowed = 0;
constexpr int MAX_CHAINED_SEEKHEADS = 8;
for(unsigned int i = 0; i < entries.size(); ++i) {
const auto &[idValue, relativeOffset] = entries[i];
const offset_t absoluteOffset = segDataOffset + relativeOffset;
switch(static_cast<Id>(idValue)) {
case Id::MkSeekHead: {
if(chainedSeekHeadsFollowed++ >= MAX_CHAINED_SEEKHEADS)
break;
auto chained = readElementAt<Id::MkSeekHead, MkSeekHead>(
file, absoluteOffset, maxOffset);
if(!chained)
break;
if(const auto parsed = chained->parse(segDataOffset)) {
for(const auto &entry : parsed->entryList())
entries.append(entry);
}
break;
}
case Id::MkCues:
if(!skipCues) {
if(!((cues = readElementAt<Id::MkCues, MkCues>(
file, absoluteOffset, maxOffset))))
return false;
}
break;
case Id::MkInfo:
if(!((info = readElementAt<Id::MkInfo, MkInfo>(
file, absoluteOffset, maxOffset))))
return false;
break;
case Id::MkTracks:
if(!((tracks = readElementAt<Id::MkTracks, MkTracks>(
file, absoluteOffset, maxOffset))))
return false;
break;
case Id::MkTags:
if(!((tags = readElementAt<Id::MkTags, MkTags>(
file, absoluteOffset, maxOffset))))
return false;
accumulateVoidPadding(tags.get());
break;
case Id::MkAttachments:
if(!((attachments = readElementAt<Id::MkAttachments, MkAttachments>(
file, absoluteOffset, maxOffset))))
return false;
accumulateVoidPadding(attachments.get());
break;
case Id::MkChapters:
if(!((chapters = readElementAt<Id::MkChapters, MkChapters>(
file, absoluteOffset, maxOffset))))
return false;
accumulateVoidPadding(chapters.get());
break;
default:
break;
}
}
return true;
}
else if(id == Id::VoidElement) {
if(pendingPaddingTarget) {
accumulatedPadding += element->getSize();
pendingPaddingTarget->setPadding(accumulatedPadding);
}
element->skipData(file);
}
else if(id == Id::MkCues) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
if(!skipCues) {
cues = element_cast<Id::MkCues>(std::move(element));
if(!cues->read(file))
return false;
}
else {
element->skipData(file);
}
cues = element_cast<Id::MkCues>(std::move(element));
if(!cues->read(file))
return false;
}
else if(id == Id::MkInfo) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
info = element_cast<Id::MkInfo>(std::move(element));
if(!info->read(file))
return false;
}
else if(id == Id::MkTracks) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
tracks = element_cast<Id::MkTracks>(std::move(element));
if(!tracks->read(file))
return false;
}
else if(id == Id::MkTags) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
tags = element_cast<Id::MkTags>(std::move(element));
if(!tags->read(file))
return false;
pendingPaddingTarget = tags.get();
}
else if(id == Id::MkAttachments) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
attachments = element_cast<Id::MkAttachments>(std::move(element));
if(!attachments->read(file))
return false;
pendingPaddingTarget = attachments.get();
}
else if(id == Id::MkChapters) {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
chapters = element_cast<Id::MkChapters>(std::move(element));
if(!chapters->read(file))
return false;
pendingPaddingTarget = chapters.get();
}
else {
pendingPaddingTarget = nullptr;
accumulatedPadding = 0;
if(id == Id::VoidElement
&& seekHead
&& seekHeadIndex == i - 1)
seekHead->setPadding(element->getSize());
element->skipData(file);
}
i++;
}
return true;
}

View File

@@ -51,7 +51,6 @@ namespace TagLib {
offset_t segmentDataOffset() const;
bool read(File &file) override;
bool readLimited(File &file, offset_t scanLimit);
std::unique_ptr<Matroska::Tag> parseTag() const;
std::unique_ptr<Matroska::Attachments> parseAttachments() const;
std::unique_ptr<Matroska::Chapters> parseChapters() const;

View File

@@ -47,7 +47,7 @@ std::unique_ptr<Matroska::Tag> EBML::MkTags::parse() const
{
auto mTag = std::make_unique<Matroska::Tag>();
mTag->setOffset(offset);
mTag->setSize(getSize() + padding);
mTag->setSize(getSize());
mTag->setID(static_cast<Matroska::Element::ID>(id));
// Loop through each <Tag> element

View File

@@ -19,10 +19,7 @@
***************************************************************************/
#include "ebmlutils.h"
#include <algorithm>
#include <random>
#include "tbytevector.h"
#include "matroskafile.h"
#include "tutils.h"

View File

@@ -19,9 +19,7 @@
***************************************************************************/
#include "ebmlvoidelement.h"
#include <algorithm>
#include "ebmlutils.h"
#include "tbytevector.h"

View File

@@ -19,9 +19,6 @@
***************************************************************************/
#include "matroskaattachments.h"
#include <algorithm>
#include "matroskaattachedfile.h"
#include "ebmlmkattachments.h"
#include "ebmlmasterelement.h"
@@ -140,14 +137,5 @@ ByteVector Matroska::Attachments::renderInternal()
attachments.appendElement(std::move(attachedFileElement));
}
// Pad to the previous size so the element keeps its slot in the file,
// unless this element is the trailing element of the segment in
// AvoidInsert mode -- shrinking from the end never inserts anything.
if(writeStyle() != WriteStyle::Compact &&
!(writeStyle() == WriteStyle::AvoidInsert && isTrailingInSegment())) {
const auto beforeSize = sizeRenderedOrWritten();
if(beforeSize > 0)
attachments.setMinRenderSize(beforeSize);
}
return attachments.render();
}

View File

@@ -153,7 +153,7 @@ namespace TagLib {
Time timeStart() const;
/*!
* Returns the timestamp of the end of the chapter in nanoseconds.
* Returns the timestamp of the start of the chapter in nanoseconds.
*/
Time timeEnd() const;

View File

@@ -24,9 +24,6 @@
***************************************************************************/
#include "matroskachapters.h"
#include <algorithm>
#include "matroskachapteredition.h"
#include "ebmlstringelement.h"
#include "ebmlbinaryelement.h"
@@ -150,14 +147,5 @@ ByteVector Matroska::Chapters::renderInternal()
chapters.appendElement(std::move(chapterEditionElement));
}
// Pad to the previous size so the element keeps its slot in the file,
// unless this element is the trailing element of the segment in
// AvoidInsert mode -- shrinking from the end never inserts anything.
if(writeStyle() != WriteStyle::Compact &&
!(writeStyle() == WriteStyle::AvoidInsert && isTrailingInSegment())) {
const auto beforeSize = sizeRenderedOrWritten();
if(beforeSize > 0)
chapters.setMinRenderSize(beforeSize);
}
return chapters.render();
}

View File

@@ -23,7 +23,6 @@
#include "tlist.h"
#include "tfile.h"
#include "tbytevector.h"
#include "ebmlvoidelement.h"
using namespace TagLib;
@@ -43,14 +42,6 @@ public:
// therefore rendering is required by default and needs to be explicitly set
// using setNeedsRender(false) together with overriding the write() method.
bool needsRender = true;
WriteStyle writeStyle = WriteStyle::Compact;
bool isLastElement = true;
bool isTrailingInSegment = false;
offset_t appendOffset = 0;
// Populated during render() for AvoidInsert+grow+non-last: the offset and
// original size of the slot that should be overwritten with a Void element.
offset_t voidAtOffset = 0;
offset_t voidAtSize = 0;
};
Matroska::Element::Element(ID id) :
@@ -125,24 +116,8 @@ bool Matroska::Element::render()
const auto data = renderInternal();
setNeedsRender(false);
if(const auto afterSize = data.size(); afterSize != beforeSize) {
if(e->writeStyle == WriteStyle::AvoidInsert && !e->isLastElement
&& afterSize > beforeSize && beforeSize > 0) {
// Record old slot for void-overwrite, move element to end of segment.
e->voidAtOffset = e->offset;
e->voidAtSize = beforeSize;
e->offset = e->appendOffset;
// Notify listeners that a new element of afterSize bytes appeared at
// appendOffset (which is past all other elements, so no offset shifts).
if(!emitSizeChanged(static_cast<offset_t>(afterSize))) {
return false;
}
// Update appendOffset for any subsequent AvoidInsert-grow in this round.
e->appendOffset += static_cast<offset_t>(afterSize);
}
else {
if(!emitSizeChanged(afterSize - beforeSize)) {
return false;
}
if(!emitSizeChanged(afterSize - beforeSize)) {
return false;
}
}
@@ -186,55 +161,8 @@ offset_t Matroska::Element::sizeRenderedOrWritten() const
return dataSize != 0 ? dataSize : e->size;
}
void Matroska::Element::setWriteStyle(WriteStyle style)
{
e->writeStyle = style;
}
Matroska::WriteStyle Matroska::Element::writeStyle() const
{
return e->writeStyle;
}
void Matroska::Element::setIsLastElement(bool isLast)
{
e->isLastElement = isLast;
}
void Matroska::Element::setAppendOffset(offset_t appendOffset)
{
e->appendOffset = appendOffset;
}
void Matroska::Element::setIsTrailingInSegment(bool isTrailing)
{
e->isTrailingInSegment = isTrailing;
}
bool Matroska::Element::isTrailingInSegment() const
{
return e->isTrailingInSegment;
}
bool Matroska::Element::wasMoved() const
{
// voidAtSize is set when the element was moved during render().
// After write() it is cleared, but the caller checks before write().
return e->voidAtOffset != 0 || e->voidAtSize != 0;
}
void Matroska::Element::write(File &file)
{
if(e->voidAtSize > 0) {
// AvoidInsert: overwrite the old slot with a Void element.
const auto voidData = EBML::VoidElement::renderSize(e->voidAtSize);
file.insert(voidData, e->voidAtOffset, e->voidAtSize);
e->voidAtOffset = 0;
// The element was moved to a new position (end of segment),
// so there are no existing bytes to replace at the new offset.
e->size = 0;
e->voidAtSize = 0;
}
file.insert(e->data, e->offset, e->size);
e->size = e->data.size();
}

View File

@@ -26,7 +26,6 @@
#include "taglib_export.h"
#include "taglib.h"
#include "tlist.h"
#include "matroskawritestyle.h"
namespace TagLib {
class File;
@@ -58,17 +57,6 @@ namespace TagLib {
bool emitSizeChanged(offset_t delta);
virtual bool sizeChanged(Element &caller, offset_t delta);
void setWriteStyle(WriteStyle style);
WriteStyle writeStyle() const;
void setIsLastElement(bool isLast);
void setAppendOffset(offset_t appendOffset);
bool wasMoved() const;
//! Mark this element as the trailing element of the segment (no other
//! element follows it in the file). Trailing elements may shrink even
//! in non-Compact write styles because no offsets need to be preserved.
void setIsTrailingInSegment(bool isTrailing);
bool isTrailingInSegment() const;
protected:
offset_t sizeRenderedOrWritten() const;

View File

@@ -19,10 +19,7 @@
***************************************************************************/
#include "matroskafile.h"
#include <algorithm>
#include <memory>
#include "matroskatag.h"
#include "matroskaattachments.h"
#include "matroskaattachedfile.h"
@@ -147,8 +144,6 @@ PropertyMap Matroska::File::setProperties(const PropertyMap &properties)
namespace {
constexpr offset_t FAST_SCAN_LIMIT = static_cast<offset_t>(512 * 1024);
String keyForAttachedFile(const Matroska::AttachedFile &attachedFile)
{
if(attachedFile.mediaType().startsWith("image/")) {
@@ -381,15 +376,10 @@ void Matroska::File::read(bool readProperties, Properties::ReadStyle readStyle)
head->skipData(*this);
}
offset_t maxOffset = fileLength - tell();
if (readStyle == Properties::ReadStyle::Fast && maxOffset > FAST_SCAN_LIMIT) {
maxOffset = FAST_SCAN_LIMIT;
}
// Find the Matroska segment in the file
const std::unique_ptr<EBML::MkSegment> segment(
EBML::element_cast<EBML::Element::Id::MkSegment>(
EBML::findElement(*this, EBML::Element::Id::MkSegment, maxOffset)
EBML::findElement(*this, EBML::Element::Id::MkSegment, fileLength - tell())
)
);
if(!segment) {
@@ -399,18 +389,14 @@ void Matroska::File::read(bool readProperties, Properties::ReadStyle readStyle)
}
// Read the segment into memory from file
d->segment = segment->parseSegment();
maxOffset = segment->getDataSize();
if (readStyle == Properties::ReadStyle::Fast && maxOffset > FAST_SCAN_LIMIT) {
maxOffset = FAST_SCAN_LIMIT;
}
if(!segment->readLimited(*this, maxOffset)) {
if(!segment->read(*this)) {
debug("Failed to read segment");
setValid(false);
return;
}
// Parse the elements
d->segment = segment->parseSegment();
d->seekHead = segment->parseSeekHead();
d->cues = segment->parseCues();
d->tag = segment->parseTag();
@@ -448,11 +434,6 @@ void Matroska::File::read(bool readProperties, Properties::ReadStyle readStyle)
}
bool Matroska::File::save()
{
return save(WriteStyle::Compact);
}
bool Matroska::File::save(WriteStyle writeStyle)
{
if(readOnly()) {
debug("Matroska::File::save() -- File is read only.");
@@ -516,75 +497,6 @@ bool Matroska::File::save(WriteStyle writeStyle)
renderList.sort(sortAscending);
renderList.append(newElements);
// Configure write style on each data element. Determines whether elements
// may be padded (DoNotShrink/AvoidInsert) or moved to the end (AvoidInsert).
// New elements (no prior size) are always written compactly.
if(writeStyle != WriteStyle::Compact) {
// Determine which existing data element has the highest file offset
// (i.e., is "last" among the data elements, before cues/seekHead/segment).
// New elements always go after existing ones and are treated as compact.
const Element *lastDataElement = nullptr;
for(const auto element : renderList) {
if(element->size() > 0)
lastDataElement = element;
}
// For AvoidInsert: an existing data element (Tags, Chapters, Attachments)
// located before the LAST Cluster must not be grown in-place. Doing so
// would shift later clusters and invalidate their cue positions. Such
// elements are voided at their original position and appended at the
// end of the segment instead. The boundary is the maximum cluster offset
// (derived from cue-point cluster positions). If no cue points are
// available, the Cues element offset is used as a safe upper bound
// (Cues are always after the last Cluster). A value of 0 means
// "no boundary" any offset compares >= 0, so the boundary check is
// a no-op in non-AvoidInsert modes.
offset_t audioBoundary = 0;
if(writeStyle == WriteStyle::AvoidInsert && d->cues) {
const offset_t segDataOffset = d->segment->dataOffset();
for(const auto &cp : d->cues->cuePointList()) {
for(const auto &ct : cp->cueTrackList()) {
audioBoundary = std::max(audioBoundary,
segDataOffset + ct->getClusterPosition());
}
}
if(audioBoundary == 0)
audioBoundary = d->cues->offset();
}
for(const auto element : renderList) {
if(element->size() > 0) {
element->setWriteStyle(writeStyle);
// An element is "last" only if it has the highest data-element
// offset AND sits past the last cluster. The latter is always true
// when audioBoundary == 0 (DoNotShrink, or AvoidInsert without cues).
element->setIsLastElement(element == lastDataElement
&& element->offset() >= audioBoundary);
}
}
// For AvoidInsert: identify the segment-trailing element (highest offset
// among data elements, Cues, SeekHead). The trailing element may shrink
// without padding -- there is nothing after it whose offset would shift,
// so a trailing void would be wasted space.
if(writeStyle == WriteStyle::AvoidInsert) {
Element *trailing = nullptr;
offset_t maxOffset = 0;
const auto consider = [&](Element *e) {
if(e && e->size() > 0 && e->offset() > maxOffset) {
maxOffset = e->offset();
trailing = e;
}
};
for(const auto element : renderList)
consider(element);
consider(d->cues.get());
consider(d->seekHead.get());
if(trailing)
trailing->setIsTrailingInSegment(true);
}
}
// Add our new elements to the Seek Head (if the file has one)
if(d->seekHead) {
const auto segmentDataOffset = d->segment->dataOffset();
@@ -627,12 +539,6 @@ bool Matroska::File::save(WriteStyle writeStyle)
bool rendering = true;
while(rendering && renderRound < 5) {
rendering = false;
// Initialize appendOffset for AvoidInsert elements at the start of each round.
if(writeStyle == WriteStyle::AvoidInsert) {
const offset_t appendOffset = d->segment->endOffset();
for(const auto element : renderList)
element->setAppendOffset(appendOffset);
}
for(const auto element : renderList) {
if(element->needsRender()) {
rendering = true;
@@ -644,51 +550,6 @@ bool Matroska::File::save(WriteStyle writeStyle)
++renderRound;
}
// For AvoidInsert: elements that were moved during rendering may have
// stale offsets if in-place elements grew after the move was computed.
// Re-assign their offsets sequentially from the correct position.
if(writeStyle == WriteStyle::AvoidInsert) {
// Collect moved elements in render order (= ascending original-offset order
// = order they appear in renderList before any re-sort).
List<Element *> movedElements;
offset_t totalMovedSize = 0;
for(const auto element : renderList) {
if(element->wasMoved()) {
movedElements.append(element);
totalMovedSize += static_cast<offset_t>(element->data().size());
}
}
if(!movedElements.isEmpty()) {
// The segment end includes in-place growths AND all moved element sizes.
// The moved elements start right after all in-place content.
offset_t appendAt = d->segment->endOffset() - totalMovedSize;
for(const auto element : movedElements) {
element->setOffset(appendAt);
appendAt += static_cast<offset_t>(element->data().size());
}
}
}
// For elements that were moved to the end by AvoidInsert, update their
// seek head entry to reflect the new file position.
if(writeStyle == WriteStyle::AvoidInsert && d->seekHead) {
const offset_t segDataOffset = d->segment->dataOffset();
for(const auto element : renderList) {
if(element->wasMoved()) {
d->seekHead->updateEntry(element->id(), element->offset() - segDataOffset);
}
}
// Re-render the seekHead (and anything it affects) after updating entries.
// The seekHead slot was pre-padded, so this should not cause size changes.
d->seekHead->setNeedsRender(true);
for(const auto element : renderList) {
if(element->needsRender()) {
if(!element->render())
return false;
}
}
}
// Write out to file
renderList.sort(sortAscending);
for(const auto element : renderList)

View File

@@ -24,7 +24,6 @@
#include "taglib_export.h"
#include "tfile.h"
#include "matroskaproperties.h"
#include "matroskawritestyle.h"
//! An implementation of Matroska metadata
namespace TagLib::Matroska {
@@ -146,13 +145,6 @@ namespace TagLib::Matroska {
*/
bool save() override;
/*!
* Save the file with the specified write style.
*
* This returns \c true if the save was successful.
*/
bool save(WriteStyle style);
/*!
* Returns a pointer to the attachments of the file.
*

View File

@@ -19,9 +19,6 @@
***************************************************************************/
#include "matroskaseekhead.h"
#include <algorithm>
#include "ebmlmkseekhead.h"
#include "ebmlbinaryelement.h"
#include "ebmluintelement.h"
@@ -57,6 +54,7 @@ bool Matroska::SeekHead::isValid(TagLib::File &file) const
void Matroska::SeekHead::addEntry(const Element &element)
{
entries.append({element.id(), element.offset()});
debug("adding to seekhead");
setNeedsRender(true);
}
@@ -66,22 +64,6 @@ void Matroska::SeekHead::addEntry(ID id, offset_t offset)
setNeedsRender(true);
}
void Matroska::SeekHead::updateEntry(ID id, offset_t newOffset)
{
for(auto &entry : entries) {
if(entry.first == id) {
entry.second = newOffset;
setNeedsRender(true);
return;
}
}
}
const List<std::pair<unsigned int, offset_t>> &Matroska::SeekHead::entryList() const
{
return entries;
}
ByteVector Matroska::SeekHead::renderInternal()
{
const auto beforeSize = sizeRenderedOrWritten();

View File

@@ -39,8 +39,6 @@ namespace TagLib {
bool isValid(TagLib::File &file) const;
void addEntry(const Element &element);
void addEntry(ID id, offset_t offset);
void updateEntry(ID id, offset_t offset);
const List<std::pair<unsigned int, offset_t>> &entryList() const;
void write(TagLib::File &file) override;
void sort();
bool sizeChanged(Element &caller, offset_t delta) override;

View File

@@ -69,8 +69,3 @@ offset_t Matroska::Segment::dataOffset() const
{
return offset() + sizeLength;
}
offset_t Matroska::Segment::endOffset() const
{
return dataOffset() + dataSize;
}

View File

@@ -33,7 +33,6 @@ namespace TagLib::Matroska {
bool render() override;
bool sizeChanged(Element &caller, offset_t delta) override;
offset_t dataOffset() const;
offset_t endOffset() const;
private:
ByteVector renderInternal() override;

View File

@@ -19,11 +19,9 @@
***************************************************************************/
#include "matroskatag.h"
#include <algorithm>
#include <array>
#include <tuple>
#include "ebmlmasterelement.h"
#include "ebmlstringelement.h"
#include "ebmlbinaryelement.h"
@@ -366,16 +364,6 @@ ByteVector Matroska::Tag::renderInternal()
}
tags.appendElement(std::move(tag));
}
// Pad to the previous size so the element keeps its slot in the file,
// unless this element is the trailing element of the segment in
// AvoidInsert mode -- shrinking from the end never inserts anything,
// so the trailing void would be wasted space.
if(writeStyle() != WriteStyle::Compact &&
!(writeStyle() == WriteStyle::AvoidInsert && isTrailingInSegment())) {
const auto beforeSize = sizeRenderedOrWritten();
if(beforeSize > 0)
tags.setMinRenderSize(beforeSize);
}
return tags.render();
}
@@ -563,11 +551,10 @@ StringList Matroska::Tag::complexPropertyKeys() const
{
StringList keys;
for(const SimpleTag &t : std::as_const(d->tags)) {
if((t.type() != SimpleTag::StringType ||
t.trackUid() != 0 || t.editionUid() != 0 ||
t.chapterUid() != 0 || t.attachmentUid() != 0 ||
translateTag(t.name(), t.targetTypeValue()).isEmpty()) &&
!keys.contains(t.name())) {
if(t.type() != SimpleTag::StringType ||
t.trackUid() != 0 || t.editionUid() != 0 ||
t.chapterUid() != 0 || t.attachmentUid() != 0 ||
translateTag(t.name(), t.targetTypeValue()).isEmpty()) {
keys.append(t.name());
}
}

View File

@@ -1,49 +0,0 @@
/***************************************************************************
copyright : (C) 2026 by Urs Fleisch
email : ufleisch@users.sourceforge.net
***************************************************************************/
/***************************************************************************
* 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_MATROSKAWRITESTYLE_H
#define TAGLIB_MATROSKAWRITESTYLE_H
namespace TagLib::Matroska {
/*!
* Controls the trade-off between file size and write speed when saving.
* Mode of writing tags, attachments and chapters to the file.
* For very large files and/or slow (network) filesystems, using
* \c AvoidInsert will reduce write time significantly.
*/
enum class WriteStyle {
//! Write tags, attachments and chapters as compact as possible (default).
Compact,
//! Do not shrink elements; add void padding when content gets smaller.
//! Allow inserts when content gets larger.
DoNotShrink,
//! Like \c DoNotShrink but also avoid inserts for non-last elements:
//! replace a growing non-last element with a void of the old size and
//! append the new element at the end of the segment.
AvoidInsert
};
}
#endif //TAGLIB_MATROSKAWRITESTYLE_H

View File

@@ -25,8 +25,6 @@
#include "modfile.h"
#include <algorithm>
#include "tstringlist.h"
#include "tdebug.h"
#include "tpropertymap.h"

View File

@@ -25,7 +25,6 @@
#include "mp4atom.h"
#include <algorithm>
#include <array>
#include <climits>
#include <utility>
@@ -52,7 +51,7 @@ public:
AtomList children;
};
MP4::Atom::Atom(File *file, int depth)
MP4::Atom::Atom(File *file)
: d(std::make_unique<AtomPrivate>(file->tell()))
{
d->children.setAutoDelete(true);
@@ -110,13 +109,8 @@ MP4::Atom::Atom(File *file, int depth)
else if(d->name == "stsd") {
file->seek(8, File::Current);
}
static constexpr int MAX_MP4_ATOM_DEPTH = 64;
if(depth > MAX_MP4_ATOM_DEPTH) {
debug("MP4: Maximum nesting depth exceeded");
return;
}
while(file->tell() < d->offset + d->length) {
auto child = new MP4::Atom(file, depth + 1);
auto child = new MP4::Atom(file);
d->children.append(child);
if(child->d->length == 0)
return;
@@ -128,11 +122,6 @@ MP4::Atom::Atom(File *file, int depth)
file->seek(d->offset + d->length);
}
MP4::Atom::Atom(File *file)
: Atom(file, 0)
{
}
MP4::Atom::~Atom() = default;
MP4::Atom *
@@ -223,8 +212,6 @@ public:
MP4::Atoms::Atoms(File *file) :
d(std::make_unique<AtomsPrivate>())
{
static constexpr int MAX_MP4_ATOM_COUNT_PER_LEVEL = 5000;
d->atoms.setAutoDelete(true);
file->seek(0, File::End);
@@ -235,13 +222,6 @@ MP4::Atoms::Atoms(File *file) :
d->atoms.append(atom);
if (atom->length() == 0)
break;
if(d->atoms.size() > MAX_MP4_ATOM_COUNT_PER_LEVEL) {
debug("MP4: Maximum atom count exceeded");
// Make sure the file is detected as invalid.
d->atoms.clear();
break;
}
}
}

View File

@@ -35,48 +35,27 @@ namespace TagLib {
namespace MP4 {
enum AtomDataType {
//! For use with tags for which no type needs to be indicated because only one type is allowed
TypeImplicit = 0,
//! Without any count or null terminator
TypeUTF8 = 1,
//! Also known as UTF-16BE
TypeUTF16 = 2,
//! Deprecated unless it is needed for special Japanese characters
TypeSJIS = 3,
//! The HTML file header specifies which HTML version
TypeHTML = 6,
//! The XML header must identify the DTD or schemas
TypeXML = 7,
//! Also known as GUID; stored as 16 bytes in binary (valid as an ID)
TypeUUID = 8,
//! Stored as UTF-8 text (valid as an ID)
TypeISRC = 9,
//! Stored as UTF-8 text (valid as an ID)
TypeMI3P = 10,
//! (Deprecated) A GIF image
TypeGIF = 12,
//! A JPEG image
TypeJPEG = 13,
//! A PNG image
TypePNG = 14,
//! Absolute, in UTF-8 characters
TypeURL = 15,
//! In milliseconds, 32-bit integer
TypeDuration = 16,
//! In UTC, counting seconds since midnight, January 1, 1904; 32 or 64-bits
TypeDateTime = 17,
//! A list of enumerated values
TypeGenred = 18,
//! A signed big-endian integer with length one of { 1,2,3,4,8 } bytes
TypeInteger = 21,
//! RIAA parental advisory; { -1=no, 1=yes, 0=unspecified }, 8-bit integer
TypeRIAAPA = 24,
//! Universal Product Code, in text UTF-8 format (valid as an ID)
TypeUPC = 25,
//! Windows bitmap image
TypeBMP = 27,
//! Undefined
TypeUndefined = 255
TypeImplicit = 0, // for use with tags for which no type needs to be indicated because only one type is allowed
TypeUTF8 = 1, // without any count or null terminator
TypeUTF16 = 2, // also known as UTF-16BE
TypeSJIS = 3, // deprecated unless it is needed for special Japanese characters
TypeHTML = 6, // the HTML file header specifies which HTML version
TypeXML = 7, // the XML header must identify the DTD or schemas
TypeUUID = 8, // also known as GUID; stored as 16 bytes in binary (valid as an ID)
TypeISRC = 9, // stored as UTF-8 text (valid as an ID)
TypeMI3P = 10, // stored as UTF-8 text (valid as an ID)
TypeGIF = 12, // (deprecated) a GIF image
TypeJPEG = 13, // a JPEG image
TypePNG = 14, // a PNG image
TypeURL = 15, // absolute, in UTF-8 characters
TypeDuration = 16, // in milliseconds, 32-bit integer
TypeDateTime = 17, // in UTC, counting seconds since midnight, January 1, 1904; 32 or 64-bits
TypeGenred = 18, // a list of enumerated values
TypeInteger = 21, // a signed big-endian integer with length one of { 1,2,3,4,8 } bytes
TypeRIAAPA = 24, // RIAA parental advisory; { -1=no, 1=yes, 0=unspecified }, 8-bit integer
TypeUPC = 25, // Universal Product Code, in text UTF-8 format (valid as an ID)
TypeBMP = 27, // Windows bitmap image
TypeUndefined = 255 // undefined
};
#ifndef DO_NOT_DOCUMENT
@@ -110,9 +89,6 @@ namespace TagLib {
const ByteVector &name() const;
const AtomList &children() const;
protected:
Atom(File *file, int depth);
private:
class AtomPrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE

View File

@@ -1,89 +0,0 @@
/**************************************************************************
copyright : (C) 2026 by Ryan Francesconi
**************************************************************************/
/***************************************************************************
* 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 "mp4chapter.h"
#include "tstring.h"
using namespace TagLib;
class MP4::Chapter::ChapterPrivate
{
public:
ChapterPrivate() = default;
~ChapterPrivate() = default;
String title;
long long startTime {0};
};
MP4::Chapter::Chapter(const String &title, long long startTime) :
d(std::make_unique<ChapterPrivate>())
{
d->title = title;
d->startTime = startTime;
}
MP4::Chapter::Chapter(const Chapter &other) :
d(std::make_unique<ChapterPrivate>(*other.d))
{
}
MP4::Chapter::Chapter(Chapter &&other) noexcept = default;
MP4::Chapter::~Chapter() = default;
MP4::Chapter &MP4::Chapter::operator=(const Chapter &other)
{
Chapter(other).swap(*this);
return *this;
}
MP4::Chapter &MP4::Chapter::operator=(
Chapter &&other) noexcept = default;
bool MP4::Chapter::operator==(const Chapter &other) const
{
return title() == other.title() && startTime() == other.startTime();
}
bool MP4::Chapter::operator!=(const Chapter &other) const
{
return !(*this == other);
}
void MP4::Chapter::swap(Chapter &other) noexcept
{
using std::swap;
swap(d, other.d);
}
const String &MP4::Chapter::title() const
{
return d->title;
}
long long MP4::Chapter::startTime() const
{
return d->startTime;
}

View File

@@ -1,108 +0,0 @@
/**************************************************************************
copyright : (C) 2026 by Ryan Francesconi
**************************************************************************/
/***************************************************************************
* 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_MP4CHAPTER_H
#define TAGLIB_MP4CHAPTER_H
#include <memory>
#include "taglib_export.h"
#include "tlist.h"
namespace TagLib {
class String;
namespace MP4 {
/*!
* A single Nero-style chapter marker.
*/
class TAGLIB_EXPORT Chapter {
public:
/*!
* Construct a chapter.
*/
Chapter(const String &title, long long startTime);
/*!
* Construct a chapter as a copy of \a other.
*/
Chapter(const Chapter &other);
/*!
* Construct a chapter moving from \a other.
*/
Chapter(Chapter &&other) noexcept;
/*!
* Destroys this chapter.
*/
~Chapter();
/*!
* Copies the contents of \a other into this object.
*/
Chapter &operator=(const Chapter &other);
/*!
* Moves the contents of \a other into this object.
*/
Chapter &operator=(Chapter &&other) noexcept;
/*!
* Returns \c true if the chapter and \a other contain the same data.
*/
bool operator==(const Chapter &other) const;
/*!
* Returns \c true if the chapter and \a other differ in data.
*/
bool operator!=(const Chapter &other) const;
/*!
* Exchanges the content of the object with the content of \a other.
*/
void swap(Chapter &other) noexcept;
/*!
* Returns the title representing the chapter.
*/
const String &title() const;
/*!
* Returns the start time in milliseconds.
*/
long long startTime() const;
private:
class ChapterPrivate;
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
std::unique_ptr<ChapterPrivate> d;
};
//! List of chapters.
using ChapterList = List<Chapter>;
} // namespace MP4
} // namespace TagLib
#endif

View File

@@ -1,126 +0,0 @@
/**************************************************************************
copyright : (C) 2006 by Urs Fleisch
email : ufleisch@users.sourceforge.net
**************************************************************************/
/***************************************************************************
* 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_MP4CHAPTERHOLDER_H
#define TAGLIB_MP4CHAPTERHOLDER_H
#include "mp4chapter.h"
namespace TagLib {
class File;
namespace MP4 {
/*!
* Base class to hold chapters and store modified state.
*/
class ChapterHolder {
public:
/*!
* Get list of chapters.
*/
ChapterList chapters() const { return chapterList; }
/*!
* Set list of chapters.
*/
void setChapters(const ChapterList &chapters) { chapterList = chapters; }
/*!
* Returns \c true if the list of chapters has been modified.
*/
bool isModified() const { return modified; }
/*!
* Set if the contained chapters are modified.
*/
void setModified(bool chaptersModified) { modified = chaptersModified; }
protected:
ChapterList chapterList;
bool modified = false;
};
/*!
* Lazily fetch list of chapters.
* @tparam T class derived from ChapterHolder and implementing read(File *)
* @param holder unique pointer to holder, initially null
* @param file file with chapters
* @return list of chapters, empty if no chapters found.
*/
template <typename T>
ChapterList getChaptersLazy(std::unique_ptr<T> &holder, TagLib::File *file)
{
if (!holder) {
holder = std::make_unique<T>();
holder->read(file);
}
return holder->chapters();
}
/*!
* Lazily set a list of chapters.
* @tparam T class derived from ChapterHolder
* @param holder unique pointer to holder, initially null
* @param chapters list of chapters to set
*/
template <typename T>
void setChaptersLazy(std::unique_ptr<T> &holder, const ChapterList& chapters)
{
if (!holder) {
holder = std::make_unique<T>();
// The chapters have not been read before, so we do not know their
// current state and mark them as modified. Otherwise, the check below
// would not set the chapters if they are empty.
holder->setModified(true);
}
if(holder->isModified() || holder->chapters() != chapters) {
holder->setChapters(chapters);
holder->setModified(true);
}
}
/*!
* Save a list of chapters if it has been modified.
* @tparam T class derived from ChapterHolder and implementing write(File *)
* @param holder unique pointer to holder, initially null
* @param file file with chapters
* @return true if write successful or not modified.
*/
template <typename T>
bool saveChaptersIfModified(std::unique_ptr<T> &holder, TagLib::File *file)
{
if(holder && holder->isModified()) {
if(holder->write(file)) {
holder->setModified(false);
return true;
}
return false;
}
return true;
}
} // namespace MP4
} // namespace TagLib
#endif

View File

@@ -30,8 +30,6 @@
#include "tagutils.h"
#include "mp4itemfactory.h"
#include "mp4nerochapterlist.h"
#include "mp4qtchapterlist.h"
using namespace TagLib;
@@ -50,8 +48,6 @@ public:
std::unique_ptr<MP4::Tag> tag;
std::unique_ptr<MP4::Atoms> atoms;
std::unique_ptr<MP4::Properties> properties;
std::unique_ptr<MP4::NeroChapterList> neroChapterList;
std::unique_ptr<MP4::QtChapterList> qtChapterList;
};
////////////////////////////////////////////////////////////////////////////////
@@ -115,26 +111,6 @@ MP4::Properties *MP4::File::audioProperties() const
return d->properties.get();
}
MP4::ChapterList MP4::File::neroChapters()
{
return getChaptersLazy(d->neroChapterList, this);
}
void MP4::File::setNeroChapters(const ChapterList& chapters)
{
setChaptersLazy(d->neroChapterList, chapters);
}
MP4::ChapterList MP4::File::qtChapters()
{
return getChaptersLazy(d->qtChapterList, this);
}
void MP4::File::setQtChapters(const ChapterList& chapters)
{
setChaptersLazy(d->qtChapterList, chapters);
}
void
MP4::File::read(bool readProperties)
{
@@ -172,9 +148,7 @@ MP4::File::save()
return false;
}
return d->tag->save() &&
saveChaptersIfModified(d->neroChapterList, this) &&
saveChaptersIfModified(d->qtChapterList, this);
return d->tag->save();
}
bool

View File

@@ -31,7 +31,6 @@
#include "mp4tag.h"
#include "tag.h"
#include "mp4properties.h"
#include "mp4chapter.h"
namespace TagLib {
//! An implementation of MP4 (AAC, ALAC, ...) metadata
@@ -131,26 +130,6 @@ namespace TagLib {
*/
Properties *audioProperties() const override;
/*!
* Returns the Nero style chapters for this file.
*/
ChapterList neroChapters();
/*!
* Sets the Nero style chapters for this file.
*/
void setNeroChapters(const ChapterList &chapters);
/*!
* Returns the QuickTime chapters for this file.
*/
ChapterList qtChapters();
/*!
* Sets the QuickTime style chapters for this file.
*/
void setQtChapters(const ChapterList &chapters);
/*!
* Save the file.
*

View File

@@ -25,7 +25,6 @@
#include "mp4itemfactory.h"
#include <mutex>
#include <utility>
#include "tbytevector.h"
@@ -48,8 +47,6 @@ public:
NameHandlerMap handlerTypeForName;
Map<ByteVector, String> propertyKeyForName;
Map<String, ByteVector> nameForPropertyKey;
mutable std::once_flag handlerMapOnce;
mutable std::once_flag propertyMapsOnce;
};
ItemFactory ItemFactory::factory;
@@ -242,11 +239,9 @@ std::pair<String, StringList> ItemFactory::itemToProperty(
String ItemFactory::propertyKeyForName(const ByteVector &name) const
{
std::call_once(d->propertyMapsOnce, [this] {
if(d->propertyKeyForName.isEmpty()) {
d->propertyKeyForName = namePropertyMap();
for(const auto &[k, t] : std::as_const(d->propertyKeyForName))
d->nameForPropertyKey[t] = k;
});
}
String key = d->propertyKeyForName.value(name);
if(key.isEmpty() && name.startsWith(freeFormPrefix)) {
key = name.mid(std::size(freeFormPrefix) - 1);
@@ -256,11 +251,14 @@ String ItemFactory::propertyKeyForName(const ByteVector &name) const
ByteVector ItemFactory::nameForPropertyKey(const String &key) const
{
std::call_once(d->propertyMapsOnce, [this] {
d->propertyKeyForName = namePropertyMap();
for(const auto &[k, t] : std::as_const(d->propertyKeyForName))
if(d->nameForPropertyKey.isEmpty()) {
if(d->propertyKeyForName.isEmpty()) {
d->propertyKeyForName = namePropertyMap();
}
for(const auto &[k, t] : std::as_const(d->propertyKeyForName)) {
d->nameForPropertyKey[t] = k;
});
}
}
ByteVector name = d->nameForPropertyKey.value(key);
if(name.isEmpty() && !key.isEmpty()) {
const auto &firstChar = key[0];
@@ -319,9 +317,9 @@ ItemFactory::NameHandlerMap ItemFactory::nameHandlerMap() const
ItemFactory::ItemHandlerType ItemFactory::handlerTypeForName(
const ByteVector &name) const
{
std::call_once(d->handlerMapOnce, [this] {
if(d->handlerTypeForName.isEmpty()) {
d->handlerTypeForName = nameHandlerMap();
});
}
auto type = d->handlerTypeForName.value(name, ItemHandlerType::Unknown);
if (type == ItemHandlerType::Unknown && name.size() == 4) {
type = ItemHandlerType::Text;

View File

@@ -1,320 +0,0 @@
/**************************************************************************
copyright : (C) 2026 by Ryan Francesconi
**************************************************************************/
/***************************************************************************
* 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 "mp4nerochapterlist.h"
#include <algorithm>
#include "tdebug.h"
#include "mp4file.h"
#include "mp4atom.h"
using namespace TagLib;
namespace
{
ByteVector renderAtom(const ByteVector &name, const ByteVector &data)
{
return ByteVector::fromUInt(data.size() + 8) + name + data;
}
// Update parent atom sizes along a path when child size changes by delta.
// Mirrors MP4::Tag::updateParents().
void updateParentSizes(TagLib::File *file, const MP4::AtomList &path,
offset_t delta, int ignore = 0)
{
if(static_cast<int>(path.size()) <= ignore)
return;
auto itEnd = path.end();
std::advance(itEnd, 0 - ignore);
for(auto it = path.begin(); it != itEnd; ++it) {
file->seek((*it)->offset());
if(const long size = file->readBlock(4).toUInt(); size == 1) {
// 64-bit size
file->seek(4, TagLib::File::Current);
const long long longSize = file->readBlock(8).toLongLong();
file->seek((*it)->offset() + 8);
file->writeBlock(ByteVector::fromLongLong(longSize + delta));
}
else {
// 32-bit size
file->seek((*it)->offset());
file->writeBlock(ByteVector::fromUInt(static_cast<unsigned int>(size + delta)));
}
}
}
// Update stco/co64/tfhd chunk offsets when file content shifts.
// Mirrors MP4::Tag::updateOffsets().
void updateChunkOffsets(TagLib::File *file, const MP4::Atoms *atoms,
offset_t delta, offset_t offset)
{
if(const MP4::Atom *moov = atoms->find("moov")) {
const MP4::AtomList stco = moov->findall("stco", true);
for(const auto &atom : stco) {
if(atom->offset() > offset)
atom->addToOffset(delta);
file->seek(atom->offset() + 12);
ByteVector data = file->readBlock(atom->length() - 12);
unsigned int count = data.toUInt();
file->seek(atom->offset() + 16);
unsigned int pos = 4;
const unsigned int maxPos = data.size() - 4;
while(count-- && pos <= maxPos) {
auto o = static_cast<offset_t>(data.toUInt(pos));
if(o > offset)
o += delta;
file->writeBlock(ByteVector::fromUInt(static_cast<unsigned int>(o)));
pos += 4;
}
}
const MP4::AtomList co64 = moov->findall("co64", true);
for(const auto &atom : co64) {
if(atom->offset() > offset)
atom->addToOffset(delta);
file->seek(atom->offset() + 12);
ByteVector data = file->readBlock(atom->length() - 12);
unsigned int count = data.toUInt();
file->seek(atom->offset() + 16);
unsigned int pos = 4;
const unsigned int maxPos = data.size() - 8;
while(count-- && pos <= maxPos) {
long long o = data.toLongLong(pos);
if(o > offset)
o += delta;
file->writeBlock(ByteVector::fromLongLong(o));
pos += 8;
}
}
}
if(const MP4::Atom *moof = atoms->find("moof")) {
const MP4::AtomList tfhd = moof->findall("tfhd", true);
for(const auto &atom : tfhd) {
if(atom->offset() > offset)
atom->addToOffset(delta);
file->seek(atom->offset() + 9);
ByteVector data = file->readBlock(atom->length() - 9);
if(const unsigned int flags = data.toUInt(0, 3, true);
flags & 1) {
long long o = data.toLongLong(7U);
if(o > offset)
o += delta;
file->seek(atom->offset() + 16);
file->writeBlock(ByteVector::fromLongLong(o));
}
}
}
}
// Build the binary payload for a chpl atom (version 1).
ByteVector renderChplData(const MP4::ChapterList &chapters)
{
const unsigned int count = std::min(chapters.size(), 255U);
ByteVector data;
// Version (1 byte) + flags (3 bytes) + reserved (4 bytes)
data.append(static_cast<char>(0x01)); // version 1
data.append(ByteVector(3, '\0')); // flags
data.append(ByteVector(4, '\0')); // reserved
// Chapter count
data.append(static_cast<char>(count & 0xFF));
unsigned int i = 0;
for(const auto &ch : chapters) {
if(i++ >= count)
break;
// Start time: 8 bytes big-endian, on-disk format is 100-nanosecond units
data.append(ByteVector::fromLongLong(ch.startTime() * 10000LL));
// Title: 1-byte length + UTF-8 bytes (max 255 bytes)
ByteVector titleBytes = ch.title().data(String::UTF8);
const unsigned int titleLen = std::min(titleBytes.size(), 255U);
data.append(static_cast<char>(titleLen & 0xFF));
if(titleLen > 0)
data.append(titleBytes.mid(0, titleLen));
}
return data;
}
// Parse the binary content of a chpl atom into a ChapterList.
MP4::ChapterList parseChplData(const ByteVector &data)
{
MP4::ChapterList chapters;
// Minimum: version(1) + flags(3) + count(1) = 5 bytes (version 0 layout)
if(data.size() < 5)
return chapters;
unsigned int pos = 0;
const auto version = static_cast<unsigned char>(data[pos++]);
// Skip flags (3 bytes)
pos += 3;
// Version 1 has 4 reserved bytes
if(version >= 1)
pos += 4;
if(pos >= data.size())
return chapters;
const unsigned int count = static_cast<unsigned char>(data[pos++]);
for(unsigned int i = 0; i < count && pos + 9 <= data.size(); ++i) {
const long long startTime100ns = data.toLongLong(pos);
pos += 8;
const unsigned int titleLen = static_cast<unsigned char>(data[pos++]);
String title;
if(titleLen > 0 && pos + titleLen <= data.size()) {
title = String(data.mid(pos, titleLen), String::UTF8);
pos += titleLen;
}
chapters.append(MP4::Chapter(title, startTime100ns / 10000LL));
}
return chapters;
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
bool MP4::NeroChapterList::read(TagLib::File *file)
{
const Atoms atoms(file);
const Atom *chpl = atoms.find("moov", "udta", "chpl");
modified = false;
chapterList.clear();
if(chpl) {
// Read the atom content (skip 8-byte atom header)
file->seek(chpl->offset() + 8);
const ByteVector data = file->readBlock(chpl->length() - 8);
chapterList = parseChplData(data);
return true;
}
return false;
}
bool MP4::NeroChapterList::write(TagLib::File *file)
{
// Writing an empty list is equivalent to removing the chapters.
if(chapterList.isEmpty())
return remove(file);
const Atoms atoms(file);
if(!atoms.find("moov")) {
debug("MP4ChapterList::write() -- No moov atom found");
return false;
}
const ByteVector chplPayload = renderChplData(chapterList);
const ByteVector chplAtom = renderAtom("chpl", chplPayload);
if(const Atom *existingChpl = atoms.find("moov", "udta", "chpl")) {
// Replace existing chpl atom
const offset_t offset = existingChpl->offset();
const offset_t oldLength = existingChpl->length();
const offset_t delta = static_cast<offset_t>(chplAtom.size()) - oldLength;
file->insert(chplAtom, offset, oldLength);
if(delta != 0) {
// Update parent sizes: moov and udta
const AtomList parentPath = atoms.path("moov", "udta", "chpl");
updateParentSizes(file, parentPath, delta, 1); // ignore chpl itself
updateChunkOffsets(file, &atoms, delta, offset);
}
}
else {
// Need to insert a new chpl atom
if(AtomList udtaPath = atoms.path("moov", "udta"); udtaPath.size() == 2) {
// udta exists -- insert chpl at the beginning of udta's content
const offset_t insertOffset = udtaPath.back()->offset() + 8;
file->insert(chplAtom, insertOffset, 0);
updateParentSizes(file, udtaPath, chplAtom.size());
updateChunkOffsets(file, &atoms, chplAtom.size(), insertOffset);
}
else {
// No udta -- insert udta + chpl at the beginning of moov's content
const ByteVector udtaAtom = renderAtom("udta", chplAtom);
AtomList moovPath = atoms.path("moov");
if(moovPath.isEmpty()) {
debug("MP4ChapterList::write() -- No moov atom in path");
return false;
}
const offset_t insertOffset = moovPath.back()->offset() + 8;
file->insert(udtaAtom, insertOffset, 0);
updateParentSizes(file, moovPath, udtaAtom.size());
updateChunkOffsets(file, &atoms, udtaAtom.size(), insertOffset);
}
}
modified = false;
return true;
}
bool MP4::NeroChapterList::remove(TagLib::File *file)
{
const Atoms atoms(file);
chapterList.clear();
modified = false;
const Atom *chpl = atoms.find("moov", "udta", "chpl");
if(!chpl) {
// No chpl atom -- nothing to remove
return true;
}
const offset_t offset = chpl->offset();
const offset_t length = chpl->length();
file->removeBlock(offset, length);
// Update parent sizes with negative delta
const AtomList parentPath = atoms.path("moov", "udta", "chpl");
updateParentSizes(file, parentPath, -length, 1); // ignore chpl itself
updateChunkOffsets(file, &atoms, -length, offset);
return true;
}

View File

@@ -1,66 +0,0 @@
/**************************************************************************
copyright : (C) 2026 by Ryan Francesconi
**************************************************************************/
/***************************************************************************
* 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_MP4CHAPTERLIST_H
#define TAGLIB_MP4CHAPTERLIST_H
#include "mp4chapterholder.h"
namespace TagLib {
class File;
namespace MP4 {
/*!
* Reads, writes, and removes Nero-style chapter markers (chpl atom)
* from MP4 files. Operates independently of MP4::Tag -- the chpl atom
* lives at moov/udta/chpl, a sibling of the metadata ilst path.
*/
class NeroChapterList : public ChapterHolder
{
public:
/*!
* Reads chapter markers from the already-opened \a file.
* Returns \c false if the file has no chpl atom.
*/
bool read(TagLib::File *file);
/*!
* Writes chapter markers to the already-opened \a file,
* replacing any existing chpl atom.
* The chapter count is capped at 255 (Nero format limit).
* Returns \c true on success.
*/
bool write(TagLib::File *file);
/*!
* Removes the chpl atom from the already-opened \a file.
* Returns \c true on success, or if no chpl atom exists.
*/
bool remove(TagLib::File *file);
};
} // namespace MP4
} // namespace TagLib
#endif

View File

@@ -224,7 +224,7 @@ MP4::Properties::read(File *file, const Atoms *atoms)
pos += 10;
if(const unsigned int bitrateValue = data.toUInt(pos);
bitrateValue != 0 || d->length <= 0) {
d->bitrate = static_cast<int>(bitrateValue / 1000.0 + 0.5);
d->bitrate = static_cast<int>((bitrateValue + 500) / 1000.0 + 0.5);
}
else {
d->bitrate = static_cast<int>(

File diff suppressed because it is too large Load Diff

View File

@@ -1,78 +0,0 @@
/**************************************************************************
copyright : (C) 2026 by Ryan Francesconi
**************************************************************************/
/***************************************************************************
* 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_MP4QTCHAPTERLIST_H
#define TAGLIB_MP4QTCHAPTERLIST_H
#include "mp4chapterholder.h"
namespace TagLib {
class File;
namespace MP4 {
/*!
* Reads, writes, and removes QuickTime-style chapter tracks from MP4
* files. A QT chapter track is a disabled text track (\c hdlr type
* \c "text") referenced by a \c chap track-reference in the audio
* track's \c tref box. This format is understood by QuickTime,
* iTunes, Final Cut, Logic, DaVinci Resolve, Twisted Wave, and most
* other Apple/macOS software.
*
* The existing \c MP4ChapterList class handles Nero-style \c chpl
* atoms, which are a different (and less widely supported) chapter
* format.
*
* Chapter times use the same 100-nanosecond unit convention as
* \c MP4ChapterList so that existing \c Chapter / \c ChapterList
* types can be shared.
*/
class QtChapterList : public ChapterHolder
{
public:
/*!
* Reads chapter markers from the QuickTime chapter track in the
* already-opened \a file.
* Returns \c false if the file has no chapter track.
*/
bool read(TagLib::File *file);
/*!
* Writes chapter markers as a QuickTime chapter track to the
* already-opened \a file, replacing any existing chapter track.
* Returns \c true on success.
*/
bool write(TagLib::File *file);
/*!
* Removes the QuickTime chapter track and its \c tref/chap
* reference from the already-opened \a file.
* Returns \c true on success, or if no chapter track exists.
*/
bool remove(TagLib::File *file);
};
} // namespace MP4
} // namespace TagLib
#endif

View File

@@ -200,8 +200,7 @@ MP4::Tag::updateOffsets(offset_t delta, offset_t offset)
unsigned int count = data.toUInt();
d->file->seek(atom->offset() + 16);
unsigned int pos = 4;
const unsigned int maxPos = data.size() - 4;
while(count-- && pos <= maxPos) {
while(count--) {
auto o = static_cast<offset_t>(data.toUInt(pos));
if(o > offset) {
o += delta;
@@ -221,8 +220,7 @@ MP4::Tag::updateOffsets(offset_t delta, offset_t offset)
unsigned int count = data.toUInt();
d->file->seek(atom->offset() + 16);
unsigned int pos = 4;
const unsigned int maxPos = data.size() - 8;
while(count-- && pos <= maxPos) {
while(count--) {
long long o = data.toLongLong(pos);
if(o > offset) {
o += delta;

View File

@@ -93,32 +93,28 @@ namespace TagLib {
unsigned long sampleFrames() const;
/*!
* Returns the track gain as an integer value.
*
* To convert to dB: trackGain in dB = 64.82 - (trackGain / 256)
* Returns the track gain as an integer value,
* to convert to dB: trackGain in dB = 64.82 - (trackGain / 256)
*/
int trackGain() const;
/*!
* Returns the track peak as an integer value.
*
* To convert to dB: trackPeak in dB = trackPeak / 256 \n
* To convert to floating [-1..1]: trackPeak = 10^(trackPeak / 256 / 20)/32768
* Returns the track peak as an integer value,
* to convert to dB: trackPeak in dB = trackPeak / 256
* to convert to floating [-1..1]: trackPeak = 10^(trackPeak / 256 / 20)/32768
*/
int trackPeak() const;
/*!
* Returns the album gain as an integer value.
*
* To convert to dB: albumGain in dB = 64.82 - (albumGain / 256)
* Returns the album gain as an integer value,
* to convert to dB: albumGain in dB = 64.82 - (albumGain / 256)
*/
int albumGain() const;
/*!
* Returns the album peak as an integer value.
*
* To convert to dB: albumPeak in dB = albumPeak / 256 \n
* To convert to floating [-1..1]: albumPeak = 10^(albumPeak / 256 / 20)/32768
* Returns the album peak as an integer value,
* to convert to dB: albumPeak in dB = albumPeak / 256
* to convert to floating [-1..1]: albumPeak = 10^(albumPeak / 256 / 20)/32768
*/
int albumPeak() const;

View File

@@ -58,9 +58,7 @@ namespace TagLib {
};
/*!
* Event types defined in
* <a href="https://github.com/taglib/taglib/blob/master/taglib/mpeg/id3v2/id3v2.4.0-frames.txt">
* id3v2.4.0-frames.txt</a> 4.5. Event timing codes.
* Event types defined in id3v2.4.0-frames.txt 4.5. Event timing codes.
*/
enum EventType {
Padding = 0x00,

View File

@@ -25,7 +25,6 @@
#include "textidentificationframe.h"
#include <algorithm>
#include <array>
#include <utility>

View File

@@ -45,53 +45,52 @@ namespace TagLib {
* identification frames. There are a number of variations on this. Those
* enumerated in the ID3v2.4 standard are:
*
* %Frame | Description
* :----: | :-----------------------------------------------
* TALB | Album/Movie/Show title
* TBPM | BPM (beats per minute)
* TCOM | Composer
* TCON | Content type
* TCOP | Copyright message
* TDEN | Encoding time
* TDLY | Playlist delay
* TDOR | Original release time
* TDRC | Recording time
* TDRL | Release time
* TDTG | Tagging time
* TENC | Encoded by
* TEXT | Lyricist/Text writer
* TFLT | %File type
* TIPL | Involved people list
* TIT1 | Content group description
* TIT2 | Title/songname/content description
* TIT3 | Subtitle/Description refinement
* TKEY | Initial key
* TLAN | Language(s)
* TLEN | Length
* TMCL | Musician credits list
* TMED | Media type
* TMOO | Mood
* TOAL | Original album/movie/show title
* TOFN | Original filename
* TOLY | Original lyricist(s)/text writer(s)
* TOPE | Original artist(s)/performer(s)
* TOWN | %File owner/licensee
* TPE1 | Lead performer(s)/Soloist(s)
* TPE2 | Band/orchestra/accompaniment
* TPE3 | Conductor/performer refinement
* TPE4 | Interpreted, remixed, or otherwise modified by
* TPOS | Part of a set
* TPRO | Produced notice
* TPUB | Publisher
* TRCK | Track number/Position in set
* TRSN | Internet radio station name
* TRSO | Internet radio station owner
* TSOA | Album sort order
* TSOP | Performer sort order
* TSOT | Title sort order
* TSRC | ISRC (international standard recording code)
* TSSE | Software/Hardware and settings used for encoding
* TSST | Set subtitle
* <ul>
* <li><b>TALB</b> Album/Movie/Show title</li>
* <li><b>TBPM</b> BPM (beats per minute)</li>
* <li><b>TCOM</b> Composer</li>
* <li><b>TCON</b> Content type</li>
* <li><b>TCOP</b> Copyright message</li>
* <li><b>TDEN</b> Encoding time</li>
* <li><b>TDLY</b> Playlist delay</li>
* <li><b>TDOR</b> Original release time</li>
* <li><b>TDRC</b> Recording time</li>
* <li><b>TDRL</b> Release time</li>
* <li><b>TDTG</b> Tagging time</li>
* <li><b>TENC</b> Encoded by</li>
* <li><b>TEXT</b> Lyricist/Text writer</li>
* <li><b>TFLT</b> %File type</li>
* <li><b>TIPL</b> Involved people list</li>
* <li><b>TIT1</b> Content group description</li>
* <li><b>TIT2</b> Title/songname/content description</li>
* <li><b>TIT3</b> Subtitle/Description refinement</li>
* <li><b>TKEY</b> Initial key</li>
* <li><b>TLAN</b> Language(s)</li>
* <li><b>TLEN</b> Length</li>
* <li><b>TMCL</b> Musician credits list</li>
* <li><b>TMED</b> Media type</li>
* <li><b>TMOO</b> Mood</li>
* <li><b>TOAL</b> Original album/movie/show title</li>
* <li><b>TOFN</b> Original filename</li>
* <li><b>TOLY</b> Original lyricist(s)/text writer(s)</li>
* <li><b>TOPE</b> Original artist(s)/performer(s)</li>
* <li><b>TOWN</b> %File owner/licensee</li>
* <li><b>TPE1</b> Lead performer(s)/Soloist(s)</li>
* <li><b>TPE2</b> Band/orchestra/accompaniment</li>
* <li><b>TPE3</b> Conductor/performer refinement</li>
* <li><b>TPE4</b> Interpreted, remixed, or otherwise modified by</li>
* <li><b>TPOS</b> Part of a set</li>
* <li><b>TPRO</b> Produced notice</li>
* <li><b>TPUB</b> Publisher</li>
* <li><b>TRCK</b> Track number/Position in set</li>
* <li><b>TRSN</b> Internet radio station name</li>
* <li><b>TRSO</b> Internet radio station owner</li>
* <li><b>TSOA</b> Album sort order</li>
* <li><b>TSOP</b> Performer sort order</li>
* <li><b>TSOT</b> Title sort order</li>
* <li><b>TSRC</b> ISRC (international standard recording code)</li>
* <li><b>TSSE</b> Software/Hardware and settings used for encoding</li>
* <li><b>TSST</b> Set subtitle</li>
* </ul>
*
* The ID3v2 Frames document gives a description of each of these formats

View File

@@ -121,11 +121,9 @@ std::pair<Frame::Header *, bool> FrameFactory::prepareFrameHeader(
// A quick sanity check -- make sure that the frameID is 4 uppercase Latin1
// characters. Also make sure that there is data in the frame.
// A frame size of zero is invalid, but tolerated here to later only drop the
// frame but not the whole tag.
if(frameID.size() != (version < 3U ? 3U : 4U) ||
header->frameSize() < static_cast<unsigned int>(header->dataLengthIndicator() ? 4 : 0) ||
header->frameSize() <= static_cast<unsigned int>(header->dataLengthIndicator() ? 4 : 0) ||
header->frameSize() > data.size())
{
delete header;

View File

@@ -879,6 +879,13 @@ void ID3v2::Tag::parse(const ByteVector &origData)
if(!frame)
return;
// Checks to make sure that frame parsed correctly.
if(frame->size() <= 0) {
delete frame;
return;
}
if(frame->header()->version() == headerVersion) {
frameDataPosition += frame->size() + frame->headerSize();
} else {
@@ -888,14 +895,7 @@ void ID3v2::Tag::parse(const ByteVector &origData)
Frame::Header origHeader(origData, headerVersion);
frameDataPosition += origHeader.frameSize() + origHeader.size();
}
if(frame->size() > 0) {
addFrame(frame);
} else {
// A frame with size 0 is invalid, drop it. "A frame must be at least 1
// byte big" (id3v2.4.0-structure.txt - 4, id3v2.3.0.txt - 3.3).
delete frame;
}
addFrame(frame);
}
d->factory->rebuildAggregateFrames(this);

View File

@@ -25,8 +25,6 @@
#include "mpegfile.h"
#include <algorithm>
#include "taglib_config.h"
#include "id3v2framefactory.h"
#include "tdebug.h"

View File

@@ -239,7 +239,7 @@ void MPEG::Header::parse(File *file, offset_t offset, bool checkLength)
(static_cast<unsigned char>(frameLengthData[0]) << 3) |
(static_cast<unsigned char>(frameLengthData[1]) >> 5);
d->bitrate = static_cast<int>(d->frameLength * d->sampleRate / 1024.0 + 0.5) * 8 / 1000;
d->bitrate = static_cast<int>(d->frameLength * d->sampleRate / 1024.0 + 0.5) * 8 / 1024;
}
}
else {

View File

@@ -45,7 +45,6 @@ public:
int inputSampleRate { 0 };
int channels { 0 };
int opusVersion { 0 };
int outputGain { 0 };
};
////////////////////////////////////////////////////////////////////////////////
@@ -94,11 +93,6 @@ int Opus::Properties::opusVersion() const
return d->opusVersion;
}
int Opus::Properties::outputGain() const
{
return d->outputGain;
}
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////
@@ -128,10 +122,9 @@ void Opus::Properties::read(File *file)
// *Input Sample Rate* (32 bits, unsigned, little endian)
d->inputSampleRate = data.toUInt(pos, false);
pos += 4;
// pos += 4;
// *Output Gain* (16 bits, signed, little endian)
d->outputGain = data.toShort(pos, false);
// pos += 2;
// *Channel Mapping Family* (8 bits, unsigned)

View File

@@ -80,7 +80,7 @@ namespace TagLib {
* Returns the sample rate in Hz.
*
* \note Always returns 48000, because Opus can decode any stream at a
* sample rate of 8, 12, 16, 24, or 48 kHz.
* sample rate of 8, 12, 16, 24, or 48 kHz,
*/
int sampleRate() const override;
@@ -101,13 +101,6 @@ namespace TagLib {
*/
int opusVersion() const;
/*!
* Returns the output gain in signed Q7.8 fixed-point format.
*
* To convert the value to dB, divide it by 256.0.
*/
int outputGain() const;
private:
void read(File *file);

View File

@@ -298,7 +298,7 @@ void RIFF::File::read()
seek(offset);
const ByteVector chnkName = readBlock(4);
unsigned int chunkSize = readBlock(4).toUInt(bigEndian);
const unsigned int chunkSize = readBlock(4).toUInt(bigEndian);
if(!isValidChunkName(chnkName)) {
debug("RIFF::File::read() -- Chunk '" + chnkName + "' has invalid ID");
@@ -306,12 +306,8 @@ void RIFF::File::read()
}
if(static_cast<long long>(offset) + 8 + chunkSize > length()) {
// Clamp to available bytes rather than rejecting the chunk outright.
// Some encoders write a correct data chunk but with a slightly too-large
// declared size, or place the data chunk outside the declared RIFF boundary.
// Lenient parsers (ffmpeg, QuickTime) handle this by clamping; we do the same.
debug("RIFF::File::read() -- Chunk '" + chnkName + "' is truncated; clamping size to available bytes.");
chunkSize = static_cast<unsigned int>(length() - offset - 8);
debug("RIFF::File::read() -- Chunk '" + chnkName + "' has invalid size (larger than the file size)");
break;
}
Chunk chunk;

View File

@@ -55,11 +55,6 @@ public:
bool hasID3v2 { false };
bool hasInfo { false };
bool hasiXML { false };
bool hasBEXT { false };
String iXMLData;
ByteVector bextData;
};
////////////////////////////////////////////////////////////////////////////////
@@ -113,26 +108,6 @@ RIFF::Info::Tag *RIFF::WAV::File::InfoTag() const
return d->tag.access<RIFF::Info::Tag>(InfoIndex, false);
}
String RIFF::WAV::File::iXMLData() const
{
return d->iXMLData;
}
void RIFF::WAV::File::setiXMLData(const String &data)
{
d->iXMLData = data;
}
ByteVector RIFF::WAV::File::BEXTData() const
{
return d->bextData;
}
void RIFF::WAV::File::setBEXTData(const ByteVector &data)
{
d->bextData = data;
}
void RIFF::WAV::File::strip(TagTypes tags)
{
removeTagChunks(tags);
@@ -185,26 +160,6 @@ bool RIFF::WAV::File::save(TagTypes tags, StripTags strip, ID3v2::Version versio
if(strip == StripOthers)
File::strip(static_cast<TagTypes>(AllTags & ~tags));
if(!d->bextData.isEmpty()) {
removeChunk("bext");
setChunkData("bext", d->bextData);
d->hasBEXT = true;
}
else if(d->hasBEXT) {
removeChunk("bext");
d->hasBEXT = false;
}
if(!d->iXMLData.isEmpty()) {
removeChunk("iXML");
setChunkData("iXML", d->iXMLData.data(String::UTF8));
d->hasiXML = true;
}
else if(d->hasiXML) {
removeChunk("iXML");
d->hasiXML = false;
}
if(tags & ID3v2) {
removeTagChunks(ID3v2);
@@ -236,16 +191,6 @@ bool RIFF::WAV::File::hasInfoTag() const
return d->hasInfo;
}
bool RIFF::WAV::File::hasiXMLData() const
{
return d->hasiXML;
}
bool RIFF::WAV::File::hasBEXTData() const
{
return d->hasBEXT;
}
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////
@@ -274,14 +219,6 @@ void RIFF::WAV::File::read(bool readProperties)
}
}
}
else if(name == "iXML") {
d->hasiXML = true;
d->iXMLData = String(chunkData(i), String::UTF8);
}
else if(name == "bext") {
d->hasBEXT = true;
d->bextData = chunkData(i);
}
}
if(!d->tag[ID3v2Index])

View File

@@ -134,42 +134,6 @@ namespace TagLib {
*/
Info::Tag *InfoTag() const;
/*!
* Returns the raw iXML chunk data as a String.
* Empty if no iXML chunk is present.
*
* \see setiXMLData()
* \see hasiXMLData()
*/
String iXMLData() const;
/*!
* Sets the iXML chunk data. Pass an empty string to remove the
* iXML chunk on save.
*
* \see iXMLData()
* \see hasiXMLData()
*/
void setiXMLData(const String &data);
/*!
* Returns the raw BEXT (Broadcast Audio Extension) chunk data
* as a ByteVector. Empty if no BEXT chunk is present.
*
* \see setBEXTData()
* \see hasBEXTData()
*/
ByteVector BEXTData() const;
/*!
* Sets the BEXT chunk data. Pass an empty ByteVector to remove
* the BEXT chunk on save.
*
* \see BEXTData()
* \see hasBEXTData()
*/
void setBEXTData(const ByteVector &data);
/*!
* This will strip the tags that match the OR-ed together TagTypes from the
* file. By default it strips all tags. It returns \c true if the tags are
@@ -227,20 +191,6 @@ namespace TagLib {
*/
bool hasInfoTag() const;
/*!
* Returns whether or not the file on disk actually has an iXML chunk.
*
* \see iXMLTag
*/
bool hasiXMLData() const;
/*!
* Returns whether or not the file on disk actually has a BEXT chunk.
*
* \see bextTag
*/
bool hasBEXTData() const;
/*!
* Returns whether or not the given \a stream can be opened as a WAV
* file.

View File

@@ -104,11 +104,6 @@ namespace {
bool VariableLengthInput::getRiceGolombCode(int32_t &i32, int32_t k)
{
// k must be in [0, 31]: values outside this range would cause shift-by-32
// (UB for int32_t) or negative shifts, and are invalid for this format.
if(k < 0 || k > 31)
return false;
static constexpr uint32_t sMaskTable[] = {
0x0,
0x1, 0x3, 0x7, 0xf,

View File

@@ -52,20 +52,13 @@ namespace TagLib {
//! Returns the Shorten file version (1-3).
int shortenVersion() const;
//! Returns the file type (0-10).
//! Value | %File type
//! :---: | :--------------------------------
//! 0 | 8-bit µ-law
//! 1 | signed 8-bit PCM
//! 2 | unsigned 8-bit PCM
//! 3 | signed big-endian 16-bit PCM
//! 4 | unsigned big-endian 16-bit PCM
//! 5 | signed little-endian 16-bit PCM
//! 6 | unsigned little-endian 16-bit PCM
//! 7 | 8-bit ITU-T G.711 µ-law
//! 8 | 8-bit µ-law
//! 9 | 8-bit A-law
//! 10 | 8-bit ITU-T G.711 A-law
//! Returns the file type (0-9).
//! 0 = 8-bit µ-law,
//! 1 = signed 8-bit PCM, 2 = unsigned 8-bit PCM,
//! 3 = signed big-endian 16-bit PCM, 4 = unsigned big-endian 16-bit PCM,
//! 5 = signed little-endian 16-bit PCM, 6 = unsigned little-endian 16-bit PCM,
//! 7 = 8-bit ITU-T G.711 µ-law, 8 = 8-bit µ-law,
//! 9 = 8-bit A-law, 10 = 8-bit ITU-T G.711 A-law
int fileType() const;
int bitsPerSample() const;
unsigned long sampleFrames() const;

View File

@@ -25,7 +25,6 @@
#include "tagunion.h"
#include <algorithm>
#include <array>
#include "tstringlist.h"

View File

@@ -27,8 +27,8 @@
#define TAGLIB_H
#define TAGLIB_MAJOR_VERSION 2
#define TAGLIB_MINOR_VERSION 3
#define TAGLIB_PATCH_VERSION 0
#define TAGLIB_MINOR_VERSION 2
#define TAGLIB_PATCH_VERSION 1
#if (defined(_MSC_VER) && _MSC_VER >= 1600)
#define TAGLIB_CONSTRUCT_BITSET(x) static_cast<unsigned long long>(x)

View File

@@ -87,7 +87,6 @@ SET(test_runner_SRCS
test_complexproperties.cpp
test_file.cpp
test_fileref.cpp
test_fileref_detect.cpp
test_id3v1.cpp
test_id3v2.cpp
test_id3v2framefactory.cpp

Binary file not shown.

View File

@@ -1,484 +0,0 @@
/***************************************************************************
copyright : (C) 2026 by TagLib developers
***************************************************************************/
/***************************************************************************
* 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 "taglib_config.h"
#include "tfilestream.h"
#include "tbytevectorstream.h"
#include "fileref.h"
#include "mpegfile.h"
#ifdef TAGLIB_WITH_VORBIS
#include "flacfile.h"
#include "oggflacfile.h"
#include "opusfile.h"
#include "speexfile.h"
#include "vorbisfile.h"
#endif
#ifdef TAGLIB_WITH_APE
#include "apefile.h"
#include "mpcfile.h"
#include "wavpackfile.h"
#endif
#ifdef TAGLIB_WITH_ASF
#include "asffile.h"
#endif
#ifdef TAGLIB_WITH_TRUEAUDIO
#include "trueaudiofile.h"
#endif
#ifdef TAGLIB_WITH_MP4
#include "mp4file.h"
#endif
#ifdef TAGLIB_WITH_RIFF
#include "aifffile.h"
#include "wavfile.h"
#endif
#ifdef TAGLIB_WITH_DSF
#include "dsdifffile.h"
#include "dsffile.h"
#endif
#ifdef TAGLIB_WITH_SHORTEN
#include "shortenfile.h"
#endif
#ifdef TAGLIB_WITH_MATROSKA
#include "matroskafile.h"
#endif
#include <cppunit/extensions/HelperMacros.h>
#include "utils.h"
using namespace TagLib;
// Files not covered by detection tests and the reason why:
// (All of these return null because no format's isSupported() matches them)
//
// MOD/S3M/IT/XM formats have no isSupported() implementation at all,
// so content-based detection is impossible for these files:
// changed.mod, test.mod, changed.s3m, test.s3m, test.it,
// changed.xm, test.xm, stripped.xm
//
// bare ID3 tag data without any surrounding audio stream:
// 005411.id3, broken-tenc.id3, unsynch.id3
//
// null bytes / truly unsupported binary format:
// no-extension, unsupported-extension.xx
//
// .mp3-named files where MPEG::File::isSupported() returns false because the
// MPEG frame scanner cannot find any valid frames in the content:
// garbage.mp3 (random binary data with no MPEG sync bytes),
// compressed_id3_frame.mp3 (zlib-compressed ID3 frame inflates to garbage
// that the frame scanner cannot parse past),
// duplicate_id3v2.mp3 (two consecutive ID3v2 headers confuse the size
// calculation, shifting the scan past any real frames),
// excessive_alloc.mp3 (APIC frame carries a crafted huge size field that
// the ID3v2 skip overshoots the actual frames),
// extended-header.mp3 (ID3v2.4 extended header flag causes incorrect size
// skip so the scanner starts inside the header),
// w000.mp3 (malformed file with no discoverable MPEG sync bytes)
//
// MPC SV4/SV5: MPC::File::isSupported() only recognises "MPCK" (SV8) and
// "MP+" (SV7); older SV4/SV5 streams have no standardised magic bytes:
// sv4_header.mpc, sv5_header.mpc
//
// MP4 with 64-bit atom sizes: first box is "moov" rather than the required
// "ftyp", so MP4::File::isSupported() returns false:
// 64bit.mp4
//
// corrupt AIFF: the FORM header is present but the sub-type bytes at offset 8
// are garbled (0x80 0x46 instead of 'AIFF'/'AIFC'), so
// RIFF::AIFF::File::isSupported() returns false:
// excessive_alloc.aif
namespace {
template <typename T> void detectByContent(const char *testFile) {
FileStream fs(TEST_FILE_PATH_C(testFile));
CPPUNIT_ASSERT(fs.isOpen());
const ByteVector data = fs.readBlock(fs.length());
ByteVectorStream bvs(data);
const FileRef f(&bvs);
CPPUNIT_ASSERT(!f.isNull());
CPPUNIT_ASSERT(dynamic_cast<T *>(f.file()) != nullptr);
}
void detectNullByContent(const char *testFile) {
FileStream fs(TEST_FILE_PATH_C(testFile));
CPPUNIT_ASSERT(fs.isOpen());
const ByteVector data = fs.readBlock(fs.length());
ByteVectorStream bvs(data);
const FileRef f(&bvs);
CPPUNIT_ASSERT(f.isNull());
}
} // namespace
class TestFileRefDetectByContent : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(TestFileRefDetectByContent);
// MPEG (always available)
CPPUNIT_TEST(testApeId3v1Mp3);
CPPUNIT_TEST(testApeId3v2Mp3);
CPPUNIT_TEST(testApeMp3);
CPPUNIT_TEST(testBladeencMp3);
CPPUNIT_TEST(testEmpty1sAac);
CPPUNIT_TEST(testId3v22TdaMp3);
CPPUNIT_TEST(testInvalidFrames1Mp3);
CPPUNIT_TEST(testInvalidFrames2Mp3);
CPPUNIT_TEST(testInvalidFrames3Mp3);
CPPUNIT_TEST(testItunes10Mp3);
CPPUNIT_TEST(testLameCbrMp3);
CPPUNIT_TEST(testLameVbrMp3);
CPPUNIT_TEST(testMpeg2Mp3);
CPPUNIT_TEST(testRareFramesMp3);
CPPUNIT_TEST(testTocManyChildrenMp3);
CPPUNIT_TEST(testXingMp3);
#ifdef TAGLIB_WITH_VORBIS
// Ogg::Vorbis::File
CPPUNIT_TEST(testEmptyOgg);
CPPUNIT_TEST(testEmptyVorbisOga);
CPPUNIT_TEST(testLowercaseFieldsOgg);
CPPUNIT_TEST(testTestOgg);
// Ogg::FLAC::File
CPPUNIT_TEST(testEmptyFlacOga);
// FLAC::File
CPPUNIT_TEST(testEmptySeektableFlac);
CPPUNIT_TEST(testMultipleVcFlac);
CPPUNIT_TEST(testNoTagsFlac);
CPPUNIT_TEST(testSilence44SFlac);
CPPUNIT_TEST(testSinewaveFlac);
CPPUNIT_TEST(testZeroSizedPaddingFlac);
CPPUNIT_TEST(testFLACWithMPEGSyncBytes);
// Ogg::Speex::File
CPPUNIT_TEST(testEmptySpx);
// Ogg::Opus::File
CPPUNIT_TEST(testCorrectnessGainSilentOutputOpus);
// Corrupt files: isSupported() returns true but isValid() returns false
CPPUNIT_TEST(testNullSegfaultOga);
#endif
#ifdef TAGLIB_WITH_APE
// MPC::File
CPPUNIT_TEST(testClickMpc);
CPPUNIT_TEST(testInfloopMpc);
CPPUNIT_TEST(testSegfaultMpc);
CPPUNIT_TEST(testSegfault2Mpc);
CPPUNIT_TEST(testSv8HeaderMpc);
CPPUNIT_TEST(testZerodivMpc);
// WavPack::File
CPPUNIT_TEST(testClickWv);
CPPUNIT_TEST(testDsdStereoWv);
CPPUNIT_TEST(testFourChannelsWv);
CPPUNIT_TEST(testInfloopWv);
CPPUNIT_TEST(testNoLengthWv);
CPPUNIT_TEST(testNonStandardRateWv);
CPPUNIT_TEST(testTaggedWv);
// APE::File
CPPUNIT_TEST(testLongloopApe);
CPPUNIT_TEST(testMac390HdrApe);
CPPUNIT_TEST(testMac396Ape);
CPPUNIT_TEST(testMac399Id3v2Ape);
CPPUNIT_TEST(testMac399TaggedApe);
CPPUNIT_TEST(testMac399Ape);
CPPUNIT_TEST(testZerodivApe);
#endif
#ifdef TAGLIB_WITH_TRUEAUDIO
CPPUNIT_TEST(testEmptyTta);
CPPUNIT_TEST(testTaggedTta);
#endif
#ifdef TAGLIB_WITH_MP4
CPPUNIT_TEST(testBlankVideoM4v);
CPPUNIT_TEST(testCovrJunkM4a);
CPPUNIT_TEST(testEmptyAlacM4a);
CPPUNIT_TEST(testGnreM4a);
CPPUNIT_TEST(testHasTagsM4a);
CPPUNIT_TEST(testIlstIsLastM4a);
CPPUNIT_TEST(testInfloopM4a);
CPPUNIT_TEST(testNoTags3g2);
CPPUNIT_TEST(testNoTagsM4a);
CPPUNIT_TEST(testNonFullMetaM4a);
CPPUNIT_TEST(testNonprintableAtomTypeM4a);
CPPUNIT_TEST(testZeroLengthMdatM4a);
#endif
#ifdef TAGLIB_WITH_ASF
CPPUNIT_TEST(testLosslessWma);
CPPUNIT_TEST(testSilence1Wma);
#endif
#ifdef TAGLIB_WITH_RIFF
// RIFF::AIFF::File
CPPUNIT_TEST(testAlawAifc);
CPPUNIT_TEST(testDuplicateId3v2Aiff);
CPPUNIT_TEST(testEmptyAiff);
CPPUNIT_TEST(testNoiseAif);
CPPUNIT_TEST(testNoiseOddAif);
CPPUNIT_TEST(testSegfaultAif);
// RIFF::WAV::File
CPPUNIT_TEST(testAlawWav);
CPPUNIT_TEST(testDuplicateTagsWav);
CPPUNIT_TEST(testEmptyWav);
CPPUNIT_TEST(testFloat64Wav);
CPPUNIT_TEST(testInfloopWav);
CPPUNIT_TEST(testInvalidChunkWav);
CPPUNIT_TEST(testPcmWithFactChunkWav);
CPPUNIT_TEST(testSegfaultWav);
CPPUNIT_TEST(testUint8weWav);
CPPUNIT_TEST(testZeroSizeChunkWav);
#endif
#ifdef TAGLIB_WITH_DSF
CPPUNIT_TEST(testEmpty10msDsf);
CPPUNIT_TEST(testEmpty10msDff);
#endif
#ifdef TAGLIB_WITH_SHORTEN
CPPUNIT_TEST(test2SecSilenceShn);
#endif
#ifdef TAGLIB_WITH_MATROSKA
CPPUNIT_TEST(testNoTagsMka);
CPPUNIT_TEST(testNoTagsWebm);
CPPUNIT_TEST(testOptimizedMkv);
CPPUNIT_TEST(testTagsBeforeCuesMkv);
#endif
CPPUNIT_TEST_SUITE_END();
public:
// -- MPEG::File (always available) --
void testApeId3v1Mp3() { detectByContent<MPEG::File>("ape-id3v1.mp3"); }
void testApeId3v2Mp3() { detectByContent<MPEG::File>("ape-id3v2.mp3"); }
void testApeMp3() { detectByContent<MPEG::File>("ape.mp3"); }
void testBladeencMp3() { detectByContent<MPEG::File>("bladeenc.mp3"); }
void testEmpty1sAac() { detectByContent<MPEG::File>("empty1s.aac"); }
void testId3v22TdaMp3() { detectByContent<MPEG::File>("id3v22-tda.mp3"); }
void testInvalidFrames1Mp3() {
detectByContent<MPEG::File>("invalid-frames1.mp3");
}
void testInvalidFrames2Mp3() {
detectByContent<MPEG::File>("invalid-frames2.mp3");
}
void testInvalidFrames3Mp3() {
detectByContent<MPEG::File>("invalid-frames3.mp3");
}
void testItunes10Mp3() { detectByContent<MPEG::File>("itunes10.mp3"); }
void testLameCbrMp3() { detectByContent<MPEG::File>("lame_cbr.mp3"); }
void testLameVbrMp3() { detectByContent<MPEG::File>("lame_vbr.mp3"); }
void testMpeg2Mp3() { detectByContent<MPEG::File>("mpeg2.mp3"); }
void testRareFramesMp3() {
detectByContent<MPEG::File>("rare_frames.mp3");
}
void testTocManyChildrenMp3() {
detectByContent<MPEG::File>("toc_many_children.mp3");
}
void testXingMp3() { detectByContent<MPEG::File>("xing.mp3"); }
#ifdef TAGLIB_WITH_VORBIS
// -- Ogg::Vorbis::File --
void testEmptyOgg() { detectByContent<Ogg::Vorbis::File>("empty.ogg"); }
void testEmptyVorbisOga() {
detectByContent<Ogg::Vorbis::File>("empty_vorbis.oga");
}
void testLowercaseFieldsOgg() {
detectByContent<Ogg::Vorbis::File>("lowercase-fields.ogg");
}
void testTestOgg() { detectByContent<Ogg::Vorbis::File>("test.ogg"); }
// -- Ogg::FLAC::File --
void testEmptyFlacOga() {
detectByContent<Ogg::FLAC::File>("empty_flac.oga");
}
// -- FLAC::File --
void testEmptySeektableFlac() {
detectByContent<FLAC::File>("empty-seektable.flac");
}
void testMultipleVcFlac() {
detectByContent<FLAC::File>("multiple-vc.flac");
}
void testNoTagsFlac() { detectByContent<FLAC::File>("no-tags.flac"); }
void testSilence44SFlac() {
detectByContent<FLAC::File>("silence-44-s.flac");
}
void testSinewaveFlac() { detectByContent<FLAC::File>("sinewave.flac"); }
void testZeroSizedPaddingFlac() {
detectByContent<FLAC::File>("zero-sized-padding.flac");
}
void testFLACWithMPEGSyncBytes() {
detectByContent<FLAC::File>("mpeg-sync-flac.flac");
}
// -- Ogg::Speex::File --
void testEmptySpx() { detectByContent<Ogg::Speex::File>("empty.spx"); }
// -- Ogg::Opus::File --
void testCorrectnessGainSilentOutputOpus() {
detectByContent<Ogg::Opus::File>("correctness_gain_silent_output.opus");
}
// segfault.oga: Ogg::FLAC::File::isSupported() returns true (valid Ogg
// container with a fLaC marker), but the FLAC metadata header inside is
// corrupt so Ogg::FLAC::File::isValid() returns false.
void testNullSegfaultOga() { detectNullByContent("segfault.oga"); }
#endif
#ifdef TAGLIB_WITH_APE
// -- MPC::File --
void testClickMpc() { detectByContent<MPC::File>("click.mpc"); }
void testInfloopMpc() { detectByContent<MPC::File>("infloop.mpc"); }
void testSegfaultMpc() { detectByContent<MPC::File>("segfault.mpc"); }
void testSegfault2Mpc() { detectByContent<MPC::File>("segfault2.mpc"); }
void testSv8HeaderMpc() { detectByContent<MPC::File>("sv8_header.mpc"); }
void testZerodivMpc() { detectByContent<MPC::File>("zerodiv.mpc"); }
// -- WavPack::File --
void testClickWv() { detectByContent<WavPack::File>("click.wv"); }
void testDsdStereoWv() { detectByContent<WavPack::File>("dsd_stereo.wv"); }
void testFourChannelsWv() {
detectByContent<WavPack::File>("four_channels.wv");
}
void testInfloopWv() { detectByContent<WavPack::File>("infloop.wv"); }
void testNoLengthWv() { detectByContent<WavPack::File>("no_length.wv"); }
void testNonStandardRateWv() {
detectByContent<WavPack::File>("non_standard_rate.wv");
}
void testTaggedWv() { detectByContent<WavPack::File>("tagged.wv"); }
// -- APE::File --
void testLongloopApe() { detectByContent<APE::File>("longloop.ape"); }
void testMac390HdrApe() { detectByContent<APE::File>("mac-390-hdr.ape"); }
void testMac396Ape() { detectByContent<APE::File>("mac-396.ape"); }
void testMac399Id3v2Ape() {
detectByContent<APE::File>("mac-399-id3v2.ape");
}
void testMac399TaggedApe() {
detectByContent<APE::File>("mac-399-tagged.ape");
}
void testMac399Ape() { detectByContent<APE::File>("mac-399.ape"); }
void testZerodivApe() { detectByContent<APE::File>("zerodiv.ape"); }
#endif
#ifdef TAGLIB_WITH_TRUEAUDIO
// -- TrueAudio::File --
void testEmptyTta() { detectByContent<TrueAudio::File>("empty.tta"); }
void testTaggedTta() { detectByContent<TrueAudio::File>("tagged.tta"); }
#endif
#ifdef TAGLIB_WITH_MP4
// -- MP4::File --
void testBlankVideoM4v() { detectByContent<MP4::File>("blank_video.m4v"); }
void testCovrJunkM4a() { detectByContent<MP4::File>("covr-junk.m4a"); }
void testEmptyAlacM4a() { detectByContent<MP4::File>("empty_alac.m4a"); }
void testGnreM4a() { detectByContent<MP4::File>("gnre.m4a"); }
void testHasTagsM4a() { detectByContent<MP4::File>("has-tags.m4a"); }
void testIlstIsLastM4a() {
detectByContent<MP4::File>("ilst-is-last.m4a");
}
void testInfloopM4a() { detectByContent<MP4::File>("infloop.m4a"); }
void testNoTags3g2() { detectByContent<MP4::File>("no-tags.3g2"); }
void testNoTagsM4a() { detectByContent<MP4::File>("no-tags.m4a"); }
void testNonFullMetaM4a() {
detectByContent<MP4::File>("non-full-meta.m4a");
}
void testNonprintableAtomTypeM4a() {
detectByContent<MP4::File>("nonprintable-atom-type.m4a");
}
void testZeroLengthMdatM4a() {
detectByContent<MP4::File>("zero-length-mdat.m4a");
}
#endif
#ifdef TAGLIB_WITH_ASF
// -- ASF::File --
void testLosslessWma() { detectByContent<ASF::File>("lossless.wma"); }
void testSilence1Wma() { detectByContent<ASF::File>("silence-1.wma"); }
#endif
#ifdef TAGLIB_WITH_RIFF
// -- RIFF::AIFF::File --
void testAlawAifc() { detectByContent<RIFF::AIFF::File>("alaw.aifc"); }
void testDuplicateId3v2Aiff() {
detectByContent<RIFF::AIFF::File>("duplicate_id3v2.aiff");
}
void testEmptyAiff() { detectByContent<RIFF::AIFF::File>("empty.aiff"); }
void testNoiseAif() { detectByContent<RIFF::AIFF::File>("noise.aif"); }
void testNoiseOddAif() {
detectByContent<RIFF::AIFF::File>("noise_odd.aif");
}
void testSegfaultAif() {
detectByContent<RIFF::AIFF::File>("segfault.aif");
}
// -- RIFF::WAV::File --
void testAlawWav() { detectByContent<RIFF::WAV::File>("alaw.wav"); }
void testDuplicateTagsWav() {
detectByContent<RIFF::WAV::File>("duplicate_tags.wav");
}
void testEmptyWav() { detectByContent<RIFF::WAV::File>("empty.wav"); }
void testFloat64Wav() { detectByContent<RIFF::WAV::File>("float64.wav"); }
void testInfloopWav() { detectByContent<RIFF::WAV::File>("infloop.wav"); }
void testInvalidChunkWav() {
detectByContent<RIFF::WAV::File>("invalid-chunk.wav");
}
void testPcmWithFactChunkWav() {
detectByContent<RIFF::WAV::File>("pcm_with_fact_chunk.wav");
}
void testSegfaultWav() { detectByContent<RIFF::WAV::File>("segfault.wav"); }
void testUint8weWav() { detectByContent<RIFF::WAV::File>("uint8we.wav"); }
void testZeroSizeChunkWav() {
detectByContent<RIFF::WAV::File>("zero-size-chunk.wav");
}
#endif
#ifdef TAGLIB_WITH_DSF
// -- DSF::File --
void testEmpty10msDsf() { detectByContent<DSF::File>("empty10ms.dsf"); }
// -- DSDIFF::File --
void testEmpty10msDff() { detectByContent<DSDIFF::File>("empty10ms.dff"); }
#endif
#ifdef TAGLIB_WITH_SHORTEN
// -- Shorten::File --
void test2SecSilenceShn() {
detectByContent<Shorten::File>("2sec-silence.shn");
}
#endif
#ifdef TAGLIB_WITH_MATROSKA
// -- Matroska::File --
void testNoTagsMka() { detectByContent<Matroska::File>("no-tags.mka"); }
void testNoTagsWebm() { detectByContent<Matroska::File>("no-tags.webm"); }
void testOptimizedMkv() {
detectByContent<Matroska::File>("optimized.mkv");
}
void testTagsBeforeCuesMkv() {
detectByContent<Matroska::File>("tags-before-cues.mkv");
}
#endif
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestFileRefDetectByContent);

View File

@@ -28,7 +28,6 @@
#include "tstringlist.h"
#include "tpropertymap.h"
#include "tbytevectorstream.h"
#include "tag.h"
#include "flacfile.h"
#include "xiphcomment.h"
@@ -68,14 +67,6 @@ class TestFLAC : public CppUnit::TestFixture
CPPUNIT_TEST(testRemoveXiphField);
CPPUNIT_TEST(testEmptySeekTable);
CPPUNIT_TEST(testPictureStoredAfterComment);
CPPUNIT_TEST(testReadiXMLDirect);
CPPUNIT_TEST(testReadiXMLRiffWrapped);
CPPUNIT_TEST(testReadBEXTDirect);
CPPUNIT_TEST(testReadBEXTRiffWrapped);
CPPUNIT_TEST(testWriteiXMLAndBEXT);
CPPUNIT_TEST(testWriteEmptyClearsiXMLAndBEXT);
CPPUNIT_TEST(testHasiXMLAndBEXTReflectFileState);
CPPUNIT_TEST(testRoundTripPreservesUnknownApplicationBlock);
CPPUNIT_TEST_SUITE_END();
public:
@@ -672,248 +663,6 @@ public:
CPPUNIT_ASSERT(fileData.startsWith(expectedData));
}
// Build a 4-byte FLAC metadata-block header:
// <1 bit last><7 bit type><24 bit length, big-endian>.
static ByteVector flacBlockHeader(unsigned int payloadSize, int blockType, bool isLast)
{
ByteVector h = ByteVector::fromUInt(payloadSize);
h[0] = static_cast<char>(blockType | (isLast ? 0x80 : 0x00));
return h;
}
// Build the body of an APPLICATION/"riff"-wrapped RIFF chunk:
// [appID="riff"][FOURCC][LE size][data].
static ByteVector riffWrappedAppData(const ByteVector &fourcc, const ByteVector &data)
{
ByteVector body("riff", 4);
body.append(fourcc);
body.append(ByteVector::fromUInt(data.size(), false));
body.append(data);
return body;
}
// Build a minimal synthetic FLAC stream: "fLaC" + zero-init STREAMINFO +
// one APPLICATION block (which gets the last-block flag). Caller passes
// the full APPLICATION block payload starting with the 4-byte appID.
static ByteVector synthFlacWithApp(const ByteVector &appPayload)
{
ByteVector flac("fLaC", 4);
flac.append(flacBlockHeader(34, 0, false)); // STREAMINFO header
flac.append(ByteVector(34, '\0')); // STREAMINFO body
flac.append(flacBlockHeader(appPayload.size(), 2, true));
flac.append(appPayload);
return flac;
}
void testReadiXMLDirect()
{
const String xml("<BWFXML><IXML_VERSION>1.0</IXML_VERSION></BWFXML>");
ByteVector appPayload("iXML", 4);
appPayload.append(xml.data(String::UTF8));
ByteVector data = synthFlacWithApp(appPayload);
ByteVectorStream stream(data);
FLAC::File f(&stream, false);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasiXMLData());
CPPUNIT_ASSERT(!f.hasBEXTData());
CPPUNIT_ASSERT_EQUAL(xml, f.iXMLData());
}
void testReadiXMLRiffWrapped()
{
const String xml("<BWFXML><SCENE>1</SCENE></BWFXML>");
const ByteVector appPayload =
riffWrappedAppData("iXML", xml.data(String::UTF8));
ByteVector data = synthFlacWithApp(appPayload);
ByteVectorStream stream(data);
FLAC::File f(&stream, false);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasiXMLData());
CPPUNIT_ASSERT_EQUAL(xml, f.iXMLData());
}
void testReadBEXTDirect()
{
const ByteVector bext("test bext data");
ByteVector appPayload("bext", 4);
appPayload.append(bext);
ByteVector data = synthFlacWithApp(appPayload);
ByteVectorStream stream(data);
FLAC::File f(&stream, false);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasBEXTData());
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT_EQUAL(bext, f.BEXTData());
}
void testReadBEXTRiffWrapped()
{
const ByteVector bext("test bext data");
const ByteVector appPayload = riffWrappedAppData("bext", bext);
ByteVector data = synthFlacWithApp(appPayload);
ByteVectorStream stream(data);
FLAC::File f(&stream, false);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasBEXTData());
CPPUNIT_ASSERT_EQUAL(bext, f.BEXTData());
}
void testWriteiXMLAndBEXT()
{
ScopedFileCopy copy("silence-44-s", ".flac");
const string newname = copy.fileName();
const String xml("<BWFXML><IXML_VERSION>1.6</IXML_VERSION></BWFXML>");
const ByteVector bext("bext payload bytes");
{
FLAC::File f(newname.c_str());
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT(!f.hasBEXTData());
f.setiXMLData(xml);
f.setBEXTData(bext);
f.save();
}
{
FLAC::File f(newname.c_str());
CPPUNIT_ASSERT(f.hasiXMLData());
CPPUNIT_ASSERT(f.hasBEXTData());
CPPUNIT_ASSERT_EQUAL(xml, f.iXMLData());
CPPUNIT_ASSERT_EQUAL(bext, f.BEXTData());
}
// On-disk format check: written blocks must use the IANA-registered
// "riff" wrapper, not the direct "iXML"/"bext" application IDs.
const ByteVector fileBytes = PlainFile(newname.c_str()).readAll();
ByteVector expectediXMLApp("riff", 4);
expectediXMLApp.append("iXML");
expectediXMLApp.append(ByteVector::fromUInt(xml.data(String::UTF8).size(), false));
expectediXMLApp.append(xml.data(String::UTF8));
CPPUNIT_ASSERT(fileBytes.find(expectediXMLApp) >= 0);
ByteVector expectedBEXTApp("riff", 4);
expectedBEXTApp.append("bext");
expectedBEXTApp.append(ByteVector::fromUInt(bext.size(), false));
expectedBEXTApp.append(bext);
CPPUNIT_ASSERT(fileBytes.find(expectedBEXTApp) >= 0);
}
void testWriteEmptyClearsiXMLAndBEXT()
{
ScopedFileCopy copy("silence-44-s", ".flac");
const string newname = copy.fileName();
{
FLAC::File f(newname.c_str());
f.setiXMLData("<BWFXML/>");
f.setBEXTData(ByteVector("bext"));
f.save();
}
{
FLAC::File f(newname.c_str());
CPPUNIT_ASSERT(f.hasiXMLData());
CPPUNIT_ASSERT(f.hasBEXTData());
f.setiXMLData(String());
f.setBEXTData(ByteVector());
f.save();
}
{
FLAC::File f(newname.c_str());
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT(!f.hasBEXTData());
CPPUNIT_ASSERT(f.iXMLData().isEmpty());
CPPUNIT_ASSERT(f.BEXTData().isEmpty());
}
}
void testHasiXMLAndBEXTReflectFileState()
{
// hasiXMLData() / hasBEXTData() must report whether the *file* carries an
// iXML / bext APPLICATION block, not whether in-memory payload happens to
// be non-empty. Regression test for an issue where the accessors were
// implemented as !data.isEmpty() and so flipped on as soon as set*Data()
// was called, before save(), and missed a real-but-empty block.
ScopedFileCopy copy("silence-44-s", ".flac");
const string newname = copy.fileName();
{
FLAC::File f(newname.c_str());
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT(!f.hasBEXTData());
f.setiXMLData("<BWFXML/>");
f.setBEXTData(ByteVector("bext"));
// File hasn't been saved yet — file still has no blocks.
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT(!f.hasBEXTData());
f.save();
// After save the blocks are on disk.
CPPUNIT_ASSERT(f.hasiXMLData());
CPPUNIT_ASSERT(f.hasBEXTData());
}
{
FLAC::File f(newname.c_str());
CPPUNIT_ASSERT(f.hasiXMLData());
CPPUNIT_ASSERT(f.hasBEXTData());
f.setiXMLData(String());
f.setBEXTData(ByteVector());
// In-memory payload now empty but the file still carries both blocks.
CPPUNIT_ASSERT(f.hasiXMLData());
CPPUNIT_ASSERT(f.hasBEXTData());
f.save();
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT(!f.hasBEXTData());
}
}
void testRoundTripPreservesUnknownApplicationBlock()
{
// Source: silence-44-s with an extra APPLICATION/"SMED" block injected
// just before its existing VORBIS_COMMENT block. Goal: setting iXML and
// saving must not disturb the SMED block (it's an unrecognized app ID).
const ByteVector smedAppPayload("SMED", 4);
ByteVector smedExtra("opaque sequoia metadata payload");
ByteVector smedBlock = smedAppPayload;
smedBlock.append(smedExtra);
// Splice a fresh APPLICATION/SMED block into a synthetic FLAC. Use the
// file we just built as the input stream so we don't have to mutate a
// real FLAC's seek table / picture offsets.
ByteVector flac("fLaC", 4);
flac.append(flacBlockHeader(34, 0, false));
flac.append(ByteVector(34, '\0'));
flac.append(flacBlockHeader(smedBlock.size(), 2, true));
flac.append(smedBlock);
ByteVectorStream stream(flac);
{
FLAC::File f(&stream, false);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(!f.hasiXMLData());
f.setiXMLData("<BWFXML/>");
f.save();
}
// SMED block must still be present on disk after save.
ByteVector saved = *stream.data();
CPPUNIT_ASSERT(saved.find(smedAppPayload) >= 0);
CPPUNIT_ASSERT(saved.find(smedExtra) >= 0);
// And the iXML data must round-trip.
ByteVectorStream stream2(saved);
FLAC::File f2(&stream2, false);
CPPUNIT_ASSERT(f2.isValid());
CPPUNIT_ASSERT(f2.hasiXMLData());
CPPUNIT_ASSERT_EQUAL(String("<BWFXML/>"), f2.iXMLData());
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestFLAC);

View File

@@ -156,10 +156,6 @@ class TestMatroska : public CppUnit::TestFixture
CPPUNIT_TEST(testOpenInvalid);
CPPUNIT_TEST(testSegmentSizeChange);
CPPUNIT_TEST(testChapters);
CPPUNIT_TEST(testSaveTypes);
CPPUNIT_TEST(testSaveTypesBeforeCues);
CPPUNIT_TEST(testSaveTypesNoTrailingVoid);
CPPUNIT_TEST(testSaveTypesReclaimVoid);
CPPUNIT_TEST_SUITE_END();
public:
@@ -1253,530 +1249,6 @@ public:
CPPUNIT_ASSERT(origData == fileData);
}
void testSaveTypesBeforeCues()
{
// tags-before-cues.mkv layout:
// SeekHead | Void | SegInfo | Tracks | Tags | Cluster | Cues
//
// Verify all three WriteStyles correctly grow the Tags element which
// sits *before* the Cluster:
// - Compact / DoNotShrink: bytes are inserted before the Cluster, the
// Cluster shifts, the seek head and cue cluster positions must be
// updated accordingly; the file must remain valid and tag content
// must round-trip.
// - AvoidInsert: the Tags element is replaced with a Void at its
// original position and appended at the end of the segment, so the
// Cluster must NOT shift; tag content must round-trip.
const ByteVector origData =
PlainFile(TEST_FILE_PATH_C("tags-before-cues.mkv")).readAll();
// Cluster ID 0x1F43B675 does not appear in the SeekHead of this file,
// so find() returns the offset of the actual Cluster element.
const ByteVector clusterId = ByteVector::fromUInt(0x1F43B675U, true);
const ByteVector tagsId = ByteVector::fromUInt(0x1254C367U, true);
const ByteVector cuesId = ByteVector::fromUInt(0x1C53BB6BU, true);
const int origClusterPos = origData.find(clusterId);
CPPUNIT_ASSERT(origClusterPos > 0);
const String longTitle =
"An Extremely Long Title Value That Is Definitely Larger Than The Original "
"Tags Element In The File Because It Contains Many Characters To Ensure "
"That The AvoidInsert Move-To-End Behavior Triggers Here";
const String longArtist =
"An Extremely Long Artist Name Value That Is Also Larger Than The Original "
"Tags Element And Together With The Title Tag Makes The Rendered Output "
"Exceed The Original Tags Size So The AvoidInsert Triggers";
for(const auto writeStyle : {Matroska::WriteStyle::Compact,
Matroska::WriteStyle::DoNotShrink,
Matroska::WriteStyle::AvoidInsert}) {
const auto wsLabel = String::number(static_cast<int>(writeStyle)).to8Bit();
ScopedFileCopy copy("tags-before-cues", ".mkv");
const string newname = copy.fileName();
// Save with Tags significantly larger than the original Tags element.
{
Matroska::File f(newname.c_str());
CPPUNIT_ASSERT_MESSAGE("Open ws=" + wsLabel, f.isValid());
auto tag = f.tag(true);
tag->clearSimpleTags();
tag->addSimpleTag(Matroska::SimpleTag(
String("TITLE"), longTitle,
Matroska::SimpleTag::TargetTypeValue::Track));
tag->addSimpleTag(Matroska::SimpleTag(
String("ARTIST"), longArtist,
Matroska::SimpleTag::TargetTypeValue::Track));
CPPUNIT_ASSERT_MESSAGE("Save ws=" + wsLabel, f.save(writeStyle));
}
// File must be valid: Accurate mode verifies seek-head and cue positions.
// Tag content must round-trip exactly.
{
Matroska::File f(newname.c_str(), true, AudioProperties::Accurate);
CPPUNIT_ASSERT_MESSAGE("Reopen valid ws=" + wsLabel, f.isValid());
auto tag = f.tag(false);
CPPUNIT_ASSERT_MESSAGE("Tag exists ws=" + wsLabel, tag != nullptr);
const auto &simpleTags = tag->simpleTagsList();
bool foundTitle = false, foundArtist = false;
for(const auto &st : simpleTags) {
if(st.name() == "TITLE" && st.toString() == longTitle)
foundTitle = true;
else if(st.name() == "ARTIST" && st.toString() == longArtist)
foundArtist = true;
}
CPPUNIT_ASSERT_MESSAGE("TITLE roundtrip ws=" + wsLabel, foundTitle);
CPPUNIT_ASSERT_MESSAGE("ARTIST roundtrip ws=" + wsLabel, foundArtist);
}
const ByteVector newData = PlainFile(newname.c_str()).readAll();
const int newClusterPos = newData.find(clusterId);
CPPUNIT_ASSERT_MESSAGE("Cluster present ws=" + wsLabel, newClusterPos > 0);
if(writeStyle == Matroska::WriteStyle::AvoidInsert) {
// Cluster must not shift in AvoidInsert mode.
CPPUNIT_ASSERT_EQUAL_MESSAGE(
"AvoidInsert must not shift Cluster",
origClusterPos, newClusterPos);
// Tags must be appended after Cues.
const int cuesPos = newData.find(cuesId, newClusterPos);
const int newTagsPos = newData.find(tagsId, cuesPos + 4);
CPPUNIT_ASSERT_MESSAGE("Tags appended after Cues ws=" + wsLabel,
newTagsPos > cuesPos);
}
else {
// Compact / DoNotShrink: Tags grew in place, so Cluster must have
// shifted to a higher offset.
CPPUNIT_ASSERT_MESSAGE(
"Cluster must shift when growing in place ws=" + wsLabel,
newClusterPos > origClusterPos);
}
}
}
void testSaveTypesNoTrailingVoid()
{
// After AvoidInsert moved the Tags element to the end of the segment,
// a subsequent save with smaller content must not leave a trailing
// EBML void at the very end of the segment. The trailing element may
// shrink freely because no element follows it.
ScopedFileCopy copy("tags-before-cues", ".mkv");
const string newname = copy.fileName();
// Round 1: enlarge Tags so they get moved to the end.
{
Matroska::File f(newname.c_str());
CPPUNIT_ASSERT(f.isValid());
auto tag = f.tag(true);
tag->clearSimpleTags();
tag->addSimpleTag(Matroska::SimpleTag(
String("TITLE"),
String("An Extremely Long Title Value That Is Definitely Larger Than The Original "
"Tags Element In The File Because It Contains Many Characters To Ensure "
"That The AvoidInsert Move-To-End Behavior Triggers Here"),
Matroska::SimpleTag::TargetTypeValue::Track));
tag->addSimpleTag(Matroska::SimpleTag(
String("ARTIST"),
String("An Extremely Long Artist Name Value That Is Also Larger Than The Original "
"Tags Element And Together With The Title Tag Makes The Rendered Output "
"Exceed The Original Tags Size So The AvoidInsert Triggers"),
Matroska::SimpleTag::TargetTypeValue::Track));
CPPUNIT_ASSERT(f.save(Matroska::WriteStyle::AvoidInsert));
}
const size_t sizeAfterRound1 = PlainFile(newname.c_str()).readAll().size();
// Round 2: shrink Tags. The trailing element must shrink without
// leaving a void at the end.
{
Matroska::File f(newname.c_str());
CPPUNIT_ASSERT(f.isValid());
auto tag = f.tag(true);
tag->clearSimpleTags();
tag->addSimpleTag(Matroska::SimpleTag(
String("TITLE"), String("X"),
Matroska::SimpleTag::TargetTypeValue::Track));
CPPUNIT_ASSERT(f.save(Matroska::WriteStyle::AvoidInsert));
}
{
Matroska::File f(newname.c_str(), true, AudioProperties::Accurate);
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.tag(false) != nullptr);
}
const ByteVector newData = PlainFile(newname.c_str()).readAll();
// File must have shrunk because the trailing Tags element shrank.
CPPUNIT_ASSERT(newData.size() < sizeAfterRound1);
// The last bytes of the file must be the (small) Tags element, not a
// Void element. Find the Tags element after the Cues element and parse
// its VINT size: the file must end exactly at Tags' end with nothing
// (no Void) after it.
const ByteVector clusterId = ByteVector::fromUInt(0x1F43B675U, true);
const ByteVector cuesId = ByteVector::fromUInt(0x1C53BB6BU, true);
const ByteVector tagsId = ByteVector::fromUInt(0x1254C367U, true);
const int clusterPos = newData.find(clusterId);
const int cuesPos = newData.find(cuesId, clusterPos);
const int tagsPos = newData.find(tagsId, cuesPos + 4);
CPPUNIT_ASSERT(tagsPos > cuesPos);
// Decode VINT data size of the Tags element. The first byte after the
// 4-byte ID has a leading marker bit indicating the VINT length.
const auto vintFirst = static_cast<unsigned char>(newData[tagsPos + 4]);
int vintLen = 1;
for(int b = 0; b < 8; ++b) {
if(vintFirst & (0x80 >> b)) { vintLen = b + 1; break; }
}
unsigned long long dataSize = vintFirst & ((0x80 >> (vintLen - 1)) - 1);
for(int i = 1; i < vintLen; ++i)
dataSize = (dataSize << 8) | static_cast<unsigned char>(newData[tagsPos + 4 + i]);
const unsigned long long tagsEnd =
static_cast<unsigned long long>(tagsPos) + 4 + vintLen + dataSize;
CPPUNIT_ASSERT_EQUAL_MESSAGE(
"No trailing EBML void must remain at the end of the segment",
static_cast<unsigned long long>(newData.size()), tagsEnd);
}
void testSaveTypesReclaimVoid()
{
// After AvoidInsert moves a Tags element to the end (leaving a Void at
// its original position), a subsequent save with WriteStyle::Compact
// must produce a tightly packed file: the void left by the move must
// be reclaimed and the file must be at most as large as the original.
ScopedFileCopy copy("tags-before-cues", ".mkv");
const string newname = copy.fileName();
// Step 1: AvoidInsert with enlarged Tags -> Tags moved to end, Void in
// original slot. File grows.
{
Matroska::File f(newname.c_str());
CPPUNIT_ASSERT(f.isValid());
auto tag = f.tag(true);
tag->clearSimpleTags();
tag->addSimpleTag(Matroska::SimpleTag(String("TITLE"),
String("An Extremely Long Title Value That Is Definitely Larger Than The Original "
"Tags Element In The File Because It Contains Many Characters To Ensure "
"That The AvoidInsert Move-To-End Behavior Triggers Here"),
Matroska::SimpleTag::TargetTypeValue::Track));
tag->addSimpleTag(Matroska::SimpleTag(String("ARTIST"),
String("An Extremely Long Artist Name Value That Is Also Larger Than The Original "
"Tags Element And Together With The Title Tag Makes The Rendered Output "
"Exceed The Original Tags Size So The AvoidInsert Triggers"),
Matroska::SimpleTag::TargetTypeValue::Track));
CPPUNIT_ASSERT(f.save(Matroska::WriteStyle::AvoidInsert));
}
const size_t sizeAfterAvoidInsert =
PlainFile(newname.c_str()).readAll().size();
CPPUNIT_ASSERT(sizeAfterAvoidInsert >
PlainFile(TEST_FILE_PATH_C("tags-before-cues.mkv")).readAll().size());
// Step 2: Save again with Compact and short tag values. Compact must
// reclaim the void left by the prior move and produce a file no
// larger than the original.
{
Matroska::File f(newname.c_str());
CPPUNIT_ASSERT(f.isValid());
auto tag = f.tag(true);
tag->clearSimpleTags();
tag->addSimpleTag(Matroska::SimpleTag(String("TITLE"), String("X"),
Matroska::SimpleTag::TargetTypeValue::Track));
CPPUNIT_ASSERT(f.save(Matroska::WriteStyle::Compact));
}
const size_t sizeAfterCompact =
PlainFile(newname.c_str()).readAll().size();
CPPUNIT_ASSERT_MESSAGE(
"Compact must reclaim space after AvoidInsert grew the file",
sizeAfterCompact < sizeAfterAvoidInsert);
// Reference: applying Compact directly to the original file with the
// same tiny tags. Note: an orphan Void left in the middle of the
// segment by AvoidInsert is not currently reclaimed by Compact (it is
// attached as padding to a neighbouring element), so the post-Compact
// size is allowed to be slightly larger than the reference. The
// result must, however, be no larger than the original input file.
ScopedFileCopy reference("tags-before-cues", ".mkv");
{
Matroska::File f(reference.fileName().c_str());
CPPUNIT_ASSERT(f.isValid());
auto tag = f.tag(true);
tag->clearSimpleTags();
tag->addSimpleTag(Matroska::SimpleTag(String("TITLE"), String("X"),
Matroska::SimpleTag::TargetTypeValue::Track));
CPPUNIT_ASSERT(f.save(Matroska::WriteStyle::Compact));
}
const size_t referenceCompactSize =
PlainFile(reference.fileName().c_str()).readAll().size();
CPPUNIT_ASSERT(referenceCompactSize <= sizeAfterCompact);
// File must round-trip correctly.
{
Matroska::File f(newname.c_str(), true, AudioProperties::Accurate);
CPPUNIT_ASSERT(f.isValid());
auto tag = f.tag(false);
CPPUNIT_ASSERT(tag != nullptr);
bool foundTitle = false;
for(const auto &st : tag->simpleTagsList()) {
if(st.name() == "TITLE" && st.toString() == "X") {
foundTitle = true;
break;
}
}
CPPUNIT_ASSERT(foundTitle);
}
}
void testSaveTypes()
{
// Helper lambdas for adding data of different sizes
// largeTags: 2 simple tags with long values
const auto setLargeTags = [](Matroska::File &f) {
auto tag = f.tag(true);
tag->addSimpleTag(Matroska::SimpleTag(String("TITLE"),
String("A Very Long Title That Takes Up A Lot Of Space In The File 1234567890"),
Matroska::SimpleTag::TargetTypeValue::Track));
tag->addSimpleTag(Matroska::SimpleTag(String("ARTIST"),
String("A Very Long Artist Name That Takes Up A Lot Of Space In The File 1234567890"),
Matroska::SimpleTag::TargetTypeValue::Track));
};
const auto setSmallTags = [](Matroska::File &f) {
auto tag = f.tag(true);
tag->addSimpleTag(Matroska::SimpleTag(String("TITLE"), String("Short"),
Matroska::SimpleTag::TargetTypeValue::Track));
};
const auto setMediumTags = [](Matroska::File &f) {
auto tag = f.tag(true);
tag->addSimpleTag(Matroska::SimpleTag(String("TITLE"), String("Medium Title 12345678901234"),
Matroska::SimpleTag::TargetTypeValue::Track));
tag->addSimpleTag(Matroska::SimpleTag(String("ARTIST"), String("Medium Artist"),
Matroska::SimpleTag::TargetTypeValue::Track));
};
const auto setExtraLargeTags = [](Matroska::File &f) {
auto tag = f.tag(true);
tag->addSimpleTag(Matroska::SimpleTag(String("TITLE"),
String("An Extremely Long Title That Is Even Larger Than The Previous Large Title "
"With Extra Content To Ensure Growth 0123456789ABCDEF"),
Matroska::SimpleTag::TargetTypeValue::Track));
tag->addSimpleTag(Matroska::SimpleTag(String("ARTIST"),
String("An Extremely Long Artist Name Exceeding The Prior Large Artist Value "
"With Even More Content To Guarantee Growth 0123456789ABCDEF"),
Matroska::SimpleTag::TargetTypeValue::Track));
};
const auto setLargeAttachments = [](Matroska::File &f) {
auto atts = f.attachments(true);
atts->addAttachedFile(Matroska::AttachedFile(
ByteVector(200, 'x'), "cover.jpg", "image/jpeg", 111ULL, "Cover"));
};
const auto setSmallAttachments = [](Matroska::File &f) {
auto atts = f.attachments(true);
atts->addAttachedFile(Matroska::AttachedFile(
ByteVector(20, 'x'), "img.png", "image/png", 222ULL, "Img"));
};
const auto setMediumAttachments = [](Matroska::File &f) {
auto atts = f.attachments(true);
atts->addAttachedFile(Matroska::AttachedFile(
ByteVector(80, 'x'), "cover.jpg", "image/jpeg", 333ULL, "Cover"));
};
const auto setExtraLargeAttachments = [](Matroska::File &f) {
auto atts = f.attachments(true);
atts->addAttachedFile(Matroska::AttachedFile(
ByteVector(500, 'x'), "cover.jpg", "image/jpeg", 444ULL, "Cover"));
};
const auto setLargeChapters = [](Matroska::File &f) {
auto chs = f.chapters(true);
chs->addChapterEdition(Matroska::ChapterEdition(
List<Matroska::Chapter>{
Matroska::Chapter(0, 40000,
List{Matroska::Chapter::Display("Chapter One Long Name", "eng")},
1, false),
Matroska::Chapter(40000, 80000,
List{Matroska::Chapter::Display("Chapter Two Long Name", "eng")},
2, false),
}, true, false));
};
const auto setSmallChapters = [](Matroska::File &f) {
auto chs = f.chapters(true);
chs->addChapterEdition(Matroska::ChapterEdition(
List<Matroska::Chapter>{
Matroska::Chapter(0, 1000,
List{Matroska::Chapter::Display("A", "und")},
1, false),
}, false, false));
};
const auto setMediumChapters = [](Matroska::File &f) {
auto chs = f.chapters(true);
chs->addChapterEdition(Matroska::ChapterEdition(
List<Matroska::Chapter>{
Matroska::Chapter(0, 40000,
List{Matroska::Chapter::Display("Chapter Medium", "eng")},
1, false),
}, true, false));
};
const auto setExtraLargeChapters = [](Matroska::File &f) {
auto chs = f.chapters(true);
chs->addChapterEdition(Matroska::ChapterEdition(
List<Matroska::Chapter>{
Matroska::Chapter(0, 40000,
List{Matroska::Chapter::Display("Chapter One Extremely Long Name Here", "eng"),
Matroska::Chapter::Display("Kapitel Eins Sehr Langer Name", "deu")},
1, false),
Matroska::Chapter(40000, 80000,
List{Matroska::Chapter::Display("Chapter Two Extremely Long Name Here", "eng"),
Matroska::Chapter::Display("Kapitel Zwei Sehr Langer Name", "deu")},
2, false),
Matroska::Chapter(80000, 120000,
List{Matroska::Chapter::Display("Chapter Three Extra Large", "eng")},
3, true),
}, true, true));
};
for(const auto writeStyle : {Matroska::WriteStyle::Compact,
Matroska::WriteStyle::DoNotShrink,
Matroska::WriteStyle::AvoidInsert}) {
ScopedFileCopy copy("no-tags", ".mka");
const string newname = copy.fileName();
const int wsIdx = static_cast<int>(writeStyle);
// Verify tag/attachment/chapter content for a saved file. Each round
// uses unique identifiers (specific TITLE value, attachment UID,
// chapter timeStart) so any cross-round leakage or corruption is
// caught here.
const auto verifyRound = [&](const std::string &label,
const String &expectedTitle,
unsigned long long expectedAttachmentUid,
unsigned int expectedChapterCount,
unsigned long long expectedFirstChapterEnd) {
Matroska::File f(newname.c_str(), true, AudioProperties::Accurate);
CPPUNIT_ASSERT_MESSAGE(label + " valid", f.isValid());
auto tag = f.tag(false);
CPPUNIT_ASSERT_MESSAGE(label + " tag", tag != nullptr);
bool foundTitle = false;
for(const auto &st : tag->simpleTagsList()) {
if(st.name() == "TITLE" && st.toString() == expectedTitle) {
foundTitle = true;
break;
}
}
CPPUNIT_ASSERT_MESSAGE(label + " TITLE roundtrip", foundTitle);
auto atts = f.attachments(false);
CPPUNIT_ASSERT_MESSAGE(label + " attachments", atts != nullptr);
bool foundAtt = false;
for(const auto &a : atts->attachedFileList()) {
if(a.uid() == expectedAttachmentUid) {
foundAtt = true;
break;
}
}
CPPUNIT_ASSERT_MESSAGE(label + " attachment uid roundtrip", foundAtt);
auto chs = f.chapters(false);
CPPUNIT_ASSERT_MESSAGE(label + " chapters", chs != nullptr);
CPPUNIT_ASSERT_EQUAL_MESSAGE(label + " edition count", 1U,
chs->chapterEditionList().size());
const auto &edition = chs->chapterEditionList().front();
CPPUNIT_ASSERT_EQUAL_MESSAGE(label + " chapter count",
expectedChapterCount, edition.chapterList().size());
CPPUNIT_ASSERT_EQUAL_MESSAGE(label + " first chapter end",
expectedFirstChapterEnd, edition.chapterList()[0].timeEnd());
};
// --- Round 1: save large data ---
{
Matroska::File f(newname.c_str());
CPPUNIT_ASSERT(f.isValid());
setLargeTags(f);
setLargeAttachments(f);
setLargeChapters(f);
CPPUNIT_ASSERT_MESSAGE("Round1 save ws=" + String::number(wsIdx).to8Bit(), f.save(writeStyle));
}
const size_t sizeAfterRound1 = PlainFile(newname.c_str()).readAll().size();
verifyRound("Round1 ws=" + std::to_string(wsIdx),
String("A Very Long Title That Takes Up A Lot Of Space In The File 1234567890"),
111ULL, 2U, 40000ULL);
// --- Round 2: save smaller data → slot must not shrink for DoNotShrink/AvoidInsert ---
{
Matroska::File f(newname.c_str());
CPPUNIT_ASSERT(f.isValid());
f.tag(true)->clearSimpleTags();
f.attachments(true)->clear();
f.chapters(true)->clear();
setSmallTags(f);
setSmallAttachments(f);
setSmallChapters(f);
CPPUNIT_ASSERT_MESSAGE("Round2 save ws=" + String::number(wsIdx).to8Bit(), f.save(writeStyle));
}
const size_t sizeAfterRound2 = PlainFile(newname.c_str()).readAll().size();
verifyRound("Round2 ws=" + std::to_string(wsIdx),
String("Short"), 222ULL, 1U, 1000ULL);
if(writeStyle == Matroska::WriteStyle::Compact) {
// Compact always shrinks, so file is smaller
CPPUNIT_ASSERT(sizeAfterRound2 < sizeAfterRound1);
} else if(writeStyle == Matroska::WriteStyle::AvoidInsert) {
// AvoidInsert: existing slots are kept, but the segment-trailing
// element may shrink (no element follows it -- shrinking only
// truncates the file, no inserts are needed).
CPPUNIT_ASSERT(sizeAfterRound2 <= sizeAfterRound1);
} else {
// DoNotShrink: elements keep their original slot size.
// The file size must not be smaller than after round 1
CPPUNIT_ASSERT_EQUAL(sizeAfterRound1, sizeAfterRound2);
}
// --- Round 3: save medium data (fits in round2's slot if DoNotShrink/AvoidInsert) ---
{
Matroska::File f(newname.c_str());
CPPUNIT_ASSERT(f.isValid());
f.tag(true)->clearSimpleTags();
f.attachments(true)->clear();
f.chapters(true)->clear();
setMediumTags(f);
setMediumAttachments(f);
setMediumChapters(f);
CPPUNIT_ASSERT_MESSAGE("Round3 save ws=" + String::number(wsIdx).to8Bit(), f.save(writeStyle));
}
const size_t sizeAfterRound3 = PlainFile(newname.c_str()).readAll().size();
verifyRound("Round3 ws=" + std::to_string(wsIdx),
String("Medium Title 12345678901234"), 333ULL, 1U, 40000ULL);
if(writeStyle == Matroska::WriteStyle::Compact) {
// Compact: medium > small, but exact, so different from round2
CPPUNIT_ASSERT(sizeAfterRound3 != sizeAfterRound2);
CPPUNIT_ASSERT(sizeAfterRound3 < sizeAfterRound1);
} else if(writeStyle == Matroska::WriteStyle::AvoidInsert) {
// AvoidInsert: medium fits in round1's slot for non-trailing
// elements, but the trailing element may take less space than in
// round 1. File size therefore stays <= round 1.
CPPUNIT_ASSERT(sizeAfterRound3 <= sizeAfterRound1);
} else {
// DoNotShrink: medium fits in round1's slot (with remaining void)
// so file size stays the same as round1/round2
CPPUNIT_ASSERT_EQUAL(sizeAfterRound1, sizeAfterRound3);
}
// --- Round 4: save extra-large data (larger than round 1) ---
{
Matroska::File f(newname.c_str());
CPPUNIT_ASSERT(f.isValid());
f.tag(true)->clearSimpleTags();
f.attachments(true)->clear();
f.chapters(true)->clear();
setExtraLargeTags(f);
setExtraLargeAttachments(f);
setExtraLargeChapters(f);
CPPUNIT_ASSERT_MESSAGE("Round4 save ws=" + String::number(wsIdx).to8Bit(), f.save(writeStyle));
}
const size_t sizeAfterRound4 = PlainFile(newname.c_str()).readAll().size();
verifyRound("Round4 ws=" + std::to_string(wsIdx),
String("An Extremely Long Title That Is Even Larger Than The Previous Large Title "
"With Extra Content To Ensure Growth 0123456789ABCDEF"),
444ULL, 3U, 40000ULL);
// All styles must accommodate the larger data: file must be larger than round1
CPPUNIT_ASSERT(sizeAfterRound4 > sizeAfterRound1);
}
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestMatroska);

View File

@@ -34,7 +34,6 @@
#include "mp4atom.h"
#include "mp4file.h"
#include "mp4itemfactory.h"
#include "mp4chapterholder.h"
#include "plainfile.h"
#include <cppunit/extensions/HelperMacros.h>
#include "utils.h"
@@ -70,56 +69,6 @@ namespace
};
CustomItemFactory CustomItemFactory::factory;
class MockChapterList : public MP4::ChapterHolder {
public:
static const MP4::ChapterList mockChapters;
bool read(TagLib::File *)
{
chapterList = mockChapters;
++readCount;
return true;
}
bool write(TagLib::File *)
{
++writeCount;
return true;
}
int readCount = 0;
int writeCount = 0;
};
const MP4::ChapterList MockChapterList::mockChapters = {
MP4::Chapter("Mock", 123)
};
class MockChapterFile : public PlainFile {
public:
explicit MockChapterFile(FileName name) : PlainFile(name)
{
}
MP4::ChapterList chapters()
{
return getChaptersLazy(chapterList, this);
}
void setChapters(const MP4::ChapterList& chapters)
{
setChaptersLazy(chapterList, chapters);
}
bool save() override
{
return MP4::saveChaptersIfModified(chapterList, this);
}
std::unique_ptr<MockChapterList> chapterList;
};
} // namespace
class TestMP4 : public CppUnit::TestFixture
@@ -153,26 +102,6 @@ class TestMP4 : public CppUnit::TestFixture
CPPUNIT_TEST(testNonFullMetaAtom);
CPPUNIT_TEST(testItemFactory);
CPPUNIT_TEST(testNonPrintableAtom);
CPPUNIT_TEST(testChapterListWrite);
CPPUNIT_TEST(testChapterListRemove);
CPPUNIT_TEST(testChapterListWithExistingTags);
CPPUNIT_TEST(testChapterListReadEmpty);
CPPUNIT_TEST(testQTChapterListWrite);
CPPUNIT_TEST(testQTChapterListRemove);
CPPUNIT_TEST(testQTChapterListWithExistingTags);
CPPUNIT_TEST(testQTChapterListReadEmpty);
CPPUNIT_TEST(testQTChapterListOverwrite);
CPPUNIT_TEST(testQTChapterListTimestampPrecision);
CPPUNIT_TEST(testQTChapterListNonZeroFirstChapter);
CPPUNIT_TEST(testQTChapterListNoOrphanedMdat);
CPPUNIT_TEST(testQTChapterListSharedMdatPreservesAudio);
CPPUNIT_TEST(testQTChapterListUnicodeTitles);
CPPUNIT_TEST(testChapterListUnicodeTitles);
CPPUNIT_TEST(testQTChapterListEmptyTitleStripped);
CPPUNIT_TEST(testQTChapterListSingleEmptyTitleNotStripped);
CPPUNIT_TEST(testNeroAndQTChaptersAreIndependent);
CPPUNIT_TEST(testNeroChaptersAloneWhenNoQT);
CPPUNIT_TEST(testLazyReadingAndWritingChapters);
CPPUNIT_TEST_SUITE_END();
public:
@@ -944,847 +873,6 @@ public:
CPPUNIT_ASSERT_EQUAL(String("TITLE"), f.tag()->title());
}
}
void testChapterListWrite()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
// File should have no chapters initially
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.neroChapters();
CPPUNIT_ASSERT(chapters.isEmpty());
}
// Write chapters
{
MP4::File f(filename.c_str());
f.setNeroChapters(MP4::ChapterList{
MP4::Chapter("Introduction", 0),
MP4::Chapter("Main Content", 30000LL),
MP4::Chapter("Conclusion", 60000LL)
});
CPPUNIT_ASSERT(f.save());
}
// Read back and verify
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.neroChapters();
CPPUNIT_ASSERT_EQUAL(3U, chapters.size());
CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime());
CPPUNIT_ASSERT_EQUAL(String("Introduction"), chapters[0].title());
CPPUNIT_ASSERT_EQUAL(30000LL, chapters[1].startTime());
CPPUNIT_ASSERT_EQUAL(String("Main Content"), chapters[1].title());
CPPUNIT_ASSERT_EQUAL(60000LL, chapters[2].startTime());
CPPUNIT_ASSERT_EQUAL(String("Conclusion"), chapters[2].title());
// Overwrite with different chapters
f.setNeroChapters(MP4::ChapterList{
MP4::Chapter("Part One", 0)
});
CPPUNIT_ASSERT(f.save());
}
// Verify overwrite
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.neroChapters();
CPPUNIT_ASSERT_EQUAL(1U, chapters.size());
CPPUNIT_ASSERT_EQUAL(String("Part One"), chapters[0].title());
}
}
void testChapterListRemove()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
// Write chapters
{
MP4::File f(filename.c_str());
f.setNeroChapters(MP4::ChapterList{
MP4::Chapter("Chapter 1", 0)
});
CPPUNIT_ASSERT(f.save());
}
// Verify written
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.neroChapters();
CPPUNIT_ASSERT_EQUAL(1U, chapters.size());
// Remove chapters
f.setNeroChapters(MP4::ChapterList());
CPPUNIT_ASSERT(f.save());
}
// Verify removed
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.neroChapters();
CPPUNIT_ASSERT(chapters.isEmpty());
// Remove from file with no chapters should also succeed
f.setNeroChapters(MP4::ChapterList());
CPPUNIT_ASSERT(f.save());
}
}
void testChapterListWithExistingTags()
{
ScopedFileCopy copy("has-tags", ".m4a");
string filename = copy.fileName();
// File has existing tags -- verify they survive chapter operations
String originalArtist;
{
MP4::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
originalArtist = f.tag()->artist();
CPPUNIT_ASSERT(!originalArtist.isEmpty());
// Write chapters
f.setNeroChapters(MP4::ChapterList{
MP4::Chapter("Intro", 0),
MP4::Chapter("Verse", 10000LL)});
CPPUNIT_ASSERT(f.save());
}
// Verify chapters are written AND existing tags are preserved
{
MP4::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
MP4::ChapterList chapters = f.neroChapters();
CPPUNIT_ASSERT_EQUAL(2U, chapters.size());
CPPUNIT_ASSERT_EQUAL(String("Intro"), chapters[0].title());
CPPUNIT_ASSERT_EQUAL(String("Verse"), chapters[1].title());
CPPUNIT_ASSERT_EQUAL(originalArtist, f.tag()->artist());
// Remove chapters and verify tags still survive
f.setNeroChapters(MP4::ChapterList());
CPPUNIT_ASSERT(f.save());
}
{
MP4::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT_EQUAL(originalArtist, f.tag()->artist());
CPPUNIT_ASSERT(f.neroChapters().isEmpty());
}
}
void testChapterListReadEmpty()
{
// Reading from a file with no chpl atom should return empty list
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
{
MP4::File f(filename.c_str());
CPPUNIT_ASSERT(f.neroChapters().isEmpty());
}
}
void testQTChapterListWrite()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
// File should have no QT chapters initially
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
CPPUNIT_ASSERT(chapters.isEmpty());
}
// Write chapters (times in 100-nanosecond units)
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("Intro", 0),
MP4::Chapter("Verse", 15000LL),
MP4::Chapter("Outro", 30000LL)
});
CPPUNIT_ASSERT(f.save());
}
// Read back and verify
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
CPPUNIT_ASSERT_EQUAL(3U, chapters.size());
CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime());
CPPUNIT_ASSERT_EQUAL(String("Intro"), chapters[0].title());
CPPUNIT_ASSERT_EQUAL(15000LL, chapters[1].startTime());
CPPUNIT_ASSERT_EQUAL(String("Verse"), chapters[1].title());
CPPUNIT_ASSERT_EQUAL(30000LL, chapters[2].startTime());
CPPUNIT_ASSERT_EQUAL(String("Outro"), chapters[2].title());
}
}
void testQTChapterListRemove()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
// Write chapters first
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("Chapter 1", 0),
MP4::Chapter("Chapter 2", 10000LL)
});
CPPUNIT_ASSERT(f.save());
}
// Verify written
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
CPPUNIT_ASSERT_EQUAL(2U, chapters.size());
// Remove chapters
f.setQtChapters(MP4::ChapterList());
CPPUNIT_ASSERT(f.save());
}
// Verify removed
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
CPPUNIT_ASSERT(chapters.isEmpty());
// Remove from file with no chapters should also succeed
f.setQtChapters(MP4::ChapterList());
CPPUNIT_ASSERT(f.save());
}
}
void testQTChapterListWithExistingTags()
{
ScopedFileCopy copy("has-tags", ".m4a");
string filename = copy.fileName();
// File has existing tags -- verify they survive chapter operations
String originalArtist;
{
MP4::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
originalArtist = f.tag()->artist();
CPPUNIT_ASSERT(!originalArtist.isEmpty());
// Write chapters
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("Intro", 0),
MP4::Chapter("Verse", 10000LL)
});
CPPUNIT_ASSERT(f.save());
}
// Verify chapters are written AND existing tags are preserved
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
CPPUNIT_ASSERT_EQUAL(2U, chapters.size());
CPPUNIT_ASSERT_EQUAL(String("Intro"), chapters[0].title());
CPPUNIT_ASSERT_EQUAL(String("Verse"), chapters[1].title());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT_EQUAL(originalArtist, f.tag()->artist());
// Remove chapters and verify tags still survive
f.setQtChapters(MP4::ChapterList());
CPPUNIT_ASSERT(f.save());
}
{
MP4::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT_EQUAL(originalArtist, f.tag()->artist());
CPPUNIT_ASSERT(f.qtChapters().isEmpty());
}
}
void testQTChapterListReadEmpty()
{
// Reading from a file with no chapter track should return empty list
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
{
MP4::File f(filename.c_str());
CPPUNIT_ASSERT(f.qtChapters().isEmpty());
}
}
void testQTChapterListOverwrite()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
// Write initial chapters
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("Old1", 0),
MP4::Chapter("Old2", 5000LL)
});
CPPUNIT_ASSERT(f.save());
}
// Verify initial
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
CPPUNIT_ASSERT_EQUAL(2U, chapters.size());
}
// Overwrite with different chapters
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("New1", 0),
MP4::Chapter("New2", 10000LL),
MP4::Chapter("New3", 20000LL)
});
CPPUNIT_ASSERT(f.save());
}
// Verify overwrite
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
CPPUNIT_ASSERT_EQUAL(3U, chapters.size());
CPPUNIT_ASSERT_EQUAL(String("New1"), chapters[0].title());
CPPUNIT_ASSERT_EQUAL(String("New2"), chapters[1].title());
CPPUNIT_ASSERT_EQUAL(String("New3"), chapters[2].title());
}
}
void testQTChapterListTimestampPrecision()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
// Write chapters at precise times
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("Start", 0),
MP4::Chapter("Precise", 1500LL)
});
CPPUNIT_ASSERT(f.save());
}
// Read back and verify timestamps
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
CPPUNIT_ASSERT_EQUAL(2U, chapters.size());
CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime());
CPPUNIT_ASSERT_EQUAL(1500LL, chapters[1].startTime());
}
}
void testQTChapterListNonZeroFirstChapter()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
// Write chapters where first chapter is NOT at time 0
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("One", 10000LL),
MP4::Chapter("Two", 20000LL),
MP4::Chapter("Three", 30000LL)
});
CPPUNIT_ASSERT(f.save());
}
// Read back -- dummy chapter at time 0 should be stripped
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
CPPUNIT_ASSERT_EQUAL(3U, chapters.size());
CPPUNIT_ASSERT_EQUAL(10000LL, chapters[0].startTime());
CPPUNIT_ASSERT_EQUAL(20000LL, chapters[1].startTime());
CPPUNIT_ASSERT_EQUAL(30000LL, chapters[2].startTime());
CPPUNIT_ASSERT_EQUAL(String("One"), chapters[0].title());
CPPUNIT_ASSERT_EQUAL(String("Two"), chapters[1].title());
CPPUNIT_ASSERT_EQUAL(String("Three"), chapters[2].title());
}
}
// Regression test for the orphaned-mdat bug reported in PR #1325 by ufleisch.
// Each add/remove cycle must leave the file's mdat count unchanged. Before
// the fix, the chapter mdat appended by write() was never removed, so three
// cycles produced originalCount + 3 mdat atoms.
void testQTChapterListNoOrphanedMdat()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
// Count top-level mdat atoms using TagLib's own atom parser.
auto countMdatTagLib = [&]() -> int {
PlainFile pf(filename.c_str());
MP4::Atoms atoms(&pf);
int count = 0;
for(const auto *atom : atoms.atoms())
if(atom->name() == "mdat")
++count;
return count;
};
const int baseMdatTagLib = countMdatTagLib();
// Three add/remove cycles (the scenario ufleisch demonstrated).
for(int cycle = 0; cycle < 3; ++cycle) {
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("Chapter 1", 0),
MP4::Chapter("Chapter 2", 10000LL)
});
CPPUNIT_ASSERT(f.save());
}
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList());
CPPUNIT_ASSERT(f.save());
}
}
// No orphaned mdat atoms should remain.
CPPUNIT_ASSERT_EQUAL(baseMdatTagLib, countMdatTagLib());
}
// Regression test for the data-loss bug reported in PR #1343 by ufleisch.
// Audiobook-style files co-locate chapter text samples inside the main
// audio mdat. In that case the chapter track's stco[0] does NOT mark a
// dedicated chapter mdat -- it points into the shared audio mdat, and
// naively deleting "the mdat at stco[0] - 8" destroys the audio payload.
//
// Simulate that layout by writing a chapter track, then rewriting its
// stco[0] to point at the start of the primary audio mdat. Removing the
// chapter track must leave the audio mdat fully intact.
void testQTChapterListSharedMdatPreservesAudio()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
struct MdatInfo { offset_t offset; offset_t length; };
auto findFirstMdat = [&]() -> MdatInfo {
PlainFile pf(filename.c_str());
MP4::Atoms atoms(&pf);
for(const auto *atom : atoms.atoms())
if(atom->name() == "mdat")
return {atom->offset(), atom->length()};
return {-1, 0};
};
const MdatInfo audioMdat = findFirstMdat();
CPPUNIT_ASSERT(audioMdat.offset >= 0);
CPPUNIT_ASSERT(audioMdat.length > 16);
// Capture the audio mdat bytes so we can confirm byte-for-byte preservation.
ByteVector originalAudioMdat;
{
PlainFile pf(filename.c_str());
pf.seek(audioMdat.offset);
originalAudioMdat = pf.readBlock(audioMdat.length);
}
// Add a chapter track. write() appends its own mdat for the chapter text
// at EOF; we'll relocate stco[0] below to simulate the shared-mdat case.
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("Chapter 1", 0),
MP4::Chapter("Chapter 2", 1000LL)
});
CPPUNIT_ASSERT(f.save());
}
// Rewrite the chapter track's stco[0] to point inside the audio mdat's
// data, so findMdatContaining() will identify the audio mdat as the
// candidate target. Choosing audioMdat.offset + 8 (the data start) is
// the worst case: without the shared-mdat guard, the old code would
// treat the audio mdat header as the chapter mdat header and wipe it.
{
PlainFile pf(filename.c_str());
MP4::Atoms atoms(&pf);
const MP4::Atom *moov = atoms.find("moov");
CPPUNIT_ASSERT(moov);
const MP4::AtomList traks = moov->findall("trak");
CPPUNIT_ASSERT(traks.size() >= 2);
// The chapter trak is the most recently added -- find the one whose
// hdlr handler_type is "text".
MP4::Atom *chapterTrak = nullptr;
for(auto *t : traks) {
MP4::Atom *hdlr = t->find("mdia", "hdlr");
if(!hdlr) continue;
pf.seek(hdlr->offset());
if(ByteVector d = pf.readBlock(hdlr->length()); d.containsAt("text", 16)) {
chapterTrak = t;
break;
}
}
CPPUNIT_ASSERT(chapterTrak);
MP4::Atom *stco = chapterTrak->find("mdia", "minf", "stbl", "stco");
CPPUNIT_ASSERT(stco);
// stco payload: full-box header(4) + entry_count(4) + offsets[]
pf.seek(stco->offset() + 16);
pf.writeBlock(ByteVector::fromUInt(
static_cast<unsigned int>(audioMdat.offset + 8)));
}
// Trigger the chapter-removal path with the crafted stco[0].
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList());
CPPUNIT_ASSERT(f.save());
}
// The audio mdat must survive with its contents byte-identical.
const MdatInfo afterMdat = findFirstMdat();
CPPUNIT_ASSERT(afterMdat.offset >= 0);
CPPUNIT_ASSERT_EQUAL(audioMdat.length, afterMdat.length);
{
PlainFile pf(filename.c_str());
pf.seek(afterMdat.offset);
const ByteVector afterBytes = pf.readBlock(afterMdat.length);
CPPUNIT_ASSERT(afterBytes == originalAudioMdat);
}
}
// Unicode titles (CJK, Latin with diacritics, Cyrillic) survive the
// write -> save -> open -> read round-trip through the QT chapter track.
// This exercises the text-sample serialisation in mp4qtchapterlist.cpp.
void testQTChapterListUnicodeTitles()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
// UTF-8: 日本語, Über, Привет
const String japanese("\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e", String::UTF8);
const String german("\xc3\x9c" "ber", String::UTF8);
const String russian("\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82", String::UTF8);
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList{
MP4::Chapter(japanese, 0),
MP4::Chapter(german, 15000LL),
MP4::Chapter(russian, 30000LL)
});
CPPUNIT_ASSERT(f.save());
}
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
CPPUNIT_ASSERT_EQUAL(3U, chapters.size());
CPPUNIT_ASSERT_EQUAL(japanese, chapters[0].title());
CPPUNIT_ASSERT_EQUAL(german, chapters[1].title());
CPPUNIT_ASSERT_EQUAL(russian, chapters[2].title());
}
}
// Unicode titles survive the write -> save -> open -> read round-trip
// through the Nero chpl atom, which uses a different serialisation path
// (length-prefixed UTF-8 inside udta/chpl).
void testChapterListUnicodeTitles()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
// UTF-8: 日本語, Über, Привет
const String japanese("\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e", String::UTF8);
const String german("\xc3\x9c" "ber", String::UTF8);
const String russian("\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82", String::UTF8);
{
MP4::File f(filename.c_str());
f.setNeroChapters(MP4::ChapterList{
MP4::Chapter(japanese, 0),
MP4::Chapter(german, 15000LL),
MP4::Chapter(russian, 30000LL)
});
CPPUNIT_ASSERT(f.save());
}
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.neroChapters();
CPPUNIT_ASSERT_EQUAL(3U, chapters.size());
CPPUNIT_ASSERT_EQUAL(japanese, chapters[0].title());
CPPUNIT_ASSERT_EQUAL(german, chapters[1].title());
CPPUNIT_ASSERT_EQUAL(russian, chapters[2].title());
}
}
// When a multi-chapter list begins with an empty-titled chapter at time 0,
// that entry matches the QT dummy-marker pattern and must be stripped on
// read-back. This test documents the stripping behaviour so a regression
// is immediately detectable.
void testQTChapterListEmptyTitleStripped()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
{
MP4::File f(filename.c_str());
// First entry has an empty title at t=0. write() sees the list already
// starts at t=0 so no dummy is prepended; the empty entry is written
// as-is. read() must strip it because size > 1 && startTime()==0 &&
// title().isEmpty().
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("", 0),
MP4::Chapter("Chapter 1", 5000LL),
MP4::Chapter("Chapter 2", 10000LL)
});
CPPUNIT_ASSERT(f.save());
}
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
// The empty t=0 entry is stripped; only the two real chapters remain.
CPPUNIT_ASSERT_EQUAL(2U, chapters.size());
CPPUNIT_ASSERT_EQUAL(5000LL, chapters[0].startTime());
CPPUNIT_ASSERT_EQUAL(String("Chapter 1"), chapters[0].title());
CPPUNIT_ASSERT_EQUAL(10000LL, chapters[1].startTime());
CPPUNIT_ASSERT_EQUAL(String("Chapter 2"), chapters[1].title());
}
}
// A single chapter with an empty title at time 0 must NOT be stripped.
// The stripping rule applies only when size > 1 -- a file with exactly one
// chapter is valid and its t=0 marker is not a dummy.
void testQTChapterListSingleEmptyTitleNotStripped()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("", 0)
});
CPPUNIT_ASSERT(f.save());
}
{
MP4::File f(filename.c_str());
MP4::ChapterList chapters = f.qtChapters();
CPPUNIT_ASSERT_EQUAL(1U, chapters.size());
CPPUNIT_ASSERT_EQUAL(0LL, chapters[0].startTime());
CPPUNIT_ASSERT_EQUAL(String(""), chapters[0].title());
}
}
// Both Nero (chpl) and QT chapter tracks can coexist in the same file.
// Writing one format must not disturb the other, and removing one must
// leave the other intact -- this validates the saveChaptersIfModified lazy
// save contract in mp4file.cpp.
void testNeroAndQTChaptersAreIndependent()
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
// Write both formats in a single save.
{
MP4::File f(filename.c_str());
f.setNeroChapters(MP4::ChapterList{
MP4::Chapter("Nero 1", 0),
MP4::Chapter("Nero 2", 10000LL)
});
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("QT 1", 0),
MP4::Chapter("QT 2", 20000LL)
});
CPPUNIT_ASSERT(f.save());
}
// Verify both are present and distinct.
{
MP4::File f(filename.c_str());
const MP4::ChapterList nero = f.neroChapters();
const MP4::ChapterList qt = f.qtChapters();
CPPUNIT_ASSERT_EQUAL(2U, nero.size());
CPPUNIT_ASSERT_EQUAL(String("Nero 1"), nero[0].title());
CPPUNIT_ASSERT_EQUAL(String("Nero 2"), nero[1].title());
CPPUNIT_ASSERT_EQUAL(2U, qt.size());
CPPUNIT_ASSERT_EQUAL(String("QT 1"), qt[0].title());
CPPUNIT_ASSERT_EQUAL(String("QT 2"), qt[1].title());
// Remove only the QT track.
f.setQtChapters(MP4::ChapterList());
CPPUNIT_ASSERT(f.save());
}
// QT removed; Nero chapters must be fully intact.
{
MP4::File f(filename.c_str());
CPPUNIT_ASSERT(f.qtChapters().isEmpty());
const MP4::ChapterList nero = f.neroChapters();
CPPUNIT_ASSERT_EQUAL(2U, nero.size());
CPPUNIT_ASSERT_EQUAL(String("Nero 1"), nero[0].title());
CPPUNIT_ASSERT_EQUAL(String("Nero 2"), nero[1].title());
}
}
// Writing only Nero chapters must not accidentally create a QT chapter track,
// and writing only QT chapters must not accidentally create a Nero chpl atom.
void testNeroChaptersAloneWhenNoQT()
{
// Nero only -- QT track must remain absent.
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
{
MP4::File f(filename.c_str());
f.setNeroChapters(MP4::ChapterList{
MP4::Chapter("Nero Only", 0)
});
CPPUNIT_ASSERT(f.save());
}
{
MP4::File f(filename.c_str());
CPPUNIT_ASSERT_EQUAL(1U, f.neroChapters().size());
CPPUNIT_ASSERT(f.qtChapters().isEmpty());
}
}
// QT only -- Nero chpl atom must remain absent.
{
ScopedFileCopy copy("no-tags", ".m4a");
string filename = copy.fileName();
{
MP4::File f(filename.c_str());
f.setQtChapters(MP4::ChapterList{
MP4::Chapter("QT Only", 0)
});
CPPUNIT_ASSERT(f.save());
}
{
MP4::File f(filename.c_str());
CPPUNIT_ASSERT_EQUAL(1U, f.qtChapters().size());
CPPUNIT_ASSERT(f.neroChapters().isEmpty());
}
}
}
void testLazyReadingAndWritingChapters()
{
// No reads or writes if chapters are not used
{
MockChapterFile f(TEST_FILE_PATH_C("no-tags.m4a"));
f.save();
CPPUNIT_ASSERT(!f.chapterList);
}
// Do not read if already read, do not write if not modified
{
MockChapterFile f(TEST_FILE_PATH_C("no-tags.m4a"));
auto chapters = f.chapters();
CPPUNIT_ASSERT(chapters == MockChapterList::mockChapters);
CPPUNIT_ASSERT(f.chapterList);
CPPUNIT_ASSERT_EQUAL(1, f.chapterList->readCount);
chapters = f.chapters();
CPPUNIT_ASSERT(chapters == MockChapterList::mockChapters);
CPPUNIT_ASSERT_EQUAL(1, f.chapterList->readCount);
f.save();
CPPUNIT_ASSERT_EQUAL(1, f.chapterList->readCount);
CPPUNIT_ASSERT_EQUAL(0, f.chapterList->writeCount);
}
// Do not write if not modified
{
MockChapterFile f(TEST_FILE_PATH_C("no-tags.m4a"));
auto chapters = f.chapters();
CPPUNIT_ASSERT(chapters == MockChapterList::mockChapters);
CPPUNIT_ASSERT(f.chapterList);
CPPUNIT_ASSERT_EQUAL(1, f.chapterList->readCount);
f.setChapters(MockChapterList::mockChapters);
f.save();
CPPUNIT_ASSERT_EQUAL(1, f.chapterList->readCount);
CPPUNIT_ASSERT_EQUAL(0, f.chapterList->writeCount);
}
// Write if set without being read before
{
MockChapterFile f(TEST_FILE_PATH_C("no-tags.m4a"));
f.setChapters(MP4::ChapterList());
f.save();
CPPUNIT_ASSERT(f.chapterList);
CPPUNIT_ASSERT_EQUAL(0, f.chapterList->readCount);
CPPUNIT_ASSERT_EQUAL(1, f.chapterList->writeCount);
}
// Do write if modified
{
MockChapterFile f(TEST_FILE_PATH_C("no-tags.m4a"));
CPPUNIT_ASSERT(f.chapters() == MockChapterList::mockChapters);
CPPUNIT_ASSERT(f.chapterList);
CPPUNIT_ASSERT_EQUAL(1, f.chapterList->readCount);
const auto chapters1 = MP4::ChapterList({
MP4::Chapter("Chapter 1", 0),
});
f.setChapters(chapters1);
CPPUNIT_ASSERT(f.chapters() == chapters1);
f.save();
CPPUNIT_ASSERT(f.chapters() == chapters1);
CPPUNIT_ASSERT_EQUAL(1, f.chapterList->readCount);
CPPUNIT_ASSERT_EQUAL(1, f.chapterList->writeCount);
f.setChapters(chapters1);
f.save();
CPPUNIT_ASSERT_EQUAL(1, f.chapterList->writeCount);
auto chapters2 = MP4::ChapterList({
MP4::Chapter("Chapter 1", 0),
MP4::Chapter("Chapter 2", 1),
});
f.setChapters(chapters2);
CPPUNIT_ASSERT(f.chapters() == chapters2);
f.save();
CPPUNIT_ASSERT(f.chapters() == chapters2);
CPPUNIT_ASSERT_EQUAL(2, f.chapterList->writeCount);
chapters2 = MP4::ChapterList({
MP4::Chapter("Chapter 1", 0),
MP4::Chapter("Chapter 2", 2),
});
f.setChapters(chapters2);
f.save();
CPPUNIT_ASSERT_EQUAL(3, f.chapterList->writeCount);
f.setChapters(chapters2);
CPPUNIT_ASSERT(f.chapters() == chapters2);
f.save();
CPPUNIT_ASSERT(f.chapters() == chapters2);
CPPUNIT_ASSERT_EQUAL(3, f.chapterList->writeCount);
const auto chapters3 = MP4::ChapterList({
MP4::Chapter("Chapter 1", 0),
MP4::Chapter("Chapter 3", 2),
});
f.setChapters(chapters3);
CPPUNIT_ASSERT(f.chapters() == chapters3);
f.save();
CPPUNIT_ASSERT(f.chapters() == chapters3);
CPPUNIT_ASSERT_EQUAL(4, f.chapterList->writeCount);
f.setChapters(MP4::ChapterList());
CPPUNIT_ASSERT(f.chapters().isEmpty());
f.save();
CPPUNIT_ASSERT(f.chapters().isEmpty());
CPPUNIT_ASSERT_EQUAL(5, f.chapterList->writeCount);
}
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestMP4);

View File

@@ -58,7 +58,6 @@ public:
CPPUNIT_ASSERT_EQUAL(48000, f.audioProperties()->sampleRate());
CPPUNIT_ASSERT_EQUAL(48000, f.audioProperties()->inputSampleRate());
CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->opusVersion());
CPPUNIT_ASSERT_EQUAL(-17920, f.audioProperties()->outputGain());
}
void testReadComments()

View File

@@ -61,10 +61,6 @@ class TestWAV : public CppUnit::TestFixture
CPPUNIT_TEST(testWaveFormatExtensible);
CPPUNIT_TEST(testInvalidChunk);
CPPUNIT_TEST(testRIFFInfoProperties);
CPPUNIT_TEST(testBEXTTag);
CPPUNIT_TEST(testBEXTTagWithOtherTags);
CPPUNIT_TEST(testiXMLTag);
CPPUNIT_TEST(testiXMLTagWithOtherTags);
CPPUNIT_TEST_SUITE_END();
public:
@@ -320,7 +316,7 @@ public:
{
FileStream stream(copy.fileName().c_str());
stream.seek(0, IOStream::End);
constexpr char garbage[] = "\r2345678";
constexpr char garbage[] = "12345678";
stream.writeBlock(ByteVector(garbage, sizeof(garbage) - 1));
stream.seek(0);
contentsBeforeModification = stream.readBlock(stream.length());
@@ -486,151 +482,6 @@ public:
}
}
void testBEXTTag()
{
ScopedFileCopy copy("empty", ".wav");
string filename = copy.fileName();
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(!f.hasBEXTData());
CPPUNIT_ASSERT(f.BEXTData().isEmpty());
f.setBEXTData(ByteVector("test bext data"));
f.save();
CPPUNIT_ASSERT(f.hasBEXTData());
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasBEXTData());
CPPUNIT_ASSERT_EQUAL(ByteVector("test bext data"), f.BEXTData());
f.setBEXTData(ByteVector());
f.save();
CPPUNIT_ASSERT(!f.hasBEXTData());
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(!f.hasBEXTData());
CPPUNIT_ASSERT(f.BEXTData().isEmpty());
}
// Check if file without BEXT is same as original empty file
const ByteVector origData = PlainFile(TEST_FILE_PATH_C("empty.wav")).readAll();
const ByteVector fileData = PlainFile(filename.c_str()).readAll();
CPPUNIT_ASSERT(origData == fileData);
}
void testBEXTTagWithOtherTags()
{
ScopedFileCopy copy("empty", ".wav");
string filename = copy.fileName();
{
RIFF::WAV::File f(filename.c_str());
f.ID3v2Tag()->setTitle("ID3v2 Title");
f.InfoTag()->setTitle("INFO Title");
f.setBEXTData(ByteVector("bext payload"));
f.save();
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.hasID3v2Tag());
CPPUNIT_ASSERT(f.hasInfoTag());
CPPUNIT_ASSERT(f.hasBEXTData());
CPPUNIT_ASSERT_EQUAL(String("ID3v2 Title"), f.ID3v2Tag()->title());
CPPUNIT_ASSERT_EQUAL(String("INFO Title"), f.InfoTag()->title());
CPPUNIT_ASSERT_EQUAL(ByteVector("bext payload"), f.BEXTData());
}
}
void testiXMLTag()
{
ScopedFileCopy copy("empty", ".wav");
string filename = copy.fileName();
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT(f.iXMLData().isEmpty());
f.setiXMLData("<BWFXML><IXML_VERSION>1.0</IXML_VERSION></BWFXML>");
f.save();
CPPUNIT_ASSERT(f.hasiXMLData());
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(f.hasiXMLData());
CPPUNIT_ASSERT_EQUAL(
String("<BWFXML><IXML_VERSION>1.0</IXML_VERSION></BWFXML>"),
f.iXMLData());
f.setiXMLData(String());
f.save();
CPPUNIT_ASSERT(!f.hasiXMLData());
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT(f.iXMLData().isEmpty());
}
// Check if file without iXML is same as original empty file
const ByteVector origData = PlainFile(TEST_FILE_PATH_C("empty.wav")).readAll();
const ByteVector fileData = PlainFile(filename.c_str()).readAll();
CPPUNIT_ASSERT(origData == fileData);
}
void testiXMLTagWithOtherTags()
{
ScopedFileCopy copy("empty", ".wav");
string filename = copy.fileName();
{
RIFF::WAV::File f(filename.c_str());
f.ID3v2Tag()->setTitle("ID3v2 Title");
f.setiXMLData("<BWFXML><SCENE>1</SCENE></BWFXML>");
f.setBEXTData(ByteVector("bext data"));
f.save();
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.hasID3v2Tag());
CPPUNIT_ASSERT(f.hasiXMLData());
CPPUNIT_ASSERT(f.hasBEXTData());
CPPUNIT_ASSERT_EQUAL(String("ID3v2 Title"), f.ID3v2Tag()->title());
CPPUNIT_ASSERT_EQUAL(
String("<BWFXML><SCENE>1</SCENE></BWFXML>"),
f.iXMLData());
CPPUNIT_ASSERT_EQUAL(ByteVector("bext data"), f.BEXTData());
f.setiXMLData(String());
f.setBEXTData(ByteVector());
f.strip();
CPPUNIT_ASSERT(f.save());
}
{
RIFF::WAV::File f(filename.c_str());
CPPUNIT_ASSERT(f.isValid());
CPPUNIT_ASSERT(!f.hasID3v2Tag());
CPPUNIT_ASSERT(!f.hasiXMLData());
CPPUNIT_ASSERT(f.iXMLData().isEmpty());
CPPUNIT_ASSERT(!f.hasBEXTData());
CPPUNIT_ASSERT(f.BEXTData().isEmpty());
}
// Check if file without tags is same as original empty file
const ByteVector origData = PlainFile(TEST_FILE_PATH_C("empty.wav")).readAll();
const ByteVector fileData = PlainFile(filename.c_str()).readAll();
CPPUNIT_ASSERT(origData == fileData);
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestWAV);

View File

@@ -32,11 +32,7 @@
#else
#include <unistd.h>
#include <fcntl.h>
#ifdef __HAIKU__
#include <fcntl.h>
#else
#include <sys/fcntl.h>
#endif
#include <sys/stat.h>
#endif
#include <cstdio>