Tag Parser 12.4.0
C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags
Loading...
Searching...
No Matches
oggcontainer.cpp
Go to the documentation of this file.
1#include "./oggcontainer.h"
2
4
5#include "../backuphelper.h"
6#include "../mediafileinfo.h"
8#include "../tagtarget.h"
9
10#include <c++utilities/conversion/stringbuilder.h>
11#include <c++utilities/io/copy.h>
12
13#include <limits>
14#include <memory>
15
16using namespace std;
17using namespace CppUtilities;
18
19namespace TagParser {
20
26std::string_view OggVorbisComment::typeName() const
27{
28 switch (m_oggParams.streamFormat) {
30 return "Vorbis comment (in FLAC stream)";
32 return "Vorbis comment (in Opus stream)";
34 return "Vorbis comment (in Theora stream)";
35 default:
36 return "Vorbis comment";
37 }
38}
39
48OggContainer::OggContainer(MediaFileInfo &fileInfo, std::uint64_t startOffset)
50 , m_iterator(fileInfo.stream(), startOffset, fileInfo.size())
51 , m_validateChecksums(false)
52{
53}
54
58
60{
61 m_iterator.reset();
62}
63
75{
76 if (!target.tracks().empty()) {
77 // return the tag for the first matching track ID
78 for (auto &tag : m_tags) {
79 if (!tag->target().tracks().empty() && tag->target().tracks().front() == target.tracks().front() && !tag->oggParams().removed) {
80 return tag.get();
81 }
82 }
83 // not tag found -> try to re-use a tag which has been flagged as removed
84 for (auto &tag : m_tags) {
85 if (!tag->target().tracks().empty() && tag->target().tracks().front() == target.tracks().front()) {
86 tag->oggParams().removed = false;
87 return tag.get();
88 }
89 }
90 } else if (OggVorbisComment *comment = tag(0)) {
91 // no track ID specified -> just return the first tag (if one exists)
92 return comment;
93 } else if (!m_tags.empty()) {
94 // no track ID specified -> just return the first tag (try to re-use a tag which has been flagged as removed)
95 m_tags.front()->oggParams().removed = false;
96 return m_tags.front().get();
97 }
98
99 // a new tag needs to be created
100 // -> determine an appropriate track for the tag
101 // -> just use the first Vorbis/Opus track
102 for (const auto &track : m_tracks) {
103 if (target.tracks().empty() || target.tracks().front() == track->id()) {
104 switch (track->format().general) {
107 // check whether start page has a valid value
108 if (track->startPage() < m_iterator.pages().size()) {
109 announceComment(track->startPage(), numeric_limits<size_t>::max(), false, track->format().general);
110 m_tags.back()->setTarget(target);
111 return m_tags.back().get();
112 } else {
113 // TODO: error handling?
114 }
115 break;
116 default:;
117 }
118 // TODO: allow adding tags to FLAC tracks (not really important, because a tag should always be present)
119 }
120 }
121 return nullptr;
122}
123
125{
126 auto i = std::size_t();
127 for (const auto &tag : m_tags) {
128 if (!tag->oggParams().removed) {
129 if (index == i) {
130 return tag.get();
131 }
132 ++i;
133 }
134 }
135 return nullptr;
136}
137
139{
140 auto count = std::size_t();
141 for (const auto &tag : m_tags) {
142 if (!tag->oggParams().removed) {
143 ++count;
144 }
145 }
146 return count;
147}
148
159{
160 for (auto &existingTag : m_tags) {
161 if (static_cast<Tag *>(existingTag.get()) == tag) {
162 existingTag->removeAllFields();
163 existingTag->oggParams().removed = true;
164 return true;
165 }
166 }
167 return false;
168}
169
181{
182 for (auto &existingTag : m_tags) {
183 existingTag->removeAllFields();
184 existingTag->oggParams().removed = true;
185 }
186}
187
189{
190 CPP_UTILITIES_UNUSED(progress)
191
192 static const auto context = std::string("parsing Ogg bitstream header");
193 auto pagesSkipped = false, continueFromHere = false;
194
195 // iterate through pages using OggIterator helper class
196 try {
197 // ensure iterator is setup properly
198 for (m_iterator.removeFilter(), m_iterator.reset(); m_iterator;
199 continueFromHere ? [&] { continueFromHere = false; }() : m_iterator.nextPage()) {
200 progress.stopIfAborted();
201 const OggPage &page = m_iterator.currentPage();
202 if (m_validateChecksums && page.checksum() != OggPage::computeChecksum(stream(), page.startOffset())) {
203 diag.emplace_back(DiagLevel::Warning,
204 argsToString(
205 "The denoted checksum of the Ogg page at ", m_iterator.currentSegmentOffset(), " does not match the computed checksum."),
206 context);
207 }
209 auto lastNewStreamOffset = std::uint64_t();
210 if (const auto streamIndex = m_streamsBySerialNo.find(page.streamSerialNumber()); streamIndex != m_streamsBySerialNo.end()) {
211 stream = m_tracks[streamIndex->second].get();
212 } else {
213 // new stream serial number recognized -> add new stream
214 m_streamsBySerialNo[page.streamSerialNumber()] = m_tracks.size();
215 stream = m_tracks.emplace_back(make_unique<OggStream>(*this, m_iterator.currentPageIndex())).get();
216 lastNewStreamOffset = page.startOffset();
217 }
218 if (!pagesSkipped) {
219 stream->m_size += page.dataSize();
220 }
221 if (stream->m_currentSequenceNumber != page.sequenceNumber()) {
222 if (stream->m_currentSequenceNumber) {
223 diag.emplace_back(DiagLevel::Warning,
224 argsToString("Page of stream ", page.streamSerialNumber(), " missing; page sequence number ", stream->m_currentSequenceNumber,
225 " omitted at ", page.startOffset(), ", found ", page.sequenceNumber(), " instead."),
226 context);
227 }
228 stream->m_currentSequenceNumber = page.sequenceNumber() + 1;
229 } else {
230 ++stream->m_currentSequenceNumber;
231 }
232
233 // skip pages in the middle of a big file (still more than 100 MiB to parse) if no new track has been seen since the last 20 MiB
234 if (!fileInfo().isForcingFullParse() && (fileInfo().size() - page.startOffset()) > (100 * 0x100000)
235 && (page.startOffset() - lastNewStreamOffset) > (20 * 0x100000)) {
236 if (m_iterator.resyncAt(fileInfo().size() - (20 * 0x100000))) {
237 const OggPage &resyncedPage = m_iterator.currentPage();
238 // prevent warning about missing pages by resetting the sequence number of all streams and invalidate the stream size
239 for (auto &trackStream : m_tracks) {
240 trackStream->m_currentSequenceNumber = 0;
241 trackStream->m_size = 0;
242 }
243 pagesSkipped = continueFromHere = true;
244 diag.emplace_back(DiagLevel::Information,
245 argsToString("Pages in the middle of the file (", dataSizeToString(resyncedPage.startOffset() - page.startOffset()),
246 ") have been skipped to improve parsing speed. Hence track sizes can not be computed. Maybe not even all tracks could be "
247 "detected. Force a full parse to prevent this."),
248 context);
249 } else {
250 // abort if skipping pages didn't work
251 diag.emplace_back(DiagLevel::Critical,
252 "Unable to re-sync after skipping Ogg pages in the middle of the file. Try forcing a full parse.", context);
253 return;
254 }
255 }
256 }
257 } catch (const TruncatedDataException &) {
258 // thrown when page exceeds max size
259 diag.emplace_back(DiagLevel::Critical, "The Ogg file is truncated.", context);
260 } catch (const InvalidDataException &) {
261 // thrown when first 4 byte do not match capture pattern
262 const auto expectedOffset = m_iterator.currentSegmentOffset();
263 diag.emplace_back(DiagLevel::Critical, argsToString("Capture pattern \"OggS\" at ", expectedOffset, " expected."), context);
264 if (m_iterator.resyncAt(expectedOffset)) {
265 diag.emplace_back(DiagLevel::Warning,
266 argsToString("Found next capture pattern \"OggS\" at ", m_iterator.currentPageOffset(), ". Skipped ",
267 m_iterator.currentPageOffset() - expectedOffset, " invalid bytes."),
268 context);
269 continueFromHere = true;
270 } else {
271 diag.emplace_back(DiagLevel::Critical,
272 argsToString(
273 "Aborting after not being able to find any \"OggS\" capture patterns within 65307 bytes (from offset ", expectedOffset, ")."),
274 context);
275 }
276 }
277}
278
280{
281 // tracks needs to be parsed before because tags are stored at stream level
282 parseTracks(diag, progress);
283 auto flags = VorbisCommentFlags::None;
284 if (fileInfo().fileHandlingFlags() & MediaFileHandlingFlags::ConvertTotalFields) {
286 }
287 for (auto &comment : m_tags) {
288 OggParameter &params = comment->oggParams();
289 m_iterator.setPageIndex(params.firstPageIndex);
290 m_iterator.setSegmentIndex(params.firstSegmentIndex);
291 m_iterator.setFilter(m_iterator.currentPage().streamSerialNumber());
292 const auto startOffset = m_iterator.startOffset();
293 const auto context = argsToString("parsing tag in Ogg page at ", startOffset);
294 auto padding = std::uint64_t();
295 switch (params.streamFormat) {
297 comment->parse(m_iterator, flags, padding, diag);
298 break;
300 // skip header (has already been detected by OggStream)
301 m_iterator.ignore(8);
302 comment->parse(m_iterator, flags | VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, padding, diag);
303 break;
305 m_iterator.ignore(4);
306 comment->parse(m_iterator, flags | VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, padding, diag);
307 break;
308 default:
309 diag.emplace_back(DiagLevel::Critical, "Stream format not supported.", context);
310 }
311 params.lastPageIndex = m_iterator.currentPageIndex();
312 params.lastSegmentIndex = m_iterator.currentSegmentIndex();
313 fileInfo().reportPaddingSizeChanged(fileInfo().paddingSize() + padding);
314
315 // do a few sanity checks on the continued-flag and absolute granule position as some Ogg demuxers are picky about them
316 static constexpr auto noPacketsFinishOnPage = std::numeric_limits<std::uint64_t>::max();
317 if (params.firstPageIndex != params.lastPageIndex) {
318 const auto pageCount = params.lastPageIndex - params.firstPageIndex;
319 for (auto i = params.firstPageIndex; i < params.lastPageIndex; ++i) {
320 if (const auto &page = m_iterator.pages()[i]; page.absoluteGranulePosition() != noPacketsFinishOnPage) {
321 diag.emplace_back(DiagLevel::Warning,
322 argsToString("Tag spans over ", pageCount, " pages but absolute granule position of unfinished page at ", page.startOffset(),
323 " is not set to \"-1\" (it is ", page.absoluteGranulePosition(), ")."),
324 context);
325 }
326 }
327 for (auto i = params.firstPageIndex + 1; i <= params.lastPageIndex; ++i) {
328 if (const auto &page = m_iterator.pages()[i]; !page.isContinued()) {
329 diag.emplace_back(DiagLevel::Warning,
330 argsToString("The tag is continued in Ogg page at ", page.startOffset(), " but this page is not marked as continued packet."),
331 context);
332 }
333 }
334 }
335 if (const auto &page = m_iterator.pages()[params.lastPageIndex]; page.absoluteGranulePosition() == noPacketsFinishOnPage) {
336 diag.emplace_back(
337 DiagLevel::Warning, argsToString("Absolute granule position of final page at ", page.startOffset(), " is set to \"-1\"."), context);
338 }
339 }
340}
341
353void OggContainer::announceComment(std::size_t pageIndex, std::size_t segmentIndex, bool lastMetaDataBlock, GeneralMediaFormat mediaFormat)
354{
355 auto &tag = m_tags.emplace_back(make_unique<OggVorbisComment>());
356 tag->oggParams().set(pageIndex, segmentIndex, lastMetaDataBlock, mediaFormat);
357 tag->target().tracks().emplace_back(m_iterator.pages()[pageIndex].streamSerialNumber());
358}
359
361{
362 static const string context("parsing Ogg stream");
363 for (auto &stream : m_tracks) {
364 if (progress.isAborted()) {
366 }
367 try { // try to parse header
368 stream->parseHeader(diag, progress);
369 if (stream->duration() > m_duration) {
370 m_duration = stream->duration();
371 }
372 } catch (const Failure &) {
373 diag.emplace_back(DiagLevel::Critical, argsToString("Unable to parse stream at ", stream->startOffset(), '.'), context);
374 }
375 }
376}
377
382void OggContainer::makeVorbisCommentSegment(stringstream &buffer, CopyHelper<65307> &copyHelper, vector<std::uint32_t> &newSegmentSizes,
383 VorbisComment *comment, OggParameter *params, Diagnostics &diag)
384{
385 const auto offset = buffer.tellp();
386 switch (params->streamFormat) {
388 comment->make(buffer, VorbisCommentFlags::None, diag);
389 break;
391 BE::getBytes(static_cast<std::uint64_t>(0x4F70757354616773u), copyHelper.buffer());
392 buffer.write(copyHelper.buffer(), 8);
394 break;
396 // Vorbis comment must be wrapped in "METADATA_BLOCK_HEADER"
398 header.setLast(params->lastMetaDataBlock);
400
401 // write the header later, when the size is known
402 buffer.write(copyHelper.buffer(), 4);
403
405
406 // finally make the header
407 header.setDataSize(static_cast<std::uint32_t>(buffer.tellp() - offset - 4));
408 if (header.dataSize() > 0xFFFFFF) {
409 diag.emplace_back(
410 DiagLevel::Critical, "Size of Vorbis comment exceeds size limit for FLAC \"METADATA_BLOCK_HEADER\".", "making Vorbis Comment");
411 }
412 buffer.seekp(offset);
413 header.makeHeader(buffer);
414 buffer.seekp(header.dataSize(), ios_base::cur);
415 break;
416 }
417 default:;
418 }
419 MediaFileInfo::writePadding(buffer, fileInfo().preferredPadding());
420
421 newSegmentSizes.push_back(static_cast<std::uint32_t>(buffer.tellp() - offset));
422}
423
425{
426 const auto context = std::string("making Ogg file");
427 progress.nextStepOrStop("Prepare for rewriting Ogg file ...");
428 parseTags(diag, progress); // tags need to be parsed before the file can be rewritten
429 auto originalPath = fileInfo().path(), backupPath = std::string();
430 auto backupStream = NativeFileStream();
431
432 if (fileInfo().saveFilePath().empty()) {
433 // move current file to temp dir and reopen it as backupStream, recreate original file
434 try {
435 BackupHelper::createBackupFileCanonical(fileInfo().backupDirectory(), originalPath, backupPath, fileInfo().stream(), backupStream);
436 // recreate original file, define buffer variables
437 fileInfo().stream().open(originalPath, ios_base::out | ios_base::binary | ios_base::trunc);
438 } catch (const std::ios_base::failure &failure) {
439 diag.emplace_back(
440 DiagLevel::Critical, argsToString("Creation of temporary file (to rewrite the original file) failed: ", failure.what()), context);
441 throw;
442 }
443 } else {
444 // open the current file as backupStream and create a new outputStream at the specified "save file path"
445 try {
446 backupStream.exceptions(ios_base::badbit | ios_base::failbit);
447 backupStream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::binary);
448 fileInfo().close();
449 fileInfo().stream().open(
450 BasicFileInfo::pathForOpen(fileInfo().saveFilePath()).data(), ios_base::out | ios_base::binary | ios_base::trunc);
451 } catch (const std::ios_base::failure &failure) {
452 diag.emplace_back(DiagLevel::Critical, argsToString("Opening streams to write output file failed: ", failure.what()), context);
453 throw;
454 }
455 }
456
457 const auto totalFileSize = fileInfo().size();
458 try {
459 progress.nextStepOrStop("Writing Ogg pages ...");
460
461 // prepare iterating comments
462 OggVorbisComment *currentComment;
463 OggParameter *currentParams;
464 auto tagIterator = m_tags.cbegin(), tagEnd = m_tags.cend();
465 if (tagIterator != tagEnd) {
466 currentParams = &(currentComment = tagIterator->get())->oggParams();
467 } else {
468 currentComment = nullptr;
469 currentParams = nullptr;
470 }
471
472 // define misc variables
473 const OggPage *lastPage = nullptr;
474 static constexpr auto oggPageHeaderSize = 27u;
475 auto lastPageNewOffset = std::uint64_t();
476 auto copyHelper = CopyHelper<65307>();
477 auto updatedPageOffsets = std::vector<std::uint64_t>();
478 auto nextPageOffset = std::uint64_t();
479 auto pageSequenceNumberBySerialNo = std::unordered_map<std::uint32_t, std::uint32_t>();
480
481 // iterate through all pages of the original file
482 auto updateTick = 0u;
483 for (m_iterator.setStream(backupStream), m_iterator.removeFilter(), m_iterator.reset(); m_iterator; m_iterator.nextPage(), ++updateTick) {
484 const OggPage &currentPage = m_iterator.currentPage();
485 if (updateTick % 10) {
486 progress.updateStepPercentage(static_cast<std::uint8_t>(currentPage.startOffset() * 100ul / totalFileSize));
487 progress.stopIfAborted();
488 }
489
490 // check for gaps
491 // note: This is not just to print diag messages but also for taking into account that the parser might skip pages
492 // unless a full parse has been enforced.
493 if (lastPage && currentPage.startOffset() != nextPageOffset) {
494 m_iterator.pages().resize(m_iterator.currentPageIndex() - 1); // drop all further pages after the last consecutively parsed one
495 if (m_iterator.resyncAt(nextPageOffset)) {
496 // try again at the page we've just found
497 const auto actuallyNextPageOffset = m_iterator.currentPageOffset();
498 if (actuallyNextPageOffset != nextPageOffset) {
499 diag.emplace_back(DiagLevel::Critical,
500 argsToString("Expected Ogg page at offset ", nextPageOffset, " but found the next Ogg page only at offset ",
501 actuallyNextPageOffset, ". Skipped ", (actuallyNextPageOffset - nextPageOffset), " invalid bytes."),
502 context);
503 nextPageOffset = actuallyNextPageOffset;
504 }
505 m_iterator.previousPage();
506 continue;
507 } else {
508 diag.emplace_back(DiagLevel::Critical,
509 argsToString(
510 "Expected Ogg page at offset ", nextPageOffset, " but could not find any further pages. Skipped the rest of the file."),
511 context);
512 break;
513 }
514 }
515 const auto pageSize = currentPage.totalSize();
516 auto &pageSequenceNumber = pageSequenceNumberBySerialNo[currentPage.streamSerialNumber()];
517 lastPage = &currentPage;
518 lastPageNewOffset = static_cast<std::uint64_t>(stream().tellp());
519 nextPageOffset = currentPage.startOffset() + pageSize;
520
521 // check whether the Vorbis Comment is present in this Ogg page
522 if (currentComment && m_iterator.currentPageIndex() >= currentParams->firstPageIndex
523 && m_iterator.currentPageIndex() <= currentParams->lastPageIndex && !currentPage.segmentSizes().empty()) {
524 // page needs to be rewritten (not just copied)
525 // -> write segments to a buffer first
526 auto buffer = std::stringstream(std::ios_base::in | std::ios_base::out | std::ios_base::binary);
527 auto newSegmentSizes = std::vector<std::uint32_t>();
528 newSegmentSizes.reserve(currentPage.segmentSizes().size());
529 auto segmentOffset = m_iterator.currentSegmentOffset();
530 auto segmentIndex = std::vector<std::uint32_t>::size_type();
531 for (const auto segmentSize : currentPage.segmentSizes()) {
532 if (!segmentSize) {
533 ++segmentIndex;
534 continue;
535 }
536 // check whether this segment contains the Vorbis Comment
537 if (currentParams
538 && (m_iterator.currentPageIndex() >= currentParams->firstPageIndex && segmentIndex >= currentParams->firstSegmentIndex)
539 && (m_iterator.currentPageIndex() <= currentParams->lastPageIndex && segmentIndex <= currentParams->lastSegmentIndex)) {
540 // prevent making the comment twice if it spreads over multiple pages/segments
541 if (!currentParams->removed
542 && ((m_iterator.currentPageIndex() == currentParams->firstPageIndex
543 && m_iterator.currentSegmentIndex() == currentParams->firstSegmentIndex))) {
544 makeVorbisCommentSegment(buffer, copyHelper, newSegmentSizes, currentComment, currentParams, diag);
545 }
546
547 // proceed with next comment?
548 if (m_iterator.currentPageIndex() > currentParams->lastPageIndex
549 || (m_iterator.currentPageIndex() == currentParams->lastPageIndex && segmentIndex > currentParams->lastSegmentIndex)) {
550 if (++tagIterator != tagEnd) {
551 currentParams = &(currentComment = tagIterator->get())->oggParams();
552 } else {
553 currentComment = nullptr;
554 currentParams = nullptr;
555 }
556 }
557 } else {
558 // copy other segments unchanged
559 backupStream.seekg(static_cast<std::streamoff>(segmentOffset));
560 copyHelper.copy(backupStream, buffer, segmentSize);
561 newSegmentSizes.push_back(segmentSize);
562
563 // check whether there is a new comment to be inserted into the current page
564 if (currentParams && m_iterator.currentPageIndex() == currentParams->lastPageIndex
565 && currentParams->firstSegmentIndex == numeric_limits<size_t>::max()) {
566 if (!currentParams->removed) {
567 makeVorbisCommentSegment(buffer, copyHelper, newSegmentSizes, currentComment, currentParams, diag);
568 }
569 // proceed with next comment
570 if (++tagIterator != tagEnd) {
571 currentParams = &(currentComment = tagIterator->get())->oggParams();
572 } else {
573 currentComment = nullptr;
574 currentParams = nullptr;
575 }
576 }
577 }
578 segmentOffset += segmentSize;
579 ++segmentIndex;
580 }
581
582 // write buffered data to actual stream
583 if (auto newSegmentSizesIterator = newSegmentSizes.cbegin(), newSegmentSizesEnd = newSegmentSizes.cend();
584 newSegmentSizesIterator != newSegmentSizesEnd) {
585 auto bytesLeft = *newSegmentSizesIterator;
586 auto continuePreviousSegment = false, needsZeroLacingValue = false;
587 // write pages until all data in the buffer is written
588 while (newSegmentSizesIterator != newSegmentSizesEnd) {
589 // memorize offset to update checksum later
590 updatedPageOffsets.push_back(static_cast<std::uint64_t>(stream().tellp()));
591 // copy page header from original file (except for the segment table)
592 backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
593 copyHelper.copy(backupStream, stream(), oggPageHeaderSize);
594 // use flags of original page as base and adjust "continued packet"-flag as needed
595 auto flags = (currentPage.headerTypeFlag() & 0xFE) | (continuePreviousSegment ? 0x01 : 0x00);
596 continuePreviousSegment = true;
597 // ensure "first page of logical bitstream"-flag is cleared for additional pages we need to insert
598 // ensure "last page of logical bitstream"-flag is cleared for the first page
599 flags = flags & (newSegmentSizesIterator != newSegmentSizes.cbegin() ? 0xFD : 0xF);
600 // override flags copied from original file
601 stream().seekp(-22, ios_base::cur);
602 stream().put(static_cast<char>(flags));
603 // update absolute granule position later (8 byte) and keep stream serial number (4 byte)
604 stream().seekp(12, ios_base::cur);
605 // adjust page sequence number
606 writer().writeUInt32LE(pageSequenceNumber);
607 // skip checksum (4 bytes) and number of page segments (1 byte); those are update later
608 stream().seekp(5, ios_base::cur);
609 // write segment sizes as long as there are segment sizes to be written and
610 // the max number of segment sizes (255) is not exceeded
611 auto segmentSizesWritten = std::int16_t(); // in the current page header only
612 auto currentSize = std::uint32_t();
613 while ((bytesLeft || needsZeroLacingValue) && segmentSizesWritten < 0xFF) {
614 while (bytesLeft > 0xFF && segmentSizesWritten < 0xFF) {
615 stream().put(static_cast<char>(0xFF));
616 currentSize += 0xFF;
617 bytesLeft -= 0xFF;
618 ++segmentSizesWritten;
619 }
620 if ((bytesLeft || needsZeroLacingValue) && segmentSizesWritten < 0xFF) {
621 // bytes left is here <= 0xFF
622 stream().put(static_cast<char>(bytesLeft));
623 currentSize += bytesLeft;
624 needsZeroLacingValue = bytesLeft == 0xFF;
625 bytesLeft = 0;
626 ++segmentSizesWritten;
627 }
628 if (!bytesLeft && !needsZeroLacingValue) {
629 // sizes for the segment have been written
630 // -> continue with next segment
631 if (++newSegmentSizesIterator != newSegmentSizesEnd) {
632 bytesLeft = *newSegmentSizesIterator;
633 continuePreviousSegment = false;
634 }
635 }
636 }
637
638 // remove continue flag if there are no bytes left in the current segment
639 if (!bytesLeft && !needsZeroLacingValue) {
640 continuePreviousSegment = false;
641 }
642
643 // set the absolute granule postion
644 if (newSegmentSizesIterator != newSegmentSizesEnd) {
645 // set absolute granule position to special value "-1" if there are still bytes to be written in the current packet
646 stream().seekp(-21 - segmentSizesWritten, ios_base::cur);
647 writer().writeInt64LE(-1);
648 stream().seekp(12, ios_base::cur);
649 } else if (currentParams->lastPageIndex != currentParams->firstPageIndex) {
650 // ensure the written absolute granule position matches the one from the last page of the existing file
651 backupStream.seekg(static_cast<streamoff>(m_iterator.pages()[currentParams->lastPageIndex].startOffset() + 6));
652 stream().seekp(-21 - segmentSizesWritten, ios_base::cur);
653 copyHelper.copy(backupStream, stream(), 8);
654 stream().seekp(12, ios_base::cur);
655 } else {
656 // leave the absolute granule position unchanged
657 stream().seekp(-1 - segmentSizesWritten, ios_base::cur);
658 }
659
660 // page is full or all segment data has been covered
661 // -> write segment table size (segmentSizesWritten) and segment data
662 // -> seek back and write updated page segment number
663 stream().put(static_cast<char>(segmentSizesWritten));
664 stream().seekp(segmentSizesWritten, ios_base::cur);
665 // -> write actual page data
666 copyHelper.copy(buffer, stream(), currentSize);
667
668 ++pageSequenceNumber;
669 }
670 }
671
672 } else {
673 if (pageSequenceNumber != m_iterator.currentPageIndex()) {
674 // just update page sequence number
675 backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
676 updatedPageOffsets.push_back(static_cast<std::uint64_t>(stream().tellp())); // memorize offset to update checksum later
677 copyHelper.copy(backupStream, stream(), oggPageHeaderSize);
678 stream().seekp(-9, ios_base::cur);
679 writer().writeUInt32LE(pageSequenceNumber);
680 stream().seekp(5, ios_base::cur);
681 copyHelper.copy(backupStream, stream(), pageSize - oggPageHeaderSize);
682 } else {
683 // copy page unchanged
684 backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
685 copyHelper.copy(backupStream, stream(), pageSize);
686 }
687 ++pageSequenceNumber;
688 }
689 }
690
691 // report new size
692 fileInfo().reportSizeChanged(static_cast<std::uint64_t>(stream().tellp()));
693 progress.updateStepPercentage(100);
694
695 // "save as path" is now the regular path
696 if (!fileInfo().saveFilePath().empty()) {
697 fileInfo().reportPathChanged(fileInfo().saveFilePath());
698 fileInfo().setSaveFilePath(string());
699 }
700
701 // close backups stream; reopen new file as readable stream
702 auto &stream = fileInfo().stream();
703 backupStream.close();
704 fileInfo().close();
705 stream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::out | ios_base::binary);
706
707 // ensure the "last page of logical bitstream"-flag is set on the last Ogg page (in case the last page was written/modified by us)
708 if (lastPage && lastPageNewOffset) {
709 const auto offset = static_cast<std::streamoff>(lastPageNewOffset + 5ul);
710 stream.seekg(offset);
711 if (const auto flag = stream.get(); !(flag & 0x04)) {
712 updatedPageOffsets.emplace_back(lastPageNewOffset);
713 stream.seekp(offset);
714 stream.put(static_cast<char>(flag | 0x04));
715 }
716 }
717
718 // update checksums of modified pages
719 progress.nextStepOrStop("Updating checksums ...");
720 updateTick = 0u;
721 for (auto offset : updatedPageOffsets) {
722 if (updateTick++ % 10) {
723 progress.updateStepPercentage(static_cast<std::uint8_t>(offset * 100ul / fileInfo().size()));
724 progress.stopIfAborted();
725 }
727 }
728
729 // prevent deferring final write operations (to catch and handle possible errors here)
730 stream.flush();
731 progress.updateStepPercentage(100);
732
733 // clear iterator
734 m_iterator.clear(stream, startOffset(), fileInfo().size());
735
736 } catch (...) {
737 m_iterator.setStream(fileInfo().stream());
738 BackupHelper::handleFailureAfterFileModifiedCanonical(fileInfo(), originalPath, backupPath, fileInfo().stream(), backupStream, diag, context);
739 }
740}
741
742} // namespace TagParser
The AbortableProgressFeedback class provides feedback about an ongoing operation via callbacks.
bool isAborted() const
Returns whether the operation has been aborted via tryToAbort().
void stopIfAborted() const
Throws an OperationAbortedException if aborted.
void nextStepOrStop(const std::string &step, std::uint8_t stepPercentage=0)
Throws an OperationAbortedException if aborted; otherwise the data for the next step is set.
std::iostream & stream()
Returns the related stream.
std::uint64_t startOffset() const
Returns the start offset in the related stream.
void parseTracks(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the tracks of the file if not parsed yet.
CppUtilities::BinaryWriter & writer()
Returns the related BinaryWriter.
void parseTags(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the tag information if not parsed yet.
CppUtilities::TimeSpan m_duration
std::uint64_t id() const
Returns the track ID if known; otherwise returns 0.
MediaFormat format() const
Returns the format of the track if known; otherwise returns MediaFormat::Unknown.
void reportPathChanged(std::string_view newPath)
Call this function to report that the path changed.
const std::string & path() const
Returns the path of the current file.
std::uint64_t size() const
Returns size of the current file in bytes.
CppUtilities::NativeFileStream & stream()
Returns the std::fstream for the current instance.
void close()
A possibly opened std::fstream will be closed.
static std::string_view pathForOpen(std::string_view url)
Returns removes the "file:/" prefix from url to be able to pass it to functions like open(),...
void reportSizeChanged(std::uint64_t newSize)
Call this function to report that the size changed.
void updateStepPercentage(std::uint8_t stepPercentage)
Updates the current step percentage and invokes the second callback specified on construction (or the...
The Diagnostics class is a container for DiagMessage.
The class inherits from std::exception and serves as base class for exceptions thrown by the elements...
The FlacMetaDataBlockHeader class is a FLAC "METADATA_BLOCK_HEADER" parser and maker.
constexpr std::uint32_t dataSize() const
Returns the length in bytes of the meta data (excluding the size of the header itself).
void setType(FlacMetaDataBlockType type)
Sets the block type.
void setLast(std::uint8_t last)
Sets whether this is the last metadata block before the audio blocks.
void setDataSize(std::uint32_t dataSize)
Sets the length in bytes of the meta data (excluding the size of the header itself).
void makeHeader(std::ostream &outputStream)
Writes the header to the specified outputStream.
The GenericContainer class helps parsing header, track, tag and chapter information of a file.
The exception that is thrown when the data to be parsed or to be made seems invalid and therefore can...
The MediaFileInfo class allows to read and write tag information providing a container/tag format ind...
void setSaveFilePath(std::string_view saveFilePath)
Sets the "save file path".
static void writePadding(std::ostream &outputStream, uint64_t size)
Writes the specified number of zeroes to outputStream.
void reportPaddingSizeChanged(std::uint64_t newPaddingSize)
Sets the padding size.
GeneralMediaFormat general
void internalParseTags(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to parse the tags.
OggVorbisComment * tag(std::size_t index) override
Returns the tag with the specified index.
std::size_t tagCount() const override
Returns the number of tags attached to the container.
void internalParseTracks(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to parse the tracks.
OggContainer(MediaFileInfo &fileInfo, std::uint64_t startOffset)
Constructs a new container for the specified stream at the specified startOffset.
void internalParseHeader(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to parse the header.
void removeAllTags() override
Actually just flags all tags as removed and clears all assigned fields.
OggVorbisComment * createTag(const TagTarget &target) override
Creates a new tag.
void reset() override
Discards all parsing results.
void internalMakeFile(Diagnostics &diag, AbortableProgressFeedback &progress) override
Internally called to make the file.
bool removeTag(Tag *tag) override
Actually just flags the specified tag as removed and clears all assigned fields.
void nextPage()
Increases the current position by one page.
void setSegmentIndex(std::vector< std::uint32_t >::size_type index)
Sets the current segment index.
void removeFilter()
Removes a previously set filter.
void setStream(std::istream &stream)
Sets the stream.
std::vector< std::uint32_t >::size_type currentSegmentIndex() const
Returns the index of the current segment (in the current page) if the iterator is valid; otherwise an...
void clear(std::istream &stream, std::uint64_t startOffset, std::uint64_t streamSize)
Sets the stream and related parameters and clears all available pages.
void previousPage()
Decreases the current position by one page.
std::uint64_t currentSegmentOffset() const
Returns the start offset of the current segment in the input stream if the iterator is valid; otherwi...
std::uint64_t currentPageOffset() const
Returns the start offset of the current Ogg page.
const OggPage & currentPage() const
Returns the current Ogg page.
const std::vector< OggPage > & pages() const
Returns a vector of containing the Ogg pages that have been fetched yet.
bool resyncAt(std::uint64_t offset)
Fetches the next page at the specified offset.
std::uint64_t startOffset() const
Returns the start offset (which has been specified when constructing the iterator).
void setPageIndex(std::vector< OggPage >::size_type index)
Sets the current page index.
std::vector< OggPage >::size_type currentPageIndex() const
Returns the index of the current page if the iterator is valid; otherwise an undefined index is retur...
void reset()
Resets the iterator to point at the first segment of the first page (matching the filter if set).
void setFilter(std::uint32_t streamSerialId)
Allows to filter pages by the specified streamSerialId.
void ignore(std::size_t count=1)
Advances the position of the next character to be read from the Ogg stream by count bytes.
The OggPage class is used to parse Ogg pages.
std::uint32_t sequenceNumber() const
Returns the page sequence number.
Definition oggpage.h:185
std::uint32_t dataSize() const
Returns the data size in byte.
Definition oggpage.h:238
std::uint32_t totalSize() const
Returns the total size of the page in byte.
Definition oggpage.h:246
std::uint32_t streamSerialNumber() const
Returns the stream serial number.
Definition oggpage.h:166
const std::vector< std::uint32_t > & segmentSizes() const
Returns the sizes of the segments of the page in byte.
Definition oggpage.h:220
std::uint8_t headerTypeFlag() const
Returns the header type flag.
Definition oggpage.h:105
std::uint64_t startOffset() const
Returns the start offset of the page.
Definition oggpage.h:86
std::uint32_t checksum() const
Returns the page checksum.
Definition oggpage.h:200
static std::uint32_t computeChecksum(std::istream &stream, std::uint64_t startOffset)
Computes the actual checksum of the page read from the specified stream at the specified startOffset.
Definition oggpage.cpp:79
static void updateChecksum(std::iostream &stream, std::uint64_t startOffset)
Updates the checksum of the page read from the specified stream at the specified startOffset.
Definition oggpage.cpp:116
Implementation of TagParser::AbstractTrack for Ogg streams.
std::size_t startPage() const
Definition oggstream.h:34
Specialization of TagParser::VorbisComment for Vorbis comments inside an Ogg stream.
OggParameter & oggParams()
Returns the Ogg parameter for the comment.
std::string_view typeName() const override
Returns the type name of the tag as C-style string.
The exception that is thrown when an operation has been stopped and thus not successfully completed b...
The TagTarget class specifies the target of a tag.
const IdContainerType & tracks() const
Returns the tracks.
Definition tagtarget.h:105
The Tag class is used to store, read and write tag information.
const TagTarget & target() const
Definition tag.h:257
The exception that is thrown when the data to be parsed is truncated and therefore can not be parsed ...
Implementation of TagParser::Tag for Vorbis comments.
TAG_PARSER_EXPORT void handleFailureAfterFileModifiedCanonical(MediaFileInfo &fileInfo, const std::string &originalPath, const std::string &backupPath, CppUtilities::NativeFileStream &outputStream, CppUtilities::NativeFileStream &backupStream, Diagnostics &diag, const std::string &context="making file")
Handles a failure/abort which occurred after the file has been modified.
TAG_PARSER_EXPORT void createBackupFileCanonical(const std::string &backupDir, std::string &originalPath, std::string &backupPath, CppUtilities::NativeFileStream &originalStream, CppUtilities::NativeFileStream &backupStream)
Creates a backup file like createBackupFile() but canonicalizes originalPath before doing the backup.
Contains all classes and functions of the TagInfo library.
Definition aaccodebook.h:10
GeneralMediaFormat
The GeneralMediaFormat enum specifies the general format of media data (PCM, MPEG-4,...
Definition mediaformat.h:30
The OggParameter struct holds the Ogg parameter for a VorbisComment.
GeneralMediaFormat streamFormat
std::size_t lastSegmentIndex
std::size_t firstSegmentIndex
void set(std::size_t pageIndex, std::size_t segmentIndex, bool lastMetaDataBlock, GeneralMediaFormat streamFormat=GeneralMediaFormat::Vorbis)
Sets the firstPageIndex/lastPageIndex, the firstSegmentIndex/lastSegmentIndex, whether the associated...