First pass of implementation

This commit is contained in:
Adam Hathcock
2026-04-04 10:44:23 +01:00
parent ea6946c48d
commit 817bd2a95e
3 changed files with 1150 additions and 0 deletions

View File

@@ -0,0 +1,992 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.IO;
/// <summary>
/// MemoryStream implementation backed by pooled byte arrays.
/// </summary>
public sealed class PooledMemoryStream : MemoryStream
{
public const int DefaultBlockSize = 81920;
private const int MaxStreamLength = int.MaxValue;
private enum StorageMode
{
Segmented,
Contiguous,
}
private readonly ArrayPool<byte> _arrayPool;
private readonly int _blockSize;
private readonly List<byte[]> _detachedExposedBuffers = new();
private StorageMode _mode;
private List<byte[]>? _blocks;
private byte[]? _contiguousBuffer;
private bool _contiguousBufferExposed;
private bool _isOpen;
private bool _writable;
private bool _expandable;
private bool _exposable;
private int _origin;
private int _position;
private int _length;
private int _capacity;
private int _allocatedCapacity;
public PooledMemoryStream()
: this(0) { }
public PooledMemoryStream(int capacity)
: this(capacity, DefaultBlockSize, ArrayPool<byte>.Shared) { }
public PooledMemoryStream(int capacity, int blockSize)
: this(capacity, blockSize, ArrayPool<byte>.Shared) { }
public PooledMemoryStream(int capacity, int blockSize, ArrayPool<byte> arrayPool)
{
ThrowHelper.ThrowIfNull(arrayPool, nameof(arrayPool));
ThrowHelper.ThrowIfNegative(capacity, nameof(capacity));
ThrowHelper.ThrowIfNegativeOrZero(blockSize, nameof(blockSize));
_arrayPool = arrayPool;
_blockSize = blockSize;
_mode = StorageMode.Segmented;
_blocks = new List<byte[]>();
_isOpen = true;
_writable = true;
_expandable = true;
_exposable = true;
_origin = 0;
_position = 0;
_length = 0;
_capacity = capacity;
EnsureSegmentedAllocated(capacity);
}
public PooledMemoryStream(byte[] buffer)
: this(buffer, writable: true) { }
public PooledMemoryStream(byte[] buffer, bool writable)
: this(buffer, 0, buffer?.Length ?? 0, writable, publiclyVisible: false) { }
public PooledMemoryStream(byte[] buffer, int index, int count)
: this(buffer, index, count, writable: true, publiclyVisible: false) { }
public PooledMemoryStream(byte[] buffer, int index, int count, bool writable)
: this(buffer, index, count, writable, publiclyVisible: false) { }
public PooledMemoryStream(
byte[] buffer,
int index,
int count,
bool writable,
bool publiclyVisible
)
{
ThrowHelper.ThrowIfNull(buffer, nameof(buffer));
ThrowHelper.ThrowIfNegative(index, nameof(index));
ThrowHelper.ThrowIfNegative(count, nameof(count));
if (buffer.Length - index < count)
{
throw new ArgumentException("Offset and length are out of bounds.");
}
_arrayPool = ArrayPool<byte>.Shared;
_blockSize = DefaultBlockSize;
_mode = StorageMode.Segmented;
_blocks = new List<byte[]>();
_origin = 0;
_position = 0;
_length = count;
_capacity = count;
_writable = writable;
_expandable = false;
_exposable = publiclyVisible;
_isOpen = true;
EnsureSegmentedAllocated(_capacity);
if (count > 0)
{
CopyToSegmented(0, buffer, index, count);
}
}
public override bool CanRead => _isOpen;
public override bool CanSeek => _isOpen;
public override bool CanWrite => _writable;
public override long Length
{
get
{
EnsureNotClosed();
return _length - _origin;
}
}
public override long Position
{
get
{
EnsureNotClosed();
return _position - _origin;
}
set
{
ThrowHelper.ThrowIfNegative(value, nameof(value));
EnsureNotClosed();
ThrowHelper.ThrowIfGreaterThan(value, MaxStreamLength - _origin, nameof(value));
_position = _origin + (int)value;
}
}
public override int Capacity
{
get
{
EnsureNotClosed();
return _capacity - _origin;
}
set
{
if (value < Length)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
EnsureNotClosed();
if (!_expandable && value != Capacity)
{
throw new NotSupportedException("Memory stream is not expandable.");
}
var target = _origin + value;
if (target == _capacity)
{
return;
}
SetCapacityAbsolute(target);
}
}
public override void Flush() { }
public override Task FlushAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
return Task.CompletedTask;
}
public override long Seek(long offset, SeekOrigin loc)
{
EnsureNotClosed();
var anchor = loc switch
{
SeekOrigin.Begin => _origin,
SeekOrigin.Current => _position,
SeekOrigin.End => _length,
_ => throw new ArgumentException("Invalid seek origin.", nameof(loc)),
};
var target = anchor + offset;
if (target < _origin)
{
throw new IOException("Attempted to seek before the beginning of the stream.");
}
if (target > MaxStreamLength)
{
throw new ArgumentOutOfRangeException(nameof(offset));
}
_position = (int)target;
return _position - _origin;
}
public override void SetLength(long value)
{
ThrowHelper.ThrowIfNegative(value, nameof(value));
ThrowHelper.ThrowIfGreaterThan(value, MaxStreamLength, nameof(value));
EnsureWritable();
var newLength = _origin + (int)value;
if (newLength > _capacity)
{
EnsureCapacityForAppend(newLength);
}
if (newLength > _length)
{
ClearRange(_length, newLength - _length);
}
_length = newLength;
if (_position > newLength)
{
_position = newLength;
}
}
public override int Read(byte[] buffer, int offset, int count)
{
ValidateReadWriteBufferArguments(buffer, offset, count);
EnsureNotClosed();
var available = _length - _position;
if (available <= 0)
{
return 0;
}
if (count > available)
{
count = available;
}
switch (_mode)
{
case StorageMode.Contiguous:
Buffer.BlockCopy(_contiguousBuffer!, _position, buffer, offset, count);
break;
case StorageMode.Segmented:
CopyFromSegmented(_position, buffer, offset, count);
break;
}
_position += count;
return count;
}
public override int ReadByte()
{
EnsureNotClosed();
if (_position >= _length)
{
return -1;
}
byte value;
switch (_mode)
{
case StorageMode.Contiguous:
value = _contiguousBuffer![_position];
break;
default:
{
var blockIndex = _position / _blockSize;
var blockOffset = _position % _blockSize;
value = _blocks![blockIndex][blockOffset];
break;
}
}
_position++;
return value;
}
public override void Write(byte[] buffer, int offset, int count)
{
ValidateReadWriteBufferArguments(buffer, offset, count);
EnsureWritable();
if (count == 0)
{
return;
}
var endPosition = _position + count;
if (endPosition < 0)
{
throw new IOException("Stream is too long.");
}
if (endPosition > _capacity)
{
EnsureCapacityForAppend(endPosition);
}
if (_position > _length)
{
ClearRange(_length, _position - _length);
}
switch (_mode)
{
case StorageMode.Contiguous:
Buffer.BlockCopy(buffer, offset, _contiguousBuffer!, _position, count);
break;
case StorageMode.Segmented:
CopyToSegmented(_position, buffer, offset, count);
break;
}
_position = endPosition;
if (_position > _length)
{
_length = _position;
}
}
public override void WriteByte(byte value)
{
EnsureWritable();
var endPosition = _position + 1;
if (endPosition < 0)
{
throw new IOException("Stream is too long.");
}
if (endPosition > _capacity)
{
EnsureCapacityForAppend(endPosition);
}
if (_position > _length)
{
ClearRange(_length, _position - _length);
}
switch (_mode)
{
case StorageMode.Contiguous:
_contiguousBuffer![_position] = value;
break;
default:
{
var blockIndex = _position / _blockSize;
var blockOffset = _position % _blockSize;
_blocks![blockIndex][blockOffset] = value;
break;
}
}
_position = endPosition;
if (_position > _length)
{
_length = _position;
}
}
public override byte[] GetBuffer()
{
EnsureNotClosed();
if (!_exposable)
{
throw new UnauthorizedAccessException("Memory stream buffer is not publicly visible.");
}
EnsureContiguous();
_contiguousBufferExposed = true;
return _contiguousBuffer!;
}
public override bool TryGetBuffer(out ArraySegment<byte> buffer)
{
if (!_exposable)
{
buffer = default;
return false;
}
EnsureNotClosed();
EnsureContiguous();
_contiguousBufferExposed = true;
buffer = new ArraySegment<byte>(_contiguousBuffer!, 0, _length);
return true;
}
public override byte[] ToArray()
{
EnsureNotClosed();
var count = _length - _origin;
if (count == 0)
{
return Array.Empty<byte>();
}
var copy = new byte[count];
switch (_mode)
{
case StorageMode.Contiguous:
Buffer.BlockCopy(_contiguousBuffer!, _origin, copy, 0, count);
break;
case StorageMode.Segmented:
CopyFromSegmented(_origin, copy, 0, count);
break;
}
return copy;
}
public override void WriteTo(Stream stream)
{
ThrowHelper.ThrowIfNull(stream, nameof(stream));
EnsureNotClosed();
var count = _length - _origin;
if (count == 0)
{
return;
}
switch (_mode)
{
case StorageMode.Contiguous:
stream.Write(_contiguousBuffer!, _origin, count);
break;
case StorageMode.Segmented:
{
var position = _origin;
var remaining = count;
while (remaining > 0)
{
var blockIndex = position / _blockSize;
var blockOffset = position % _blockSize;
var toWrite = Math.Min(remaining, _blockSize - blockOffset);
stream.Write(_blocks![blockIndex], blockOffset, toWrite);
position += toWrite;
remaining -= toWrite;
}
break;
}
}
}
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<int>(cancellationToken);
}
try
{
return Task.FromResult(Read(buffer, offset, count));
}
catch (Exception ex)
{
return Task.FromException<int>(ex);
}
}
public override Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
try
{
Write(buffer, offset, count);
return Task.CompletedTask;
}
catch (Exception ex)
{
return Task.FromException(ex);
}
}
#if !LEGACY_DOTNET
public override int Read(Span<byte> buffer)
{
EnsureNotClosed();
var available = _length - _position;
if (available <= 0)
{
return 0;
}
var count = Math.Min(available, buffer.Length);
switch (_mode)
{
case StorageMode.Contiguous:
_contiguousBuffer.AsSpan(_position, count).CopyTo(buffer);
break;
case StorageMode.Segmented:
{
var sourcePosition = _position;
var destinationOffset = 0;
var remaining = count;
while (remaining > 0)
{
var blockIndex = sourcePosition / _blockSize;
var blockOffset = sourcePosition % _blockSize;
var toCopy = Math.Min(remaining, _blockSize - blockOffset);
_blocks!
[blockIndex]
.AsSpan(blockOffset, toCopy)
.CopyTo(buffer.Slice(destinationOffset, toCopy));
sourcePosition += toCopy;
destinationOffset += toCopy;
remaining -= toCopy;
}
break;
}
}
_position += count;
return count;
}
public override void Write(ReadOnlySpan<byte> buffer)
{
EnsureWritable();
if (buffer.Length == 0)
{
return;
}
var endPosition = _position + buffer.Length;
if (endPosition < 0)
{
throw new IOException("Stream is too long.");
}
if (endPosition > _capacity)
{
EnsureCapacityForAppend(endPosition);
}
if (_position > _length)
{
ClearRange(_length, _position - _length);
}
switch (_mode)
{
case StorageMode.Contiguous:
buffer.CopyTo(_contiguousBuffer.AsSpan(_position, buffer.Length));
break;
case StorageMode.Segmented:
{
var sourceOffset = 0;
var destinationPosition = _position;
var remaining = buffer.Length;
while (remaining > 0)
{
var blockIndex = destinationPosition / _blockSize;
var blockOffset = destinationPosition % _blockSize;
var toCopy = Math.Min(remaining, _blockSize - blockOffset);
buffer
.Slice(sourceOffset, toCopy)
.CopyTo(_blocks![blockIndex].AsSpan(blockOffset, toCopy));
sourceOffset += toCopy;
destinationPosition += toCopy;
remaining -= toCopy;
}
break;
}
}
_position = endPosition;
if (_position > _length)
{
_length = _position;
}
}
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (cancellationToken.IsCancellationRequested)
{
return ValueTask.FromCanceled<int>(cancellationToken);
}
try
{
return ValueTask.FromResult(Read(buffer.Span));
}
catch (Exception ex)
{
return ValueTask.FromException<int>(ex);
}
}
public override ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (cancellationToken.IsCancellationRequested)
{
return ValueTask.FromCanceled(cancellationToken);
}
try
{
Write(buffer.Span);
return ValueTask.CompletedTask;
}
catch (Exception ex)
{
return ValueTask.FromException(ex);
}
}
#endif
protected override void Dispose(bool disposing)
{
if (_isOpen)
{
_isOpen = false;
_writable = false;
_expandable = false;
if (disposing)
{
ReturnPooledBuffers();
}
}
base.Dispose(disposing);
}
private void EnsureNotClosed()
{
if (!_isOpen)
{
throw new ObjectDisposedException(nameof(PooledMemoryStream));
}
}
private void EnsureWritable()
{
EnsureNotClosed();
if (!_writable)
{
throw new NotSupportedException("Stream does not support writing.");
}
}
private void EnsureCapacityForAppend(int requiredLength)
{
if (requiredLength < 0)
{
throw new IOException("Stream is too long.");
}
if (requiredLength <= _capacity)
{
return;
}
if (!_expandable)
{
throw new NotSupportedException("Memory stream is not expandable.");
}
var nextCapacity = RoundUpToBlockBoundary(requiredLength);
SetCapacityAbsolute(nextCapacity);
}
private void SetCapacityAbsolute(int newCapacity)
{
ThrowHelper.ThrowIfLessThan(newCapacity, _length, nameof(newCapacity));
switch (_mode)
{
case StorageMode.Contiguous:
if (newCapacity > _allocatedCapacity)
{
DemoteContiguousToSegmented();
EnsureSegmentedAllocated(newCapacity);
}
break;
case StorageMode.Segmented:
EnsureSegmentedAllocated(newCapacity);
break;
}
_capacity = newCapacity;
if (_length > _capacity)
{
_length = _capacity;
}
if (_position > _capacity)
{
_position = _capacity;
}
}
private void EnsureContiguous()
{
if (_mode == StorageMode.Contiguous)
{
return;
}
var requested = Math.Max(_capacity, 1);
var contiguous = _arrayPool.Rent(requested);
if (_length > 0)
{
CopyFromSegmented(0, contiguous, 0, _length);
}
ReturnSegmentedBlocks();
_mode = StorageMode.Contiguous;
_contiguousBuffer = contiguous;
_contiguousBufferExposed = false;
_allocatedCapacity = contiguous.Length;
}
private void DemoteContiguousToSegmented()
{
var contiguous = _contiguousBuffer;
if (contiguous is null)
{
return;
}
var requiredCapacity = Math.Max(_capacity, _length);
_mode = StorageMode.Segmented;
_blocks = new List<byte[]>();
_contiguousBuffer = null;
EnsureSegmentedAllocated(requiredCapacity);
if (_length > 0)
{
CopyToSegmented(0, contiguous, 0, _length);
}
if (_contiguousBufferExposed)
{
_detachedExposedBuffers.Add(contiguous);
_contiguousBufferExposed = false;
}
else
{
_arrayPool.Return(contiguous);
}
}
private void EnsureSegmentedAllocated(int capacity)
{
if (_mode != StorageMode.Segmented)
{
throw new InvalidOperationException(
"Segmented allocation requested while not in segmented mode."
);
}
var requiredAllocated = RoundUpToBlockBoundary(capacity);
var requiredBlocks = requiredAllocated == 0 ? 0 : requiredAllocated / _blockSize;
_blocks ??= new List<byte[]>();
while (_blocks.Count < requiredBlocks)
{
_blocks.Add(_arrayPool.Rent(_blockSize));
}
while (_blocks.Count > requiredBlocks)
{
var index = _blocks.Count - 1;
var block = _blocks[index];
_blocks.RemoveAt(index);
_arrayPool.Return(block);
}
_allocatedCapacity = requiredAllocated;
}
private int RoundUpToBlockBoundary(int value)
{
if (value <= 0)
{
return 0;
}
var rounded = ((long)value + _blockSize - 1) / _blockSize * _blockSize;
if (rounded > MaxStreamLength)
{
throw new IOException("Stream is too long.");
}
return (int)rounded;
}
private void ClearRange(int absoluteStart, int count)
{
if (count <= 0)
{
return;
}
switch (_mode)
{
case StorageMode.Contiguous:
Array.Clear(_contiguousBuffer!, absoluteStart, count);
break;
case StorageMode.Segmented:
{
var position = absoluteStart;
var remaining = count;
while (remaining > 0)
{
var blockIndex = position / _blockSize;
var blockOffset = position % _blockSize;
var toClear = Math.Min(remaining, _blockSize - blockOffset);
Array.Clear(_blocks![blockIndex], blockOffset, toClear);
position += toClear;
remaining -= toClear;
}
break;
}
}
}
private void CopyFromSegmented(
int absoluteSourcePosition,
byte[] destination,
int offset,
int count
)
{
var sourcePosition = absoluteSourcePosition;
var destinationOffset = offset;
var remaining = count;
while (remaining > 0)
{
var blockIndex = sourcePosition / _blockSize;
var blockOffset = sourcePosition % _blockSize;
var toCopy = Math.Min(remaining, _blockSize - blockOffset);
Buffer.BlockCopy(
_blocks![blockIndex],
blockOffset,
destination,
destinationOffset,
toCopy
);
sourcePosition += toCopy;
destinationOffset += toCopy;
remaining -= toCopy;
}
}
private void CopyToSegmented(
int absoluteDestinationPosition,
byte[] source,
int offset,
int count
)
{
var sourceOffset = offset;
var destinationPosition = absoluteDestinationPosition;
var remaining = count;
while (remaining > 0)
{
var blockIndex = destinationPosition / _blockSize;
var blockOffset = destinationPosition % _blockSize;
var toCopy = Math.Min(remaining, _blockSize - blockOffset);
Buffer.BlockCopy(source, sourceOffset, _blocks![blockIndex], blockOffset, toCopy);
sourceOffset += toCopy;
destinationPosition += toCopy;
remaining -= toCopy;
}
}
private void ReturnSegmentedBlocks()
{
if (_blocks is null)
{
return;
}
for (var i = 0; i < _blocks.Count; i++)
{
_arrayPool.Return(_blocks[i]);
}
_blocks.Clear();
}
private void ReturnPooledBuffers()
{
if (_mode == StorageMode.Segmented)
{
ReturnSegmentedBlocks();
_blocks = null;
}
if (_mode == StorageMode.Contiguous && _contiguousBuffer is not null)
{
_arrayPool.Return(_contiguousBuffer);
_contiguousBuffer = null;
}
for (var i = 0; i < _detachedExposedBuffers.Count; i++)
{
_arrayPool.Return(_detachedExposedBuffers[i]);
}
_detachedExposedBuffers.Clear();
}
private static void ValidateReadWriteBufferArguments(byte[] buffer, int offset, int count)
{
ThrowHelper.ThrowIfNull(buffer, nameof(buffer));
ThrowHelper.ThrowIfNegative(offset, nameof(offset));
ThrowHelper.ThrowIfNegative(count, nameof(count));
if (buffer.Length - offset < count)
{
throw new ArgumentException("Offset and length are out of bounds.");
}
}
}

