From 817bd2a95e0355e912e6a256d2ce8e3166cc51cc Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 4 Apr 2026 10:44:23 +0100 Subject: [PATCH] First pass of implementation --- src/SharpCompress/IO/PooledMemoryStream.cs | 992 ++++++++++++++++++ src/SharpCompress/ThrowHelper.cs | 9 + .../Streams/PooledMemoryStreamTests.cs | 149 +++ 3 files changed, 1150 insertions(+) create mode 100644 src/SharpCompress/IO/PooledMemoryStream.cs create mode 100644 tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs new file mode 100644 index 00000000..58bc748f --- /dev/null +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -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; + +/// +/// MemoryStream implementation backed by pooled byte arrays. +/// +public sealed class PooledMemoryStream : MemoryStream +{ + public const int DefaultBlockSize = 81920; + + private const int MaxStreamLength = int.MaxValue; + + private enum StorageMode + { + Segmented, + Contiguous, + } + + private readonly ArrayPool _arrayPool; + private readonly int _blockSize; + + private readonly List _detachedExposedBuffers = new(); + + private StorageMode _mode; + private List? _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.Shared) { } + + public PooledMemoryStream(int capacity, int blockSize) + : this(capacity, blockSize, ArrayPool.Shared) { } + + public PooledMemoryStream(int capacity, int blockSize, ArrayPool 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(); + _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.Shared; + _blockSize = DefaultBlockSize; + + _mode = StorageMode.Segmented; + _blocks = new List(); + _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 buffer) + { + if (!_exposable) + { + buffer = default; + return false; + } + + EnsureNotClosed(); + + EnsureContiguous(); + _contiguousBufferExposed = true; + buffer = new ArraySegment(_contiguousBuffer!, 0, _length); + return true; + } + + public override byte[] ToArray() + { + EnsureNotClosed(); + + var count = _length - _origin; + if (count == 0) + { + return Array.Empty(); + } + + 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 ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception ex) + { + return Task.FromException(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 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 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 ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + try + { + return ValueTask.FromResult(Read(buffer.Span)); + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } + } + + public override ValueTask WriteAsync( + ReadOnlyMemory 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(); + _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(); + + 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."); + } + } +} diff --git a/src/SharpCompress/ThrowHelper.cs b/src/SharpCompress/ThrowHelper.cs index 9d598107..c202d87c 100644 --- a/src/SharpCompress/ThrowHelper.cs +++ b/src/SharpCompress/ThrowHelper.cs @@ -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) { diff --git a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs new file mode 100644 index 00000000..28af7f1c --- /dev/null +++ b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs @@ -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(() => 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(() => stream.ReadByte()); + Assert.Throws(() => stream.ToArray()); + Assert.Throws(() => stream.GetBuffer()); + } + + private sealed class TrackingArrayPool : ArrayPool + { + public readonly System.Collections.Generic.List RentRequests = new(); + public readonly System.Collections.Generic.List 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); + } + } + } +}