using System; using System.Buffers; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using SharpCompress.Compressors; using SharpCompress.Compressors.Deflate; using SharpCompress.IO; using SharpCompress.Readers; using SharpCompress.Test.Mocks; using Xunit; namespace SharpCompress.Test.Streams; public class DeflateReadAllocationTests { [Theory] [InlineData(false)] [InlineData(true)] public void Bit_Length_Trees_Keep_Empty_And_Oversubscribed_Validation(bool oversubscribed) { var lengths = new int[19]; if (oversubscribed) { lengths[0] = lengths[1] = lengths[2] = 1; } var depth = 7; var offset = 0; var codec = new ZlibCodec(); var result = new InfTree().inflate_trees_bits( lengths, ref depth, ref offset, new int[8192], codec ); Assert.Equal(ZlibConstants.Z_DATA_ERROR, result); Assert.Equal( oversubscribed ? "oversubscribed dynamic bit lengths tree" : "incomplete dynamic bit lengths tree", codec.Message ); if (!oversubscribed) { Assert.Equal(0, depth); Assert.Equal(-1, offset); } } [Theory] [InlineData(false)] [InlineData(true)] public async Task Fixed_And_Stored_Blocks_Keep_ReadAhead(bool useAsync) { // Non-final fixed empty block, then final stored block containing 1,2,3. byte[] packed = [0x02, 0x04, 0x03, 0, 0xFC, 0xFF, 1, 2, 3, 99, 98]; using var input = new MemoryStream(packed); using var decoder = new SharpCompress.Compressors.Deflate.DeflateStream( input, CompressionMode.Decompress, CompressionLevel.Default, leaveOpen: true ); var bytes = new byte[1]; using var actual = new MemoryStream(); int read; while ( ( read = useAsync ? await decoder.ReadAsync(bytes, 0, 1, CancellationToken.None) : decoder.Read(bytes, 0, 1) ) != 0 ) { actual.Write(bytes, 0, read); } Assert.Equal(new byte[] { 1, 2, 3 }, actual.ToArray()); using var remaining = decoder.InputBuffer; Assert.Equal(new byte[] { 99, 98 }, remaining.ToArray()); } #if !LEGACY_DOTNET [Fact] public async Task NonSeekable_Reader_Does_Not_Allocate_Compressed_Payload_Copies() { var expected = new byte[2 * 1024 * 1024]; new Random(1234).NextBytes(expected); using var packed = new MemoryStream(); using ( var zip = new System.IO.Compression.ZipArchive( packed, System.IO.Compression.ZipArchiveMode.Create, leaveOpen: true ) ) { using var entry = zip.CreateEntry("random.bin").Open(); entry.Write(expected); } packed.Position = 0; using var input = new ForwardOnlyStream(packed); await using var reader = await ReaderFactory.OpenAsyncReader( input, ReaderOptions.ForExternalStream, CancellationToken.None ); Assert.True(await reader.MoveToNextEntryAsync(CancellationToken.None)); await using var decoded = await reader.OpenEntryStreamAsync(CancellationToken.None); var buffer = new byte[32768]; var total = await decoded.ReadAsync(buffer.AsMemory(), CancellationToken.None); Assert.True(expected.AsSpan(0, total).SequenceEqual(buffer.AsSpan(0, total))); var start = GC.GetAllocatedBytesForCurrentThread(); int count; while ((count = await decoded.ReadAsync(buffer.AsMemory(), CancellationToken.None)) != 0) { Assert.True(expected.AsSpan(total, count).SequenceEqual(buffer.AsSpan(0, count))); total += count; } var allocated = GC.GetAllocatedBytesForCurrentThread() - start; Assert.Equal(expected.Length, total); // Memory-backed I/O completes synchronously, keeping this counter on one thread. // A temporary array per compressed refill would allocate about 2 MiB. Assert.InRange(allocated, 0, 131072); Assert.True(input.CanRead); } [Theory] [InlineData(false)] [InlineData(true)] public async Task RingBuffered_Memory_Read_Preserves_Slices_Across_Replay_And_Fresh_Input( bool nonArray ) { var expected = Enumerable.Range(0, 64).Select(i => (byte)i).ToArray(); using var input = new ForwardOnlyStream(new MemoryStream(expected)); using var stream = SharpCompressStream.Create(input, 16); var bytes = Enumerable.Repeat((byte)0xCC, 22).ToArray(); using var owner = new NonArrayMemory(bytes); var memory = (nonArray ? owner.Memory : bytes.AsMemory()).Slice(5, 12); Assert.Equal(12, await stream.ReadAsync(memory, CancellationToken.None)); Assert.Equal(12, await stream.ReadAsync(memory, CancellationToken.None)); // The ring has wrapped. Combine six replayed bytes with six fresh bytes. stream.Position = 18; Assert.Equal(12, await stream.ReadAsync(memory, CancellationToken.None)); Assert.Equal(expected.Skip(18).Take(12), bytes.Skip(5).Take(12)); Assert.Equal(30, stream.Position); // Replay the fresh bytes too, verifying the source offset used when recording. stream.Position = 24; Assert.Equal(12, await stream.ReadAsync(memory, CancellationToken.None)); Assert.Equal(expected.Skip(24).Take(12), bytes.Skip(5).Take(12)); Assert.Equal(36, stream.Position); Assert.All(bytes.Take(5), b => Assert.Equal(0xCC, b)); Assert.All(bytes.Skip(17), b => Assert.Equal(0xCC, b)); } [Theory] [InlineData(false)] [InlineData(true)] public async Task RingBuffer_Fresh_Input_Does_Not_Allocate_Per_Read(bool nonArray) { const int length = 1024 * 1024; using var input = new ForwardOnlyStream(new MemoryStream(new byte[length])); using var stream = SharpCompressStream.Create(input, 8192); var buffer = new byte[4106]; using var owner = new NonArrayMemory(buffer); var memory = (nonArray ? owner.Memory : buffer.AsMemory()).Slice(5, 4096); var total = await stream.ReadAsync(memory, CancellationToken.None); var start = GC.GetAllocatedBytesForCurrentThread(); int count; while ((count = await stream.ReadAsync(memory, CancellationToken.None)) != 0) { total += count; } var allocated = GC.GetAllocatedBytesForCurrentThread() - start; Assert.Equal(length, total); Assert.InRange(allocated, 0, 65536); } [Theory] [InlineData(false)] [InlineData(true)] public async Task RingBuffer_Replay_Does_Not_Allocate_Per_Read(bool nonArray) { using var input = new ForwardOnlyStream(new MemoryStream(new byte[32])); using var stream = SharpCompressStream.Create(input, 16); var buffer = new byte[22]; using var owner = new NonArrayMemory(buffer); var memory = (nonArray ? owner.Memory : buffer.AsMemory()).Slice(5, 12); await stream.ReadAsync(memory, CancellationToken.None); stream.Position = 0; await stream.ReadAsync(memory, CancellationToken.None); var start = GC.GetAllocatedBytesForCurrentThread(); var total = 0; for (var i = 0; i < 10000; i++) { stream.Position = 0; total += await stream.ReadAsync(memory, CancellationToken.None); } var allocated = GC.GetAllocatedBytesForCurrentThread() - start; Assert.Equal(120000, total); Assert.InRange(allocated, 0, 65536); } [Fact] public async Task NonArray_Fallback_Reuses_Its_Rental_Across_Many_Reads() { const int length = 1024 * 1024; using var input = new MemoryInput(Compress(new byte[length]), false); using var decoder = CreateDecoder(input); using var owner = new NonArrayMemory(new byte[64]); var memory = owner.Memory; var total = await decoder.ReadAsync(memory, CancellationToken.None); var start = GC.GetAllocatedBytesForCurrentThread(); int count; while ((count = await decoder.ReadAsync(memory, CancellationToken.None)) != 0) { total += count; } var allocated = GC.GetAllocatedBytesForCurrentThread() - start; Assert.Equal(length, total); // A lost 64-byte rental per read would allocate over a megabyte here. // Leave ample room for incidental initialization rather than asserting zero. Assert.InRange(allocated, 0, 65536); Assert.Null(decoder._z.OutputBuffer); } [Fact] public void Generated_Sync_Read_Uses_The_Existing_Array_Overload() { var expected = new byte[4096]; new Random(1234).NextBytes(expected); using var source = new ArrayOnlyInput(Compress(expected)); using var decoder = CreateDecoder(source); using var output = new MemoryStream(); decoder.CopyTo(output, 8192); Assert.Equal(expected, output.ToArray()); Assert.True(source.ArrayReads > 0); Assert.Null(decoder._z.OutputBuffer); } private sealed class ArrayOnlyInput(byte[] bytes) : MemoryStream(bytes, writable: false) { public int ArrayReads { get; private set; } public override int Read(byte[] buffer, int offset, int count) { ArrayReads++; return base.Read(buffer, offset, count); } public override int Read(Span buffer) => throw new NotSupportedException("Sync reads must retain array semantics."); } [Theory] [InlineData(false, false)] [InlineData(false, true)] [InlineData(true, false)] [InlineData(true, true)] public async Task Memory_Read_Preserves_Slices_And_Releases_Output(bool nonArray, bool suspend) { var expected = new byte[32769]; new Random(4321).NextBytes(expected); using var input = new MemoryInput(Compress(expected), suspend); using var decoder = CreateDecoder(input); var bytes = Enumerable.Repeat((byte)0xCC, 79).ToArray(); using var owner = new NonArrayMemory(bytes); var destination = nonArray ? owner.Memory : bytes.AsMemory(); input.BeforeRead = () => { Assert.NotNull(decoder._z.OutputBuffer); if (!nonArray) { Assert.Same(bytes, decoder._z.OutputBuffer); } else { Assert.NotSame(bytes, decoder._z.OutputBuffer); } }; using var actual = new MemoryStream(); while (true) { var count = await decoder.ReadAsync(destination.Slice(7, 63), CancellationToken.None); Assert.Null(decoder._z.OutputBuffer); Assert.All(bytes.Take(7), b => Assert.Equal(0xCC, b)); Assert.All(bytes.Skip(70), b => Assert.Equal(0xCC, b)); if (count == 0) { break; } actual.Write(bytes, 7, count); // No caller-memory reference may survive until the next operation. Array.Fill(bytes, (byte)0xCC); } Assert.Equal(expected, actual.ToArray()); Assert.Equal(0, await decoder.ReadAsync(Memory.Empty, CancellationToken.None)); Assert.Null(decoder._z.OutputBuffer); Assert.True(input.CanRead); } [Theory] [InlineData(false)] [InlineData(true)] public async Task Memory_Read_Cancellation_And_Failure_Release_Output(bool nonArray) { var bytes = new byte[128]; using var owner = new NonArrayMemory(bytes); var memory = nonArray ? owner.Memory : bytes.AsMemory(); using var cancelled = new CancellationTokenSource(); cancelled.Cancel(); using (var input = new MemoryInput(Compress(bytes), false)) using (var decoder = CreateDecoder(input)) { await Assert.ThrowsAnyAsync(async () => await decoder.ReadAsync(memory, cancelled.Token) ); Assert.Null(decoder._z.OutputBuffer); } using (var input = new MemoryInput(Compress(bytes), true) { Fail = true }) using (var decoder = CreateDecoder(input)) { await Assert.ThrowsAsync(async () => await decoder.ReadAsync(memory, CancellationToken.None) ); Assert.Null(decoder._z.OutputBuffer); } using (var input = new MemoryInput(new byte[] { 7 }, false)) using (var decoder = CreateDecoder(input)) { await Assert.ThrowsAsync(async () => await decoder.ReadAsync(memory, CancellationToken.None) ); Assert.Null(decoder._z.OutputBuffer); } } private static ZlibBaseStream CreateDecoder(Stream input) => new( input, CompressionMode.Decompress, CompressionLevel.Default, ZlibStreamFlavor.DEFLATE, leaveOpen: true, Encoding.UTF8 ); private sealed class MemoryInput(byte[] bytes, bool suspend) : Stream { private int position; private bool disposed; public Action? BeforeRead { get; set; } public bool Fail { get; set; } public override bool CanRead => !disposed; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => throw new NotSupportedException(); public override long Position { get => position; set => throw new NotSupportedException(); } public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); public override Task ReadAsync( byte[] buffer, int offset, int count, CancellationToken cancellationToken ) => throw new NotSupportedException("Only memory reads are supported."); public override async ValueTask ReadAsync( Memory buffer, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); BeforeRead?.Invoke(); if (suspend) { await Task.Yield(); } if (Fail) { throw new IOException("Input failed."); } var count = Math.Min(Math.Min(buffer.Length, 17), bytes.Length - position); bytes.AsMemory(position, count).CopyTo(buffer); position += count; return count; } public override void Flush() { } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); protected override void Dispose(bool disposing) { disposed = true; base.Dispose(disposing); } } private sealed class NonArrayMemory(byte[] bytes) : MemoryManager { public override Span GetSpan() => bytes; public override MemoryHandle Pin(int elementIndex = 0) => throw new NotSupportedException(); public override void Unpin() { } protected override void Dispose(bool disposing) { } } #endif private static byte[] Compress(byte[] bytes) { using var packed = new MemoryStream(); using ( var encoder = new System.IO.Compression.DeflateStream( packed, System.IO.Compression.CompressionLevel.Optimal, leaveOpen: true ) ) { encoder.Write(bytes, 0, bytes.Length); } return packed.ToArray(); } }