mirror of
https://github.com/taglib/taglib.git
synced 2026-11-05 22:18:50 -05:00
Toolkit: Add DeferredWriteStream, which moves displaced bytes once (#1476)
An IOStream over another stream that records writes, insertions, removals and truncations in memory and applies them in one commit(), moving each displaced byte at most once. bytesToMove() reports what commit() would move, and writeTo() writes the result to another stream instead. Opt-in; no File subclass changes. See #1474.
This commit is contained in:
@@ -90,6 +90,7 @@ set(tag_HDRS
|
||||
toolkit/tbytevectorlist.h
|
||||
toolkit/tvariant.h
|
||||
toolkit/tbytevectorstream.h
|
||||
toolkit/tdeferredwritestream.h
|
||||
toolkit/tiostream.h
|
||||
toolkit/tfile.h
|
||||
toolkit/tfilestream.h
|
||||
@@ -511,6 +512,7 @@ set(toolkit_SRCS
|
||||
toolkit/tbytevectorlist.cpp
|
||||
toolkit/tvariant.cpp
|
||||
toolkit/tbytevectorstream.cpp
|
||||
toolkit/tdeferredwritestream.cpp
|
||||
toolkit/tiostream.cpp
|
||||
toolkit/tfile.cpp
|
||||
toolkit/tfilestream.cpp
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
/***************************************************************************
|
||||
copyright : (C) 2026 by Ryan Francesconi
|
||||
***************************************************************************/
|
||||
|
||||
/***************************************************************************
|
||||
* This library is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU Lesser General Public License version *
|
||||
* 2.1 as published by the Free Software Foundation. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, but *
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
* Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with this library; if not, write to the Free Software *
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
|
||||
* 02110-1301 USA *
|
||||
* *
|
||||
* Alternatively, this file is available under the Mozilla Public *
|
||||
* License Version 1.1. You may obtain a copy of the License at *
|
||||
* http://www.mozilla.org/MPL/ *
|
||||
***************************************************************************/
|
||||
|
||||
#include "tdeferredwritestream.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "tdebug.h"
|
||||
#include "tstring.h"
|
||||
|
||||
using namespace TagLib;
|
||||
|
||||
namespace
|
||||
{
|
||||
// The block size moved bytes are copied in when committing.
|
||||
constexpr offset_t moveBlockSize = 1024 * 1024;
|
||||
|
||||
// A run of the content: either bytes of the underlying stream starting at
|
||||
// source, or bytes held in data.
|
||||
struct Piece
|
||||
{
|
||||
bool isOriginal;
|
||||
offset_t source;
|
||||
ByteVector data;
|
||||
offset_t size;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
class DeferredWriteStream::DeferredWriteStreamPrivate
|
||||
{
|
||||
public:
|
||||
DeferredWriteStreamPrivate(IOStream *stream) :
|
||||
stream(stream)
|
||||
{
|
||||
}
|
||||
|
||||
IOStream *stream;
|
||||
// Pieces in content order; starts[i] is where pieces[i] begins.
|
||||
std::vector<Piece> pieces;
|
||||
std::vector<offset_t> starts;
|
||||
offset_t position { 0 };
|
||||
bool changed { false };
|
||||
|
||||
void reset()
|
||||
{
|
||||
pieces.clear();
|
||||
starts.clear();
|
||||
const offset_t length = stream->length();
|
||||
if(length > 0) {
|
||||
pieces.push_back({ true, 0, ByteVector(), length });
|
||||
starts.push_back(0);
|
||||
}
|
||||
changed = false;
|
||||
}
|
||||
|
||||
offset_t contentLength() const
|
||||
{
|
||||
return pieces.empty() ? 0 : starts.back() + pieces.back().size;
|
||||
}
|
||||
|
||||
void updateStarts(size_t from)
|
||||
{
|
||||
starts.resize(pieces.size());
|
||||
for(size_t i = from; i < pieces.size(); ++i)
|
||||
starts[i] = i == 0 ? 0 : starts[i - 1] + pieces[i - 1].size;
|
||||
}
|
||||
|
||||
// Ensures a piece boundary at offset, which must not exceed the content
|
||||
// length; returns the index of the piece that starts there.
|
||||
size_t split(offset_t offset)
|
||||
{
|
||||
const auto it = std::upper_bound(starts.begin(), starts.end(), offset);
|
||||
if(it == starts.begin())
|
||||
return 0;
|
||||
|
||||
const size_t index = static_cast<size_t>(it - starts.begin()) - 1;
|
||||
const offset_t inner = offset - starts[index];
|
||||
if(inner == 0)
|
||||
return index;
|
||||
if(inner >= pieces[index].size)
|
||||
return index + 1;
|
||||
|
||||
Piece &piece = pieces[index];
|
||||
Piece tail { piece.isOriginal, piece.source + inner, ByteVector(), piece.size - inner };
|
||||
if(!piece.isOriginal) {
|
||||
tail.data = piece.data.mid(static_cast<unsigned int>(inner));
|
||||
piece.data.resize(static_cast<unsigned int>(inner));
|
||||
}
|
||||
piece.size = inner;
|
||||
|
||||
pieces.insert(pieces.begin() + static_cast<std::ptrdiff_t>(index) + 1, tail);
|
||||
updateStarts(index + 1);
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
// Replaces the content in [start, start + length) with data; start may be
|
||||
// past the end, which is filled with zero bytes first.
|
||||
void replace(offset_t start, offset_t length, const ByteVector &data)
|
||||
{
|
||||
const offset_t total = contentLength();
|
||||
if(start > total) {
|
||||
replace(total, 0, ByteVector(static_cast<unsigned int>(start - total), '\0'));
|
||||
}
|
||||
|
||||
const offset_t end = std::min(start + length, contentLength());
|
||||
const size_t first = split(start);
|
||||
const size_t last = split(end);
|
||||
pieces.erase(pieces.begin() + static_cast<std::ptrdiff_t>(first),
|
||||
pieces.begin() + static_cast<std::ptrdiff_t>(last));
|
||||
|
||||
if(!data.isEmpty()) {
|
||||
pieces.insert(pieces.begin() + static_cast<std::ptrdiff_t>(first),
|
||||
{ false, 0, data, static_cast<offset_t>(data.size()) });
|
||||
}
|
||||
|
||||
updateStarts(first);
|
||||
merge(first);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Joins in-memory pieces around index, so repeated small writes do not
|
||||
// fragment the table.
|
||||
void merge(size_t index)
|
||||
{
|
||||
size_t from = index > 0 ? index - 1 : 0;
|
||||
while(from + 1 < pieces.size() && from <= index + 1) {
|
||||
Piece &a = pieces[from];
|
||||
Piece &b = pieces[from + 1];
|
||||
if(!a.isOriginal && !b.isOriginal) {
|
||||
a.data.append(b.data);
|
||||
a.size += b.size;
|
||||
pieces.erase(pieces.begin() + static_cast<std::ptrdiff_t>(from) + 1);
|
||||
}
|
||||
else if(a.isOriginal && b.isOriginal && a.source + a.size == b.source) {
|
||||
a.size += b.size;
|
||||
pieces.erase(pieces.begin() + static_cast<std::ptrdiff_t>(from) + 1);
|
||||
}
|
||||
else {
|
||||
++from;
|
||||
}
|
||||
}
|
||||
updateStarts(index > 0 ? index - 1 : 0);
|
||||
}
|
||||
|
||||
ByteVector read(offset_t offset, offset_t length)
|
||||
{
|
||||
ByteVector result;
|
||||
const offset_t end = std::min(offset + length, contentLength());
|
||||
if(offset >= end)
|
||||
return result;
|
||||
|
||||
const auto it = std::upper_bound(starts.begin(), starts.end(), offset);
|
||||
size_t index = static_cast<size_t>(it - starts.begin()) - 1;
|
||||
|
||||
while(offset < end && index < pieces.size()) {
|
||||
const Piece &piece = pieces[index];
|
||||
const offset_t inner = offset - starts[index];
|
||||
const offset_t count = std::min(piece.size - inner, end - offset);
|
||||
|
||||
if(piece.isOriginal) {
|
||||
stream->seek(piece.source + inner);
|
||||
result.append(stream->readBlock(static_cast<size_t>(count)));
|
||||
}
|
||||
else {
|
||||
result.append(piece.data.mid(static_cast<unsigned int>(inner), static_cast<unsigned int>(count)));
|
||||
}
|
||||
|
||||
offset += count;
|
||||
++index;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Copies count bytes of the underlying stream from source to destination,
|
||||
// in blocks ordered so that an overlapping range is read before it is
|
||||
// overwritten.
|
||||
void move(offset_t source, offset_t destination, offset_t count)
|
||||
{
|
||||
if(destination < source) {
|
||||
for(offset_t done = 0; done < count; done += moveBlockSize) {
|
||||
const offset_t block = std::min(moveBlockSize, count - done);
|
||||
stream->seek(source + done);
|
||||
const ByteVector bytes = stream->readBlock(static_cast<size_t>(block));
|
||||
stream->seek(destination + done);
|
||||
stream->writeBlock(bytes);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for(offset_t remaining = count; remaining > 0;) {
|
||||
const offset_t block = std::min(moveBlockSize, remaining);
|
||||
remaining -= block;
|
||||
stream->seek(source + remaining);
|
||||
const ByteVector bytes = stream->readBlock(static_cast<size_t>(block));
|
||||
stream->seek(destination + remaining);
|
||||
stream->writeBlock(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// public members
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
DeferredWriteStream::DeferredWriteStream(IOStream *stream) :
|
||||
d(std::make_unique<DeferredWriteStreamPrivate>(stream))
|
||||
{
|
||||
d->reset();
|
||||
}
|
||||
|
||||
DeferredWriteStream::~DeferredWriteStream() = default;
|
||||
|
||||
bool DeferredWriteStream::commit()
|
||||
{
|
||||
if(!d->changed)
|
||||
return true;
|
||||
|
||||
if(!isOpen() || readOnly()) {
|
||||
debug("DeferredWriteStream::commit() -- stream is not writable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Insertions and removals never reorder the underlying bytes, so original
|
||||
// pieces appear in source order. That is what makes moving them in place
|
||||
// safe: those moving towards the start are copied first, front to back,
|
||||
// then those moving towards the end, back to front.
|
||||
offset_t previousEnd = 0;
|
||||
for(const auto &piece : d->pieces) {
|
||||
if(!piece.isOriginal)
|
||||
continue;
|
||||
if(piece.source < previousEnd) {
|
||||
debug("DeferredWriteStream::commit() -- pieces out of order.");
|
||||
return false;
|
||||
}
|
||||
previousEnd = piece.source + piece.size;
|
||||
}
|
||||
|
||||
// Original pieces moving the same distance with only changed bytes between
|
||||
// them are moved as one run; the bytes carried along under the changes are
|
||||
// overwritten when the changes are written.
|
||||
struct Run { offset_t source; offset_t destination; offset_t size; };
|
||||
std::vector<Run> runs;
|
||||
for(size_t i = 0; i < d->pieces.size(); ++i) {
|
||||
const Piece &piece = d->pieces[i];
|
||||
if(!piece.isOriginal)
|
||||
continue;
|
||||
const offset_t destination = d->starts[i];
|
||||
if(!runs.empty()) {
|
||||
Run &last = runs.back();
|
||||
if(destination - piece.source == last.destination - last.source &&
|
||||
(i == 0 || !d->pieces[i - 1].isOriginal)) {
|
||||
last.size = piece.source + piece.size - last.source;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
runs.push_back({ piece.source, destination, piece.size });
|
||||
}
|
||||
|
||||
for(const auto &run : runs) {
|
||||
if(run.destination < run.source)
|
||||
d->move(run.source, run.destination, run.size);
|
||||
}
|
||||
|
||||
for(auto it = runs.rbegin(); it != runs.rend(); ++it) {
|
||||
if(it->destination > it->source)
|
||||
d->move(it->source, it->destination, it->size);
|
||||
}
|
||||
|
||||
for(size_t i = 0; i < d->pieces.size(); ++i) {
|
||||
const Piece &piece = d->pieces[i];
|
||||
if(!piece.isOriginal) {
|
||||
d->stream->seek(d->starts[i]);
|
||||
d->stream->writeBlock(piece.data);
|
||||
}
|
||||
}
|
||||
|
||||
const offset_t length = d->contentLength();
|
||||
if(d->stream->length() > length)
|
||||
d->stream->truncate(length);
|
||||
|
||||
d->reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeferredWriteStream::hasChanges() const
|
||||
{
|
||||
return d->changed;
|
||||
}
|
||||
|
||||
offset_t DeferredWriteStream::bytesToMove() const
|
||||
{
|
||||
offset_t total = 0;
|
||||
for(size_t i = 0; i < d->pieces.size(); ++i) {
|
||||
if(d->pieces[i].isOriginal && d->starts[i] != d->pieces[i].source)
|
||||
total += d->pieces[i].size;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
bool DeferredWriteStream::writeTo(IOStream *destination)
|
||||
{
|
||||
if(!destination->isOpen() || destination->readOnly()) {
|
||||
debug("DeferredWriteStream::writeTo() -- destination is not writable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const offset_t length = d->contentLength();
|
||||
destination->seek(0);
|
||||
for(offset_t done = 0; done < length; done += moveBlockSize) {
|
||||
destination->writeBlock(d->read(done, std::min(moveBlockSize, length - done)));
|
||||
}
|
||||
if(destination->length() > length)
|
||||
destination->truncate(length);
|
||||
return true;
|
||||
}
|
||||
|
||||
FileName DeferredWriteStream::name() const
|
||||
{
|
||||
return d->stream->name();
|
||||
}
|
||||
|
||||
ByteVector DeferredWriteStream::readBlock(size_t length)
|
||||
{
|
||||
ByteVector bytes = d->read(d->position, static_cast<offset_t>(length));
|
||||
d->position += bytes.size();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void DeferredWriteStream::writeBlock(const ByteVector &data)
|
||||
{
|
||||
d->replace(d->position, data.size(), data);
|
||||
d->position += data.size();
|
||||
}
|
||||
|
||||
void DeferredWriteStream::insert(const ByteVector &data, offset_t start, size_t replace)
|
||||
{
|
||||
d->replace(start, static_cast<offset_t>(replace), data);
|
||||
d->position = start + data.size();
|
||||
}
|
||||
|
||||
void DeferredWriteStream::removeBlock(offset_t start, size_t length)
|
||||
{
|
||||
d->replace(start, static_cast<offset_t>(length), ByteVector());
|
||||
d->position = d->contentLength();
|
||||
}
|
||||
|
||||
bool DeferredWriteStream::readOnly() const
|
||||
{
|
||||
return d->stream->readOnly();
|
||||
}
|
||||
|
||||
bool DeferredWriteStream::isOpen() const
|
||||
{
|
||||
return d->stream->isOpen();
|
||||
}
|
||||
|
||||
void DeferredWriteStream::seek(offset_t offset, Position p)
|
||||
{
|
||||
switch(p) {
|
||||
case Beginning:
|
||||
d->position = offset;
|
||||
break;
|
||||
case Current:
|
||||
d->position += offset;
|
||||
break;
|
||||
case End:
|
||||
d->position = d->contentLength() + offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DeferredWriteStream::clear()
|
||||
{
|
||||
d->stream->clear();
|
||||
}
|
||||
|
||||
offset_t DeferredWriteStream::tell() const
|
||||
{
|
||||
return d->position;
|
||||
}
|
||||
|
||||
offset_t DeferredWriteStream::length()
|
||||
{
|
||||
return d->contentLength();
|
||||
}
|
||||
|
||||
void DeferredWriteStream::truncate(offset_t length)
|
||||
{
|
||||
const offset_t total = d->contentLength();
|
||||
if(length < total)
|
||||
d->replace(length, total - length, ByteVector());
|
||||
else if(length > total)
|
||||
d->replace(total, 0, ByteVector(static_cast<unsigned int>(length - total), '\0'));
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/***************************************************************************
|
||||
copyright : (C) 2026 by Ryan Francesconi
|
||||
***************************************************************************/
|
||||
|
||||
/***************************************************************************
|
||||
* This library is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU Lesser General Public License version *
|
||||
* 2.1 as published by the Free Software Foundation. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, but *
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
* Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with this library; if not, write to the Free Software *
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
|
||||
* 02110-1301 USA *
|
||||
* *
|
||||
* Alternatively, this file is available under the Mozilla Public *
|
||||
* License Version 1.1. You may obtain a copy of the License at *
|
||||
* http://www.mozilla.org/MPL/ *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef TAGLIB_DEFERREDWRITESTREAM_H
|
||||
#define TAGLIB_DEFERREDWRITESTREAM_H
|
||||
|
||||
#include "tbytevector.h"
|
||||
#include "tiostream.h"
|
||||
#include "taglib_export.h"
|
||||
#include "taglib.h"
|
||||
|
||||
namespace TagLib {
|
||||
|
||||
//! A stream that keeps every change to another stream in memory until commit()
|
||||
|
||||
/*!
|
||||
* Writes, insertions, removals and truncations are recorded against the
|
||||
* underlying stream instead of applied to it, and reads see the result.
|
||||
* commit() then writes the final content in one pass: bytes that end up
|
||||
* where they started are not written at all, and bytes that move are moved
|
||||
* once, however many insertions and removals moved them.
|
||||
*
|
||||
* A save that changes several blocks ahead of the audio data, each of which
|
||||
* would move all of the audio when written to a FileStream, moves it at most
|
||||
* once through this stream.
|
||||
*
|
||||
* Only changed bytes are held in memory; unchanged ranges are read from the
|
||||
* underlying stream on demand. The underlying stream must not be changed by
|
||||
* anything else until commit() returns.
|
||||
*/
|
||||
class TAGLIB_EXPORT DeferredWriteStream : public IOStream
|
||||
{
|
||||
public:
|
||||
/*!
|
||||
* Construct a DeferredWriteStream over \a stream, which is not owned and
|
||||
* must outlive it.
|
||||
*/
|
||||
DeferredWriteStream(IOStream *stream);
|
||||
|
||||
/*!
|
||||
* Destroys this DeferredWriteStream instance. Changes not committed are
|
||||
* discarded.
|
||||
*/
|
||||
~DeferredWriteStream() override;
|
||||
|
||||
DeferredWriteStream(const DeferredWriteStream &) = delete;
|
||||
DeferredWriteStream &operator=(const DeferredWriteStream &) = delete;
|
||||
|
||||
/*!
|
||||
* Writes every recorded change to the underlying stream. Returns \c false
|
||||
* if the underlying stream is read only or not open, in which case nothing
|
||||
* is written. Afterwards the stream reads the underlying stream directly
|
||||
* again.
|
||||
*/
|
||||
bool commit();
|
||||
|
||||
/*!
|
||||
* Returns \c true if there are changes not yet committed.
|
||||
*/
|
||||
bool hasChanges() const;
|
||||
|
||||
/*!
|
||||
* Returns how many bytes of the underlying stream commit() would move to
|
||||
* a new position; 0 when it would only overwrite or append.
|
||||
*/
|
||||
offset_t bytesToMove() const;
|
||||
|
||||
/*!
|
||||
* Writes the content, changes included, to \a destination from its
|
||||
* start, truncating it to the content's length. The underlying stream and
|
||||
* the recorded changes are left as they are. Returns \c false if
|
||||
* \a destination is read only or not open.
|
||||
*/
|
||||
bool writeTo(IOStream *destination);
|
||||
|
||||
/*!
|
||||
* Returns the underlying stream's name.
|
||||
*/
|
||||
FileName name() const override;
|
||||
|
||||
/*!
|
||||
* Reads a block of size \a length at the current get pointer.
|
||||
*/
|
||||
ByteVector readBlock(size_t length) override;
|
||||
|
||||
/*!
|
||||
* Records \a data as written at the current get pointer.
|
||||
*/
|
||||
void writeBlock(const ByteVector &data) override;
|
||||
|
||||
/*!
|
||||
* Records \a data as inserted at position \a start, overwriting \a replace
|
||||
* bytes of the content.
|
||||
*/
|
||||
void insert(const ByteVector &data, offset_t start = 0, size_t replace = 0) override;
|
||||
|
||||
/*!
|
||||
* Records the removal of \a length bytes starting at \a start.
|
||||
*/
|
||||
void removeBlock(offset_t start = 0, size_t length = 0) override;
|
||||
|
||||
/*!
|
||||
* Returns the underlying stream's read only state.
|
||||
*/
|
||||
bool readOnly() const override;
|
||||
|
||||
/*!
|
||||
* Returns the underlying stream's open state.
|
||||
*/
|
||||
bool isOpen() const override;
|
||||
|
||||
/*!
|
||||
* Move the I/O pointer to \a offset in the stream from position \a p. This
|
||||
* defaults to seeking from the beginning of the stream.
|
||||
*
|
||||
* \see Position
|
||||
*/
|
||||
void seek(offset_t offset, Position p = Beginning) override;
|
||||
|
||||
/*!
|
||||
* Resets the underlying stream's end-of-stream and error flags.
|
||||
*/
|
||||
void clear() override;
|
||||
|
||||
/*!
|
||||
* Returns the current offset within the stream.
|
||||
*/
|
||||
offset_t tell() const override;
|
||||
|
||||
/*!
|
||||
* Returns the length of the stream, changes included.
|
||||
*/
|
||||
offset_t length() override;
|
||||
|
||||
/*!
|
||||
* Records the stream as truncated, or extended with zero bytes, to
|
||||
* \a length.
|
||||
*/
|
||||
void truncate(offset_t length) override;
|
||||
|
||||
private:
|
||||
class DeferredWriteStreamPrivate;
|
||||
TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE
|
||||
std::unique_ptr<DeferredWriteStreamPrivate> d;
|
||||
};
|
||||
|
||||
} // namespace TagLib
|
||||
|
||||
#endif
|
||||
@@ -81,6 +81,7 @@ SET(test_runner_SRCS
|
||||
test_bytevector.cpp
|
||||
test_bytevectorlist.cpp
|
||||
test_bytevectorstream.cpp
|
||||
test_deferredwritestream.cpp
|
||||
test_string.cpp
|
||||
test_propertymap.cpp
|
||||
test_variant.cpp
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
/***************************************************************************
|
||||
copyright : (C) 2026 by Ryan Francesconi
|
||||
***************************************************************************/
|
||||
|
||||
/***************************************************************************
|
||||
* This library is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU Lesser General Public License version *
|
||||
* 2.1 as published by the Free Software Foundation. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, but *
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
* Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with this library; if not, write to the Free Software *
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
|
||||
* 02110-1301 USA *
|
||||
* *
|
||||
* Alternatively, this file is available under the Mozilla Public *
|
||||
* License Version 1.1. You may obtain a copy of the License at *
|
||||
* http://www.mozilla.org/MPL/ *
|
||||
***************************************************************************/
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <string>
|
||||
|
||||
#include "tbytevectorstream.h"
|
||||
#include "tdeferredwritestream.h"
|
||||
#include "tfilestream.h"
|
||||
#include "mp4file.h"
|
||||
#include "mp4tag.h"
|
||||
#include "mp4chapter.h"
|
||||
#include "plainfile.h"
|
||||
#include <cppunit/extensions/HelperMacros.h>
|
||||
#include "utils.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace TagLib;
|
||||
|
||||
namespace
|
||||
{
|
||||
// Patterned rather than constant, so a moved range never reads back as unchanged.
|
||||
ByteVector pattern(unsigned int size)
|
||||
{
|
||||
ByteVector data(size);
|
||||
unsigned int state = 0x13579BDF;
|
||||
for(unsigned int i = 0; i < size; ++i) {
|
||||
state = state * 1664525 + 1013904223;
|
||||
data[i] = static_cast<char>(state >> 24);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
ByteVector wholeContent(IOStream &stream)
|
||||
{
|
||||
stream.seek(0);
|
||||
return stream.readBlock(static_cast<size_t>(stream.length()));
|
||||
}
|
||||
|
||||
class CountingStream : public ByteVectorStream
|
||||
{
|
||||
public:
|
||||
using ByteVectorStream::ByteVectorStream;
|
||||
long long written { 0 };
|
||||
|
||||
void writeBlock(const ByteVector &data) override
|
||||
{
|
||||
written += data.size();
|
||||
ByteVectorStream::writeBlock(data);
|
||||
}
|
||||
};
|
||||
|
||||
MP4::ChapterList chapters()
|
||||
{
|
||||
MP4::ChapterList list;
|
||||
for(int i = 0; i < 3; ++i)
|
||||
list.append(MP4::Chapter(String("Chapter " + to_string(i + 1)), i * 100LL));
|
||||
return list;
|
||||
}
|
||||
|
||||
void editMP4(MP4::File &f)
|
||||
{
|
||||
f.tag()->setTitle("A title long enough to grow the moov atom");
|
||||
f.setNeroChapters(chapters());
|
||||
f.setQtChapters(chapters());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class TestDeferredWriteStream : public CppUnit::TestFixture
|
||||
{
|
||||
CPPUNIT_TEST_SUITE(TestDeferredWriteStream);
|
||||
CPPUNIT_TEST(testRandomEditsMatchByteVectorStream);
|
||||
CPPUNIT_TEST(testCommitWritesOnlyWhatMoved);
|
||||
CPPUNIT_TEST(testGrowthMovesTheRestOnce);
|
||||
CPPUNIT_TEST(testBytesToMove);
|
||||
CPPUNIT_TEST(testReadOnlyCommitRefused);
|
||||
CPPUNIT_TEST(testMP4SaveMatchesFileStream);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
|
||||
// Replays seeded random writes, insertions, removals and truncations against a
|
||||
// DeferredWriteStream and a ByteVectorStream, comparing their content after each
|
||||
// operation, then compares writeTo() and commit() with the expected bytes.
|
||||
void testRandomEditsMatchByteVectorStream()
|
||||
{
|
||||
for(unsigned int seed = 0; seed < 200; ++seed) {
|
||||
ByteVectorStream underlying(pattern(20000));
|
||||
ByteVectorStream reference(pattern(20000));
|
||||
DeferredWriteStream deferred(&underlying);
|
||||
mt19937 random(seed);
|
||||
|
||||
auto below = [&random](long long bound) {
|
||||
return bound <= 0 ? 0LL : static_cast<long long>(random() % static_cast<unsigned long long>(bound));
|
||||
};
|
||||
auto bytes = [&random](long long count) {
|
||||
ByteVector data(static_cast<unsigned int>(count));
|
||||
for(unsigned int i = 0; i < data.size(); ++i)
|
||||
data[i] = static_cast<char>(random());
|
||||
return data;
|
||||
};
|
||||
|
||||
for(int operation = 0; operation < 24; ++operation) {
|
||||
const long long length = reference.length();
|
||||
|
||||
// Weighted so most of the original content survives to be moved by the commit.
|
||||
const auto kind = random() % 20;
|
||||
if(kind < 6) {
|
||||
const long long at = below(length + 16);
|
||||
const ByteVector data = bytes(1 + below(300));
|
||||
reference.seek(at);
|
||||
reference.writeBlock(data);
|
||||
deferred.seek(at);
|
||||
deferred.writeBlock(data);
|
||||
}
|
||||
else if(kind < 12) {
|
||||
const long long at = below(length + 1);
|
||||
const auto replace = static_cast<size_t>(below(min(length - at, 64LL) + 1));
|
||||
const ByteVector data = bytes(below(2000));
|
||||
reference.insert(data, at, replace);
|
||||
deferred.insert(data, at, replace);
|
||||
}
|
||||
else if(kind < 17) {
|
||||
const long long at = below(length + 1);
|
||||
const auto count = static_cast<size_t>(below(min(length - at, 2000LL) + 1));
|
||||
reference.removeBlock(at, count);
|
||||
deferred.removeBlock(at, count);
|
||||
}
|
||||
else if(kind < 18) {
|
||||
const long long to = max(0LL, length - 64 + below(128));
|
||||
reference.truncate(to);
|
||||
deferred.truncate(to);
|
||||
}
|
||||
else {
|
||||
const long long at = below(length + 1);
|
||||
const auto count = static_cast<size_t>(below(500));
|
||||
reference.seek(at);
|
||||
deferred.seek(at);
|
||||
CPPUNIT_ASSERT_EQUAL(reference.readBlock(count), deferred.readBlock(count));
|
||||
CPPUNIT_ASSERT_EQUAL(reference.tell(), deferred.tell());
|
||||
}
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(reference.length(), deferred.length());
|
||||
CPPUNIT_ASSERT_EQUAL(wholeContent(reference), wholeContent(deferred));
|
||||
}
|
||||
|
||||
ByteVectorStream copy(pattern(1));
|
||||
CPPUNIT_ASSERT(deferred.writeTo(©));
|
||||
CPPUNIT_ASSERT_EQUAL(*reference.data(), *copy.data());
|
||||
|
||||
CPPUNIT_ASSERT(deferred.commit());
|
||||
CPPUNIT_ASSERT_EQUAL(*reference.data(), *underlying.data());
|
||||
CPPUNIT_ASSERT_EQUAL(*reference.data(), wholeContent(deferred));
|
||||
}
|
||||
}
|
||||
|
||||
// Growing a block near the start and shrinking one further on moves only the bytes
|
||||
// between them; the rest of the stream keeps its place and is not written.
|
||||
void testCommitWritesOnlyWhatMoved()
|
||||
{
|
||||
CountingStream underlying(pattern(1 << 20));
|
||||
DeferredWriteStream deferred(&underlying);
|
||||
deferred.insert(ByteVector(16, 'x'), 100, 0);
|
||||
deferred.removeBlock(4116, 16);
|
||||
|
||||
CPPUNIT_ASSERT(deferred.commit());
|
||||
CPPUNIT_ASSERT_EQUAL(16LL + 4000LL, underlying.written);
|
||||
}
|
||||
|
||||
void testGrowthMovesTheRestOnce()
|
||||
{
|
||||
CountingStream underlying(pattern(1 << 20));
|
||||
DeferredWriteStream deferred(&underlying);
|
||||
deferred.insert(ByteVector(16, 'x'), 100, 0);
|
||||
|
||||
CPPUNIT_ASSERT(deferred.commit());
|
||||
CPPUNIT_ASSERT_EQUAL(16LL + (1LL << 20) - 100LL, underlying.written);
|
||||
}
|
||||
|
||||
// Overwriting and appending leave every original byte where it was; inserting or
|
||||
// removing at 100 moves the bytes after the edit.
|
||||
void testBytesToMove()
|
||||
{
|
||||
const ByteVector data(16, 'x');
|
||||
{
|
||||
ByteVectorStream underlying(pattern(1000));
|
||||
DeferredWriteStream deferred(&underlying);
|
||||
CPPUNIT_ASSERT(!deferred.hasChanges());
|
||||
deferred.seek(100);
|
||||
deferred.writeBlock(data);
|
||||
CPPUNIT_ASSERT(deferred.hasChanges());
|
||||
CPPUNIT_ASSERT_EQUAL(static_cast<offset_t>(0), deferred.bytesToMove());
|
||||
}
|
||||
{
|
||||
ByteVectorStream underlying(pattern(1000));
|
||||
DeferredWriteStream deferred(&underlying);
|
||||
deferred.seek(0, IOStream::End);
|
||||
deferred.writeBlock(data);
|
||||
CPPUNIT_ASSERT_EQUAL(static_cast<offset_t>(0), deferred.bytesToMove());
|
||||
}
|
||||
{
|
||||
ByteVectorStream underlying(pattern(1000));
|
||||
DeferredWriteStream deferred(&underlying);
|
||||
deferred.insert(data, 100, 0);
|
||||
CPPUNIT_ASSERT_EQUAL(static_cast<offset_t>(900), deferred.bytesToMove());
|
||||
}
|
||||
{
|
||||
ByteVectorStream underlying(pattern(1000));
|
||||
DeferredWriteStream deferred(&underlying);
|
||||
deferred.removeBlock(100, 16);
|
||||
CPPUNIT_ASSERT_EQUAL(static_cast<offset_t>(884), deferred.bytesToMove());
|
||||
}
|
||||
}
|
||||
|
||||
void testReadOnlyCommitRefused()
|
||||
{
|
||||
const ByteVector before = PlainFile(TEST_FILE_PATH_C("blank_video.m4v")).readAll();
|
||||
|
||||
FileStream file(TEST_FILE_PATH_C("blank_video.m4v"), true);
|
||||
DeferredWriteStream deferred(&file);
|
||||
deferred.insert(ByteVector("x", 1), 0, 0);
|
||||
|
||||
CPPUNIT_ASSERT(!deferred.commit());
|
||||
CPPUNIT_ASSERT_EQUAL(before, PlainFile(TEST_FILE_PATH_C("blank_video.m4v")).readAll());
|
||||
}
|
||||
|
||||
// A save of a title and both chapter lists on a file with moov ahead of mdat comes
|
||||
// out byte-identical through the deferring stream.
|
||||
void testMP4SaveMatchesFileStream()
|
||||
{
|
||||
const ByteVector original = PlainFile(TEST_FILE_PATH_C("blank_video.m4v")).readAll();
|
||||
|
||||
ByteVectorStream plain(original);
|
||||
{
|
||||
MP4::File f(&plain);
|
||||
CPPUNIT_ASSERT(f.isValid());
|
||||
editMP4(f);
|
||||
CPPUNIT_ASSERT(f.save());
|
||||
}
|
||||
|
||||
ScopedFileCopy copy("blank_video", ".m4v");
|
||||
offset_t moved = 0;
|
||||
{
|
||||
FileStream file(copy.fileName().c_str());
|
||||
DeferredWriteStream deferred(&file);
|
||||
MP4::File f(&deferred);
|
||||
CPPUNIT_ASSERT(f.isValid());
|
||||
editMP4(f);
|
||||
CPPUNIT_ASSERT(f.save());
|
||||
moved = deferred.bytesToMove();
|
||||
CPPUNIT_ASSERT(deferred.commit());
|
||||
}
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(*plain.data(), PlainFile(copy.fileName().c_str()).readAll());
|
||||
|
||||
// The grown moov displaces all of mdat (and the part of moov after the edit).
|
||||
const offset_t mdatSize = static_cast<offset_t>(original.size()) - (original.find("mdat") - 4);
|
||||
CPPUNIT_ASSERT(moved >= mdatSize);
|
||||
}
|
||||
};
|
||||
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(TestDeferredWriteStream);
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "tbytevector.h"
|
||||
#include "tbytevectorlist.h"
|
||||
#include "tbytevectorstream.h"
|
||||
#include "tdeferredwritestream.h"
|
||||
#include "tdebuglistener.h"
|
||||
#include "tfile.h"
|
||||
#include "tfilestream.h"
|
||||
@@ -179,6 +180,7 @@ public:
|
||||
CPPUNIT_ASSERT_EQUAL(classSize(0, false), sizeof(TagLib::ByteVector));
|
||||
CPPUNIT_ASSERT_EQUAL(classSize(2, false), sizeof(TagLib::ByteVectorList));
|
||||
CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::ByteVectorStream));
|
||||
CPPUNIT_ASSERT_EQUAL(classSize(1, true), sizeof(TagLib::DeferredWriteStream));
|
||||
CPPUNIT_ASSERT_EQUAL(classSize(0, true), sizeof(TagLib::DebugListener));
|
||||
CPPUNIT_ASSERT_EQUAL(classSize(0, true), sizeof(TagLib::File));
|
||||
CPPUNIT_ASSERT_EQUAL(classSize(1, false), sizeof(TagLib::FileRef));
|
||||
|
||||
Reference in New Issue
Block a user