tanya/source/tanya/container/buffer.d

691 lines
17 KiB
D

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This module contains buffers designed for C-style input/output APIs.
*
* Copyright: Eugene Wissner 2016-2017.
* License: $(LINK2 https://www.mozilla.org/en-US/MPL/2.0/,
* Mozilla Public License, v. 2.0).
* Authors: $(LINK2 mailto:info@caraus.de, Eugene Wissner)
*/
module tanya.container.buffer;
import std.traits;
import tanya.memory;
version (unittest)
{
private int fillBuffer(ubyte[] buffer,
in size_t size,
int start = 0,
int end = 10) @nogc pure nothrow
in
{
assert(start < end);
}
body
{
auto numberRead = end - start;
for (ubyte i; i < numberRead; ++i)
{
buffer[i] = cast(ubyte) (start + i);
}
return numberRead;
}
}
/**
* Self-expanding buffer, that can be used with functions returning the number
* of the read bytes.
*
* This buffer supports asynchronous reading. It means you can pass a new chunk
* to an asynchronous read function during you are working with already
* available data. But only one asynchronous call at a time is supported. Be
* sure to call $(D_PSYMBOL ReadBuffer.clear()) before you append the result
* of the pended asynchronous call.
*
* Params:
* T = Buffer type.
*/
struct ReadBuffer(T = ubyte)
if (isScalarType!T)
{
/// Internal buffer.
private T[] buffer_;
/// Filled buffer length.
private size_t length_;
/// Start of available data.
private size_t start;
/// Last position returned with $(D_KEYWORD []).
private size_t ring;
/// Available space.
private immutable size_t minAvailable = 1024;
/// Size by which the buffer will grow.
private immutable size_t blockSize = 8192;
invariant
{
assert(length_ <= buffer_.length);
assert(blockSize > 0);
assert(minAvailable > 0);
}
/**
* Creates a new read buffer.
*
* Params:
* size = Initial buffer size and the size by which the buffer
* will grow.
* minAvailable = minimal size should be always available to fill.
* So it will reallocate if $(D_INLINECODE
* $(D_PSYMBOL free) < $(D_PARAM minAvailable)).
* allocator = Allocator.
*/
this(in size_t size,
in size_t minAvailable = 1024,
shared Allocator allocator = defaultAllocator) @trusted
{
this(allocator);
this.minAvailable = minAvailable;
this.blockSize = size;
buffer_ = cast(T[]) allocator_.allocate(size * T.sizeof);
}
/// Ditto.
this(shared Allocator allocator)
in
{
assert(allocator_ is null);
}
body
{
allocator_ = allocator;
}
/**
* Deallocates the internal buffer.
*/
~this() @trusted
{
allocator.deallocate(buffer_);
}
///
unittest
{
ReadBuffer!ubyte b;
assert(b.capacity == 0);
assert(b.length == 0);
}
/**
* Returns: The size of the internal buffer.
*/
@property size_t capacity() const
{
return buffer_.length;
}
/**
* Returns: Data size.
*/
@property size_t length() const
{
return length_ - start;
}
/// Ditto.
alias opDollar = length;
/**
* Clears the buffer.
*
* Returns: $(D_KEYWORD this).
*/
void clear()
{
start = length_ = ring;
}
/**
* Returns: Available space.
*/
@property size_t free() const
{
return length > ring ? capacity - length : capacity - ring;
}
///
unittest
{
ReadBuffer!ubyte b;
size_t numberRead;
assert(b.free == 0);
// Fills the buffer with values 0..10
numberRead = fillBuffer(b[], b.free, 0, 10);
b += numberRead;
assert(b.free == b.blockSize - numberRead);
b.clear();
assert(b.free == b.blockSize);
}
/**
* Appends some data to the buffer.
*
* Params:
* length = Number of the bytes read.
*
* Returns: $(D_KEYWORD this).
*/
ref ReadBuffer opOpAssign(string op)(in size_t length)
if (op == "+")
{
length_ += length;
ring = start;
return this;
}
///
unittest
{
ReadBuffer!ubyte b;
size_t numberRead;
ubyte[] result;
// Fills the buffer with values 0..10
numberRead = fillBuffer(b[], b.free, 0, 10);
b += numberRead;
result = b[0 .. $];
assert(result[0] == 0);
assert(result[1] == 1);
assert(result[9] == 9);
b.clear();
// It shouldn't overwrite, but append another 5 bytes to the buffer
numberRead = fillBuffer(b[], b.free, 0, 10);
b += numberRead;
numberRead = fillBuffer(b[], b.free, 20, 25);
b += numberRead;
result = b[0..$];
assert(result[0] == 0);
assert(result[1] == 1);
assert(result[9] == 9);
assert(result[10] == 20);
assert(result[14] == 24);
}
/**
* Params:
* start = Start position.
* end = End position.
*
* Returns: Array between $(D_PARAM start) and $(D_PARAM end).
*/
T[] opSlice(in size_t start, in size_t end)
{
return buffer_[this.start + start .. this.start + end];
}
/**
* Returns a free chunk of the buffer.
*
* Add ($(D_KEYWORD +=)) the number of the read bytes after using it.
*
* Returns: A free chunk of the buffer.
*/
T[] opIndex()
{
if (start > 0)
{
auto ret = buffer_[0 .. start];
ring = 0;
return ret;
}
else
{
if (capacity - length < minAvailable)
{
void[] buf = buffer_;
immutable cap = capacity;
() @trusted {
allocator.reallocate(buf, (cap + blockSize) * T.sizeof);
buffer_ = cast(T[]) buf;
}();
}
ring = length_;
return buffer_[length_ .. $];
}
}
///
unittest
{
ReadBuffer!ubyte b;
size_t numberRead;
ubyte[] result;
// Fills the buffer with values 0..10
numberRead = fillBuffer(b[], b.free, 0, 10);
b += numberRead;
assert(b.length == 10);
result = b[0 .. $];
assert(result[0] == 0);
assert(result[9] == 9);
b.clear();
assert(b.length == 0);
}
mixin DefaultAllocator;
}
private unittest
{
static assert(is(ReadBuffer!int));
}
/**
* Circular, self-expanding buffer with overflow support. Can be used with
* functions returning the number of the transferred bytes.
*
* The buffer is optimized for situations where you read all the data from it
* at once (without writing to it occasionally). It can become ineffective if
* you permanently keep some data in the buffer and alternate writing and
* reading, because it may allocate and move elements.
*
* Params:
* T = Buffer type.
*/
struct WriteBuffer(T = ubyte)
if (isScalarType!T)
{
/// Internal buffer.
private T[] buffer_;
/// Buffer start position.
private size_t start;
/// Buffer ring area size. After this position begins buffer overflow area.
private size_t ring;
/// Size by which the buffer will grow.
private immutable size_t blockSize;
/// The position of the free area in the buffer.
private size_t position;
invariant
{
assert(blockSize > 0);
// Position can refer to an element outside the buffer if the buffer is full.
assert(position <= buffer_.length);
}
/**
* Params:
* size = Initial buffer size and the size by which the buffer will
* grow.
* allocator = Allocator.
*
* Precondition: $(D_INLINECODE size > 0 && allocator !is null)
*/
this(in size_t size, shared Allocator allocator = defaultAllocator) @trusted
in
{
assert(size > 0);
assert(allocator !is null);
}
body
{
blockSize = size;
ring = size - 1;
allocator_ = allocator;
buffer_ = cast(T[]) allocator_.allocate(size * T.sizeof);
}
@disable this();
/**
* Deallocates the internal buffer.
*/
~this()
{
allocator.deallocate(buffer_);
}
/**
* Returns: The size of the internal buffer.
*/
@property size_t capacity() const
{
return buffer_.length;
}
/**
* Note that $(D_PSYMBOL length) doesn't return the real length of the data,
* but only the array length that will be returned with $(D_PSYMBOL opIndex)
* next time. Be sure to call $(D_PSYMBOL opIndex) and set $(D_KEYWORD +=)
* until $(D_PSYMBOL length) returns 0.
*
* Returns: Data size.
*/
@property size_t length() const
{
if (position > ring || position < start) // Buffer overflowed
{
return ring - start + 1;
}
else
{
return position - start;
}
}
/// Ditto.
alias opDollar = length;
///
unittest
{
auto b = WriteBuffer!ubyte(4);
ubyte[3] buf = [48, 23, 255];
b ~= buf;
assert(b.length == 3);
b += 2;
assert(b.length == 1);
b ~= buf;
assert(b.length == 2);
b += 2;
assert(b.length == 2);
b ~= buf;
assert(b.length == 5);
b += b.length;
assert(b.length == 0);
}
/**
* Returns: Available space.
*/
@property size_t free() const
{
return capacity - length;
}
/**
* Appends data to the buffer.
*
* Params:
* buffer = Buffer chunk got with $(D_PSYMBOL opIndex).
*/
ref WriteBuffer opOpAssign(string op)(in T[] buffer)
if (op == "~")
{
size_t end, start;
if (position >= this.start && position <= ring)
{
auto afterRing = ring + 1;
end = position + buffer.length;
if (end > afterRing)
{
end = afterRing;
}
start = end - position;
buffer_[position .. end] = buffer[0 .. start];
if (end == afterRing)
{
position = this.start == 0 ? afterRing : 0;
}
else
{
position = end;
}
}
// Check if we have some free space at the beginning
if (start < buffer.length && position < this.start)
{
end = position + buffer.length - start;
if (end > this.start)
{
end = this.start;
}
auto areaEnd = end - position + start;
buffer_[position .. end] = buffer[start .. areaEnd];
position = end == this.start ? ring + 1 : end - position;
start = areaEnd;
}
// And if we still haven't found any place, save the rest in the overflow area
if (start < buffer.length)
{
end = position + buffer.length - start;
if (end > capacity)
{
auto newSize = (end / blockSize * blockSize + blockSize) * T.sizeof;
() @trusted {
void[] buf = buffer_;
allocator.reallocate(buf, newSize);
buffer_ = cast(T[]) buf;
}();
}
buffer_[position .. end] = buffer[start .. $];
position = end;
if (this.start == 0)
{
ring = capacity - 1;
}
}
return this;
}
///
unittest
{
auto b = WriteBuffer!ubyte(4);
ubyte[3] buf = [48, 23, 255];
b ~= buf;
assert(b.capacity == 4);
assert(b.buffer_[0] == 48 && b.buffer_[1] == 23 && b.buffer_[2] == 255);
b += 2;
b ~= buf;
assert(b.capacity == 4);
assert(b.buffer_[0] == 23 && b.buffer_[1] == 255
&& b.buffer_[2] == 255 && b.buffer_[3] == 48);
b += 2;
b ~= buf;
assert(b.capacity == 8);
assert(b.buffer_[0] == 23 && b.buffer_[1] == 255
&& b.buffer_[2] == 48 && b.buffer_[3] == 23 && b.buffer_[4] == 255);
}
///
unittest
{
auto b = WriteBuffer!ubyte(2);
ubyte[3] buf = [48, 23, 255];
b ~= buf;
assert(b.start == 0);
assert(b.capacity == 4);
assert(b.ring == 3);
assert(b.position == 3);
}
/**
* Sets how many bytes were written. It will shrink the buffer
* appropriately. Always call it after $(D_PSYMBOL opIndex).
*
* Params:
* length = Length of the written data.
*
* Returns: $(D_KEYWORD this).
*/
ref WriteBuffer opOpAssign(string op)(in size_t length)
if (op == "+")
in
{
assert(length <= this.length);
}
body
{
auto afterRing = ring + 1;
auto oldStart = start;
if (length <= 0)
{
return this;
}
else if (position <= afterRing)
{
start += length;
if (start > 0 && position == afterRing)
{
position = oldStart;
}
}
else
{
auto overflow = position - afterRing;
if (overflow > length)
{
immutable afterLength = afterRing + length;
buffer_[start .. start + length] = buffer_[afterRing .. afterLength];
buffer_[afterRing .. afterLength] = buffer_[afterLength .. position];
position -= length;
}
else if (overflow == length)
{
buffer_[start .. start + overflow] = buffer_[afterRing .. position];
position -= overflow;
}
else
{
buffer_[start .. start + overflow] = buffer_[afterRing .. position];
position = overflow;
}
start += length;
if (start == position)
{
if (position != afterRing)
{
position = 0;
}
start = 0;
ring = capacity - 1;
}
}
if (start > ring)
{
start = 0;
}
return this;
}
///
unittest
{
auto b = WriteBuffer!ubyte(6);
ubyte[6] buf = [23, 23, 255, 128, 127, 9];
b ~= buf;
assert(b.length == 6);
b += 2;
assert(b.length == 4);
b += 4;
assert(b.length == 0);
}
/**
* Returns a chunk with data.
*
* After calling it, set $(D_KEYWORD +=) to the length could be
* written.
*
* $(D_PSYMBOL opIndex) may return only part of the data. You may need
* to call it and set $(D_KEYWORD +=) several times until
* $(D_PSYMBOL length) is 0. If all the data can be written,
* maximally 3 calls are required.
*
* Returns: A chunk of data buffer.
*/
T[] opSlice(in size_t start, in size_t end)
{
immutable internStart = this.start + start;
if (position > ring || position < start) // Buffer overflowed
{
return buffer_[this.start .. ring + 1 - length + end];
}
else
{
return buffer_[this.start .. this.start + end];
}
}
///
unittest
{
auto b = WriteBuffer!ubyte(6);
ubyte[6] buf = [23, 23, 255, 128, 127, 9];
b ~= buf;
assert(b[0 .. $] == buf[0 .. 6]);
b += 2;
assert(b[0 .. $] == buf[2 .. 6]);
b ~= buf;
assert(b[0 .. $] == buf[2 .. 6]);
b += b.length;
assert(b[0 .. $] == buf[0 .. 6]);
b += b.length;
}
/**
* After calling it, set $(D_KEYWORD +=) to the length could be
* written.
*
* $(D_PSYMBOL opIndex) may return only part of the data. You may need
* to call it and set $(D_KEYWORD +=) several times until
* $(D_PSYMBOL length) is 0. If all the data can be written,
* maximally 3 calls are required.
*
* Returns: A chunk of data buffer.
*/
T[] opIndex()
{
return opSlice(0, length);
}
mixin DefaultAllocator;
}
private unittest
{
static assert(is(typeof(WriteBuffer!int(5))));
}