View File

@@ -60,6 +60,15 @@ internal static class ThrowHelper
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfGreaterThan(long value, long other, string? paramName = null)
{
if (value > other)
{
throw new ArgumentOutOfRangeException(paramName);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfGreaterThan(uint value, uint other, string? paramName = null)
{

View File

@@ -0,0 +1,149 @@
using System;
using System.Buffers;
using System.IO;
using System.Linq;
using SharpCompress.IO;
using Xunit;
namespace SharpCompress.Test.Streams;
public class PooledMemoryStreamTests
{
[Fact]
public void GrowsUsingFixedSizeBlocks()
{
var pool = new TrackingArrayPool();
using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8, arrayPool: pool);
stream.Write(new byte[20], 0, 20);
Assert.Equal(3, pool.RentRequests.Count);
Assert.All(pool.RentRequests, requested => Assert.Equal(8, requested));
}
[Fact]
public void DisposeReturnsRentedBlocksToPool()
{
var pool = new TrackingArrayPool();
var stream = new PooledMemoryStream(capacity: 0, blockSize: 8, arrayPool: pool);
stream.Write(new byte[17], 0, 17);
stream.Dispose();
Assert.Equal(pool.RentRequests.Count, pool.ReturnedLengths.Count);
Assert.All(pool.ReturnedLengths, length => Assert.Equal(8, length));
}
[Fact]
public void GetBufferPromotesToContiguousAndLaterGrowthUsesBlocks()
{
var pool = new TrackingArrayPool();
using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8, arrayPool: pool);
stream.Write(new byte[10], 0, 10);
var contiguous = stream.GetBuffer();
Assert.Equal(16, contiguous.Length);
Assert.Contains(16, pool.RentRequests);
stream.Position = stream.Length;
stream.Write(new byte[10], 0, 10); // grow to 20, forcing demotion + block growth
var totalBlockRents = pool.RentRequests.Count(size => size == 8);
Assert.True(totalBlockRents >= 3);
}
[Fact]
public void BufferConstructorCopiesDataAndIsNonExpandable()
{
var backing = Enumerable.Range(0, 10).Select(i => (byte)i).ToArray();
using var stream = new PooledMemoryStream(
backing,
2,
4,
writable: true,
publiclyVisible: true
);
Assert.Equal(4, stream.Length);
Assert.Equal(4, stream.Capacity);
backing[2] = 255;
Assert.Equal(new byte[] { 2, 3, 4, 5 }, stream.ToArray());
Assert.Throws<NotSupportedException>(() => stream.Capacity = 5);
}
[Fact]
public void BufferConstructorTryGetBufferRespectsVisibilityFlag()
{
var backing = new byte[10];
using var hidden = new PooledMemoryStream(
backing,
1,
5,
writable: true,
publiclyVisible: false
);
Assert.False(hidden.TryGetBuffer(out _));
using var visible = new PooledMemoryStream(
backing,
1,
5,
writable: true,
publiclyVisible: true
);
Assert.True(visible.TryGetBuffer(out var segment));
Assert.Equal(0, segment.Offset);
Assert.Equal(5, segment.Count);
}
[Fact]
public void SetLengthExtendingClearsGap()
{
using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8);
stream.Position = 5;
stream.WriteByte(42);
stream.Position = 0;
var data = stream.ToArray();
Assert.Equal(6, data.Length);
Assert.Equal(0, data[0]);
Assert.Equal(0, data[4]);
Assert.Equal(42, data[5]);
}
[Fact]
public void MethodsThrowAfterDispose()
{
using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8);
stream.WriteByte(1);
stream.Dispose();
Assert.Throws<ObjectDisposedException>(() => stream.ReadByte());
Assert.Throws<ObjectDisposedException>(() => stream.ToArray());
Assert.Throws<ObjectDisposedException>(() => stream.GetBuffer());
}
private sealed class TrackingArrayPool : ArrayPool<byte>
{
public readonly System.Collections.Generic.List<int> RentRequests = new();
public readonly System.Collections.Generic.List<int> ReturnedLengths = new();
public override byte[] Rent(int minimumLength)
{
RentRequests.Add(minimumLength);
return new byte[minimumLength];
}
public override void Return(byte[] array, bool clearArray = false)
{
ReturnedLengths.Add(array.Length);
if (clearArray)
{
Array.Clear(array, 0, array.Length);
}
}
}
}