Tag Parser 12.2.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 for (auto padding = fileInfo().preferredPadding(); padding; --padding) {
420 buffer.put(0);
421 }
422 newSegmentSizes.push_back(static_cast<std::uint32_t>(buffer.tellp() - offset));
423}
424
426{
427 const auto context = std::string("making Ogg file");
428 progress.nextStepOrStop("Prepare for rewriting Ogg file ...");
429 parseTags(diag, progress); // tags need to be parsed before the file can be rewritten
430 auto originalPath = fileInfo().path(), backupPath = std::string();
431 auto backupStream = NativeFileStream();
432
433 if (fileInfo().saveFilePath().empty()) {
434 // move current file to temp dir and reopen it as backupStream, recreate original file
435 try {
436 BackupHelper::createBackupFileCanonical(fileInfo().backupDirectory(), originalPath, backupPath, fileInfo().stream(), backupStream);
437 // recreate original file, define buffer variables
438 fileInfo().stream().open(originalPath, ios_base::out | ios_base::binary | ios_base::trunc);
439 } catch (const std::ios_base::failure &failure) {
440 diag.emplace_back(
441 DiagLevel::Critical, argsToString("Creation of temporary file (to rewrite the original file) failed: ", failure.what()), context);
442 throw;
443 }
444 } else {
445 // open the current file as backupStream and create a new outputStream at the specified "save file path"
446 try {
447 backupStream.exceptions(ios_base::badbit | ios_base::failbit);
448 backupStream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::binary);
449 fileInfo().close();
450 fileInfo().stream().open(
451 BasicFileInfo::pathForOpen(fileInfo().saveFilePath()).data(), ios_base::out | ios_base::binary | ios_base::trunc);
452 } catch (const std::ios_base::failure &failure) {
453 diag.emplace_back(DiagLevel::Critical, argsToString("Opening streams to write output file failed: ", failure.what()), context);
454 throw;
455 }
456 }
457
458 const auto totalFileSize = fileInfo().size();
459 try {
460 progress.nextStepOrStop("Writing Ogg pages ...");
461
462 // prepare iterating comments
463 OggVorbisComment *currentComment;
464 OggParameter *currentParams;
465 auto tagIterator = m_tags.cbegin(), tagEnd = m_tags.cend();
466 if (tagIterator != tagEnd) {
467 currentParams = &(currentComment = tagIterator->get())->oggParams();
468 } else {
469 currentComment = nullptr;
470 currentParams = nullptr;
471 }
472
473 // define misc variables
474 const OggPage *lastPage = nullptr;
475 static constexpr auto oggPageHeaderSize = 27u;
476 auto lastPageNewOffset = std::uint64_t();
477 auto copyHelper = CopyHelper<65307>();
478 auto updatedPageOffsets = std::vector<std::uint64_t>();
479 auto nextPageOffset = std::uint64_t();
480 auto pageSequenceNumberBySerialNo = std::unordered_map<std::uint32_t, std::uint32_t>();
481
482 // iterate through all pages of the original file
483 auto updateTick = 0u;
484 for (m_iterator.setStream(backupStream), m_iterator.removeFilter(), m_iterator.reset(); m_iterator; m_iterator.nextPage(), ++updateTick) {
485 const OggPage &currentPage = m_iterator.currentPage();
486 if (updateTick % 10) {
487 progress.updateStepPercentage(static_cast<std::uint8_t>(currentPage.startOffset() * 100ul / totalFileSize));
488 progress.stopIfAborted();
489 }
490
491 // check for gaps
492 // note: This is not just to print diag messages but also for taking into account that the parser might skip pages
493 // unless a full parse has been enforced.
494 if (lastPage && currentPage.startOffset() != nextPageOffset) {
495 m_iterator.pages().resize(m_iterator.currentPageIndex() - 1); // drop all further pages after the last consecutively parsed one
496 if (m_iterator.resyncAt(nextPageOffset)) {
497 // try again at the page we've just found
498 const auto actuallyNextPageOffset = m_iterator.currentPageOffset();
499 if (actuallyNextPageOffset != nextPageOffset) {
500 diag.emplace_back(DiagLevel::Critical,
501 argsToString("Expected Ogg page at offset ", nextPageOffset, " but found the next Ogg page only at offset ",
502 actuallyNextPageOffset, ". Skipped ", (actuallyNextPageOffset - nextPageOffset), " invalid bytes."),
503 context);
504 nextPageOffset = actuallyNextPageOffset;
505 }
506 m_iterator.previousPage();
507 continue;
508 } else {
509 diag.emplace_back(DiagLevel::Critical,
510 argsToString(
511 "Expected Ogg page at offset ", nextPageOffset, " but could not find any further pages. Skipped the rest of the file."),
512 context);
513 break;
514 }
515 }
516 const auto pageSize = currentPage.totalSize();
517 auto &pageSequenceNumber = pageSequenceNumberBySerialNo[currentPage.streamSerialNumber()];
518 lastPage = &currentPage;
519 lastPageNewOffset = static_cast<std::uint64_t>(stream().tellp());
520 nextPageOffset = currentPage.startOffset() + pageSize;
521
522 // check whether the Vorbis Comment is present in this Ogg page
523 if (currentComment && m_iterator.currentPageIndex() >= currentParams->firstPageIndex
524 && m_iterator.currentPageIndex() <= currentParams->lastPageIndex && !currentPage.segmentSizes().empty()) {
525 // page needs to be rewritten (not just copied)
526 // -> write segments to a buffer first
527 auto buffer = std::stringstream(std::ios_base::in | std::ios_base::out | std::ios_base::binary);
528 auto newSegmentSizes = std::vector<std::uint32_t>();
529 newSegmentSizes.reserve(currentPage.segmentSizes().size());
530 auto segmentOffset = m_iterator.currentSegmentOffset();
531 auto segmentIndex = std::vector<std::uint32_t>::size_type();
532 for (const auto segmentSize : currentPage.segmentSizes()) {
533 if (!segmentSize) {
534 ++segmentIndex;
535 continue;
536 }
537 // check whether this segment contains the Vorbis Comment
538 if (currentParams
539 && (m_iterator.currentPageIndex() >= currentParams->firstPageIndex && segmentIndex >= currentParams->firstSegmentIndex)
540 && (m_iterator.currentPageIndex() <= currentParams->lastPageIndex && segmentIndex <= currentParams->lastSegmentIndex)) {
541 // prevent making the comment twice if it spreads over multiple pages/segments
542 if (!currentParams->removed
543 && ((m_iterator.currentPageIndex() == currentParams->firstPageIndex
544 && m_iterator.currentSegmentIndex() == currentParams->firstSegmentIndex))) {
545 makeVorbisCommentSegment(buffer, copyHelper, newSegmentSizes, currentComment, currentParams, diag);
546 }
547
548 // proceed with next comment?
549 if (m_iterator.currentPageIndex() > currentParams->lastPageIndex
550 || (m_iterator.currentPageIndex() == currentParams->lastPageIndex && segmentIndex > currentParams->lastSegmentIndex)) {
551 if (++tagIterator != tagEnd) {
552 currentParams = &(currentComment = tagIterator->get())->oggParams();
553 } else {
554 currentComment = nullptr;
555 currentParams = nullptr;
556 }
557 }
558 } else {
559 // copy other segments unchanged
560 backupStream.seekg(static_cast<std::streamoff>(segmentOffset));
561 copyHelper.copy(backupStream, buffer, segmentSize);
562 newSegmentSizes.push_back(segmentSize);
563
564 // check whether there is a new comment to be inserted into the current page
565 if (currentParams && m_iterator.currentPageIndex() == currentParams->lastPageIndex
566 && currentParams->firstSegmentIndex == numeric_limits<size_t>::max()) {
567 if (!currentParams->removed) {
568 makeVorbisCommentSegment(buffer, copyHelper, newSegmentSizes, currentComment, currentParams, diag);
569 }
570 // proceed with next comment
571 if (++tagIterator != tagEnd) {
572 currentParams = &(currentComment = tagIterator->get())->oggParams();
573 } else {
574 currentComment = nullptr;
575 currentParams = nullptr;
576 }
577 }
578 }
579 segmentOffset += segmentSize;
580 ++segmentIndex;
581 }
582
583 // write buffered data to actual stream
584 if (auto newSegmentSizesIterator = newSegmentSizes.cbegin(), newSegmentSizesEnd = newSegmentSizes.cend();
585 newSegmentSizesIterator != newSegmentSizesEnd) {
586 auto bytesLeft = *newSegmentSizesIterator;
587 auto continuePreviousSegment = false, needsZeroLacingValue = false;
588 // write pages until all data in the buffer is written
589 while (newSegmentSizesIterator != newSegmentSizesEnd) {
590 // memorize offset to update checksum later
591 updatedPageOffsets.push_back(static_cast<std::uint64_t>(stream().tellp()));
592 // copy page header from original file (except for the segment table)
593 backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
594 copyHelper.copy(backupStream, stream(), oggPageHeaderSize);
595 // use flags of original page as base and adjust "continued packet"-flag as needed
596 auto flags = (currentPage.headerTypeFlag() & 0xFE) | (continuePreviousSegment ? 0x01 : 0x00);
597 continuePreviousSegment = true;
598 // ensure "first page of logical bitstream"-flag is cleared for additional pages we need to insert
599 // ensure "last page of logical bitstream"-flag is cleared for the first page
600 flags = flags & (newSegmentSizesIterator != newSegmentSizes.cbegin() ? 0xFD : 0xF);
601 // override flags copied from original file
602 stream().seekp(-22, ios_base::cur);
603 stream().put(static_cast<char>(flags));
604 // update absolute granule position later (8 byte) and keep stream serial number (4 byte)
605 stream().seekp(12, ios_base::cur);
606 // adjust page sequence number
607 writer().writeUInt32LE(pageSequenceNumber);
608 // skip checksum (4 bytes) and number of page segments (1 byte); those are update later
609 stream().seekp(5, ios_base::cur);
610 // write segment sizes as long as there are segment sizes to be written and
611 // the max number of segment sizes (255) is not exceeded
612 auto segmentSizesWritten = std::int16_t(); // in the current page header only
613 auto currentSize = std::uint32_t();
614 while ((bytesLeft || needsZeroLacingValue) && segmentSizesWritten < 0xFF) {
615 while (bytesLeft > 0xFF && segmentSizesWritten < 0xFF) {
616 stream().put(static_cast<char>(0xFF));
617 currentSize += 0xFF;
618 bytesLeft -= 0xFF;
619 ++segmentSizesWritten;
620 }
621 if ((bytesLeft || needsZeroLacingValue) && segmentSizesWritten < 0xFF) {
622 // bytes left is here <= 0xFF
623 stream().put(static_cast<char>(bytesLeft));
624 currentSize += bytesLeft;
625 needsZeroLacingValue = bytesLeft == 0xFF;
626 bytesLeft = 0;
627 ++segmentSizesWritten;
628 }
629 if (!bytesLeft && !needsZeroLacingValue) {
630 // sizes for the segment have been written
631 // -> continue with next segment
632 if (++newSegmentSizesIterator != newSegmentSizesEnd) {
633 bytesLeft = *newSegmentSizesIterator;
634 continuePreviousSegment = false;
635 }
636 }
637 }
638
639 // remove continue flag if there are no bytes left in the current segment
640 if (!bytesLeft && !needsZeroLacingValue) {
641 continuePreviousSegment = false;
642 }
643
644 // set the absolute granule postion
645 if (newSegmentSizesIterator != newSegmentSizesEnd) {
646 // set absolute granule position to special value "-1" if there are still bytes to be written in the current packet
647 stream().seekp(-21 - segmentSizesWritten, ios_base::cur);
648 writer().writeInt64LE(-1);
649 stream().seekp(12, ios_base::cur);
650 } else if (currentParams->lastPageIndex != currentParams->firstPageIndex) {
651 // ensure the written absolute granule position matches the one from the last page of the existing file
652 backupStream.seekg(static_cast<streamoff>(m_iterator.pages()[currentParams->lastPageIndex].startOffset() + 6));
653 stream().seekp(-21 - segmentSizesWritten, ios_base::cur);
654 copyHelper.copy(backupStream, stream(), 8);
655 stream().seekp(12, ios_base::cur);
656 } else {
657 // leave the absolute granule position unchanged
658 stream().seekp(-1 - segmentSizesWritten, ios_base::cur);
659 }
660
661 // page is full or all segment data has been covered
662 // -> write segment table size (segmentSizesWritten) and segment data
663 // -> seek back and write updated page segment number
664 stream().put(static_cast<char>(segmentSizesWritten));
665 stream().seekp(segmentSizesWritten, ios_base::cur);
666 // -> write actual page data
667 copyHelper.copy(buffer, stream(), currentSize);
668
669 ++pageSequenceNumber;
670 }
671 }
672
673 } else {
674 if (pageSequenceNumber != m_iterator.currentPageIndex()) {
675 // just update page sequence number
676 backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
677 updatedPageOffsets.push_back(static_cast<std::uint64_t>(stream().tellp())); // memorize offset to update checksum later
678 copyHelper.copy(backupStream, stream(), oggPageHeaderSize);
679 stream().seekp(-9, ios_base::cur);
680 writer().writeUInt32LE(pageSequenceNumber);
681 stream().seekp(5, ios_base::cur);
682 copyHelper.copy(backupStream, stream(), pageSize - oggPageHeaderSize);
683 } else {
684 // copy page unchanged
685 backupStream.seekg(static_cast<streamoff>(currentPage.startOffset()));
686 copyHelper.copy(backupStream, stream(), pageSize);
687 }
688 ++pageSequenceNumber;
689 }
690 }
691
692 // report new size
693 fileInfo().reportSizeChanged(static_cast<std::uint64_t>(stream().tellp()));
694 progress.updateStepPercentage(100);
695
696 // "save as path" is now the regular path
697 if (!fileInfo().saveFilePath().empty()) {
698 fileInfo().reportPathChanged(fileInfo().saveFilePath());
699 fileInfo().setSaveFilePath(string());
700 }
701
702 // close backups stream; reopen new file as readable stream
703 auto &stream = fileInfo().stream();
704 backupStream.close();
705 fileInfo().close();
706 stream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::out | ios_base::binary);
707
708 // 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)
709 if (lastPage && lastPageNewOffset) {
710 const auto offset = static_cast<std::streamoff>(lastPageNewOffset + 5ul);
711 stream.seekg(offset);
712 if (const auto flag = stream.get(); !(flag & 0x04)) {
713 updatedPageOffsets.emplace_back(lastPageNewOffset);
714 stream.seekp(offset);
715 stream.put(static_cast<char>(flag | 0x04));
716 }
717 }
718
719 // update checksums of modified pages
720 progress.nextStepOrStop("Updating checksums ...");
721 updateTick = 0u;
722 for (auto offset : updatedPageOffsets) {
723 if (updateTick++ % 10) {
724 progress.updateStepPercentage(static_cast<std::uint8_t>(offset * 100ul / fileInfo().size()));
725 progress.stopIfAborted();
726 }
728 }
729
730 // prevent deferring final write operations (to catch and handle possible errors here)
731 stream.flush();
732 progress.updateStepPercentage(100);
733
734 // clear iterator
735 m_iterator.clear(stream, startOffset(), fileInfo().size());
736
737 } catch (...) {
738 m_iterator.setStream(fileInfo().stream());
739 BackupHelper::handleFailureAfterFileModifiedCanonical(fileInfo(), originalPath, backupPath, fileInfo().stream(), backupStream, diag, context);
740 }
741}
742
743} // 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".
bool isForcingFullParse() const
Returns an indication whether forcing a full parse is enabled.
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:245
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...