improved ID3v2 implementation

This commit is contained in:
Martchus 2015-12-22 17:01:25 +01:00
parent 47e7f4eea4
commit 979427beb3
5 changed files with 157 additions and 86 deletions

View File

@ -32,7 +32,7 @@ Id3v2Frame::Id3v2Frame() :
m_group(0),
m_parsedVersion(0),
m_dataSize(0),
m_frameSize(0),
m_totalSize(0),
m_padding(false)
{}
@ -45,7 +45,7 @@ Id3v2Frame::Id3v2Frame(const identifierType &id, const TagValue &value, byte gro
m_group(group),
m_parsedVersion(0),
m_dataSize(0),
m_frameSize(0),
m_totalSize(0),
m_padding(false)
{}
@ -76,8 +76,8 @@ void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize)
}
context = "parsing " + helper.id() + " frame";
m_dataSize = reader.readUInt24BE();
m_frameSize = m_dataSize + 6;
if(m_frameSize > maximalSize) {
m_totalSize = m_dataSize + 6;
if(m_totalSize > maximalSize) {
addNotification(NotificationType::Warning, "The frame is truncated and will be ignored.", "parsing " + frameIdString() + " frame");
throw TruncatedDataException();
}
@ -97,8 +97,8 @@ void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize)
m_dataSize = version >= 4
? reader.readSynchsafeUInt32BE()
: reader.readUInt32BE();
m_frameSize = m_dataSize + 10;
if(m_frameSize > maximalSize) {
m_totalSize = m_dataSize + 10;
if(m_totalSize > maximalSize) {
addNotification(NotificationType::Warning, "The frame is truncated and will be ignored.", context);
throw TruncatedDataException();
}
@ -370,7 +370,7 @@ void Id3v2Frame::cleared()
m_group = 0;
m_parsedVersion = 0;
m_dataSize = 0;
m_frameSize = 0;
m_totalSize = 0;
m_padding = false;
}

View File

