From c6afe5b0f2387a4487b2d3e556ddce6f02679e7f Mon Sep 17 00:00:00 2001 From: Martchus Date: Tue, 22 Dec 2015 23:54:35 +0100 Subject: [PATCH] make use of padding when applying changes to MP3 files --- README.md | 18 +- id3/id3v2frame.cpp | 463 ++++++++++++++++++++------------- id3/id3v2frame.h | 117 ++++++--- id3/id3v2frameids.h | 2 +- id3/id3v2tag.cpp | 151 +++++++---- id3/id3v2tag.h | 44 +++- matroska/matroskacontainer.cpp | 19 +- matroska/matroskatag.cpp | 10 +- matroska/matroskatag.h | 2 +- mediafileinfo.cpp | 225 +++++++++++----- mp4/mp4container.cpp | 10 +- mp4/mp4tag.cpp | 13 +- mp4/mp4tag.h | 2 +- mp4/mp4tagfield.cpp | 7 +- 14 files changed, 706 insertions(+), 377 deletions(-) diff --git a/README.md b/README.md index 1c60076..239b8e4 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,32 @@ The tag library can read and write the following tag formats: - Vorbis comments (cover art via "METADATA_BLOCK_PICTURE" is supported) - Matroska/WebM tags and attachments +## File layout options +### Tag position +The library allows you to choose whether tags should be placed at the beginning or at +the end of an MP4/Matroska file. + +### Padding +Padding allows adding additional tag information without rewriting the entire file +or appending the tag. Usage of padding can be configured: +- minimum/maximum padding: The file is rewritten if the padding would fall below/exceed the specifed limits. +- preferred padding: If the file needs to be rewritten the preferred padding is used. + +However, it is also possible to force rewriting the entire file. + +## Additional features The library can also display technical information such as the ID, format, language, bitrate, duration, size, timestamps, sampling frequency, FPS and other information of the tracks. It also allows to inspect and validate the element structure of MP4 and Matroska files. -For examples check out the CLI interface of my Tag Editor (which is also on Git). +## Usage +For examples check out the command line interface of my Tag Editor (which is also on Git). ## Build instructions The tagparser library depends on c++utilities and is built in the same way. It also depends on zlib. ## TODO -- Use padding to prevent rewriting the entire file to save tags. - Support more tag formats (EXIF, PDF metadata, ...). - Do tests with Matroska files which have multiple segments. diff --git a/id3/id3v2frame.cpp b/id3/id3v2frame.cpp index df16a19..1eda70d 100644 --- a/id3/id3v2frame.cpp +++ b/id3/id3v2frame.cpp @@ -39,7 +39,7 @@ Id3v2Frame::Id3v2Frame() : /*! * \brief Constructs a new Id3v2Frame with the specified \a id, \a value, \a group and \a flag. */ -Id3v2Frame::Id3v2Frame(const identifierType &id, const TagValue &value, byte group, int16 flag) : +Id3v2Frame::Id3v2Frame(const identifierType &id, const TagValue &value, const byte group, const int16 flag) : TagField(id, value), m_flag(flag), m_group(group), @@ -59,41 +59,57 @@ Id3v2Frame::Id3v2Frame(const identifierType &id, const TagValue &value, byte gro * \throws Throws Media::Failure or a derived exception when a parsing * error occurs. */ -void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize) +void Id3v2Frame::parse(BinaryReader &reader, const uint32 version, const uint32 maximalSize) { invalidateStatus(); string context("parsing ID3v2 frame"); - Id3v2FrameHelper helper(frameIdString(), *this); + + // parse header if(version < 3) { // parse header for ID3v2.1 and ID3v2.2 + // -> read ID setId(reader.readUInt24BE()); if((id() & 0xFFFF0000u) == 0) { + // padding reached m_padding = true; addNotification(NotificationType::Debug, "Frame ID starts with null-byte -> padding reached.", context); throw NoDataFoundException(); } else { m_padding = false; } - context = "parsing " + helper.id() + " frame"; + + // -> update context + context = "parsing " + frameIdString() + " frame"; + + // -> read size, check whether frame is truncated m_dataSize = reader.readUInt24BE(); 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(); } + + // -> no flags/group in ID3v2.2 m_flag = 0; m_group = 0; + } else { // parse header for ID3v2.3 and ID3v2.4 + // -> read ID setId(reader.readUInt32BE()); if((id() & 0xFF000000u) == 0) { + // padding reached m_padding = true; addNotification(NotificationType::Debug, "Frame ID starts with null-byte -> padding reached.", context); throw NoDataFoundException(); } else { m_padding = false; } - context = "parsing " + helper.id() + " frame"; + + // -> update context + context = "parsing " + frameIdString() + " frame"; + + // -> read size, check whether frame is truncated m_dataSize = version >= 4 ? reader.readSynchsafeUInt32BE() : reader.readUInt32BE(); @@ -102,27 +118,34 @@ void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize) addNotification(NotificationType::Warning, "The frame is truncated and will be ignored.", context); throw TruncatedDataException(); } + + // -> read flags and group m_flag = reader.readUInt16BE(); m_group = hasGroupInformation() ? reader.readByte() : 0; if(isEncrypted()) { + // encryption is not implemented addNotification(NotificationType::Critical, "Encrypted frames aren't supported.", context); throw VersionNotSupportedException(); } } + + // frame size mustn't be 0 if(m_dataSize <= 0) { addNotification(NotificationType::Critical, "The frame size is 0.", context); throw InvalidDataException(); } + // parse the data unique_ptr buffer; + + // -> decompress data if compressed; otherwise just read it if(isCompressed()) { - // decompress compressed data uLongf decompressedSize = version >= 4 ? reader.readSynchsafeUInt32BE() : reader.readUInt32BE(); if(decompressedSize < m_dataSize) { addNotification(NotificationType::Critical, "The decompressed size is smaller then the compressed size.", context); throw InvalidDataException(); } - unique_ptr bufferCompressed = make_unique(m_dataSize);; + auto bufferCompressed = make_unique(m_dataSize);; reader.read(bufferCompressed.get(), m_dataSize); buffer = make_unique(decompressedSize); switch(uncompress(reinterpret_cast(buffer.get()), &decompressedSize, reinterpret_cast(bufferCompressed.get()), m_dataSize)) { @@ -136,16 +159,21 @@ void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize) addNotification(NotificationType::Critical, "Decompressing failed. The input data was corrupted or incomplete.", context); throw InvalidDataException(); case Z_OK: - ; + break; + default: + addNotification(NotificationType::Critical, "Decompressing failed (unknown reason).", context); + throw InvalidDataException(); } m_dataSize = decompressedSize; } else { buffer = make_unique(m_dataSize); reader.read(buffer.get(), m_dataSize); } - if(Id3v2FrameIds::isTextfield(id())) { + + // -> get tag value depending of field type + if(Id3v2FrameIds::isTextFrame(id())) { // frame contains text - TagTextEncoding dataEncoding = helper.parseTextEncodingByte(*buffer.get()); // the first byte stores the encoding + TagTextEncoding dataEncoding = parseTextEncodingByte(*buffer.get()); // the first byte stores the encoding if((version >= 3 && (id() == Id3v2FrameIds::lTrackPosition || id() == Id3v2FrameIds::lDiskPosition)) || (version < 3 && id() == Id3v2FrameIds::sTrackPosition)) { @@ -153,40 +181,42 @@ void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize) try { PositionInSet position; if(characterSize(dataEncoding) > 1) { - position = PositionInSet(helper.parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding)); + position = PositionInSet(parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding)); } else { - position = PositionInSet(helper.parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding)); + position = PositionInSet(parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding)); } value().assignPosition(position); } catch(ConversionException &) { addNotification(NotificationType::Warning, "The value of track/disk position frame is not numeric and will be ignored.", context); } + } else if((version >= 3 && id() == Id3v2FrameIds::lLength) || (version < 3 && id() == Id3v2FrameIds::sLength)) { // frame contains length double milliseconds; try { if(characterSize(dataEncoding) > 1) { - wstring millisecondsStr = helper.parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding); + wstring millisecondsStr = parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding); milliseconds = ConversionUtilities::stringToNumber(millisecondsStr, 10); } else { - milliseconds = ConversionUtilities::stringToNumber(helper.parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding), 10); + milliseconds = ConversionUtilities::stringToNumber(parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding), 10); } value().assignTimeSpan(TimeSpan::fromMilliseconds(milliseconds)); } catch (ConversionException &) { addNotification(NotificationType::Warning, "The value of the length frame is not numeric and will be ignored.", context); } + } else if((version >= 3 && id() == Id3v2FrameIds::lGenre) || (version < 3 && id() == Id3v2FrameIds::sGenre)) { // genre/content type int genreIndex; try { if(characterSize(dataEncoding) > 1) { - wstring indexStr = helper.parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding); + wstring indexStr = parseWideString(buffer.get() + 1, m_dataSize - 1, dataEncoding); if(indexStr.front() == L'(' && indexStr.back() == L')') { indexStr = indexStr.substr(1, indexStr.length() - 2); } genreIndex = ConversionUtilities::stringToNumber(indexStr, 10); } else { - string indexStr = helper.parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding); + string indexStr = parseString(buffer.get() + 1, m_dataSize - 1, dataEncoding); if(indexStr.front() == '(' && indexStr.back() == ')') { indexStr = indexStr.substr(1, indexStr.length() - 2); } @@ -196,169 +226,64 @@ void Id3v2Frame::parse(BinaryReader &reader, int32 version, uint32 maximalSize) } catch(ConversionException &) { // genre is specified as string // string might be null terminated - auto substr = helper.parseSubstring(buffer.get() + 1, m_dataSize - 1, dataEncoding); + auto substr = parseSubstring(buffer.get() + 1, m_dataSize - 1, dataEncoding); value().assignData(get<0>(substr), get<1>(substr), TagDataType::Text, dataEncoding); } } else { // any other text frame // string might be null terminated - auto substr = helper.parseSubstring(buffer.get() + 1, m_dataSize - 1, dataEncoding); + auto substr = parseSubstring(buffer.get() + 1, m_dataSize - 1, dataEncoding); value().assignData(get<0>(substr), get<1>(substr), TagDataType::Text, dataEncoding); } + } else if(version >= 3 && id() == Id3v2FrameIds::lCover) { // frame stores picture byte type; - helper.parsePicture(buffer.get(), m_dataSize, value(), type); + parsePicture(buffer.get(), m_dataSize, value(), type); setTypeInfo(type); + } else if(version < 3 && id() == Id3v2FrameIds::sCover) { // frame stores legacy picutre byte type; - helper.parseLegacyPicture(buffer.get(), m_dataSize, value(), type); + parseLegacyPicture(buffer.get(), m_dataSize, value(), type); setTypeInfo(type); + } else if(((version >= 3 && id() == Id3v2FrameIds::lComment) || (version < 3 && id() == Id3v2FrameIds::sComment)) || ((version >= 3 && id() == Id3v2FrameIds::lUnsynchronizedLyrics) || (version < 3 && id() == Id3v2FrameIds::sUnsynchronizedLyrics))) { // comment frame or unsynchronized lyrics frame (these two frame types have the same structure) - helper.parseComment(buffer.get(), m_dataSize, value()); + parseComment(buffer.get(), m_dataSize, value()); + } else { // unknown frame value().assignData(buffer.get(), m_dataSize, TagDataType::Undefined); } } +/*! + * \brief Prepares making. + * \returns Returns a Id3v2FrameMaker object which can be used to actually make the frame. + * \remarks The field must NOT be mutated after making is prepared when it is intended to actually + * make the field using the make method of the returned object. + * \throws Throws Media::Failure or a derived exception when a making + * error occurs. + * + * This method might be useful when it is necessary to know the size of the field before making it. + */ +Id3v2FrameMaker Id3v2Frame::prepareMaking(const uint32 version) +{ + return Id3v2FrameMaker(*this, version); +} + /*! * \brief Writes the frame to a stream using the specified \a writer and the - * specified ID3v2 version. + * specified ID3v2 \a version. * * \throws Throws std::ios_base::failure when an IO error occurs. * \throws Throws Media::Failure or a derived exception when a making * error occurs. */ -void Id3v2Frame::make(IoUtilities::BinaryWriter &writer, int32 version) +void Id3v2Frame::make(BinaryWriter &writer, const uint32 version) { - invalidateStatus(); - Id3v2FrameHelper helper(frameIdString(), *this); - const string context("making " + helper.id() + " frame"); - // check if a valid frame can be build from the data - if(value().isEmpty()) { - addNotification(NotificationType::Critical, "Cannot make an empty frame.", context); - throw InvalidDataException(); - } - if(isEncrypted()) { - addNotification(NotificationType::Critical, "Cannot make an encrypted frame (isn't supported by this tagging library).", context); - throw InvalidDataException(); - } - if(m_padding) { - addNotification(NotificationType::Critical, "Cannot make a frame which is marked as padding.", context); - throw InvalidDataException(); - } - uint32 frameId = id(); - if(version >= 3) { - if(Id3v2FrameIds::isShortId(frameId)) { - // try to convert the short frame id to its long equivalent - frameId = Id3v2FrameIds::convertToLongId(frameId); - if(frameId == 0) { - addNotification(NotificationType::Critical, "The short frame ID can't be converted to its long equivalent which is needed to use the frame in a newer version of ID3v2.", context); - throw InvalidDataException(); - } - } - } else { - if(Id3v2FrameIds::isLongId(frameId)) { - // try to convert the long frame id to its short equivalent - frameId = Id3v2FrameIds::convertToShortId(frameId); - if(frameId == 0) { - addNotification(NotificationType::Critical, "The long frame ID can't be converted to its short equivalent which is needed to use the frame in the old version of ID3v2.", context); - throw InvalidDataException(); - } - } - } - if(version < 3 && (m_flag != 0 || m_group != 0)) { - addNotification(NotificationType::Warning, "The existing flag and group information is not supported by the version of ID3v2 and will be ignored/discarted.", context); - } - // create actual data, depending on the frame type - unique_ptr buffer; - uint32 decompressedSize; - // check if the frame to be written is a text frame - try { - if(Id3v2FrameIds::isTextfield(frameId)) { - if((version >= 3 && (frameId == Id3v2FrameIds::lTrackPosition || frameId == Id3v2FrameIds::lDiskPosition)) - || (version < 3 && frameId == Id3v2FrameIds::sTrackPosition)) { - // the track number or the disk number frame - helper.makeString(buffer, decompressedSize, value().toString(), TagTextEncoding::Latin1); - } else if((version >= 3 && frameId == Id3v2FrameIds::lLength) - || (version < 3 && frameId == Id3v2FrameIds::sLength)) { - // the length - helper.makeString(buffer, decompressedSize, ConversionUtilities::numberToString(value().toTimeSpan().totalMilliseconds()), TagTextEncoding::Latin1); - } else if(value().type() == TagDataType::StandardGenreIndex && ((version >= 3 && frameId == Id3v2FrameIds::lGenre) - || (version < 3 && frameId == Id3v2FrameIds::sGenre))) { - // genre/content type as standard genre index - helper.makeString(buffer, decompressedSize, ConversionUtilities::numberToString(value().toStandardGenreIndex()), TagTextEncoding::Latin1); - } else { - // any other text frame - helper.makeString(buffer, decompressedSize, value().toString(), value().dataEncoding()); // the same as a normal text frame - } - } else if(version >= 3 && frameId == Id3v2FrameIds::lCover) { - // picture frame - helper.makePicture(buffer, decompressedSize, value(), isTypeInfoAssigned() ? typeInfo() : 0); - } else if(version < 3 && frameId == Id3v2FrameIds::sCover) { - // legacy picture frame - helper.makeLegacyPicture(buffer, decompressedSize, value(), isTypeInfoAssigned() ? typeInfo() : 0); - } else if(((version >= 3 && id() == Id3v2FrameIds::lComment) - || (version < 3 && id() == Id3v2FrameIds::sComment)) - || ((version >= 3 && id() == Id3v2FrameIds::lUnsynchronizedLyrics) - || (version < 3 && id() == Id3v2FrameIds::sUnsynchronizedLyrics))) { - // the comment frame or the unsynchronized lyrics frame - helper.makeComment(buffer, decompressedSize, value()); - } else { - // an unknown frame - // create buffer - buffer = make_unique(decompressedSize = value().dataSize()); - // just write the data - copy(value().dataPointer(), value().dataPointer() + value().dataSize(), buffer.get()); - } - } catch(ConversionException &) { - addNotification(NotificationType::Critical, "Assigned value can not be converted appropriately.", context); - throw InvalidDataException(); - } - unsigned long actualSize; - if(version >= 3 && isCompressed()) { - actualSize = compressBound(decompressedSize); - auto compressedBuffer = make_unique(actualSize); - switch(compress(reinterpret_cast(compressedBuffer.get()), &actualSize, reinterpret_cast(buffer.get()), decompressedSize)) { - case Z_MEM_ERROR: - addNotification(NotificationType::Critical, "Decompressing failed. The source buffer was too small.", context); - throw InvalidDataException(); - case Z_BUF_ERROR: - addNotification(NotificationType::Critical, "Decompressing failed. The destination buffer was too small.", context); - throw InvalidDataException(); - case Z_OK: - ; - } - buffer.swap(compressedBuffer); - } else { - actualSize = decompressedSize; - } - if(version < 3) { - writer.writeUInt24BE(frameId); - writer.writeUInt24BE(actualSize); - } else { - writer.writeUInt32BE(frameId); - if(version >= 4) { - writer.writeSynchsafeUInt32BE(actualSize); - } else { - writer.writeUInt32BE(actualSize); - } - writer.writeUInt16BE(m_flag); - if(hasGroupInformation()) { - writer.writeByte(m_group); - } - if(isCompressed()) { - if(version >= 4) { - writer.writeSynchsafeUInt32BE(decompressedSize); - } else { - writer.writeUInt32BE(decompressedSize); - } - } - } - writer.write(buffer.get(), actualSize); + prepareMaking(version).make(writer); } /*! @@ -375,19 +300,183 @@ void Id3v2Frame::cleared() } /*! - * \class Media::Id3v2FrameHelper - * \brief The Id3v2FrameHelper class helps parsing and making ID3v2 frames. + * \class Media::Id3v2FrameMaker + * \brief The Id3v2FrameMaker class helps making ID3v2 frames. + * It allows to calculate the required size. + * \sa See Id3v2FrameMaker::prepareMaking() for more information. */ /*! - * \brief The Id3v2FrameHelper class helps parsing and making ID3v2 frames. - * \param id Specifies the identifier of the current frame (used to print warnings). - * \param provider Specifies the status provider to store warnings. + * \brief Prepares making the specified \a frame. + * \sa See Id3v2Frame::prepareMaking() for more information. */ -Id3v2FrameHelper::Id3v2FrameHelper(const std::string &id, StatusProvider &provider) : - m_id(id), - m_statusProvider(provider) -{} +Id3v2FrameMaker::Id3v2FrameMaker(Id3v2Frame &frame, const byte version) : + m_frame(frame), + m_frameId(m_frame.id()), + m_version(version) +{ + m_frame.invalidateStatus(); + const string context("making " + m_frame.frameIdString() + " frame"); + + // validate assigned data + if(m_frame.value().isEmpty()) { + m_frame.addNotification(NotificationType::Critical, "Cannot make an empty frame.", context); + throw InvalidDataException(); + } + if(m_frame.isEncrypted()) { + m_frame.addNotification(NotificationType::Critical, "Cannot make an encrypted frame (isn't supported by this tagging library).", context); + throw InvalidDataException(); + } + if(m_frame.hasPaddingReached()) { + m_frame.addNotification(NotificationType::Critical, "Cannot make a frame which is marked as padding.", context); + throw InvalidDataException(); + } + if(version < 3 && m_frame.isCompressed()) { + m_frame.addNotification(NotificationType::Warning, "Compression is not supported by the version of ID3v2 and won't be applied.", context); + } + if(version < 3 && (m_frame.flag() || m_frame.group())) { + m_frame.addNotification(NotificationType::Warning, "The existing flag and group information is not supported by the version of ID3v2 and will be ignored/discarted.", context); + } + + // convert frame ID if necessary + if(version >= 3) { + if(Id3v2FrameIds::isShortId(m_frameId)) { + // try to convert the short frame ID to its long equivalent + if(!(m_frameId = Id3v2FrameIds::convertToLongId(m_frameId))) { + m_frame.addNotification(NotificationType::Critical, "The short frame ID can't be converted to its long equivalent which is needed to use the frame in a newer version of ID3v2.", context); + throw InvalidDataException(); + } + } + } else { + if(Id3v2FrameIds::isLongId(m_frameId)) { + // try to convert the long frame ID to its short equivalent + if(!(m_frameId = Id3v2FrameIds::convertToShortId(m_frameId))) { + m_frame.addNotification(NotificationType::Critical, "The long frame ID can't be converted to its short equivalent which is needed to use the frame in the old version of ID3v2.", context); + throw InvalidDataException(); + } + } + } + + // make actual data depending on the frame ID + try { + if(Id3v2FrameIds::isTextFrame(m_frameId)) { + // it is a text frame + if((version >= 3 && (m_frameId == Id3v2FrameIds::lTrackPosition || m_frameId == Id3v2FrameIds::lDiskPosition)) + || (version < 3 && m_frameId == Id3v2FrameIds::sTrackPosition)) { + // track number or the disk number frame + m_frame.makeString(m_data, m_decompressedSize, m_frame.value().toString(), TagTextEncoding::Latin1); + } else if((version >= 3 && m_frameId == Id3v2FrameIds::lLength) + || (version < 3 && m_frameId == Id3v2FrameIds::sLength)) { + // length frame + m_frame.makeString(m_data, m_decompressedSize, ConversionUtilities::numberToString(m_frame.value().toTimeSpan().totalMilliseconds()), TagTextEncoding::Latin1); + } else if(m_frame.value().type() == TagDataType::StandardGenreIndex && ((version >= 3 && m_frameId == Id3v2FrameIds::lGenre) + || (version < 3 && m_frameId == Id3v2FrameIds::sGenre))) { + // pre-defined genre frame + m_frame.makeString(m_data, m_decompressedSize, ConversionUtilities::numberToString(m_frame.value().toStandardGenreIndex()), TagTextEncoding::Latin1); + } else { + // any other text frame + m_frame.makeString(m_data, m_decompressedSize, m_frame.value().toString(), m_frame.value().dataEncoding()); // the same as a normal text frame + } + + } else if(version >= 3 && m_frameId == Id3v2FrameIds::lCover) { + // picture frame + m_frame.makePicture(m_data, m_decompressedSize, m_frame.value(), m_frame.isTypeInfoAssigned() ? m_frame.typeInfo() : 0); + + } else if(version < 3 && m_frameId == Id3v2FrameIds::sCover) { + // legacy picture frame + m_frame.makeLegacyPicture(m_data, m_decompressedSize, m_frame.value(), m_frame.isTypeInfoAssigned() ? m_frame.typeInfo() : 0); + + } else if(((version >= 3 && m_frameId == Id3v2FrameIds::lComment) + || (version < 3 && m_frameId == Id3v2FrameIds::sComment)) + || ((version >= 3 && m_frameId == Id3v2FrameIds::lUnsynchronizedLyrics) + || (version < 3 && m_frameId == Id3v2FrameIds::sUnsynchronizedLyrics))) { + // the comment frame or the unsynchronized lyrics frame + m_frame.makeComment(m_data, m_decompressedSize, m_frame.value()); + + } else { + // an unknown frame + // create buffer + m_data = make_unique(m_decompressedSize = m_frame.value().dataSize()); + // just write the data + copy(m_frame.value().dataPointer(), m_frame.value().dataPointer() + m_decompressedSize, m_data.get()); + } + } catch(ConversionException &) { + m_frame.addNotification(NotificationType::Critical, "Assigned value can not be converted appropriately.", context); + throw InvalidDataException(); + } + + // apply compression if frame should be compressed + if(version >= 3 && m_frame.isCompressed()) { + m_dataSize = compressBound(m_decompressedSize); + auto compressedData = make_unique(m_decompressedSize); + switch(compress(reinterpret_cast(compressedData.get()), reinterpret_cast(&m_dataSize), reinterpret_cast(m_data.get()), m_decompressedSize)) { + case Z_MEM_ERROR: + m_frame.addNotification(NotificationType::Critical, "Decompressing failed. The source buffer was too small.", context); + throw InvalidDataException(); + case Z_BUF_ERROR: + m_frame.addNotification(NotificationType::Critical, "Decompressing failed. The destination buffer was too small.", context); + throw InvalidDataException(); + case Z_OK: + ; + } + m_data.swap(compressedData); + } else { + m_dataSize = m_decompressedSize; + } + + // calculate required size + // -> data size + m_requiredSize = m_dataSize; + if(version < 3) { + // -> header size + m_requiredSize += 3; + } else { + // -> header size + m_requiredSize += 10; + // -> group byte + if(m_frame.hasGroupInformation()) { + m_requiredSize += 1; + } + // -> decompressed size + if(version >= 3 && m_frame.isCompressed()) { + m_requiredSize += 4; + } + } +} + +/*! + * \brief Saves the frame (specified when constructing the object) using + * the specified \a writer. + * \throws Throws std::ios_base::failure when an IO error occurs. + * \throws Throws Assumes the data is already validated and thus does NOT + * throw Media::Failure or a derived exception. + */ +void Id3v2FrameMaker::make(BinaryWriter &writer) +{ + if(m_version < 3) { + writer.writeUInt24BE(m_frameId); + writer.writeUInt24BE(m_dataSize); + } else { + writer.writeUInt32BE(m_frameId); + if(m_version >= 4) { + writer.writeSynchsafeUInt32BE(m_dataSize); + } else { + writer.writeUInt32BE(m_dataSize); + } + writer.writeUInt16BE(m_frame.flag()); + if(m_frame.hasGroupInformation()) { + writer.writeByte(m_frame.group()); + } + if(m_version >= 3 && m_frame.isCompressed()) { + if(m_version >= 4) { + writer.writeSynchsafeUInt32BE(m_decompressedSize); + } else { + writer.writeUInt32BE(m_decompressedSize); + } + } + } + writer.write(m_data.get(), m_dataSize); +} /*! * \brief Returns the text encoding for the specified \a textEncodingByte. @@ -395,7 +484,7 @@ Id3v2FrameHelper::Id3v2FrameHelper(const std::string &id, StatusProvider &provid * If the \a textEncodingByte doesn't match any encoding TagTextEncoding::Latin1 is * returned and a parsing notification is added. */ -TagTextEncoding Id3v2FrameHelper::parseTextEncodingByte(byte textEncodingByte) +TagTextEncoding Id3v2Frame::parseTextEncodingByte(byte textEncodingByte) { switch(textEncodingByte) { case 0: // Ascii @@ -407,7 +496,7 @@ TagTextEncoding Id3v2FrameHelper::parseTextEncodingByte(byte textEncodingByte) case 3: // Utf 8 return TagTextEncoding::Utf8; default: - m_statusProvider.addNotification(NotificationType::Warning, "The charset of the frame is invalid. Latin-1 will be used.", "parsing encoding of frame " + m_id); + addNotification(NotificationType::Warning, "The charset of the frame is invalid. Latin-1 will be used.", "parsing encoding of frame " + frameIdString()); return TagTextEncoding::Latin1; } } @@ -415,7 +504,7 @@ TagTextEncoding Id3v2FrameHelper::parseTextEncodingByte(byte textEncodingByte) /*! * \brief Returns a text encoding byte for the specified \a textEncoding. */ -byte Id3v2FrameHelper::makeTextEncodingByte(TagTextEncoding textEncoding) +byte Id3v2Frame::makeTextEncodingByte(TagTextEncoding textEncoding) { switch(textEncoding) { case TagTextEncoding::Latin1: @@ -445,7 +534,7 @@ byte Id3v2FrameHelper::makeTextEncodingByte(TagTextEncoding textEncoding) * \remarks The length is always returned as the number of bytes, not as the number of characters (makes a difference for * UTF-16 encodings). */ -tuple Id3v2FrameHelper::parseSubstring(const char *buffer, size_t bufferSize, TagTextEncoding &encoding, bool addWarnings) +tuple Id3v2Frame::parseSubstring(const char *buffer, size_t bufferSize, TagTextEncoding &encoding, bool addWarnings) { tuple res(buffer, 0, buffer + bufferSize); switch(encoding) { @@ -466,7 +555,7 @@ tuple Id3v2FrameHelper::parseSubstring(const get<1>(res) += 2; } else { if(addWarnings) { - m_statusProvider.addNotification(NotificationType::Warning, "Wide string in frame is not terminated proberly.", "parsing termination of frame " + m_id); + addNotification(NotificationType::Warning, "Wide string in frame is not terminated proberly.", "parsing termination of frame " + frameIdString()); } break; } @@ -485,7 +574,7 @@ tuple Id3v2FrameHelper::parseSubstring(const ++get<1>(res); } else { if(addWarnings) { - m_statusProvider.addNotification(NotificationType::Warning, "String in frame is not terminated proberly.", "parsing termination of frame " + m_id); + addNotification(NotificationType::Warning, "String in frame is not terminated proberly.", "parsing termination of frame " + frameIdString()); } break; } @@ -500,9 +589,9 @@ tuple Id3v2FrameHelper::parseSubstring(const /*! * \brief Parses a substring in the specified \a buffer. * - * Same as Id3v2FrameHelper::parseSubstring() but returns the substring as string object. + * Same as Id3v2Frame::parseSubstring() but returns the substring as string object. */ -string Id3v2FrameHelper::parseString(const char *buffer, size_t dataSize, TagTextEncoding &encoding, bool addWarnings) +string Id3v2Frame::parseString(const char *buffer, size_t dataSize, TagTextEncoding &encoding, bool addWarnings) { auto substr = parseSubstring(buffer, dataSize, encoding, addWarnings); return string(get<0>(substr), get<1>(substr)); @@ -511,9 +600,9 @@ string Id3v2FrameHelper::parseString(const char *buffer, size_t dataSize, TagTex /*! * \brief Parses a substring in the specified \a buffer. * - * Same as Id3v2FrameHelper::parseSubstring() but returns the substring as wstring object. + * Same as Id3v2Frame::parseSubstring() but returns the substring as wstring object. */ -wstring Id3v2FrameHelper::parseWideString(const char *buffer, size_t dataSize, TagTextEncoding &encoding, bool addWarnings) +wstring Id3v2Frame::parseWideString(const char *buffer, size_t dataSize, TagTextEncoding &encoding, bool addWarnings) { auto substr = parseSubstring(buffer, dataSize, encoding, addWarnings); return wstring(reinterpret_cast(get<0>(substr)), get<1>(substr) / 2); @@ -528,7 +617,7 @@ wstring Id3v2FrameHelper::parseWideString(const char *buffer, size_t dataSize, T * * \remarks This method is not used anymore and might be deleted. */ -void Id3v2FrameHelper::parseBom(const char *buffer, size_t maxSize, TagTextEncoding &encoding) +void Id3v2Frame::parseBom(const char *buffer, size_t maxSize, TagTextEncoding &encoding) { switch(encoding) { case TagTextEncoding::Utf16BigEndian: @@ -542,7 +631,7 @@ void Id3v2FrameHelper::parseBom(const char *buffer, size_t maxSize, TagTextEncod default: if((maxSize >= 3) && (ConversionUtilities::BE::toUInt24(buffer) == 0x00EFBBBF)) { encoding = TagTextEncoding::Utf8; - m_statusProvider.addNotification(NotificationType::Warning, "UTF-8 byte order mark found in text frame.", "parsing byte oder mark of frame " + m_id); + addNotification(NotificationType::Warning, "UTF-8 byte order mark found in text frame.", "parsing byte oder mark of frame " + frameIdString()); } } } @@ -554,11 +643,11 @@ void Id3v2FrameHelper::parseBom(const char *buffer, size_t maxSize, TagTextEncod * \param tagValue Specifies the tag value used to store the results. * \param typeInfo Specifies a byte used to store the type info. */ -void Id3v2FrameHelper::parseLegacyPicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo) +void Id3v2Frame::parseLegacyPicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo) { static const string context("parsing ID3v2.2 picture frame"); if(maxSize < 6) { - m_statusProvider.addNotification(NotificationType::Critical, "Picture frame is incomplete.", context); + addNotification(NotificationType::Critical, "Picture frame is incomplete.", context); throw TruncatedDataException(); } const char *end = buffer + maxSize; @@ -568,7 +657,7 @@ void Id3v2FrameHelper::parseLegacyPicture(const char *buffer, size_t maxSize, Ta auto substr = parseSubstring(buffer + 5, end - 5 - buffer, dataEncoding, true); tagValue.setDescription(string(get<0>(substr), get<1>(substr)), dataEncoding); if(get<2>(substr) >= end) { - m_statusProvider.addNotification(NotificationType::Critical, "Picture frame is incomplete (actual data is missing).", context); + addNotification(NotificationType::Critical, "Picture frame is incomplete (actual data is missing).", context); throw TruncatedDataException(); } tagValue.assignData(get<2>(substr), end - get<2>(substr), TagDataType::Picture, dataEncoding); @@ -581,7 +670,7 @@ void Id3v2FrameHelper::parseLegacyPicture(const char *buffer, size_t maxSize, Ta * \param tagValue Specifies the tag value used to store the results. * \param typeInfo Specifies a byte used to store the type info. */ -void Id3v2FrameHelper::parsePicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo) +void Id3v2Frame::parsePicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo) { static const string context("parsing ID3v2.3 picture frame"); const char *end = buffer + maxSize; @@ -592,18 +681,18 @@ void Id3v2FrameHelper::parsePicture(const char *buffer, size_t maxSize, TagValue tagValue.setMimeType(string(get<0>(substr), get<1>(substr))); } if(get<2>(substr) >= end) { - m_statusProvider.addNotification(NotificationType::Critical, "Picture frame is incomplete (type info, description and actual data are missing).", context); + addNotification(NotificationType::Critical, "Picture frame is incomplete (type info, description and actual data are missing).", context); throw TruncatedDataException(); } typeInfo = static_cast(*get<2>(substr)); if(++get<2>(substr) >= end) { - m_statusProvider.addNotification(NotificationType::Critical, "Picture frame is incomplete (description and actual data are missing).", context); + addNotification(NotificationType::Critical, "Picture frame is incomplete (description and actual data are missing).", context); throw TruncatedDataException(); } substr = parseSubstring(get<2>(substr), end - get<2>(substr), dataEncoding, true); tagValue.setDescription(string(get<0>(substr), get<1>(substr)), dataEncoding); if(get<2>(substr) >= end) { - m_statusProvider.addNotification(NotificationType::Critical, "Picture frame is incomplete (actual data is missing).", context); + addNotification(NotificationType::Critical, "Picture frame is incomplete (actual data is missing).", context); throw TruncatedDataException(); } tagValue.assignData(get<2>(substr), end - get<2>(substr), TagDataType::Picture, dataEncoding); @@ -615,12 +704,12 @@ void Id3v2FrameHelper::parsePicture(const char *buffer, size_t maxSize, TagValue * \param dataSize Specifies the maximal number of bytes to read from the buffer. * \param tagValue Specifies the tag value used to store the results. */ -void Id3v2FrameHelper::parseComment(const char *buffer, size_t dataSize, TagValue &tagValue) +void Id3v2Frame::parseComment(const char *buffer, size_t dataSize, TagValue &tagValue) { static const string context("parsing comment frame"); const char *end = buffer + dataSize; if(dataSize < 6) { - m_statusProvider.addNotification(NotificationType::Critical, "Comment frame is incomplete.", context); + addNotification(NotificationType::Critical, "Comment frame is incomplete.", context); throw TruncatedDataException(); } TagTextEncoding dataEncoding = parseTextEncodingByte(*buffer); @@ -628,7 +717,7 @@ void Id3v2FrameHelper::parseComment(const char *buffer, size_t dataSize, TagValu auto substr = parseSubstring(buffer += 3, dataSize -= 4, dataEncoding, true); tagValue.setDescription(string(get<0>(substr), get<1>(substr)), dataEncoding); if(get<2>(substr) >= end) { - m_statusProvider.addNotification(NotificationType::Critical, "Comment frame is incomplete (description not terminated?).", context); + addNotification(NotificationType::Critical, "Comment frame is incomplete (description not terminated?).", context); throw TruncatedDataException(); } substr = parseSubstring(get<2>(substr), end - get<2>(substr), dataEncoding, false); @@ -641,9 +730,9 @@ void Id3v2FrameHelper::parseComment(const char *buffer, size_t dataSize, TagValu * \param value Specifies the string to make. * \param encoding Specifies the encoding of the string to make. */ -void Id3v2FrameHelper::makeString(unique_ptr &buffer, uint32 &bufferSize, const string &value, TagTextEncoding encoding) +void Id3v2Frame::makeString(unique_ptr &buffer, uint32 &bufferSize, const string &value, TagTextEncoding encoding) { - makeEncodingAndData(buffer, bufferSize, encoding, value.c_str(), value.length()); + makeEncodingAndData(buffer, bufferSize, encoding, value.data(), value.size()); } /*! @@ -653,7 +742,7 @@ void Id3v2FrameHelper::makeString(unique_ptr &buffer, uint32 &bufferSize * \param data Specifies the data. * \param dataSize Specifies the data size. */ -void Id3v2FrameHelper::makeEncodingAndData(unique_ptr &buffer, uint32 &bufferSize, TagTextEncoding encoding, const char *data, size_t dataSize) +void Id3v2Frame::makeEncodingAndData(unique_ptr &buffer, uint32 &bufferSize, TagTextEncoding encoding, const char *data, size_t dataSize) { // calculate buffer size if(!data) { @@ -681,7 +770,7 @@ void Id3v2FrameHelper::makeEncodingAndData(unique_ptr &buffer, uint32 &b /*! * \brief Writes the specified picture to the specified buffer (ID3v2.2). */ -void Id3v2FrameHelper::makeLegacyPicture(unique_ptr &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo) +void Id3v2Frame::makeLegacyPicture(unique_ptr &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo) { // calculate needed buffer size and create buffer TagTextEncoding descriptionEncoding = picture.descriptionEncoding(); @@ -724,7 +813,7 @@ void Id3v2FrameHelper::makeLegacyPicture(unique_ptr &buffer, uint32 &buf /*! * \brief Writes the specified picture to the specified buffer (ID3v2.3). */ -void Id3v2FrameHelper::makePicture(unique_ptr &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo) +void Id3v2Frame::makePicture(unique_ptr &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo) { // calculate needed buffer size and create buffer TagTextEncoding descriptionEncoding = picture.descriptionEncoding(); @@ -760,18 +849,18 @@ void Id3v2FrameHelper::makePicture(unique_ptr &buffer, uint32 &bufferSiz /*! * \brief Writes the specified comment to the specified buffer. */ -void Id3v2FrameHelper::makeComment(unique_ptr &buffer, uint32 &bufferSize, const TagValue &comment) +void Id3v2Frame::makeComment(unique_ptr &buffer, uint32 &bufferSize, const TagValue &comment) { static const string context("making comment frame"); // check type and other values are valid TagTextEncoding encoding = comment.dataEncoding(); if(!comment.description().empty() && encoding != comment.descriptionEncoding()) { - m_statusProvider.addNotification(NotificationType::Critical, "Data enoding and description encoding aren't equal.", context); + addNotification(NotificationType::Critical, "Data enoding and description encoding aren't equal.", context); throw InvalidDataException(); } const string &lng = comment.language(); if(lng.length() > 3) { - m_statusProvider.addNotification(NotificationType::Critical, "The language must be 3 bytes long (ISO-639-2).", context); + addNotification(NotificationType::Critical, "The language must be 3 bytes long (ISO-639-2).", context); throw InvalidDataException(); } // calculate needed buffer size and create buffer diff --git a/id3/id3v2frame.h b/id3/id3v2frame.h index 3d5cbff..a96a0eb 100644 --- a/id3/id3v2frame.h +++ b/id3/id3v2frame.h @@ -19,35 +19,61 @@ namespace Media { -class LIB_EXPORT Id3v2FrameHelper +class Id3v2Frame; + +class LIB_EXPORT Id3v2FrameMaker { + friend class Id3v2Frame; + public: - Id3v2FrameHelper(const std::string &id, StatusProvider &provider); - - const std::string &id() const; - - 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); - void parseLegacyPicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo); - 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); - void makePicture(std::unique_ptr &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo); - void makeComment(std::unique_ptr &buffer, uint32 &bufferSize, const TagValue &comment); + void make(IoUtilities::BinaryWriter &writer); + const Id3v2Frame &field() const; + const std::unique_ptr &data() const; + uint32 dataSize() const; + uint32 requiredSize() const; private: - std::string m_id; - StatusProvider &m_statusProvider; + Id3v2FrameMaker(Id3v2Frame &frame, const byte version); + Id3v2Frame &m_frame; + uint32 m_frameId; + const byte m_version; + std::unique_ptr m_data; + uint32 m_dataSize; + uint32 m_decompressedSize; + uint32 m_requiredSize; }; -class Id3v2Frame; +/*! + * \brief Returns the associated frame. + */ +inline const Id3v2Frame &Id3v2FrameMaker::field() const +{ + return m_frame; +} + +/*! + * \brief Returns the frame data. + */ +inline const std::unique_ptr &Id3v2FrameMaker::data() const +{ + return m_data; +} + +/*! + * \brief Returns the size of the array returned by data(). + */ +inline uint32 Id3v2FrameMaker::dataSize() const +{ + return m_dataSize; +} + +/*! + * \brief Returns number of bytes which will be written when making the frame. + */ +inline uint32 Id3v2FrameMaker::requiredSize() const +{ + return m_requiredSize; +} /*! * \brief Defines traits for the TagField implementation of the Id3v2Frame class. @@ -72,31 +98,26 @@ public: typedef Id3v2Frame implementationType; }; -/*! - * \brief Returns the ID of the current frame. - */ -inline const std::string &Id3v2FrameHelper::id() const -{ - return m_id; -} - class LIB_EXPORT Id3v2Frame : public TagField, public StatusProvider { friend class TagField; public: Id3v2Frame(); - Id3v2Frame(const identifierType &id, const TagValue &value, byte group = 0, int16 flag = 0); + Id3v2Frame(const identifierType &id, const TagValue &value, const byte group = 0, const int16 flag = 0); - void parse(IoUtilities::BinaryReader &reader, int32 version, uint32 maximalSize = 0); - void make(IoUtilities::BinaryWriter &writer, int32 version); + // parsing/making + void parse(IoUtilities::BinaryReader &reader, const uint32 version, const uint32 maximalSize = 0); + Id3v2FrameMaker prepareMaking(const uint32 version); + void make(IoUtilities::BinaryWriter &writer, const uint32 version); + // member access bool isAdditionalTypeInfoUsed() const; bool isValid() const; bool hasPaddingReached() const; std::string frameIdString() const; - int16 flag() const; - void setFlag(int16 value); + uint16 flag() const; + void setFlag(uint16 value); uint32 totalSize() const; uint32 dataSize() const; bool toDiscardWhenUnknownAndTagIsAltered() const; @@ -112,13 +133,31 @@ public: int32 parsedVersion() const; bool supportsNestedFields() const; + // parsing helper + 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); + void parseLegacyPicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo); + 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); + + // making helper + 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); + void makePicture(std::unique_ptr &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo); + void makeComment(std::unique_ptr &buffer, uint32 &bufferSize, const TagValue &comment); + protected: void cleared(); private: uint16 m_flag; byte m_group; - int32 m_parsedVersion; + uint32 m_parsedVersion; uint32 m_dataSize; uint32 m_totalSize; bool m_padding; @@ -163,7 +202,7 @@ inline std::string Id3v2Frame::frameIdString() const /*! * \brief Returns the flags. */ -inline int16 Id3v2Frame::flag() const +inline uint16 Id3v2Frame::flag() const { return m_flag; } @@ -171,7 +210,7 @@ inline int16 Id3v2Frame::flag() const /*! * \brief Sets the flags. */ -inline void Id3v2Frame::setFlag(int16 value) +inline void Id3v2Frame::setFlag(uint16 value) { m_flag = value; } diff --git a/id3/id3v2frameids.h b/id3/id3v2frameids.h index d813420..e50689c 100644 --- a/id3/id3v2frameids.h +++ b/id3/id3v2frameids.h @@ -81,7 +81,7 @@ inline bool isShortId(uint32 id) /*! * \brief Returns an indication whether the specified \a id is a text frame id. */ -inline bool isTextfield(uint32 id) +inline bool isTextFrame(uint32 id) { if(isShortId(id)) { return (id & 0x00FF0000u) == 0x00540000u; diff --git a/id3/id3v2tag.cpp b/id3/id3v2tag.cpp index 94dae0b..8085121 100644 --- a/id3/id3v2tag.cpp +++ b/id3/id3v2tag.cpp @@ -138,7 +138,7 @@ TagDataType Id3v2Tag::proposedDataType(const uint32 &id) const case lCover: case sCover: return TagDataType::Picture; default: - if(Id3v2FrameIds::isTextfield(id)) { + if(Id3v2FrameIds::isTextFrame(id)) { return TagDataType::Text; } else { return TagDataType::Undefined; @@ -181,7 +181,7 @@ 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, uint64 maximalSize) +void Id3v2Tag::parse(istream &stream, const uint64 maximalSize) { // prepare parsing invalidateStatus(); @@ -229,7 +229,7 @@ void Id3v2Tag::parse(istream &stream, uint64 maximalSize) // how many bytes remain for frames and padding? uint32 bytesRemaining = m_sizeExcludingHeader - m_extendedHeaderSize; - if(bytesRemaining > maximalSize) { + if(maximalSize && bytesRemaining > maximalSize) { bytesRemaining = maximalSize; addNotification(NotificationType::Critical, "Frames are truncated.", context); } @@ -245,7 +245,7 @@ void Id3v2Tag::parse(istream &stream, uint64 maximalSize) frame.parse(reader, majorVersion, bytesRemaining); if(frame.id()) { // add frame if parsing was successfull - if(Id3v2FrameIds::isTextfield(frame.id()) && fields().count(frame.id()) == 1) { + if(Id3v2FrameIds::isTextFrame(frame.id()) && fields().count(frame.id()) == 1) { addNotification(NotificationType::Warning, "The text frame " + frame.frameIdString() + " exists more than once.", context); } fields().insert(pair(frame.id(), frame)); @@ -264,13 +264,18 @@ void Id3v2Tag::parse(istream &stream, uint64 maximalSize) frame.invalidateNotifications(); // calculate next frame offset - bytesRemaining -= frame.totalSize(); - pos += frame.totalSize(); + if(frame.totalSize() <= bytesRemaining) { + pos += frame.totalSize(); + bytesRemaining -= frame.totalSize(); + } else { + pos += bytesRemaining; + bytesRemaining = 0; + } } // check for extended header if(hasFooter()) { - if(m_size + 10 < maximalSize) { + if(maximalSize && 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) { @@ -290,6 +295,21 @@ void Id3v2Tag::parse(istream &stream, uint64 maximalSize) } } +/*! + * \brief Prepares making. + * \returns Returns a Id3v2TagMaker object which can be used to actually make the tag. + * \remarks The tag must NOT be mutated after making is prepared when it is intended to actually + * make the tag using the make method of the returned object. + * \throws Throws Media::Failure or a derived exception when a making error occurs. + * + * This method might be useful when it is necessary to know the size of the tag before making it. + * \sa make() + */ +Id3v2TagMaker Id3v2Tag::prepareMaking() +{ + return Id3v2TagMaker(*this); +} + /*! * \brief Writes tag information to the specified \a stream. * @@ -297,50 +317,9 @@ void Id3v2Tag::parse(istream &stream, uint64 maximalSize) * \throws Throws Media::Failure or a derived exception when a making * error occurs. */ -void Id3v2Tag::make(ostream &stream) +void Id3v2Tag::make(ostream &stream, uint32 padding) { - // prepare making - invalidateStatus(); - const string context("making ID3v2 tag"); - // check if version is supported - // (the version could have been changed using setVersion(...) - if(!isVersionSupported()) { - addNotification(NotificationType::Critical, "The ID3v2 tag couldn't be created, because the target version isn't supported.", context); - throw VersionNotSupportedException(); - } - // prepare for writing - BinaryWriter writer(&stream); - // write header - writer.writeUInt24BE(0x494433u); // signature - writer.writeByte(m_majorVersion); // major version - writer.writeByte(m_revisionVersion); // revision version - writer.writeByte(m_flags & 0xBF); // flags, but without extended header or compression bit set - stream.seekp(4, ios_base::cur); // size currently unknown, write it later - streamoff framesOffset = stream.tellp(); - int framesWritten = 0; - for(auto i : fields()) { - Id3v2Frame &frame = i.second; - // write only valid frames - if(frame.isValid()) { - // make the frame - try { - frame.make(writer, m_majorVersion); - ++framesWritten; - } catch(Failure &) { - // nothing to do here since notifications will be added anyways - } - // add making notifications - addNotifications(context, frame); - } - } - // calculate and write size - streamoff endOffset = stream.tellp(); - stream.seekp(framesOffset - 4, ios_base::beg); - writer.writeSynchsafeUInt32BE(endOffset - framesOffset); - stream.seekp(endOffset, ios_base::beg); - if(framesWritten <= 0) { // add a warning notification if an empty ID3v2 tag has been written - addNotification(NotificationType::Warning, "No frames could be written, an empty ID3v2 tag has been written.", context); - } + prepareMaking().make(stream, padding); } /*! @@ -383,8 +362,8 @@ bool FrameComparer::operator()(const uint32 &lhs, const uint32 &rhs) const if(rhs == Id3v2FrameIds::lTitle || rhs == Id3v2FrameIds::sTitle) { return false; } - bool lhstextfield = Id3v2FrameIds::isTextfield(lhs); - bool rhstextfield = Id3v2FrameIds::isTextfield(rhs); + bool lhstextfield = Id3v2FrameIds::isTextFrame(lhs); + bool rhstextfield = Id3v2FrameIds::isTextFrame(rhs); if(lhstextfield && !rhstextfield) { return true; } @@ -400,4 +379,72 @@ bool FrameComparer::operator()(const uint32 &lhs, const uint32 &rhs) const return lhs < rhs; } +/*! + * \brief Prepares making the specified \a tag. + * \sa See Id3v2Tag::prepareMaking() for more information. + */ +Id3v2TagMaker::Id3v2TagMaker(Id3v2Tag &tag) : + m_tag(tag), + m_framesSize(0) +{ + tag.invalidateStatus(); + const string context("making ID3v2 tag"); + + // check if version is supported + // (the version could have been changed using setVersion()) + if(!tag.isVersionSupported()) { + tag.addNotification(NotificationType::Critical, "The ID3v2 tag version isn't supported.", context); + throw VersionNotSupportedException(); + } + + // prepare frames + m_maker.reserve(tag.fields().size()); + for(auto &pair : tag.fields()) { + try { + m_maker.emplace_back(pair.second.prepareMaking(tag.majorVersion())); + m_framesSize += m_maker.back().requiredSize(); + } catch(const Failure &) { + // nothing to do here; notifications will be added anyways + } + m_tag.addNotifications(pair.second); + } + + // calculate required size + // -> header + size of frames + m_requiredSize = 10 + m_framesSize; +} + +/*! + * \brief Saves the tag (specified when constructing the object) to the + * specified \a stream. + * \throws Throws std::ios_base::failure when an IO error occurs. + * \throws Throws Assumes the data is already validated and thus does NOT + * throw Media::Failure or a derived exception. + */ +void Id3v2TagMaker::make(std::ostream &stream, uint32 padding) +{ + BinaryWriter writer(&stream); + + // write header + // -> signature + writer.writeUInt24BE(0x494433u); + // -> version + writer.writeByte(m_tag.majorVersion()); + writer.writeByte(m_tag.revisionVersion()); + // -> flags, but without extended header or compression bit set + writer.writeByte(m_tag.flags() & 0xBF); + // -> size (excluding header) + writer.writeSynchsafeUInt32BE(m_framesSize + padding); + + // write frames + for(auto &maker : m_maker) { + maker.make(writer); + } + + // write padding + for(; padding; --padding) { + stream.put(0); + } +} + } diff --git a/id3/id3v2tag.h b/id3/id3v2tag.h index 02627ef..b70f241 100644 --- a/id3/id3v2tag.h +++ b/id3/id3v2tag.h @@ -10,11 +10,48 @@ namespace Media { +class Id3v2Tag; + struct LIB_EXPORT FrameComparer { - bool operator()(const uint32& lhs, const uint32& rhs) const; + bool operator()(const uint32 &lhs, const uint32 &rhs) const; }; +class LIB_EXPORT Id3v2TagMaker +{ + friend class Id3v2Tag; + +public: + void make(std::ostream &stream, uint32 padding); + const Id3v2Tag &tag() const; + uint64 requiredSize() const; + +private: + Id3v2TagMaker(Id3v2Tag &tag); + + Id3v2Tag &m_tag; + uint32 m_framesSize; + uint32 m_requiredSize; + std::vector m_maker; +}; + +/*! + * \brief Returns the associated tag. + */ +inline const Id3v2Tag &Id3v2TagMaker::tag() const +{ + return m_tag; +} + +/*! + * \brief Returns the number of bytes which will be written when making the tag. + * \remarks Excludes padding! + */ +inline uint64 Id3v2TagMaker::requiredSize() const +{ + return m_requiredSize; +} + class LIB_EXPORT Id3v2Tag : public FieldMapBasedTag { public: @@ -34,8 +71,9 @@ public: bool supportsDescription(KnownField field) const; bool supportsMimeType(KnownField field) const; - void parse(std::istream &sourceStream, uint64 maximalSize = 0); - void make(std::ostream &targetStream); + void parse(std::istream &sourceStream, const uint64 maximalSize = 0); + Id3v2TagMaker prepareMaking(); + void make(std::ostream &targetStream, uint32 padding); byte majorVersion() const; byte revisionVersion() const; diff --git a/matroska/matroskacontainer.cpp b/matroska/matroskacontainer.cpp index d26b8bc..b5cd01b 100644 --- a/matroska/matroskacontainer.cpp +++ b/matroska/matroskacontainer.cpp @@ -1320,14 +1320,6 @@ nonRewriteCalculations: throw; } - - // define variables needed to handle output stream and backup stream (required when rewriting the file) - string backupPath; - fstream &outputStream = fileInfo().stream(); - fstream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required - BinaryWriter outputWriter(&outputStream); - char buff[8]; // buffer used to make size denotations - if(isAborted()) { throw OperationAbortedException(); } @@ -1335,6 +1327,14 @@ nonRewriteCalculations: // setup stream(s) for writing // -> update status updateStatus("Preparing streams ..."); + + // -> define variables needed to handle output stream and backup stream (required when rewriting the file) + string backupPath; + fstream &outputStream = fileInfo().stream(); + fstream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required + BinaryWriter outputWriter(&outputStream); + char buff[8]; // buffer used to make size denotations + if(rewriteRequired) { // move current file to temp dir and reopen it as backupStream, recreate original file try { @@ -1351,7 +1351,6 @@ nonRewriteCalculations: } } else { // !rewriteRequired - // buffer currently assigned attachments for(auto &maker : attachmentMaker) { maker.bufferCurrentAttachments(); @@ -1636,8 +1635,6 @@ nonRewriteCalculations: // reparse what is written so far updateStatus("Reparsing output file ..."); - // -> report new size - fileInfo().reportSizeChanged(outputStream.tellp()); if(rewriteRequired) { // report new size fileInfo().reportSizeChanged(outputStream.tellp()); diff --git a/matroska/matroskatag.cpp b/matroska/matroskatag.cpp index 9dc284a..dff9b4d 100644 --- a/matroska/matroskatag.cpp +++ b/matroska/matroskatag.cpp @@ -232,13 +232,13 @@ MatroskaTagMaker::MatroskaTagMaker(MatroskaTag &tag) : } m_tagSize = 2 + EbmlElement::calculateSizeDenotationLength(m_targetsSize) + m_targetsSize; // calculate size of "SimpleTag" elements - m_makers.reserve(m_tag.fields().size()); + m_maker.reserve(m_tag.fields().size()); m_simpleTagsSize = 0; // including ID and size for(auto &pair : m_tag.fields()) { try { - m_makers.emplace_back(pair.second.prepareMaking()); - m_simpleTagsSize += m_makers.back().requiredSize(); - } catch(Failure &) { + m_maker.emplace_back(pair.second.prepareMaking()); + m_simpleTagsSize += m_maker.back().requiredSize(); + } catch(const Failure &) { // nothing to do here; notifications will be added anyways } m_tag.addNotifications(pair.second); @@ -298,7 +298,7 @@ void MatroskaTagMaker::make(ostream &stream) const } } // write "SimpleTag" elements using maker objects prepared previously - for(const auto &maker : m_makers) { + for(const auto &maker : m_maker) { maker.make(stream); } } diff --git a/matroska/matroskatag.h b/matroska/matroskatag.h index cec68af..1b7e2c3 100644 --- a/matroska/matroskatag.h +++ b/matroska/matroskatag.h @@ -25,7 +25,7 @@ private: MatroskaTag &m_tag; uint64 m_targetsSize; uint64 m_simpleTagsSize; - std::vector m_makers; + std::vector m_maker; uint64 m_tagSize; uint64 m_totalSize; }; diff --git a/mediafileinfo.cpp b/mediafileinfo.cpp index 5d78d20..0c653a0 100644 --- a/mediafileinfo.cpp +++ b/mediafileinfo.cpp @@ -362,7 +362,7 @@ void MediaFileInfo::parseTags() auto id3v2Tag = make_unique(); stream().seekg(offset, ios_base::beg); try { - id3v2Tag->parse(stream()); + id3v2Tag->parse(stream(), size() - offset); m_paddingSize += id3v2Tag->paddingSize(); } catch(NoDataFoundException &) { continue; @@ -622,22 +622,6 @@ void MediaFileInfo::applyChanges() previousParsingSuccessful = false; addNotification(NotificationType::Critical, "Tracks have to be parsed without critical errors before changes can be applied.", context); } -// switch(chaptersParsingStatus()) { -// case ParsingStatus::Ok: -// case ParsingStatus::NotSupported: -// break; -// default: -// previousParsingSuccessful = false; -// addNotification(NotificationType::Critical, "Chapters have to be parsed without critical errors before changes can be applied.", context); -// } -// switch(attachmentsParsingStatus()) { -// case ParsingStatus::Ok: -// case ParsingStatus::NotSupported: -// break; -// default: -// previousParsingSuccessful = false; -// addNotification(NotificationType::Critical, "Attachments have to be parsed without critical errors before changes can be applied.", context); -// } if(!previousParsingSuccessful) { throw InvalidDataException(); } @@ -1359,12 +1343,12 @@ void MediaFileInfo::makeMp3File() { const string context("making MP3 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(!isForcingRewrite() && m_id3v2Tags.empty() && m_actualId3v2TagOffsets.empty()) { 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 ..."); + updateStatus("Updating ID3v1 tag ..."); // ensure the file is still open / not readonly open(); stream().seekp(-128, ios_base::end); @@ -1375,7 +1359,7 @@ void MediaFileInfo::makeMp3File() } } else { // the currently existing ID3v1 tag shall be removed - updateStatus("No need to rewrite the whole file, just truncating it to remove ID3v1 tag ..."); + updateStatus("Removing ID3v1 tag ..."); stream().close(); if(truncate(path().c_str(), size() - 128) == 0) { reportSizeChanged(size() - 128); @@ -1388,7 +1372,7 @@ void MediaFileInfo::makeMp3File() } 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."); + updateStatus("Adding ID3v1 tag ..."); // ensure the file is still open / not readonly open(); stream().seekp(0, ios_base::end); @@ -1403,49 +1387,110 @@ void MediaFileInfo::makeMp3File() } } else { - // ID3v2 needs to be modified -> file needs to be rewritten - // TODO: take advantage of possibly available padding + // ID3v2 needs to be modified + updateStatus("Updating ID3v2 tags ..."); - // prepare for rewriting - updateStatus("Prepareing for rewriting MP3 file ..."); + // prepare ID3v2 tags + vector makers; + makers.reserve(m_id3v2Tags.size()); + uint32 tagsSize = 0; + for(auto &tag : m_id3v2Tags) { + try { + makers.emplace_back(tag->prepareMaking()); + tagsSize += makers.back().requiredSize(); + } catch(const Failure &) { + // nothing to do: notifications added anyways + } + addNotifications(*tag); + } + + // determine padding, check whether rewrite is required + bool rewriteRequired = isForcingRewrite() || (tagsSize > static_cast(m_containerOffset)); + uint32 padding; + if(!rewriteRequired) { + padding = static_cast(m_containerOffset) - tagsSize; + // check whether padding matches specifications + if(padding < minPadding() || padding > maxPadding()) { + rewriteRequired = true; + } + } + if(rewriteRequired) { + // use preferred padding when rewriting + padding = preferredPadding(); + updateStatus("Preparing streams for rewriting ..."); + } else { + updateStatus("Preparing streams for updating ..."); + } + + // setup stream(s) for writing + // -> define variables needed to handle output stream and backup stream (required when rewriting the file) string backupPath; - fstream backupStream; + fstream &outputStream = stream(); + fstream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required + + if(rewriteRequired) { + // move current file to temp dir and reopen it as backupStream, recreate original file + try { + // ensure the file is close before moving + close(); + BackupHelper::createBackupFile(path(), backupPath, backupStream); + // recreate original file, define buffer variables + outputStream.open(path(), ios_base::out | ios_base::binary | ios_base::trunc); + } catch(const ios_base::failure &) { + addNotification(NotificationType::Critical, "Creation of temporary file (to rewrite the original file) failed.", context); + throw; + } + + } else { // !rewriteRequired + // reopen original file to ensure it is opened for writing + try { + close(); + outputStream.open(path(), ios_base::in | ios_base::out | ios_base::binary); + } catch(const ios_base::failure &) { + addNotification(NotificationType::Critical, "Opening the file with write permissions failed.", context); + throw; + } + } + + // start actual writing 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 - unsigned int counter = 1; - for(auto &id3v2Tag : m_id3v2Tags) { - try { - id3v2Tag->make(stream()); - } catch(const Failure &) { - if(m_id3v2Tags.size()) { - addNotification(NotificationType::Warning, "Unable to write " + ConversionUtilities::numberToString(counter) + ". ID3v2 tag.", context); - } else { - addNotification(NotificationType::Warning, "Unable to write ID3v2 tag.", context); - } + if(!makers.empty()) { + // write ID3v2 tags + updateStatus("Writing ID3v2 tag ..."); + for(auto i = makers.begin(), end = makers.end() - 1; i != end; ++i) { + i->make(outputStream, 0); + } + // include padding into the last ID3v2 tag + makers.back().make(outputStream, padding); + } else { + // no ID3v2 tags assigned -> just write padding + for(; padding; --padding) { + outputStream.put(0); } - ++counter; } - // write media data - updateStatus("Writing MPEG audio frames ..."); - uint64 bytesRemaining = size() - m_containerOffset; + // copy / skip media data + // -> determine media data size + uint64 mediaDataSize = size() - m_containerOffset; if(m_actualExistingId3v1Tag) { - bytesRemaining -= 128; + mediaDataSize -= 128; + } + + if(rewriteRequired) { + // copy data from original file + updateStatus("Writing MPEG audio frames ..."); + backupStream.seekg(m_containerOffset); + CopyHelper<0x4000> copyHelper; + copyHelper.callbackCopy(backupStream, stream(), mediaDataSize, bind(&StatusProvider::isAborted, this), bind(&StatusProvider::updatePercentage, this, _1)); + updatePercentage(100.0); + } else { + // just skip media data + outputStream.seekp(mediaDataSize, ios_base::cur); } - 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) { + updateStatus("Writing ID3v1 tag ..."); try { m_id3v1Tag->make(stream()); } catch(Failure &) { @@ -1453,22 +1498,74 @@ void MediaFileInfo::makeMp3File() } } - // 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(); + // handle streams + if(rewriteRequired) { + // report new size + reportSizeChanged(outputStream.tellp()); + // stream is useless for further usage because it is write-only + outputStream.close(); + } else { + const auto newSize = static_cast(outputStream.tellp()); + if(newSize < size()) { + // file is smaller after the modification -> truncate + // -> close stream before truncating + outputStream.close(); + // -> truncate file + if(truncate(path().c_str(), newSize) == 0) { + reportSizeChanged(newSize); + } else { + addNotification(NotificationType::Critical, "Unable to truncate the file.", context); + } + } else { + // file is longer after the modification -> just report new size + reportSizeChanged(newSize); + } + } } catch(const OperationAbortedException &) { - addNotification(NotificationType::Information, "Rewriting file to apply new tag information has been aborted.", context); - BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, stream(), backupStream); + if(&stream() != &outputStream) { + // a temp/backup file has been created -> restore original file + addNotification(NotificationType::Information, "Rewriting the file to apply changed tag information has been aborted.", context); + try { + BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, outputStream, backupStream); + addNotification(NotificationType::Information, "The original file has been restored.", context); + } catch(const ios_base::failure &ex) { + addNotification(NotificationType::Critical, ex.what(), context); + } + } else { + addNotification(NotificationType::Information, "Applying new tag information has been aborted.", context); + } throw; - } 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); + } catch(const Failure &) { + if(&stream() != &outputStream) { + // a temp/backup file has been created -> restore original file + addNotification(NotificationType::Critical, "Rewriting the file to apply changed tag information failed.", context); + try { + BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, outputStream, backupStream); + addNotification(NotificationType::Information, "The original file has been restored.", context); + } catch(const ios_base::failure &ex) { + addNotification(NotificationType::Critical, ex.what(), context); + } + } else { + addNotification(NotificationType::Critical, "Applying new tag information failed.", context); + } + throw; + } catch(const ios_base::failure &) { + if(&stream() != &outputStream) { + // a temp/backup file has been created -> restore original file + addNotification(NotificationType::Critical, "An IO error occured when rewriting the file to apply changed tag information.", context); + try { + BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, outputStream, backupStream); + addNotification(NotificationType::Information, "The original file has been restored.", context); + } catch(const ios_base::failure &ex) { + addNotification(NotificationType::Critical, ex.what(), context); + } + } else { + addNotification(NotificationType::Critical, "An IO error occured when applying tag information.", context); + } throw; } + // TODO: reduce code duplication } } diff --git a/mp4/mp4container.cpp b/mp4/mp4container.cpp index 7ac7507..8682069 100644 --- a/mp4/mp4container.cpp +++ b/mp4/mp4container.cpp @@ -436,15 +436,16 @@ calculatePadding: throw OperationAbortedException(); } - // define variables needed to handle output stream and backup stream (required when rewriting the file) + // setup stream(s) for writing + // -> update status + updateStatus("Preparing streams ..."); + + // -> define variables needed to handle output stream and backup stream (required when rewriting the file) string backupPath; fstream &outputStream = fileInfo().stream(); fstream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required BinaryWriter outputWriter(&outputStream); - // setup stream(s) for writing - // -> update status - updateStatus("Preparing streams ..."); if(rewriteRequired) { // move current file to temp dir and reopen it as backupStream, recreate original file try { @@ -461,7 +462,6 @@ calculatePadding: } } else { // !rewriteRequired - // reopen original file to ensure it is opened for writing try { fileInfo().close(); diff --git a/mp4/mp4tag.cpp b/mp4/mp4tag.cpp index 0ea734b..f98d1f1 100644 --- a/mp4/mp4tag.cpp +++ b/mp4/mp4tag.cpp @@ -269,12 +269,17 @@ Mp4TagMaker::Mp4TagMaker(Mp4Tag &tag) : m_omitPreDefinedGenre(m_tag.fields().count(Mp4TagAtomIds::PreDefinedGenre) && m_tag.fields().count(Mp4TagAtomIds::Genre)) { m_tag.invalidateStatus(); - m_makers.reserve(m_tag.fields().size()); + m_maker.reserve(m_tag.fields().size()); for(auto &field : m_tag.fields()) { if(!field.second.value().isEmpty() && (!m_omitPreDefinedGenre || field.first == Mp4TagAtomIds::PreDefinedGenre)) { - m_makers.emplace_back(field.second.prepareMaking()); - m_ilstSize += m_makers.back().requiredSize(); + try { + m_maker.emplace_back(field.second.prepareMaking()); + m_ilstSize += m_maker.back().requiredSize(); + } catch(const Failure &) { + // nothing to do here; notifications will be added anyways + } + m_tag.addNotifications(field.second); } } if(m_ilstSize != 8) { @@ -307,7 +312,7 @@ void Mp4TagMaker::make(ostream &stream) writer.writeUInt32BE(m_ilstSize); writer.writeUInt32BE(Mp4AtomIds::ItunesList); // write fields - for(auto &maker : m_makers) { + for(auto &maker : m_maker) { maker.make(stream); } } else { diff --git a/mp4/mp4tag.h b/mp4/mp4tag.h index fe02cde..7c32531 100644 --- a/mp4/mp4tag.h +++ b/mp4/mp4tag.h @@ -24,7 +24,7 @@ private: Mp4TagMaker(Mp4Tag &tag); Mp4Tag &m_tag; - std::vector m_makers; + std::vector m_maker; uint64 m_metaSize; uint64 m_ilstSize; bool m_omitPreDefinedGenre; diff --git a/mp4/mp4tagfield.cpp b/mp4/mp4tagfield.cpp index 9d36d9b..5e2e669 100644 --- a/mp4/mp4tagfield.cpp +++ b/mp4/mp4tagfield.cpp @@ -391,7 +391,7 @@ void Mp4TagField::cleared() /*! * \brief Prepares making the specified \a field. - * \sa See Mp4TagFieldMaker::prepareMaking() for more information. + * \sa See Mp4TagField::prepareMaking() for more information. */ Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) : m_field(field), @@ -409,6 +409,7 @@ Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) : m_field.addNotification(NotificationType::Critical, "No tag value assigned.", context); throw InvalidDataException(); } + try { // try to use appropriate raw data type m_rawDataType = m_field.appropriateRawDataType(); @@ -418,6 +419,7 @@ Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) : m_rawDataType = RawDataType::Utf8; m_field.addNotification(NotificationType::Warning, "It was not possible to find an appropriate raw data type id. UTF-8 will be assumed.", context); } + try { if(!m_field.value().isEmpty()) { // there might be only mean and name info, but no data m_convertedData.exceptions(std::stringstream::failbit | std::stringstream::badbit); @@ -480,6 +482,7 @@ Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) : } throw InvalidDataException(); } + // calculate data size m_dataSize = m_field.value().isEmpty() ? 0 : (m_convertedData.tellp() ? static_cast(m_convertedData.tellp()) : m_field.value().dataSize()); @@ -491,7 +494,7 @@ Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) : /*! * \brief Saves the field (specified when constructing the object) to the - * specified \a stream. * + * specified \a stream. * \throws Throws std::ios_base::failure when an IO error occurs. * \throws Throws Assumes the data is already validated and thus does NOT * throw Media::Failure or a derived exception.