diff --git a/README.md b/README.md index d96bd2f..f4442b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# tagparser +# Tag Parser C++ library for reading and writing MP4 (iTunes), ID3, Vorbis and Matroska tags. ## Supported formats @@ -18,3 +18,7 @@ For examples check out the CLI 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, ...). diff --git a/fieldbasedtag.h b/fieldbasedtag.h index c74dfb1..56bf1df 100644 --- a/fieldbasedtag.h +++ b/fieldbasedtag.h @@ -39,14 +39,14 @@ public: virtual void removeAllFields(); const std::multimap &fields() const; std::multimap &fields(); - virtual int fieldCount() const; + virtual unsigned int fieldCount() const; virtual typename FieldType::identifierType fieldId(KnownField value) const = 0; virtual KnownField knownField(const typename FieldType::identifierType &id) const = 0; virtual bool supportsField(KnownField field) const; using Tag::proposedDataType; virtual TagDataType proposedDataType(const typename FieldType::identifierType &id) const; virtual int insertFields(const FieldMapBasedTag &from, bool overwrite); - virtual int insertValues(const Tag &from, bool overwrite); + virtual unsigned int insertValues(const Tag &from, bool overwrite); typedef FieldType fieldType; private: @@ -185,9 +185,15 @@ inline std::multimap &Fi } template -inline int FieldMapBasedTag::fieldCount() const +unsigned int FieldMapBasedTag::fieldCount() const { - return m_fields.size(); + int count = 0; + for(const auto &field : m_fields) { + if(!field.second.value().isEmpty()) { + ++count; + } + } + return count; } template @@ -244,7 +250,7 @@ int FieldMapBasedTag::insertFields(const FieldMapBasedTag -int FieldMapBasedTag::insertValues(const Tag &from, bool overwrite) +unsigned int FieldMapBasedTag::insertValues(const Tag &from, bool overwrite) { if(type() == from.type()) { // the tags are of the same type, we can insert the fields directly diff --git a/id3/id3v1tag.cpp b/id3/id3v1tag.cpp index f8ca5c4..a188b60 100644 --- a/id3/id3v1tag.cpp +++ b/id3/id3v1tag.cpp @@ -232,13 +232,14 @@ void Id3v1Tag::removeAllFields() m_genre.clearDataAndMetadata(); } -int Id3v1Tag::fieldCount() const +unsigned int Id3v1Tag::fieldCount() const { int count = 0; - for(const TagValue &value : {m_title, m_artist, m_album, + for(const auto &value : {m_title, m_artist, m_album, m_year, m_comment, m_trackPos, m_genre}) { - if(!value.isEmpty()) + if(!value.isEmpty()) { ++count; + } } return count; } diff --git a/id3/id3v1tag.h b/id3/id3v1tag.h index c71783a..742fa70 100644 --- a/id3/id3v1tag.h +++ b/id3/id3v1tag.h @@ -20,7 +20,7 @@ public: virtual bool setValueConsideringTypeInfo(KnownField field, const TagValue &value, const std::string &typeInfo); virtual bool hasField(KnownField field) const; virtual void removeAllFields(); - virtual int fieldCount() const; + virtual unsigned int fieldCount() const; virtual bool supportsField(KnownField field) const; void parse(std::istream &sourceStream, bool autoSeek); diff --git a/id3/id3v2frame.cpp b/id3/id3v2frame.cpp index 484af91..2f058b8 100644 --- a/id3/id3v2frame.cpp +++ b/id3/id3v2frame.cpp @@ -4,6 +4,7 @@ #include "../exceptions.h" #include +#include #include @@ -268,53 +269,53 @@ void Id3v2Frame::make(IoUtilities::BinaryWriter &writer, int32 version) 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 - vector buffer; + 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, value().toString(), TagTextEncoding::Latin1); + 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, ConversionUtilities::numberToString(value().toTimeSpan().totalMilliseconds()), TagTextEncoding::Latin1); + 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, ConversionUtilities::numberToString(value().toStandardGenreIndex()), TagTextEncoding::Latin1); + helper.makeString(buffer, decompressedSize, ConversionUtilities::numberToString(value().toStandardGenreIndex()), TagTextEncoding::Latin1); } else { // any other text frame - helper.makeString(buffer, value().toString(), value().dataEncoding()); // the same as a normal text frame + helper.makeString(buffer, decompressedSize, value().toString(), value().dataEncoding()); // the same as a normal text frame } } else if((version >= 3 && frameId == Id3v2FrameIds::lCover) || (version < 3 && frameId == Id3v2FrameIds::sCover)) { // picture frame - helper.makePicture(buffer, value(), isTypeInfoAssigned() ? typeInfo() : 0); + helper.makePicture(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, value()); + helper.makeComment(buffer, decompressedSize, value()); } else { // an unknown frame // create buffer - buffer.resize(value().dataSize()); + buffer = make_unique(decompressedSize = value().dataSize()); // just write the data - copy(value().dataPointer(), value().dataPointer() + value().dataSize(), buffer.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(); } - uint32 decompressedSize = buffer.size(); + unsigned long actualSize; if(version >= 3 && isCompressed()) { - uLongf destLen = compressBound(buffer.size()); - vector compressedBuffer; - compressedBuffer.resize(destLen); - switch(compress(reinterpret_cast(compressedBuffer.data()), &destLen, reinterpret_cast(buffer.data()), buffer.size())) { + 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(); @@ -325,17 +326,18 @@ void Id3v2Frame::make(IoUtilities::BinaryWriter &writer, int32 version) ; } buffer.swap(compressedBuffer); - buffer.resize(destLen); + } else { + actualSize = decompressedSize; } if(version < 3) { writer.writeUInt24BE(frameId); - writer.writeUInt24BE(buffer.size()); + writer.writeUInt24BE(actualSize); } else { writer.writeUInt32BE(frameId); if(version >= 4) { - writer.writeSynchsafeUInt32BE(buffer.size()); + writer.writeSynchsafeUInt32BE(actualSize); } else { - writer.writeUInt32BE(buffer.size()); + writer.writeUInt32BE(actualSize); } writer.writeUInt16BE(m_flag); if(hasGroupInformation()) { @@ -349,7 +351,7 @@ void Id3v2Frame::make(IoUtilities::BinaryWriter &writer, int32 version) } } } - writer.write(buffer.data(), buffer.size()); + writer.write(buffer.get(), actualSize); } /*! @@ -605,9 +607,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(vector &buffer, const string &value, TagTextEncoding encoding) +void Id3v2FrameHelper::makeString(unique_ptr &buffer, uint32 &bufferSize, const string &value, TagTextEncoding encoding) { - makeEncodingAndData(buffer, encoding, value.c_str(), value.length()); + makeEncodingAndData(buffer, bufferSize, encoding, value.c_str(), value.length()); } /*! @@ -617,7 +619,7 @@ void Id3v2FrameHelper::makeString(vector &buffer, const string &value, Tag * \param data Specifies the data. * \param dataSize Specifies the data size. */ -void Id3v2FrameHelper::makeEncodingAndData(vector &buffer, TagTextEncoding encoding, const char *data, size_t dataSize) +void Id3v2FrameHelper::makeEncodingAndData(unique_ptr &buffer, uint32 &bufferSize, TagTextEncoding encoding, const char *data, size_t dataSize) { // calculate buffer size if(!data) { @@ -627,23 +629,25 @@ void Id3v2FrameHelper::makeEncodingAndData(vector &buffer, TagTextEncoding case TagTextEncoding::Latin1: case TagTextEncoding::Utf8: case TagTextEncoding::Unspecified: // assumption - buffer.resize(1 + dataSize + 1); // allocate buffer + // allocate buffer + buffer = make_unique(bufferSize = 1 + dataSize + 1); break; case TagTextEncoding::Utf16LittleEndian: case TagTextEncoding::Utf16BigEndian: - buffer.resize(1 + dataSize + 2); // allocate buffer + // allocate buffer + buffer = make_unique(bufferSize = 1 + dataSize + 2); break; } buffer[0] = makeTextEncodingByte(encoding); // set text encoding byte if(dataSize > 0) { - copy(data, data + dataSize, buffer.data() + 1); // write string data + copy(data, data + dataSize, buffer.get() + 1); // write string data } } /*! * \brief Writes the specified picture to the specified buffer. */ -void Id3v2FrameHelper::makePicture(std::vector &buffer, const TagValue &picture, byte typeInfo) +void Id3v2FrameHelper::makePicture(unique_ptr &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo) { // calculate needed buffer size and create buffer TagTextEncoding descriptionEncoding = picture.descriptionEncoding(); @@ -656,9 +660,9 @@ void Id3v2FrameHelper::makePicture(std::vector &buffer, const TagValue &pi if(descriptionLength == string::npos) { descriptionLength = picture.description().length(); } - buffer.resize(1 + mimeTypeLength + 1 + 1 + descriptionLength + (descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? 2 : 1) + dataSize); - // note: encoding byte + mime type length + 0 byte + picture type byte + description length + 1 or 2 null bytes (depends on encoding) + data size - char *offset = buffer.data(); + buffer = make_unique(bufferSize = 1 + mimeTypeLength + 1 + 1 + descriptionLength + (descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? 2 : 1) + dataSize); + // note: encoding byte + mime type length + 0 byte + picture type byte + description length + 1 or 2 null bytes (depends on encoding) + data size + char *offset = buffer.get(); // write encoding byte *offset = makeTextEncodingByte(descriptionEncoding); // write mime type @@ -681,7 +685,7 @@ void Id3v2FrameHelper::makePicture(std::vector &buffer, const TagValue &pi /*! * \brief Writes the specified comment to the specified buffer. */ -void Id3v2FrameHelper::makeComment(std::vector &buffer, const TagValue &comment) +void Id3v2FrameHelper::makeComment(unique_ptr &buffer, uint32 &bufferSize, const TagValue &comment) { static const string context("making comment frame"); // check type and other values are valid @@ -700,9 +704,9 @@ void Id3v2FrameHelper::makeComment(std::vector &buffer, const TagValue &co if(descriptionLength == string::npos) descriptionLength = comment.description().length(); uint32 dataSize = comment.dataSize(); - buffer.resize(1 + 3 + descriptionLength + (encoding == TagTextEncoding::Utf16BigEndian || encoding == TagTextEncoding::Utf16LittleEndian ? 2 : 1) + dataSize); - // note: encoding byte + language + description length + 1 or 2 null bytes + data size - char *offset = buffer.data(); + buffer = make_unique(bufferSize = 1 + 3 + descriptionLength + (encoding == TagTextEncoding::Utf16BigEndian || encoding == TagTextEncoding::Utf16LittleEndian ? 2 : 1) + dataSize); + // note: encoding byte + language + description length + 1 or 2 null bytes + data size + char *offset = buffer.get(); // write encoding *offset = makeTextEncodingByte(encoding); // write language @@ -717,7 +721,7 @@ void Id3v2FrameHelper::makeComment(std::vector &buffer, const TagValue &co *(++offset) = 0x00; } // write actual data - string data = comment.toString(); + const auto data = comment.toString(); data.copy(++offset, data.length()); } diff --git a/id3/id3v2frame.h b/id3/id3v2frame.h index 4cc0764..1c1716f 100644 --- a/id3/id3v2frame.h +++ b/id3/id3v2frame.h @@ -32,10 +32,10 @@ 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); - void makeString(std::vector &buffer, const std::string &value, TagTextEncoding encoding); - void makeEncodingAndData(std::vector &buffer, TagTextEncoding encoding, const char *data, size_t m_dataSize); - void makePicture(std::vector &buffer, const TagValue &picture, byte typeInfo); - void makeComment(std::vector &buffer, const TagValue &comment); + 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 makePicture(std::unique_ptr &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo); + void makeComment(std::unique_ptr &buffer, uint32 &bufferSize, const TagValue &comment); private: std::string m_id; diff --git a/mediafileinfo.cpp b/mediafileinfo.cpp index fee8fd3..2adda4a 100644 --- a/mediafileinfo.cpp +++ b/mediafileinfo.cpp @@ -191,6 +191,7 @@ startParsingSignature: break; } case ContainerFormat::Ogg: m_container = make_unique(*this, m_containerOffset); + static_cast(m_container.get())->setChecksumValidationEnabled(m_forceFullParse); break; default: ; diff --git a/mp4/mp4tag.cpp b/mp4/mp4tag.cpp index 97028f6..9831b6f 100644 --- a/mp4/mp4tag.cpp +++ b/mp4/mp4tag.cpp @@ -266,9 +266,7 @@ void Mp4Tag::make(ostream &stream) int tagFieldsWritten = 0; for(auto i = fields().begin(), end = fields().end(); i != end; ++i) { Mp4TagField &field = i->second; - if(field.value().isEmpty()) { - continue; - } else { + if(!field.value().isEmpty()) { field.invalidateNotifications(); try { field.make(stream); diff --git a/mpegaudio/mpegaudioframestream.cpp b/mpegaudio/mpegaudioframestream.cpp index f46d518..09dbbd0 100644 --- a/mpegaudio/mpegaudioframestream.cpp +++ b/mpegaudio/mpegaudioframestream.cpp @@ -51,7 +51,7 @@ void MpegAudioFrameStream::internalParseHeader() if(frame.isXingBytesfieldPresent()) { uint32 xingSize = frame.xingBytesfield(); if(m_size && xingSize != m_size) { - addNotification(NotificationType::Warning, "Real length MPEG of audio frames is not equal with value provided by Xing header. The Xing header value will be used.", context); + addNotification(NotificationType::Warning, "Real length of MPEG audio frames is not equal with value provided by Xing header. The Xing header value will be used.", context); m_size = xingSize; } } diff --git a/ogg/oggcontainer.cpp b/ogg/oggcontainer.cpp index 2f311e1..d9d4235 100644 --- a/ogg/oggcontainer.cpp +++ b/ogg/oggcontainer.cpp @@ -36,6 +36,7 @@ void OggContainer::internalParseHeader() static const string context("parsing OGG bitstream header"); // iterate through pages using OggIterator helper class try { + uint32 pageSequenceNumber = 0; // ensure iterator is setup properly for(m_iterator.removeFilter(), m_iterator.reset(); m_iterator; m_iterator.nextPage()) { const OggPage &page = m_iterator.currentPage(); @@ -49,6 +50,14 @@ void OggContainer::internalParseHeader() m_streamsBySerialNo[page.streamSerialNumber()] = m_tracks.size(); m_tracks.emplace_back(new OggStream(*this, m_iterator.currentPageIndex())); } + if(pageSequenceNumber != page.sequenceNumber()) { + if(pageSequenceNumber != 0) { + addNotification(NotificationType::Warning, "Page is missing (page sequence number omitted).", context); + pageSequenceNumber = page.sequenceNumber(); + } + } else { + ++pageSequenceNumber; + } } } catch(TruncatedDataException &) { // thrown when page exceeds max size @@ -64,11 +73,13 @@ void OggContainer::internalParseHeader() void OggContainer::internalParseTags() { parseTracks(); // tracks needs to be parsed because tags are stored at stream level - for(auto i : m_commentTable) { + for(auto &i : m_commentTable) { //fileInfo().stream().seekg(get<1>(i)); - m_iterator.setPageIndex(get<0>(i)); - m_iterator.setSegmentIndex(get<1>(i)); - m_tags[get<2>(i)]->parse(m_iterator); + m_iterator.setPageIndex(i.firstPageIndex); + m_iterator.setSegmentIndex(i.firstSegmentIndex); + m_tags[i.tagIndex]->parse(m_iterator); + i.lastPageIndex = m_iterator.currentPageIndex(); + i.lastSegmentIndex = m_iterator.currentSegmentIndex(); } } @@ -100,6 +111,7 @@ void OggContainer::internalMakeFile() { const string context("making OGG file"); updateStatus("Prepare for rewriting OGG file ..."); + parseTags(); // tags need to be parsed before the file can be rewritten fileInfo().close(); string backupPath; fstream backupStream; @@ -110,10 +122,16 @@ void OggContainer::internalMakeFile() CopyHelper<65307> copy; auto commentTableIterator = m_commentTable.cbegin(), commentTableEnd = m_commentTable.cend(); vector updatedPageOffsets; + uint32 pageSequenceNumber = 0; for(m_iterator.setStream(backupStream), m_iterator.removeFilter(), m_iterator.reset(); m_iterator; m_iterator.nextPage()) { const auto ¤tPage = m_iterator.currentPage(); auto pageSize = currentPage.totalSize(); - if(commentTableIterator != commentTableEnd && m_iterator.currentPageIndex() == get<0>(*commentTableIterator) && !currentPage.segmentSizes().empty()) { + // check whether the Vorbis Comment is present in this Ogg page + // -> then the page needs to be rewritten + if(commentTableIterator != commentTableEnd + && m_iterator.currentPageIndex() >= commentTableIterator->firstPageIndex + && m_iterator.currentPageIndex() <= commentTableIterator->lastPageIndex + && !currentPage.segmentSizes().empty()) { // page needs to be rewritten (not just copied) // -> write segments to a buffer first stringstream buffer(ios_base::in | ios_base::out | ios_base::binary); @@ -121,67 +139,109 @@ void OggContainer::internalMakeFile() newSegmentSizes.reserve(currentPage.segmentSizes().size()); uint64 segmentOffset = m_iterator.currentSegmentOffset(); vector::size_type segmentIndex = 0; - for(auto segmentSize : currentPage.segmentSizes()) { - if(segmentIndex == get<1>(*commentTableIterator)) { - // make vorbis comment segment - auto offset = buffer.tellp(); - m_tags[get<2>(*commentTableIterator)]->make(buffer); - newSegmentSizes.push_back(buffer.tellp() - offset); - } else { - // copy other segments unchanged - backupStream.seekg(segmentOffset); - copy.copy(backupStream, buffer, segmentSize); - newSegmentSizes.push_back(segmentSize); + for(const auto segmentSize : currentPage.segmentSizes()) { + if(segmentSize) { + // check whether this segment contains the Vorbis Comment + if((m_iterator.currentPageIndex() > commentTableIterator->firstPageIndex || segmentIndex >= commentTableIterator->firstSegmentIndex) + && (m_iterator.currentPageIndex() < commentTableIterator->lastPageIndex || segmentIndex <= commentTableIterator->lastSegmentIndex)) { + // prevent making the comment twice if it spreads over multiple pages + if(m_iterator.currentPageIndex() == commentTableIterator->firstPageIndex) { + // make Vorbis Comment segment + auto offset = buffer.tellp(); + m_tags[commentTableIterator->tagIndex]->make(buffer); + newSegmentSizes.push_back(buffer.tellp() - offset); + } + if(m_iterator.currentPageIndex() > commentTableIterator->lastPageIndex + || (m_iterator.currentPageIndex() == commentTableIterator->lastPageIndex && segmentIndex > commentTableIterator->lastSegmentIndex)) { + ++commentTableIterator; + } + } else { + // copy other segments unchanged + backupStream.seekg(segmentOffset); + copy.copy(backupStream, buffer, segmentSize); + newSegmentSizes.push_back(segmentSize); + } + segmentOffset += segmentSize; } - segmentOffset += segmentSize; ++segmentIndex; } // write buffered data to actual stream auto newSegmentSizesIterator = newSegmentSizes.cbegin(), newSegmentSizesEnd = newSegmentSizes.cend(); - uint32 bytesLeft, currentSize; - // write pages until all data in the buffer is written - while(newSegmentSizesIterator != newSegmentSizesEnd) { - // write header - backupStream.seekg(currentPage.startOffset()); - copy.copy(backupStream, stream(), 27); // just copy from original file - updatedPageOffsets.push_back(currentPage.startOffset()); // memorize offset to update checksum later - int16 segmentSizesWritten = 0; // in the current page header only - // write segment sizes as long as there are segment sizes to be written and - // the max number of segment sizes is not exceeded - bytesLeft = *newSegmentSizesIterator; - currentSize = 0; - while(bytesLeft > 0 && segmentSizesWritten < 0xFF) { - while(bytesLeft >= 0xFF && segmentSizesWritten < 0xFF) { - stream().put(0xFF); - bytesLeft -= 0xFF; - ++segmentSizesWritten; + bool continuePreviousSegment = false; + if(newSegmentSizesIterator != newSegmentSizesEnd) { + uint32 bytesLeft = *newSegmentSizesIterator; + // write pages until all data in the buffer is written + while(newSegmentSizesIterator != newSegmentSizesEnd) { + // write header + backupStream.seekg(currentPage.startOffset()); + updatedPageOffsets.push_back(stream().tellp()); // memorize offset to update checksum later + copy.copy(backupStream, stream(), 27); // just copy header from original file + // set continue flag + stream().seekp(-22, ios_base::cur); + stream().put(currentPage.headerTypeFlag() & (continuePreviousSegment ? 0xFF : 0xFE)); + continuePreviousSegment = true; + // adjust page sequence number + stream().seekp(12, ios_base::cur); + writer().writeUInt32LE(pageSequenceNumber); + stream().seekp(5, ios_base::cur); + int16 segmentSizesWritten = 0; // in the current page header only + // write segment sizes as long as there are segment sizes to be written and + // the max number of segment sizes (255) is not exceeded + uint32 currentSize = 0; + while(bytesLeft > 0 && segmentSizesWritten < 0xFF) { + while(bytesLeft >= 0xFF && segmentSizesWritten < 0xFF) { + stream().put(0xFF); + currentSize += 0xFF; + bytesLeft -= 0xFF; + ++segmentSizesWritten; + } + if(bytesLeft > 0 && segmentSizesWritten < 0xFF) { + // bytes left is here < 0xFF + stream().put(bytesLeft); + currentSize += bytesLeft; + bytesLeft = 0; + ++segmentSizesWritten; + } + if(bytesLeft == 0) { + // sizes for the segment have been written + // -> continue with next segment + if(++newSegmentSizesIterator != newSegmentSizesEnd) { + bytesLeft = *newSegmentSizesIterator; + continuePreviousSegment = false; + } + } } - if(bytesLeft > 0 && segmentSizesWritten < 0xFF) { - stream().put(bytesLeft); - bytesLeft = 0; - ++segmentSizesWritten; - } - currentSize += *newSegmentSizesIterator - bytesLeft; + // there are no bytes left in the current segment; remove continue flag if(bytesLeft == 0) { - // sizes for the current segemnt have been written -> continue with next segment - ++newSegmentSizesIterator; - bytesLeft = newSegmentSizesIterator != newSegmentSizesEnd ? *newSegmentSizesIterator : 0; + continuePreviousSegment = false; } - } - // page is full or all segment data has already been written -> write data - if(bytesLeft == 0 || newSegmentSizesIterator != newSegmentSizesEnd) { - // seek back and write updated page segment number + // page is full or all segment data has been covered + // -> write segment table size (segmentSizesWritten) and segment data + // -> seek back and write updated page segment number stream().seekp(-1 - segmentSizesWritten, ios_base::cur); stream().put(segmentSizesWritten); stream().seekp(segmentSizesWritten, ios_base::cur); - // write actual page data + // -> write actual page data copy.copy(buffer, stream(), currentSize); + ++pageSequenceNumber; } } } else { - // copy page unchanged - backupStream.seekg(currentPage.startOffset()); - copy.copy(backupStream, stream(), pageSize); + if(pageSequenceNumber != m_iterator.currentPageIndex()) { + // just update page sequence number + backupStream.seekg(currentPage.startOffset()); + updatedPageOffsets.push_back(stream().tellp()); // memorize offset to update checksum later + copy.copy(backupStream, stream(), 27); + stream().seekp(-9, ios_base::cur); + writer().writeUInt32LE(pageSequenceNumber); + stream().seekp(5, ios_base::cur); + copy.copy(backupStream, stream(), pageSize - 27); + } else { + // copy page unchanged + backupStream.seekg(currentPage.startOffset()); + copy.copy(backupStream, stream(), pageSize); + } + ++pageSequenceNumber; } } // close backups stream; reopen new file as readable stream diff --git a/ogg/oggcontainer.h b/ogg/oggcontainer.h index 1710fe9..94b98df 100644 --- a/ogg/oggcontainer.h +++ b/ogg/oggcontainer.h @@ -16,6 +16,25 @@ namespace Media { class MediaFileInfo; +struct LIB_EXPORT VorbisCommentInfo +{ + VorbisCommentInfo(std::vector::size_type firstPageIndex, std::vector::size_type firstSegmentIndex, std::vector::size_type tagIndex); + + std::vector::size_type firstPageIndex; + std::vector::size_type firstSegmentIndex; + std::vector::size_type lastPageIndex; + std::vector::size_type lastSegmentIndex; + std::vector >::size_type tagIndex; +}; + +inline VorbisCommentInfo::VorbisCommentInfo(std::vector::size_type firstPageIndex, std::vector::size_type firstSegmentIndex, std::vector::size_type tagIndex) : + firstPageIndex(firstPageIndex), + firstSegmentIndex(firstSegmentIndex), + lastPageIndex(0), + lastSegmentIndex(0), + tagIndex(tagIndex) +{} + class LIB_EXPORT OggContainer : public GenericContainer { friend class OggStream; @@ -37,7 +56,12 @@ private: void ariseComment(std::vector::size_type pageIndex, std::vector::size_type segmentIndex); std::unordered_map >::size_type> m_streamsBySerialNo; - std::list::size_type, std::vector::size_type, std::vector >::size_type> > m_commentTable; + + /*! + * \brief Consists of first page index, first segment index, last page index, last segment index and tag index (in this order). + */ + std::list m_commentTable; + OggIterator m_iterator; bool m_validateChecksums; }; diff --git a/ogg/oggpage.cpp b/ogg/oggpage.cpp index 5f0b77d..f8b1b99 100644 --- a/ogg/oggpage.cpp +++ b/ogg/oggpage.cpp @@ -77,28 +77,28 @@ uint32 OggPage::computeChecksum(istream &stream, uint64 startOffset) stream.seekg(startOffset); uint32 crc = 0x0; uint32 *crc32Table = BinaryReader::crc32Table(); - byte val; - for(uint32 i = 0, len = 27, si = 0, sc = 0; i < len; ++i) { + byte value, segmentTableSize = 0, segmentTableIndex = 0; + for(uint32 i = 0, segmentLength = 27; i < segmentLength; ++i) { switch(i) { case 22: // bytes 22, 23, 24, 25 hold denoted checksum and must be set to zero stream.seekg(4, ios_base::cur); case 23: case 24: case 25: - val = 0; + value = 0; break; case 26: // byte 26 holds the number of segment sizes - len += (sc = (val = stream.get())); + segmentLength += (segmentTableSize = (value = stream.get())); break; default: - val = stream.get(); - if(i > 26 && si < sc) { + value = stream.get(); + if(i > 26 && segmentTableIndex < segmentTableSize) { // bytes 27 to (27 + segment size count) hold page size - len += val; - ++si; + segmentLength += value; + ++segmentTableIndex; } } - crc = (crc << 8) ^ crc32Table[((crc >> 24) & 0xff) ^ val]; + crc = (crc << 8) ^ crc32Table[((crc >> 24) & 0xFF) ^ value]; } return crc; } diff --git a/tag.cpp b/tag.cpp index 86a4f81..8987bbf 100644 --- a/tag.cpp +++ b/tag.cpp @@ -92,9 +92,9 @@ string Tag::toString() const * \param overwrite Indicates whether existing values should be overwritten. * \return Returns the number of values that have been inserted. */ -int Tag::insertValues(const Tag &from, bool overwrite) +unsigned int Tag::insertValues(const Tag &from, bool overwrite) { - int count = 0; + unsigned int count = 0; for(int i = static_cast(KnownField::Invalid) + 1, last = static_cast(KnownField::Description); i <= last; ++i) { KnownField field = static_cast(i); diff --git a/tag.h b/tag.h index 44ae7fb..9d3fe33 100644 --- a/tag.h +++ b/tag.h @@ -115,12 +115,12 @@ public: virtual bool supportsTarget() const; const TagTarget &target() const; void setTarget(const TagTarget &target); - virtual int fieldCount() const = 0; + virtual unsigned int fieldCount() const = 0; virtual bool supportsField(KnownField field) const = 0; virtual TagDataType proposedDataType(KnownField field) const; virtual bool supportsDescription(KnownField field) const; virtual bool supportsMimeType(KnownField field) const; - virtual int insertValues(const Tag &from, bool overwrite); + virtual unsigned int insertValues(const Tag &from, bool overwrite); // Tag *parent() const; // bool setParent(Tag *tag); // Tag *nestedTag(size_t index) const; diff --git a/vorbis/vorbiscomment.cpp b/vorbis/vorbiscomment.cpp index 1564196..08448a1 100644 --- a/vorbis/vorbiscomment.cpp +++ b/vorbis/vorbiscomment.cpp @@ -111,7 +111,7 @@ void VorbisComment::parse(OggIterator &iterator) invalidateStatus(); static const string context("parsing Vorbis comment"); iterator.stream().seekg(iterator.currentSegmentOffset()); - uint64 startOffset = iterator.currentSegmentOffset(); + auto startOffset = iterator.currentSegmentOffset(); try { // read signature: 0x3 + "vorbis" char sig[8]; @@ -120,8 +120,8 @@ void VorbisComment::parse(OggIterator &iterator) // read vendor (length prefixed string) { iterator.read(sig, 4); - uint32 vendorSize = LE::toUInt32(sig); - unique_ptr buff = make_unique(vendorSize); + auto vendorSize = LE::toUInt32(sig); + auto buff = make_unique(vendorSize); iterator.read(buff.get(), vendorSize); m_vendor = string(buff.get(), vendorSize); } @@ -151,6 +151,7 @@ void VorbisComment::parse(OggIterator &iterator) throw InvalidDataException(); } } catch(TruncatedDataException &) { + m_size = static_cast(static_cast(iterator.currentCharacterOffset()) - startOffset); addNotification(NotificationType::Critical, "Vorbis comment is truncated.", context); throw; } @@ -186,14 +187,16 @@ void VorbisComment::make(std::ostream &stream) // write fields for(auto i : fields()) { VorbisCommentField &field = i.second; - try { - field.make(writer); - } catch(Failure &) { - // nothing to do here since notifications will be added anyways + if(!field.value().isEmpty()) { + try { + field.make(writer); + } catch(Failure &) { + // nothing to do here since notifications will be added anyways + } + // add making notifications + addNotifications(context, field); + field.invalidateNotifications(); } - // add making notifications - addNotifications(context, field); - field.invalidateNotifications(); } // write framing byte stream.put(0x01); diff --git a/vorbis/vorbiscommentfield.cpp b/vorbis/vorbiscommentfield.cpp index 8f43611..622f385 100644 --- a/vorbis/vorbiscommentfield.cpp +++ b/vorbis/vorbiscommentfield.cpp @@ -13,6 +13,8 @@ #include #include +#include + using namespace std; using namespace IoUtilities; using namespace ConversionUtilities; @@ -52,13 +54,12 @@ void VorbisCommentField::parse(OggIterator &iterator) static const string context("parsing Vorbis comment field"); char buff[4]; iterator.read(buff, 4); - if(uint32 size = LE::toUInt32(buff)) { // read size + if(auto size = LE::toUInt32(buff)) { // read size // read data - unique_ptr data = make_unique(size); + auto data = make_unique(size); iterator.read(data.get(), size); uint32 idSize = 0; - for(const char *i = data.get(), *end = data.get() + size; i != end && (*i) != '='; ++i, ++idSize) - ; + for(const char *i = data.get(), *end = data.get() + size; i != end && *i != '='; ++i, ++idSize); // extract id setId(string(data.get(), idSize)); if(!idSize) { @@ -67,11 +68,30 @@ void VorbisCommentField::parse(OggIterator &iterator) throw InvalidDataException(); } else if(id() == VorbisCommentIds::cover()) { // extract cover value - vector buffer = decodeBase64(string(data.get() + idSize + 1, size - idSize - 1)); - Id3v2FrameHelper helper(id(), *this); - byte type; - helper.parsePicture(buffer.data(), buffer.size(), value(), type); - setTypeInfo(type); + try { + auto decoded = decodeBase64(data.get() + idSize + 1, size - idSize - 1); + stringstream ss(ios_base::in | ios_base::out | ios_base::binary); + ss.exceptions(ios_base::failbit | ios_base::badbit); + ss.rdbuf()->pubsetbuf(reinterpret_cast(decoded.first.get()), decoded.second); + BinaryReader reader(&ss); + setTypeInfo(reader.readUInt32BE()); + auto size = reader.readUInt32BE(); + value().setMimeType(reader.readString(size)); + size = reader.readUInt32BE(); + value().setDescription(reader.readString(size)); + // skip width, height, color depth, number of colors used + ss.seekg(4 * 4, ios_base::cur); + size = reader.readUInt32BE(); + auto data = make_unique(size); + ss.read(data.get(), size); + value().assignData(move(data), size, TagDataType::Picture); + } catch (const ios_base::failure &) { + addNotification(NotificationType::Critical, "An IO error occured when reading the METADATA_BLOCK_PICTURE struct.", context); + throw Failure(); + } catch (const ConversionException &) { + addNotification(NotificationType::Critical, "Base64 data from METADATA_BLOCK_PICTURE is invalid.", context); + throw InvalidDataException(); + } } else if(id().size() + 1 < size) { // extract other values (as string) setValue(string(data.get() + idSize + 1, size - idSize - 1)); @@ -92,20 +112,42 @@ void VorbisCommentField::make(BinaryWriter &writer) if(id().empty()) { addNotification(NotificationType::Critical, "The field ID is empty.", context); } - // try to convert value to string try { + // try to convert value to string string valueString; if(id() == VorbisCommentIds::cover()) { // make cover - Id3v2FrameHelper helper(id(), *this); - vector buffer; - helper.makePicture(buffer, value(), isTypeInfoAssigned() ? typeInfo() : 0); - valueString = encodeBase64(buffer); + if(value().type() != TagDataType::Picture) { + addNotification(NotificationType::Critical, "Assigned value of cover field is not picture data.", context); + throw InvalidDataException(); + } + try { + uint32 dataSize = 32 + value().mimeType().size() + value().description().size() + value().dataSize(); + auto buffer = make_unique(dataSize); + stringstream ss(ios_base::in | ios_base::out | ios_base::binary); + ss.exceptions(ios_base::failbit | ios_base::badbit); + ss.rdbuf()->pubsetbuf(buffer.get(), dataSize); + BinaryWriter writer(&ss); + writer.writeUInt32BE(typeInfo()); + writer.writeUInt32BE(value().mimeType().size()); + writer.writeString(value().mimeType()); + writer.writeUInt32BE(value().description().size()); + writer.writeString(value().description()); + writer.writeUInt32BE(0); // skip width + writer.writeUInt32BE(0); // skip height + writer.writeUInt32BE(0); // skip color depth + writer.writeUInt32BE(0); // skip number of colors used + writer.writeUInt32BE(value().dataSize()); + writer.write(value().dataPointer(), value().dataSize()); + valueString = encodeBase64(reinterpret_cast(buffer.get()), dataSize); + } catch (const ios_base::failure &) { + addNotification(NotificationType::Critical, "An IO error occured when writing the METADATA_BLOCK_PICTURE struct.", context); + throw Failure(); + } } else { // make normal string value valueString = value().toString(); } - // write size writer.writeUInt32LE(id().size() + 1 + valueString.size()); writer.writeString(id()); writer.writeChar('='); diff --git a/vorbis/vorbiscommentfield.h b/vorbis/vorbiscommentfield.h index c63a8ef..7281480 100644 --- a/vorbis/vorbiscommentfield.h +++ b/vorbis/vorbiscommentfield.h @@ -27,7 +27,7 @@ public: /*! * \brief The type info is stored using 32-bit unsigned integers. */ - typedef byte typeInfoType; + typedef uint32 typeInfoType; /*! * \brief The implementation type is VorbisCommentField. */