@ -25,8 +25,8 @@ public:
Id3v2FrameHelper(const std::string &id, StatusProvider &provider);
const std::string &id() const;
TagTextEncoding parseTextEncodingByte(byte textEncodingByte);
byte makeTextEncodingByte(TagTextEncoding textEncoding);
TagTextEncoding parseTextEncodingByte(byte textEncodingByte);
std::tuple<const char *, size_t, const char *> parseSubstring(const char *buffer, std::size_t maxSize, TagTextEncoding &encoding, bool addWarnings = false);
std::string parseString(const char *buffer, std::size_t maxSize, TagTextEncoding &encoding, bool addWarnings = false);
std::wstring parseWideString(const char *buffer, std::size_t dataSize, TagTextEncoding &encoding, bool addWarnings = false);
@ -34,6 +34,8 @@ public:
void parsePicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo);
void parseComment(const char *buffer, size_t maxSize, TagValue &tagValue);
void parseBom(const char *buffer, std::size_t maxSize, TagTextEncoding &encoding);
byte makeTextEncodingByte(TagTextEncoding textEncoding);
void makeString(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const std::string &value, TagTextEncoding encoding);
void makeEncodingAndData(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, TagTextEncoding encoding, const char *data, size_t m_dataSize);
void makeLegacyPicture(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo);
@ -95,7 +97,7 @@ public:
std::string frameIdString() const;
int16 flag() const;
void setFlag(int16 value);
uint32 frameSize() const;
uint32 totalSize() const;
uint32 dataSize() const;
bool toDiscardWhenUnknownAndTagIsAltered() const;
bool toDiscardWhenUnknownAndFileIsAltered() const;
@ -118,7 +120,7 @@ private:
byte m_group;
int32 m_parsedVersion;
uint32 m_dataSize;
uint32 m_frameSize;
uint32 m_totalSize;
bool m_padding;
};
@ -175,11 +177,11 @@ inline void Id3v2Frame::setFlag(int16 value)
}
/*!
* \brief Returns the size of the frame in bytes.
* \brief Returns the total size of the frame in bytes.
*/
inline uint32 Id3v2Frame::frameSize() const
inline uint32 Id3v2Frame::totalSize() const
{
return m_frameSize;
return m_totalSize;
}
/*!
@ -195,7 +197,7 @@ inline uint32 Id3v2Frame::dataSize() const
*/
inline bool Id3v2Frame::toDiscardWhenUnknownAndTagIsAltered() const
{
return (m_flag & 0x8000) == 0x8000;
return m_flag & 0x8000;
}
/*!
@ -203,7 +205,7 @@ inline bool Id3v2Frame::toDiscardWhenUnknownAndTagIsAltered() const
*/
inline bool Id3v2Frame::toDiscardWhenUnknownAndFileIsAltered() const
{
return (m_flag & 0x4000) == 0x4000;
return m_flag & 0x4000;
}
/*!
@ -211,7 +213,7 @@ inline bool Id3v2Frame::toDiscardWhenUnknownAndFileIsAltered() const
*/
inline bool Id3v2Frame::isReadOnly() const
{
return (m_flag & 0x2000) == 0x2000;
return m_flag & 0x2000;
}
/*!
@ -219,7 +221,7 @@ inline bool Id3v2Frame::isReadOnly() const
*/
inline bool Id3v2Frame::isCompressed() const
{
return m_parsedVersion >= 4 ? (m_flag & 0x8) == 0x8 : (m_flag & 0x80) == 0x80;
return m_parsedVersion >= 4 ? m_flag & 0x8 : m_flag & 0x80;
}
/*!
@ -228,7 +230,7 @@ inline bool Id3v2Frame::isCompressed() const
*/
inline bool Id3v2Frame::isEncrypted() const
{
return m_parsedVersion >= 4 ? (m_flag & 0x4) == 0x8 : (m_flag & 0x40) == 0x40;
return m_parsedVersion >= 4 ? m_flag & 0x4 : m_flag & 0x40;
}
/*!
@ -236,7 +238,7 @@ inline bool Id3v2Frame::isEncrypted() const
*/
inline bool Id3v2Frame::hasGroupInformation() const
{
return m_parsedVersion >= 4 ? (m_flag & 0x40) == 0x40 : (m_flag & 0x20) == 0x20;
return m_parsedVersion >= 4 ? m_flag & 0x40 : m_flag & 0x20;
}
/*!
@ -244,7 +246,7 @@ inline bool Id3v2Frame::hasGroupInformation() const
*/
inline bool Id3v2Frame::isUnsynchronized() const
{
return m_parsedVersion >= 4 ? (m_flag & 0x2) == 0x2 : false;
return m_parsedVersion >= 4 ? m_flag & 0x2 : false;
}
/*!
@ -252,7 +254,7 @@ inline bool Id3v2Frame::isUnsynchronized() const
*/
inline bool Id3v2Frame::hasDataLengthIndicator() const
{
return m_parsedVersion >= 4 ? (m_flag & 0x1) == 0x1 : isCompressed();
return m_parsedVersion >= 4 ? m_flag & 0x1 : isCompressed();
}
/*!

View File

@ -181,13 +181,20 @@ bool Id3v2Tag::setValue(const typename Id3v2Frame::identifierType &id, const Tag
* \throws Throws Media::Failure or a derived exception when a parsing
* error occurs.
*/
void Id3v2Tag::parse(istream &stream)
void Id3v2Tag::parse(istream &stream, uint64 maximalSize)
{
// prepare parsing
invalidateStatus();
const string context("parsing ID3v2 tag");
BinaryReader reader(&stream);
uint64 startOffset = stream.tellg();
// check whether the header is truncated
if(maximalSize && maximalSize < 10) {
addNotification(NotificationType::Critical, "ID3v2 header is truncated (at least 10 bytes expected).", context);
throw TruncatedDataException();
}
// read signature: ID3
if(reader.readUInt24BE() == 0x494433u) {
// read header data
@ -196,7 +203,7 @@ void Id3v2Tag::parse(istream &stream)
setVersion(majorVersion, revisionVersion);
m_flags = reader.readByte();
m_sizeExcludingHeader = reader.readSynchsafeUInt32BE();
m_size = m_sizeExcludingHeader + 10;
m_size = 10 + m_sizeExcludingHeader;
if(m_sizeExcludingHeader == 0) {
addNotification(NotificationType::Warning, "ID3v2 tag seems to be empty.", context);
} else {
@ -205,50 +212,77 @@ void Id3v2Tag::parse(istream &stream)
addNotification(NotificationType::Critical, "The ID3v2 tag couldn't be parsed, because its version is not supported.", context);
throw VersionNotSupportedException();
}
// read extended header (if present)
if(hasExtendedHeader()) {
if(maximalSize && maximalSize < 14) {
addNotification(NotificationType::Critical, "Extended header denoted but not present.", context);
throw TruncatedDataException();
}
m_extendedHeaderSize = reader.readSynchsafeUInt32BE();
if(m_extendedHeaderSize < 6 || m_extendedHeaderSize > m_sizeExcludingHeader) {
addNotification(NotificationType::Critical, "Extended header is invalid.", context);
throw InvalidDataException();
if(m_extendedHeaderSize < 6 || m_extendedHeaderSize > m_sizeExcludingHeader || (maximalSize && maximalSize < (10 + m_extendedHeaderSize))) {
addNotification(NotificationType::Critical, "Extended header is invalid/truncated.", context);
throw TruncatedDataException();
}
stream.seekg(m_extendedHeaderSize - 4, ios_base::cur);
}
// how many bytes remain for frames and padding?
uint32 bytesRemaining = m_sizeExcludingHeader - m_extendedHeaderSize;
if(bytesRemaining > maximalSize) {
bytesRemaining = maximalSize;
addNotification(NotificationType::Critical, "Frames are truncated.", context);
}
// read frames
istream::pos_type pos = stream.tellg();
uint32 frameSize;
int32 bytesRemaining = m_sizeExcludingHeader - m_extendedHeaderSize;
auto pos = stream.tellg();
Id3v2Frame frame;
const uint32 &frameId = frame.id();
do {
while(bytesRemaining) {
// seek to next frame
stream.seekg(pos);
// parse frame
try {
frame.parse(reader, majorVersion, bytesRemaining);
if(frameId) {
if(frame.id()) {
// add frame if parsing was successfull
if(Id3v2FrameIds::isTextfield(frameId) && fields().count(frame.id()) == 1) {
if(Id3v2FrameIds::isTextfield(frame.id()) && fields().count(frame.id()) == 1) {
addNotification(NotificationType::Warning, "The text frame " + frame.frameIdString() + " exists more than once.", context);
}
fields().insert(pair<fieldType::identifierType, fieldType>(frameId, frame));
fields().insert(pair<fieldType::identifierType, fieldType>(frame.id(), frame));
}
} catch(NoDataFoundException &) {
} catch(const NoDataFoundException &) {
if(frame.hasPaddingReached()) {
m_paddingSize = (startOffset + m_size) - pos;
m_paddingSize = startOffset + m_size - pos;
break;
}
} catch(Failure &) {
} catch(const Failure &) {
// nothing to do here since notifications will be added anyways
}
// add parsing notifications of frame
addNotifications(context, frame);
frame.invalidateNotifications();
// calculate next frame offset
frameSize = frame.frameSize();
bytesRemaining -= frameSize;
pos += frameSize;
} while (bytesRemaining > 0);
bytesRemaining -= frame.totalSize();
pos += frame.totalSize();
}
// check for extended header
if(hasFooter()) {
if(m_size + 10 < maximalSize) {
// the footer does not provide additional information, just check the signature
stream.seekg(startOffset + (m_size += 10));
if(reader.readUInt24LE() != 0x494433u) {
addNotification(NotificationType::Critical, "Footer signature is invalid.", context);
}
// skip remaining footer
stream.seekg(7, ios_base::cur);
} else {
addNotification(NotificationType::Critical, "Footer denoted but not present.", context);
throw TruncatedDataException();
}
}
}
} else {
addNotification(NotificationType::Critical, "Signature is invalid.", context);

View File

@ -34,7 +34,7 @@ public:
bool supportsDescription(KnownField field) const;
bool supportsMimeType(KnownField field) const;
void parse(std::istream &sourceStream);
void parse(std::istream &sourceStream, uint64 maximalSize = 0);
void make(std::ostream &targetStream);
byte majorVersion() const;
@ -140,7 +140,7 @@ inline byte Id3v2Tag::flags() const
*/
inline bool Id3v2Tag::isUnsynchronisationUsed() const
{
return (m_flags & 0x80) == 0x80;
return m_flags & 0x80;
}
/*!
@ -148,7 +148,7 @@ inline bool Id3v2Tag::isUnsynchronisationUsed() const
*/
inline bool Id3v2Tag::hasExtendedHeader() const
{
return (m_majorVersion >= 3) && ((m_flags & 0x40) == 0x40);
return (m_majorVersion >= 3) && (m_flags & 0x40);
}
/*!
@ -156,7 +156,7 @@ inline bool Id3v2Tag::hasExtendedHeader() const
*/
inline bool Id3v2Tag::isExperimental() const
{
return (m_majorVersion >= 3) && ((m_flags & 0x20) == 0x20);
return (m_majorVersion >= 3) && (m_flags & 0x20);
}
/*!
@ -164,7 +164,7 @@ inline bool Id3v2Tag::isExperimental() const
*/
inline bool Id3v2Tag::hasFooter() const
{
return (m_majorVersion >= 3) && ((m_flags & 0x10) == 0x10);
return (m_majorVersion >= 3) && (m_flags & 0x10);
}
/*!

View File

@ -38,8 +38,10 @@
#include <iomanip>
#include <ios>
#include <system_error>
#include <functional>
using namespace std;
using namespace std::placeholders;
using namespace IoUtilities;
using namespace ConversionUtilities;
using namespace ChronoUtilities;
@ -144,13 +146,16 @@ void MediaFileInfo::parseContainerFormat()
// there's no need to read the container format twice
return;
}
invalidateStatus();
static const string context("parsing file header");
open(); // ensure the file is open
m_containerFormat = ContainerFormat::Unknown;
// file size
m_paddingSize = 0;
m_containerOffset = 0;
// read signatrue
char buff[16];
const char *const buffEnd = buff + sizeof(buff), *buffOffset;
@ -158,34 +163,50 @@ startParsingSignature:
if(size() - m_containerOffset >= 16) {
stream().seekg(m_containerOffset, ios_base::beg);
stream().read(buff, sizeof(buff));
// skip zero bytes/padding
size_t bytesSkipped = 0;
for(buffOffset = buff; buffOffset != buffEnd && !(*buffOffset); ++buffOffset, ++bytesSkipped);
if(bytesSkipped >= 4) {
m_containerOffset += bytesSkipped;
// give up after 0x100 bytes
if((m_paddingSize += bytesSkipped) >= 0x100u) {
m_containerParsingStatus = ParsingStatus::NotSupported;
m_containerFormat = ContainerFormat::Unknown;
return;
}
// try again
goto startParsingSignature;
}
if(m_paddingSize) {
addNotification(NotificationType::Warning, ConversionUtilities::numberToString(m_paddingSize) + " zero-bytes skipped at the beginning of the file.", context);
}
// parse signature
switch(m_containerFormat = parseSignature(buff, sizeof(buff))) {
switch((m_containerFormat = parseSignature(buff, sizeof(buff)))) {
case ContainerFormat::Id2v2Tag:
// save position of ID3v2 tag
m_actualId3v2TagOffsets.push_back(m_containerOffset);
if(m_actualId3v2TagOffsets.size() == 2) {
addNotification(NotificationType::Warning, "There is more then just one ID3v2 header at the beginning of the file.", context);
}
stream().seekg(m_containerOffset + 6, ios_base::beg);
stream().read(buff, 4);
// read ID3v2 header
stream().seekg(m_containerOffset + 5, ios_base::beg);
stream().read(buff, 5);
// set the container offset to skip ID3v2 header
m_containerOffset += ConversionUtilities::toNormalInt(ConversionUtilities::BE::toUInt32(buff)) + 10;
goto startParsingSignature; // read signature again
m_containerOffset += ConversionUtilities::toNormalInt(ConversionUtilities::BE::toUInt32(buff + 1)) + 10;
if((*buff) & 0x10) {
// footer present
m_containerOffset += 10;
}
// continue reading signature
goto startParsingSignature;
case ContainerFormat::Mp4: {
m_container = make_unique<Mp4Container>(*this, m_containerOffset);
NotificationList notifications;
@ -196,6 +217,7 @@ startParsingSignature:
}
addNotifications(notifications);
break;
} case ContainerFormat::Ebml: {
auto container = make_unique<MatroskaContainer>(*this, m_containerOffset);
NotificationList notifications;
@ -226,6 +248,8 @@ startParsingSignature:
;
}
}
// set parsing status
if(m_containerParsingStatus == ParsingStatus::NotParsedYet) {
if(m_containerFormat == ContainerFormat::Unknown) {
m_containerParsingStatus = ParsingStatus::NotSupported;
@ -1334,19 +1358,23 @@ void MediaFileInfo::invalidated()
void MediaFileInfo::makeMp3File()
{
const string context("making MP3 file");
// there's no need to rewrite the complete file
// there's no need to rewrite the complete file if there is just are not ID3v2 tags present or to be written
if(m_id3v2Tags.size() == 0 && m_actualId3v2TagOffsets.size() == 0) {
if(m_actualExistingId3v1Tag) { // there is currently an ID3v1 tag at the end of the file
if(m_id3v1Tag) { // the file shall still have an ID3v1 tag
if(m_actualExistingId3v1Tag) {
// there is currently an ID3v1 tag at the end of the file
if(m_id3v1Tag) {
// the file shall still have an ID3v1 tag
updateStatus("No need to rewrite the whole file, just writing ID3v1 tag ...");
open(); // ensure the file is still open
// ensure the file is still open / not readonly
open();
stream().seekp(-128, ios_base::end);
try {
m_id3v1Tag->make(stream());
} catch(Failure &) {
addNotification(NotificationType::Warning, "Unable to write ID3v1 tag.", context);
}
} else { // the currently existing id3v1 tag shall be removed
} else {
// the currently existing ID3v1 tag shall be removed
updateStatus("No need to rewrite the whole file, just truncating it to remove ID3v1 tag ...");
stream().close();
if(truncate(path().c_str(), size() - 128) == 0) {
@ -1356,10 +1384,13 @@ void MediaFileInfo::makeMp3File()
throw ios_base::failure("Unable to truncate file to remove ID3v1 tag.");
}
}
} else { // the doesn't file need to be rewritten
} else {
// there is currently no ID3v1 tag at the end of the file
if(m_id3v1Tag) {
updateStatus("No need to rewrite the whole file, just writing ID3v1 tag.");
open(); // ensure the file is still open
// ensure the file is still open / not readonly
open();
stream().seekp(0, ios_base::end);
try {
m_id3v1Tag->make(stream());
@ -1370,23 +1401,30 @@ void MediaFileInfo::makeMp3File()
addNotification(NotificationType::Information, "Nothing to be changed.", context);
}
}
} else { // the file needs to be rewritten
} else {
// ID3v2 needs to be modified -> file needs to be rewritten
// TODO: take advantage of possibly available padding
// prepare for rewriting
updateStatus("Prepareing for rewriting MP3 file ...");
close(); // close the file (if its opened)
string backupPath;
fstream backupStream;
try {
close();
BackupHelper::createBackupFile(path(), backupPath, backupStream);
backupStream.seekg(m_containerOffset);
// recreate original file with new/changed ID3 tags
stream().open(path(), ios_base::out | ios_base::binary | ios_base::trunc);
updateStatus("Writing ID3v2 tag ...");
// write id3v2 tags
int counter = 1;
// write ID3v2 tags
unsigned int counter = 1;
for(auto &id3v2Tag : m_id3v2Tags) {
try {
id3v2Tag->make(stream());
} catch(Failure &) {
} catch(const Failure &) {
if(m_id3v2Tags.size()) {
addNotification(NotificationType::Warning, "Unable to write " + ConversionUtilities::numberToString(counter) + ". ID3v2 tag.", context);
} else {
@ -1395,25 +1433,17 @@ void MediaFileInfo::makeMp3File()
}
++counter;
}
// recopy backup
updateStatus("Writing mpeg audio frames ...");
uint64 remainingBytes = size() - backupStream.tellg(), read;
// write media data
updateStatus("Writing MPEG audio frames ...");
uint64 bytesRemaining = size() - m_containerOffset;
if(m_actualExistingId3v1Tag) {
remainingBytes -= 128;
bytesRemaining -= 128;
}
const unsigned int bufferSize = 0x4000;
char buffer[bufferSize];
while(remainingBytes > 0) {
if(isAborted()) {
throw OperationAbortedException();
}
backupStream.read(buffer, remainingBytes > bufferSize ? bufferSize : remainingBytes);
read = backupStream.gcount();
stream().write(buffer, read);
remainingBytes -= read;
updatePercentage(static_cast<double>(backupStream.tellg()) / static_cast<double>(size()));
}
// write id3v1 tag
CopyHelper<0x4000> copyHelper;
copyHelper.callbackCopy(backupStream, stream(), bytesRemaining, bind(&StatusProvider::isAborted, this), bind(&StatusProvider::updatePercentage, this, _1));
// write ID3v1 tag
updateStatus("Writing ID3v1 tag ...");
if(m_id3v1Tag) {
try {
@ -1422,14 +1452,19 @@ void MediaFileInfo::makeMp3File()
addNotification(NotificationType::Warning, "Unable to write ID3v1 tag.", context);
}
}
stream().flush(); // ensure everything has been actually written
reportSizeChanged(stream().tellp()); // report new size
close(); // stream is useless for further usage because it is write only
} catch(OperationAbortedException &) {
// ensure everything has been actually written
stream().flush();
// report new size
reportSizeChanged(stream().tellp());
// stream is useless for further usage because it is write-only
close();
} catch(const OperationAbortedException &) {
addNotification(NotificationType::Information, "Rewriting file to apply new tag information has been aborted.", context);
BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, stream(), backupStream);
throw;
} catch(ios_base::failure &ex) {
} catch(const ios_base::failure &ex) {
addNotification(NotificationType::Critical, "IO error occured when rewriting file to apply new tag information.\n" + string(ex.what()), context);
BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, stream(), backupStream);
throw;