From ad5c655c4587240d02b50f084226a254fd2c06a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:09:05 +0000 Subject: [PATCH 1/8] Initial plan From df59c5cb9d3aa7e0ff1efdf88ae47327b3dcb6fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:26:00 +0000 Subject: [PATCH 2/8] Plan: Fix potential ZipArchive.IsZipFile failure on Linux Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/packages.lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 9ebb1677..b85a38f7 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -335,9 +335,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.17, )", - "resolved": "8.0.17", - "contentHash": "x5/y4l8AtshpBOrCZdlE4txw8K3e3s9meBFeZeR3l8hbbku2V7kK6ojhXvrbjg1rk3G+JqL1BI26gtgc1ZrdUw==" + "requested": "[8.0.20, )", + "resolved": "8.0.20", + "contentHash": "Rhcto2AjGvTO62+/VTmBpumBOmqIGp7nYEbTbmEXkCq4yPGxV8whju3/HsIA/bKyo2+DggaYk5+/8sxb1AbPTw==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", From db98e5f39ba7dd4eae5c9de04628fccafd10eed3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:34:53 +0000 Subject: [PATCH 3/8] Fix ArchiveFactory.Open to avoid double-wrapping SharpCompressStream Use SharpCompressStream.Create instead of constructor to properly handle streams that are already wrapped. This prevents potential buffering issues when opening ZIP files, particularly on Linux systems. Added tests to verify both raw FileStream and pre-wrapped stream scenarios. Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Archives/ArchiveFactory.cs | 2 +- tests/SharpCompress.Test/ArchiveTests.cs | 32 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 870092f1..94368ece 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -20,7 +20,7 @@ public static class ArchiveFactory public static IArchive Open(Stream stream, ReaderOptions? readerOptions = null) { readerOptions ??= new ReaderOptions(); - stream = new SharpCompressStream(stream, bufferSize: readerOptions.BufferSize); + stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize); return FindFactory(stream).Open(stream, readerOptions); } diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 2849a71f..2f47e121 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -622,4 +622,36 @@ public class ArchiveTests : ReaderTests VerifyFiles(); } } + + [Fact] + public void ArchiveFactory_Open_WithPreWrappedStream() + { + // Test that ArchiveFactory.Open works correctly with a stream that's already wrapped + // This addresses the issue where ZIP files fail to open on Linux + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.noEmptyDirs.zip"); + + // Open with a pre-wrapped stream + using (var fileStream = File.OpenRead(testArchive)) + using (var wrappedStream = SharpCompressStream.Create(fileStream, bufferSize: 32768)) + using (var archive = ArchiveFactory.Open(wrappedStream)) + { + Assert.Equal(ArchiveType.Zip, archive.Type); + Assert.Equal(3, archive.Entries.Count()); + } + } + + [Fact] + public void ArchiveFactory_Open_WithRawFileStream() + { + // Test that ArchiveFactory.Open works correctly with a raw FileStream + // This is the common use case reported in the issue + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.noEmptyDirs.zip"); + + using (var stream = File.OpenRead(testArchive)) + using (var archive = ArchiveFactory.Open(stream)) + { + Assert.Equal(ArchiveType.Zip, archive.Type); + Assert.Equal(3, archive.Entries.Count()); + } + } } From ea77666b4a42b974b21d388cf6e1dd55c1bc0c56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:38:38 +0000 Subject: [PATCH 4/8] Final verification: All tests pass, no security issues Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/packages.lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index b85a38f7..904860ce 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -335,9 +335,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.20, )", - "resolved": "8.0.20", - "contentHash": "Rhcto2AjGvTO62+/VTmBpumBOmqIGp7nYEbTbmEXkCq4yPGxV8whju3/HsIA/bKyo2+DggaYk5+/8sxb1AbPTw==" + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "B3etT5XQ2nlWkZGO2m/ytDYrOmSsQG1XNBaM6ZYlX5Ch/tDrMFadr0/mK6gjZwaQc55g+5+WZMw4Cz3m8VEF7g==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", From bc06f3179d4a855b5ad63de8108513bd3b56e1ff Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 30 Oct 2025 14:42:46 +0000 Subject: [PATCH 5/8] add basics for async bzip2 --- .../Compressors/BZip2/BZip2Stream.cs | 28 ++- .../Compressors/BZip2/CBZip2InputStream.cs | 24 ++ .../Compressors/BZip2/CBZip2OutputStream.cs | 17 ++ .../BZip2/BZip2StreamAsyncTests.cs | 231 ++++++++++++++++++ 4 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 tests/SharpCompress.Test/Compressors/BZip2/BZip2StreamAsyncTests.cs diff --git a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs index 5b9884ca..e3325f27 100644 --- a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs +++ b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; namespace SharpCompress.Compressors.BZip2; @@ -96,13 +98,37 @@ public sealed class BZip2Stream : Stream, IStreamStack public override void SetLength(long value) => stream.SetLength(value); -#if !NETFRAMEWORK&& !NETSTANDARD2_0 +#if !NETFRAMEWORK && !NETSTANDARD2_0 public override int Read(Span buffer) => stream.Read(buffer); public override void Write(ReadOnlySpan buffer) => stream.Write(buffer); + + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) => await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + + public override async ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) => await stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); #endif + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) => await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) => await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + public override void Write(byte[] buffer, int offset, int count) => stream.Write(buffer, offset, count); diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs index abff4973..df164b11 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs @@ -2,6 +2,8 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; /* @@ -1127,6 +1129,28 @@ internal class CBZip2InputStream : Stream, IStreamStack return k; } + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var c = -1; + int k; + for (k = 0; k < count; ++k) + { + cancellationToken.ThrowIfCancellationRequested(); + c = ReadByte(); + if (c == -1) + { + break; + } + buffer[k + offset] = (byte)c; + } + return await Task.FromResult(k); + } + public override long Seek(long offset, SeekOrigin origin) => 0; public override void SetLength(long value) { } diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs index 555c6fcb..cebfc6b7 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; /* @@ -2022,6 +2024,21 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack } } + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + for (var k = 0; k < count; ++k) + { + cancellationToken.ThrowIfCancellationRequested(); + WriteByte(buffer[k + offset]); + } + await Task.CompletedTask; + } + public override bool CanRead => false; public override bool CanSeek => false; diff --git a/tests/SharpCompress.Test/Compressors/BZip2/BZip2StreamAsyncTests.cs b/tests/SharpCompress.Test/Compressors/BZip2/BZip2StreamAsyncTests.cs new file mode 100644 index 00000000..0c10ca64 --- /dev/null +++ b/tests/SharpCompress.Test/Compressors/BZip2/BZip2StreamAsyncTests.cs @@ -0,0 +1,231 @@ +using System; +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Compressors.BZip2; +using Xunit; + +namespace SharpCompress.Test.BZip2; + +public class BZip2StreamAsyncTests +{ + private byte[] CreateTestData(int size) + { + var data = new byte[size]; + // Create compressible data with repetitive pattern + for (int i = 0; i < size; i++) + { + data[i] = (byte)('A' + (i % 26)); + } + return data; + } + + [Fact] + public async Task BZip2CompressDecompressAsyncTest() + { + var testData = CreateTestData(10000); + byte[] compressed; + + // Compress + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Compress, + false + ) + ) + { + await bzip2Stream.WriteAsync(testData, 0, testData.Length); + (bzip2Stream as BZip2Stream)?.Finish(); + } + compressed = memoryStream.ToArray(); + } + + // Verify compression occurred + Assert.True(compressed.Length > 0); + Assert.True(compressed.Length < testData.Length); + + // Decompress + byte[] decompressed; + using (var memoryStream = new MemoryStream(compressed)) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Decompress, + false + ) + ) + { + decompressed = new byte[testData.Length]; + var totalRead = 0; + int bytesRead; + while ( + ( + bytesRead = await bzip2Stream.ReadAsync( + decompressed, + totalRead, + testData.Length - totalRead + ) + ) > 0 + ) + { + totalRead += bytesRead; + } + } + } + + // Verify decompression + Assert.Equal(testData, decompressed); + } + + [Fact] + public async Task BZip2ReadAsyncWithCancellationTest() + { + var testData = Encoding.ASCII.GetBytes(new string('A', 5000)); // Repetitive data compresses well + byte[] compressed; + + // Compress + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Compress, + false + ) + ) + { + await bzip2Stream.WriteAsync(testData, 0, testData.Length); + (bzip2Stream as BZip2Stream)?.Finish(); + } + compressed = memoryStream.ToArray(); + } + + // Decompress with cancellation support + using (var memoryStream = new MemoryStream(compressed)) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Decompress, + false + ) + ) + { + var buffer = new byte[1024]; + var cts = new System.Threading.CancellationTokenSource(); + + // Read should complete without cancellation + var bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length, cts.Token); + Assert.True(bytesRead > 0); + } + } + } + + [Fact] + public async Task BZip2MultipleAsyncWritesTest() + { + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Compress, + false + ) + ) + { + var data1 = Encoding.ASCII.GetBytes("Hello "); + var data2 = Encoding.ASCII.GetBytes("World"); + var data3 = Encoding.ASCII.GetBytes("!"); + + await bzip2Stream.WriteAsync(data1, 0, data1.Length); + await bzip2Stream.WriteAsync(data2, 0, data2.Length); + await bzip2Stream.WriteAsync(data3, 0, data3.Length); + + (bzip2Stream as BZip2Stream)?.Finish(); + } + + var compressed = memoryStream.ToArray(); + Assert.True(compressed.Length > 0); + + // Decompress and verify + using (var readStream = new MemoryStream(compressed)) + { + using ( + var bzip2Stream = new BZip2Stream( + readStream, + SharpCompress.Compressors.CompressionMode.Decompress, + false + ) + ) + { + var result = new StringBuilder(); + var buffer = new byte[256]; + int bytesRead; + while ((bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + result.Append(Encoding.ASCII.GetString(buffer, 0, bytesRead)); + } + + Assert.Equal("Hello World!", result.ToString()); + } + } + } + } + + [Fact] + public async Task BZip2LargeDataAsyncTest() + { + var largeData = CreateTestData(100000); + + // Compress + byte[] compressed; + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Compress, + false + ) + ) + { + await bzip2Stream.WriteAsync(largeData, 0, largeData.Length); + (bzip2Stream as BZip2Stream)?.Finish(); + } + compressed = memoryStream.ToArray(); + } + + // Decompress + byte[] decompressed; + using (var memoryStream = new MemoryStream(compressed)) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Decompress, + false + ) + ) + { + decompressed = new byte[largeData.Length]; + var totalRead = 0; + int bytesRead; + var buffer = new byte[4096]; + while ((bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + Array.Copy(buffer, 0, decompressed, totalRead, bytesRead); + totalRead += bytesRead; + } + } + } + + // Verify + Assert.Equal(largeData, decompressed); + } +} From a136084e11e40a30b0c670484216e3021262a2b9 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 30 Oct 2025 14:52:51 +0000 Subject: [PATCH 6/8] add adc async --- src/SharpCompress/Compressors/ADC/ADCBase.cs | 180 ++++++++++++++++++ .../Compressors/ADC/ADCStream.cs | 72 +++++++ tests/SharpCompress.Test/AdcAsyncTest.cs | 61 ++++++ 3 files changed, 313 insertions(+) create mode 100644 tests/SharpCompress.Test/AdcAsyncTest.cs diff --git a/src/SharpCompress/Compressors/ADC/ADCBase.cs b/src/SharpCompress/Compressors/ADC/ADCBase.cs index 6fc755a9..b5b9781e 100644 --- a/src/SharpCompress/Compressors/ADC/ADCBase.cs +++ b/src/SharpCompress/Compressors/ADC/ADCBase.cs @@ -24,10 +24,29 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; +using System.Buffers; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Compressors.ADC; +/// +/// Result of an ADC decompression operation +/// +public class AdcDecompressResult +{ + /// + /// Number of bytes read from input + /// + public int BytesRead { get; set; } + + /// + /// Decompressed output buffer + /// + public byte[]? Output { get; set; } +} + /// /// Provides static methods for decompressing Apple Data Compression data /// @@ -78,6 +97,167 @@ public static class ADCBase public static int Decompress(byte[] input, out byte[]? output, int bufferSize = 262144) => Decompress(new MemoryStream(input), out output, bufferSize); + /// + /// Decompresses a byte buffer asynchronously that's compressed with ADC + /// + /// Compressed buffer + /// Max size for decompressed data + /// Cancellation token + /// Result containing bytes read and decompressed data + public static async Task DecompressAsync( + byte[] input, + int bufferSize = 262144, + CancellationToken cancellationToken = default + ) => await DecompressAsync(new MemoryStream(input), bufferSize, cancellationToken); + + /// + /// Decompresses a stream asynchronously that's compressed with ADC + /// + /// Stream containing compressed data + /// Max size for decompressed data + /// Cancellation token + /// Result containing bytes read and decompressed data + public static async Task DecompressAsync( + Stream input, + int bufferSize = 262144, + CancellationToken cancellationToken = default + ) + { + var result = new AdcDecompressResult(); + + if (input is null || input.Length == 0) + { + result.BytesRead = 0; + result.Output = null; + return await Task.FromResult(result); + } + + var start = (int)input.Position; + var position = (int)input.Position; + int chunkSize; + int offset; + int chunkType; + var buffer = ArrayPool.Shared.Rent(bufferSize); + var outPosition = 0; + var full = false; + byte[] temp = ArrayPool.Shared.Rent(3); + + try + { + while (position < input.Length) + { + cancellationToken.ThrowIfCancellationRequested(); + var readByte = input.ReadByte(); + if (readByte == -1) + { + break; + } + + chunkType = GetChunkType((byte)readByte); + + switch (chunkType) + { + case PLAIN: + chunkSize = GetChunkSize((byte)readByte); + if (outPosition + chunkSize > bufferSize) + { + full = true; + break; + } + + var readCount = await input.ReadAsync(buffer, outPosition, chunkSize, cancellationToken); + outPosition += readCount; + position += readCount + 1; + break; + case TWO_BYTE: + chunkSize = GetChunkSize((byte)readByte); + temp[0] = (byte)readByte; + temp[1] = (byte)input.ReadByte(); + offset = GetOffset(temp.AsSpan(0, 2)); + if (outPosition + chunkSize > bufferSize) + { + full = true; + break; + } + + if (offset == 0) + { + var lastByte = buffer[outPosition - 1]; + for (var i = 0; i < chunkSize; i++) + { + buffer[outPosition] = lastByte; + outPosition++; + } + + position += 2; + } + else + { + for (var i = 0; i < chunkSize; i++) + { + buffer[outPosition] = buffer[outPosition - offset - 1]; + outPosition++; + } + + position += 2; + } + + break; + case THREE_BYTE: + chunkSize = GetChunkSize((byte)readByte); + temp[0] = (byte)readByte; + temp[1] = (byte)input.ReadByte(); + temp[2] = (byte)input.ReadByte(); + offset = GetOffset(temp.AsSpan(0, 3)); + if (outPosition + chunkSize > bufferSize) + { + full = true; + break; + } + + if (offset == 0) + { + var lastByte = buffer[outPosition - 1]; + for (var i = 0; i < chunkSize; i++) + { + buffer[outPosition] = lastByte; + outPosition++; + } + + position += 3; + } + else + { + for (var i = 0; i < chunkSize; i++) + { + buffer[outPosition] = buffer[outPosition - offset - 1]; + outPosition++; + } + + position += 3; + } + + break; + } + + if (full) + { + break; + } + } + + var output = new byte[outPosition]; + Array.Copy(buffer, output, outPosition); + result.BytesRead = position - start; + result.Output = output; + return result; + } finally + { + ArrayPool.Shared.Return(buffer); + ArrayPool.Shared.Return(temp); + } + } + /// /// Decompresses a stream that's compressed with ADC /// diff --git a/src/SharpCompress/Compressors/ADC/ADCStream.cs b/src/SharpCompress/Compressors/ADC/ADCStream.cs index 70e8fd8b..1282c394 100644 --- a/src/SharpCompress/Compressors/ADC/ADCStream.cs +++ b/src/SharpCompress/Compressors/ADC/ADCStream.cs @@ -28,6 +28,8 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; namespace SharpCompress.Compressors.ADC; @@ -187,6 +189,76 @@ public sealed class ADCStream : Stream, IStreamStack return copied; } + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + if (count == 0) + { + return 0; + } + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (offset < buffer.GetLowerBound(0)) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if ((offset + count) > buffer.GetLength(0)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (_outBuffer is null) + { + var result = await ADCBase.DecompressAsync( + _stream, + cancellationToken: cancellationToken + ); + _outBuffer = result.Output; + _outPosition = 0; + } + + var inPosition = offset; + var toCopy = count; + var copied = 0; + + while (_outPosition + toCopy >= _outBuffer.Length) + { + cancellationToken.ThrowIfCancellationRequested(); + var piece = _outBuffer.Length - _outPosition; + Array.Copy(_outBuffer, _outPosition, buffer, inPosition, piece); + inPosition += piece; + copied += piece; + _position += piece; + toCopy -= piece; + var result = await ADCBase.DecompressAsync( + _stream, + cancellationToken: cancellationToken + ); + _outBuffer = result.Output; + _outPosition = 0; + if (result.BytesRead == 0 || _outBuffer is null || _outBuffer.Length == 0) + { + return copied; + } + } + + Array.Copy(_outBuffer, _outPosition, buffer, inPosition, toCopy); + _outPosition += toCopy; + _position += toCopy; + copied += toCopy; + return await Task.FromResult(copied); + } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); diff --git a/tests/SharpCompress.Test/AdcAsyncTest.cs b/tests/SharpCompress.Test/AdcAsyncTest.cs new file mode 100644 index 00000000..02ad8888 --- /dev/null +++ b/tests/SharpCompress.Test/AdcAsyncTest.cs @@ -0,0 +1,61 @@ +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Compressors.ADC; +using Xunit; + +namespace SharpCompress.Test; + +public class AdcAsyncTest : TestBase +{ + [Fact] + public async Task TestAdcStreamAsyncWholeChunk() + { + using var decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin")); + var decompressed = new byte[decFs.Length]; + decFs.Read(decompressed, 0, decompressed.Length); + + using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin")); + using var decStream = new ADCStream(cmpFs); + var test = new byte[262144]; + + await decStream.ReadAsync(test, 0, test.Length); + + Assert.Equal(decompressed, test); + } + + [Fact] + public async Task TestAdcStreamAsync() + { + using var decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin")); + var decompressed = new byte[decFs.Length]; + decFs.Read(decompressed, 0, decompressed.Length); + + using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin")); + using var decStream = new ADCStream(cmpFs); + using var decMs = new MemoryStream(); + var test = new byte[512]; + var count = 0; + + do + { + count = await decStream.ReadAsync(test, 0, test.Length); + decMs.Write(test, 0, count); + } while (count > 0); + + Assert.Equal(decompressed, decMs.ToArray()); + } + + [Fact] + public async Task TestAdcStreamAsyncWithCancellation() + { + using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin")); + using var decStream = new ADCStream(cmpFs); + var test = new byte[512]; + var cts = new System.Threading.CancellationTokenSource(); + + // Read should complete without cancellation + var bytesRead = await decStream.ReadAsync(test, 0, test.Length, cts.Token); + + Assert.True(bytesRead > 0); + } +} From eb188051d44ad7de493cf0803b300ed5b8bb1883 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 30 Oct 2025 14:53:11 +0000 Subject: [PATCH 7/8] fmt --- src/SharpCompress/Compressors/ADC/ADCBase.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/Compressors/ADC/ADCBase.cs b/src/SharpCompress/Compressors/ADC/ADCBase.cs index b5b9781e..92bb2e52 100644 --- a/src/SharpCompress/Compressors/ADC/ADCBase.cs +++ b/src/SharpCompress/Compressors/ADC/ADCBase.cs @@ -165,7 +165,12 @@ public static class ADCBase break; } - var readCount = await input.ReadAsync(buffer, outPosition, chunkSize, cancellationToken); + var readCount = await input.ReadAsync( + buffer, + outPosition, + chunkSize, + cancellationToken + ); outPosition += readCount; position += readCount + 1; break; @@ -251,7 +256,8 @@ public static class ADCBase result.BytesRead = position - start; result.Output = output; return result; - } finally + } + finally { ArrayPool.Shared.Return(buffer); ArrayPool.Shared.Return(temp); From ccc8587e5f8b5f41101b0ba30b06fb90a44c9593 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 31 Oct 2025 10:55:33 +0000 Subject: [PATCH 8/8] review fixes --- src/SharpCompress/Compressors/ADC/ADCBase.cs | 2 +- src/SharpCompress/Compressors/ADC/ADCStream.cs | 2 +- src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs | 4 ++-- src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs | 4 ++-- src/SharpCompress/Writers/Zip/ZipWriter.cs | 4 ++-- tests/SharpCompress.Test/AdcAsyncTest.cs | 2 +- .../{Compressors => }/BZip2/BZip2StreamAsyncTests.cs | 2 +- tests/SharpCompress.Test/{Compressors => }/Rar/RarCRCTest.cs | 0 8 files changed, 10 insertions(+), 10 deletions(-) rename tests/SharpCompress.Test/{Compressors => }/BZip2/BZip2StreamAsyncTests.cs (98%) rename tests/SharpCompress.Test/{Compressors => }/Rar/RarCRCTest.cs (100%) diff --git a/src/SharpCompress/Compressors/ADC/ADCBase.cs b/src/SharpCompress/Compressors/ADC/ADCBase.cs index 92bb2e52..35301b52 100644 --- a/src/SharpCompress/Compressors/ADC/ADCBase.cs +++ b/src/SharpCompress/Compressors/ADC/ADCBase.cs @@ -129,7 +129,7 @@ public static class ADCBase { result.BytesRead = 0; result.Output = null; - return await Task.FromResult(result); + return result; } var start = (int)input.Position; diff --git a/src/SharpCompress/Compressors/ADC/ADCStream.cs b/src/SharpCompress/Compressors/ADC/ADCStream.cs index 1282c394..935207c9 100644 --- a/src/SharpCompress/Compressors/ADC/ADCStream.cs +++ b/src/SharpCompress/Compressors/ADC/ADCStream.cs @@ -256,7 +256,7 @@ public sealed class ADCStream : Stream, IStreamStack _outPosition += toCopy; _position += toCopy; copied += toCopy; - return await Task.FromResult(copied); + return copied; } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs index df164b11..e466cc07 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs @@ -1129,7 +1129,7 @@ internal class CBZip2InputStream : Stream, IStreamStack return k; } - public override async Task ReadAsync( + public override Task ReadAsync( byte[] buffer, int offset, int count, @@ -1148,7 +1148,7 @@ internal class CBZip2InputStream : Stream, IStreamStack } buffer[k + offset] = (byte)c; } - return await Task.FromResult(k); + return Task.FromResult(k); } public override long Seek(long offset, SeekOrigin origin) => 0; diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs index cebfc6b7..db470ff2 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs @@ -2024,7 +2024,7 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack } } - public override async Task WriteAsync( + public override Task WriteAsync( byte[] buffer, int offset, int count, @@ -2036,7 +2036,7 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack cancellationToken.ThrowIfCancellationRequested(); WriteByte(buffer[k + offset]); } - await Task.CompletedTask; + return Task.CompletedTask; } public override bool CanRead => false; diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs index daee1ed6..f867c8a9 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs @@ -160,7 +160,7 @@ public class ZipWriter : AbstractWriter WriteDirectoryEntry(normalizedName, options); } - public override async Task WriteDirectoryAsync( + public override Task WriteDirectoryAsync( string directoryName, DateTime? modificationTime, CancellationToken cancellationToken = default @@ -168,7 +168,7 @@ public class ZipWriter : AbstractWriter { // Synchronous implementation is sufficient for directory entries WriteDirectory(directoryName, modificationTime); - await Task.CompletedTask.ConfigureAwait(false); + return Task.CompletedTask; } private void WriteDirectoryEntry(string directoryPath, ZipWriterEntryOptions options) diff --git a/tests/SharpCompress.Test/AdcAsyncTest.cs b/tests/SharpCompress.Test/AdcAsyncTest.cs index 02ad8888..185174aa 100644 --- a/tests/SharpCompress.Test/AdcAsyncTest.cs +++ b/tests/SharpCompress.Test/AdcAsyncTest.cs @@ -51,7 +51,7 @@ public class AdcAsyncTest : TestBase using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin")); using var decStream = new ADCStream(cmpFs); var test = new byte[512]; - var cts = new System.Threading.CancellationTokenSource(); + using var cts = new System.Threading.CancellationTokenSource(); // Read should complete without cancellation var bytesRead = await decStream.ReadAsync(test, 0, test.Length, cts.Token); diff --git a/tests/SharpCompress.Test/Compressors/BZip2/BZip2StreamAsyncTests.cs b/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs similarity index 98% rename from tests/SharpCompress.Test/Compressors/BZip2/BZip2StreamAsyncTests.cs rename to tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs index 0c10ca64..c801ab0d 100644 --- a/tests/SharpCompress.Test/Compressors/BZip2/BZip2StreamAsyncTests.cs +++ b/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs @@ -117,7 +117,7 @@ public class BZip2StreamAsyncTests ) { var buffer = new byte[1024]; - var cts = new System.Threading.CancellationTokenSource(); + using var cts = new System.Threading.CancellationTokenSource(); // Read should complete without cancellation var bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length, cts.Token); diff --git a/tests/SharpCompress.Test/Compressors/Rar/RarCRCTest.cs b/tests/SharpCompress.Test/Rar/RarCRCTest.cs similarity index 100% rename from tests/SharpCompress.Test/Compressors/Rar/RarCRCTest.cs rename to tests/SharpCompress.Test/Rar/RarCRCTest.cs