diff --git a/id3/id3v2frame.cpp b/id3/id3v2frame.cpp index ccb8944..df16a19 100644 --- a/id3/id3v2frame.cpp +++ b/id3/id3v2frame.cpp @@ -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; } diff --git a/id3/id3v2frame.h b/id3/id3v2frame.h index 64057b5..3d5cbff 100644 --- a/id3/id3v2frame.h +++ b/id3/id3v2frame.h @@ -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 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 &buffer, uint32 &bufferSize, const std::string &value, TagTextEncoding encoding); void makeEncodingAndData(std::unique_ptr &buffer, uint32 &bufferSize, TagTextEncoding encoding, const char *data, size_t m_dataSize); void makeLegacyPicture(std::unique_ptr &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(); } /*! diff --git a/id3/id3v2tag.cpp b/id3/id3v2tag.cpp index 96172d0..94dae0b 100644 --- a/id3/id3v2tag.cpp +++ b/id3/id3v2tag.cpp @@ -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(frameId, frame)); + fields().insert(pair(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); diff --git a/id3/id3v2tag.h b/id3/id3v2tag.h index b208f97..02627ef 100644 --- a/id3/id3v2tag.h +++ b/id3/id3v2tag.h @@ -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); } /*! diff --git a/mediafileinfo.cpp b/mediafileinfo.cpp index f48d215..5d78d20 100644 --- a/mediafileinfo.cpp +++ b/mediafileinfo.cpp @@ -38,8 +38,10 @@ #include #include #include +#include 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(*this, m_containerOffset); NotificationList notifications; @@ -196,6 +217,7 @@ startParsingSignature: } addNotifications(notifications); break; + } case ContainerFormat::Ebml: { auto container = make_unique(*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(backupStream.tellg()) / static_cast(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;