From a8f6f188438091800a093866cf318f244c39b985 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 31 Jul 2026 14:21:24 +0100 Subject: [PATCH 01/12] Fix SeekableSharpCompressStream.StopRecording() to rewind to recorded position When SeekableSharpCompressStream.StopRecording() was called (e.g. after the tar probe in GZipFactory.TryOpenReader), it cleared _recordedPosition without seeking back to it. This left the underlying FileStream at the advanced position reached during the probe, so the subsequent GZipReader would start reading from the wrong offset, producing Key=null and 0 extracted bytes. Fix: seek back to _recordedPosition before clearing it, matching the behavior of the non-seekable SharpCompressStream.StopRecording() which rewinds _logicalPosition. Also enhance the existing GZip_ReaderFactory_FlatGZip test to actually verify the extracted content (not just MoveToNextEntry returns true), and add an async variant. Closes #1391 --- .../IO/SeekableSharpCompressStream.cs | 11 ++++++- .../GZip/GZipReaderAsyncTests.cs | 30 +++++++++++++++++++ .../GZip/GZipReaderTests.cs | 6 ++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/SharpCompress/IO/SeekableSharpCompressStream.cs b/src/SharpCompress/IO/SeekableSharpCompressStream.cs index bad123bb..e3aa9bb5 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.cs @@ -83,7 +83,16 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream public override void StartRecording(int? minBufferSize = null) => _recordedPosition = _stream.Position; - public override void StopRecording() => _recordedPosition = null; + public override void StopRecording() + { + if (_recordedPosition.HasValue) + { + // Seek back to the recording anchor position, matching the behavior of the + // non-seekable SharpCompressStream.StopRecording() which rewinds _logicalPosition. + _stream.Seek(_recordedPosition.Value, SeekOrigin.Begin); + } + _recordedPosition = null; + } protected override void Dispose(bool disposing) { diff --git a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs index 83d2b352..a3da7e04 100644 --- a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs @@ -1,4 +1,5 @@ using System.IO; +using System.IO.Compression; using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -37,4 +38,33 @@ public class GZipReaderAsyncTests : ReaderTests } } } + + [Fact] + public async ValueTask GZip_ReaderFactory_FlatGZip_Async() + { + var source = new byte[2048]; + for (var i = 0; i < source.Length; i++) + { + source[i] = 0xFF; + } + + var gzipPath = Path.Combine(SCRATCH_FILES_PATH, "Flat.bin.gz"); + using (var output = File.Create(gzipPath)) + await using (var gzip = new GZipStream(output, CompressionMode.Compress)) + { + await gzip.WriteAsync(source, 0, source.Length); + } + + await using Stream stream = File.OpenRead(gzipPath); + await using var reader = await ReaderFactory.OpenAsyncReader(stream); + Assert.IsType(reader); + Assert.True(await reader.MoveToNextEntryAsync()); + + using var ms = new MemoryStream(); + await reader.WriteEntryToAsync(ms); + Assert.Equal(source.Length, ms.Length); + Assert.Equal(source, ms.ToArray()); + + Assert.False(await reader.MoveToNextEntryAsync()); + } } diff --git a/tests/SharpCompress.Test/GZip/GZipReaderTests.cs b/tests/SharpCompress.Test/GZip/GZipReaderTests.cs index fb3936a8..ab870da2 100644 --- a/tests/SharpCompress.Test/GZip/GZipReaderTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipReaderTests.cs @@ -48,6 +48,12 @@ public class GZipReaderTests : ReaderTests using var reader = ReaderFactory.OpenReader(stream); Assert.IsType(reader); Assert.True(reader.MoveToNextEntry()); + + using var ms = new MemoryStream(); + reader.WriteEntryTo(ms); + Assert.Equal(source.Length, ms.Length); + Assert.Equal(source, ms.ToArray()); + Assert.False(reader.MoveToNextEntry()); } } From 82c44cb5a5f9299418f37d171adb1b83d3add772 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 31 Jul 2026 14:52:58 +0100 Subject: [PATCH 02/12] remove async dispose --- tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs index a3da7e04..98914780 100644 --- a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs @@ -50,12 +50,12 @@ public class GZipReaderAsyncTests : ReaderTests var gzipPath = Path.Combine(SCRATCH_FILES_PATH, "Flat.bin.gz"); using (var output = File.Create(gzipPath)) - await using (var gzip = new GZipStream(output, CompressionMode.Compress)) + using (var gzip = new GZipStream(output, CompressionMode.Compress)) { await gzip.WriteAsync(source, 0, source.Length); } - await using Stream stream = File.OpenRead(gzipPath); + using Stream stream = File.OpenRead(gzipPath); await using var reader = await ReaderFactory.OpenAsyncReader(stream); Assert.IsType(reader); Assert.True(await reader.MoveToNextEntryAsync()); From cfdfad26b614661a1c43497395812a2fab1a5b87 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Aug 2026 03:40:04 +0000 Subject: [PATCH 03/12] Fix RAR5 async decompression corruption in UnpWriteBufAsync Three bugs in the async version of UnpWriteBuf (used only in the async decompression path) were causing data corruption when decompressing RAR5 archives containing executable filters (E8/E8E9/ARM) whose blocks spanned the 4MB write boundary (UNPACK_MAX_WRITE): 1. WrittenFileSize was only incremented inside `if (OutMem != null)`, but should always be incremented after ApplyFilter (matching sync behavior). 2. UnpSomeRead = true was missing from the filter processing path. 3. In the NotAllFiltersProcessed else branch (filter intersects write border): - WrPtr was not updated to WrittenBorder, corrupting the window pointer used to compute WriteBorder on the next flush cycle - Remaining filters had NextWindow set to true (inverted logic), when sync correctly sets them to false so they are processed next time All three changes align the async path with the sync UnpWriteBuf logic. --- global.json | 2 +- .../Rar/UnpackV2017/Unpack.unpack50_async.cs | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/global.json b/global.json index f5d6141c..8a7554cf 100644 --- a/global.json +++ b/global.json @@ -3,4 +3,4 @@ "version": "10.0.301", "rollForward": "disable" } -} +} \ No newline at end of file diff --git a/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack50_async.cs b/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack50_async.cs index cabce8cf..fbb2d607 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack50_async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack50_async.cs @@ -394,28 +394,34 @@ internal partial class Unpack { await UnpIO_UnpWriteAsync(OutMem, 0, BlockLength, cancellationToken) .ConfigureAwait(false); - WrittenFileSize += BlockLength; } + UnpSomeRead = true; + WrittenFileSize += BlockLength; WrittenBorder = BlockEnd; WriteSizeLeft = (UnpPtr - WrittenBorder) & MaxWinMask; } } else { - NotAllFiltersProcessed = true; + // Current filter intersects the window write border, so we adjust + // the window border to process this filter next time, not now. + WrPtr = WrittenBorder; + + // Since Filter start position can only increase, we quit processing + // all following filters for this data block and reset 'NextWindow' + // flag for them. for (var J = I; J < Filters.Count; J++) { var fltj = Filters[J]; - if ( - fltj.Type != FILTER_NONE - && fltj.NextWindow == false - && ((fltj.BlockStart - WrPtr) & MaxWinMask) < FullWriteSize - ) + if (fltj.Type != FILTER_NONE) { - fltj.NextWindow = true; + fltj.NextWindow = false; } } + + // Do not write data left after current filter now. + NotAllFiltersProcessed = true; break; } } From bc06a9df8ba3f262a7a9a8231a0630872b22dd29 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 1 Aug 2026 12:43:44 +0100 Subject: [PATCH 04/12] Some clean up --- global.json | 2 +- .../Compressors/Rar/RarCrcStream.Async.cs | 15 ++++----- .../Compressors/Rar/RarCrcStream.cs | 33 ++++++++----------- src/SharpCompress/packages.lock.json | 12 +++---- .../SharpCompress.AotSmoke/packages.lock.json | 24 +++++++------- 5 files changed, 39 insertions(+), 47 deletions(-) diff --git a/global.json b/global.json index 8a7554cf..e784f38f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.301", + "version": "10.0.302", "rollForward": "disable" } } \ No newline at end of file diff --git a/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs b/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs index 2aedc9a6..a76a9ed6 100644 --- a/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs +++ b/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; @@ -31,11 +30,11 @@ internal partial class RarCrcStream : RarStream .ConfigureAwait(false); if (result != 0) { - currentCrc = RarCRC.CheckCrc(currentCrc, buffer, offset, result); + _currentCrc = RarCRC.CheckCrc(_currentCrc, buffer, offset, result); } else if ( - !disableCRC - && GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0) + !_disableCrc + && GetCrc() != BitConverter.ToUInt32(_readStream.NotNull().CurrentCrc.NotNull(), 0) && count != 0 ) { @@ -56,16 +55,16 @@ internal partial class RarCrcStream : RarStream var result = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); if (result != 0) { - currentCrc = RarCRC.CheckCrc(currentCrc, buffer.Span, 0, result); + _currentCrc = RarCRC.CheckCrc(_currentCrc, buffer.Span, 0, result); } else if ( - !disableCRC - && GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0) + !_disableCrc + && GetCrc() != BitConverter.ToUInt32(_readStream.NotNull().CurrentCrc.NotNull(), 0) && buffer.Length != 0 ) { // NOTE: we use the last FileHeader in a multipart volume to check CRC - throw new InvalidFormatException("file crc mismatch"); + throw new InvalidFormatException("file crc mismatch: " + _key); } return result; diff --git a/src/SharpCompress/Compressors/Rar/RarCrcStream.cs b/src/SharpCompress/Compressors/Rar/RarCrcStream.cs index a9025e19..233b05be 100644 --- a/src/SharpCompress/Compressors/Rar/RarCrcStream.cs +++ b/src/SharpCompress/Compressors/Rar/RarCrcStream.cs @@ -1,7 +1,4 @@ using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Rar.Headers; @@ -9,9 +6,10 @@ namespace SharpCompress.Compressors.Rar; internal partial class RarCrcStream : RarStream { - private readonly MultiVolumeReadOnlyStreamBase readStream; - private uint currentCrc; - private readonly bool disableCRC; + private readonly string? _key; + private readonly MultiVolumeReadOnlyStreamBase _readStream; + private uint _currentCrc; + private readonly bool _disableCrc; private RarCrcStream( IRarUnpack unpack, @@ -20,8 +18,9 @@ internal partial class RarCrcStream : RarStream ) : base(unpack, fileHeader, readStream) { - this.readStream = readStream; - disableCRC = fileHeader.IsEncrypted; + this._readStream = readStream; + _key = fileHeader.FileName; + _disableCrc = fileHeader.IsEncrypted; ResetCrc(); } @@ -36,31 +35,25 @@ internal partial class RarCrcStream : RarStream } // Async methods moved to RarCrcStream.Async.cs + public uint GetCrc() => ~_currentCrc; - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - } - - public uint GetCrc() => ~currentCrc; - - public void ResetCrc() => currentCrc = 0xffffffff; + public void ResetCrc() => _currentCrc = 0xffffffff; public override int Read(byte[] buffer, int offset, int count) { var result = base.Read(buffer, offset, count); if (result != 0) { - currentCrc = RarCRC.CheckCrc(currentCrc, buffer, offset, result); + _currentCrc = RarCRC.CheckCrc(_currentCrc, buffer, offset, result); } else if ( - !disableCRC - && GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0) + !_disableCrc + && GetCrc() != BitConverter.ToUInt32(_readStream.NotNull().CurrentCrc.NotNull(), 0) && count != 0 ) { // NOTE: we use the last FileHeader in a multipart volume to check CRC - throw new InvalidFormatException("file crc mismatch"); + throw new InvalidFormatException("file crc mismatch: " + _key); } return result; diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 54571844..c49dccb6 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -321,9 +321,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.9, )", - "resolved": "10.0.9", - "contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg==" + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -441,9 +441,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.28, )", - "resolved": "8.0.28", - "contentHash": "XMqgVjlLxLqWmEh3c49haXLQwsMNtvo6YscUaqfvEGfg1iA8hnYgkUVq3i9Zu9gKeNKMWiiZKVwZExc/qyEAsQ==" + "requested": "[8.0.29, )", + "resolved": "8.0.29", + "contentHash": "HSBTfrkIZijz8z3ybLRKB7E8rHk4QQufFwpHa9fc5CMIgRhRzdn4mBGmlyXZqaueiMPtuJcnjresGvSTfaW8Mg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.AotSmoke/packages.lock.json b/tests/SharpCompress.AotSmoke/packages.lock.json index 19031145..9e1690d2 100644 --- a/tests/SharpCompress.AotSmoke/packages.lock.json +++ b/tests/SharpCompress.AotSmoke/packages.lock.json @@ -4,15 +4,15 @@ "net10.0": { "Microsoft.DotNet.ILCompiler": { "type": "Direct", - "requested": "[10.0.9, )", - "resolved": "10.0.9", - "contentHash": "4y+VsQOcs4EiTSINdCpCWi/aLRbIbGTxSezQXd8uGVhzbDRm1FNVTZDyCUQixE0+g9UFusvfxVcF68YYz7RzxA==" + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tnG8ntt/Bk6odvHREnGLMo3PEiihy5iSlIFVp0JbIo00GKtNRt2k73eKZbPqR5yaJNIa3z8R86YLwbxfqpb17g==" }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.9, )", - "resolved": "10.0.9", - "contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg==" + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -76,17 +76,17 @@ "net10.0/linux-x64": { "Microsoft.DotNet.ILCompiler": { "type": "Direct", - "requested": "[10.0.9, )", - "resolved": "10.0.9", - "contentHash": "4y+VsQOcs4EiTSINdCpCWi/aLRbIbGTxSezQXd8uGVhzbDRm1FNVTZDyCUQixE0+g9UFusvfxVcF68YYz7RzxA==", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tnG8ntt/Bk6odvHREnGLMo3PEiihy5iSlIFVp0JbIo00GKtNRt2k73eKZbPqR5yaJNIa3z8R86YLwbxfqpb17g==", "dependencies": { - "runtime.linux-x64.Microsoft.DotNet.ILCompiler": "10.0.9" + "runtime.linux-x64.Microsoft.DotNet.ILCompiler": "10.0.10" } }, "runtime.linux-x64.Microsoft.DotNet.ILCompiler": { "type": "Transitive", - "resolved": "10.0.9", - "contentHash": "45CVefG8S0eUKUJ4LBWOi8FOAgMJOP6exW9l5M9OjvQaGR7jvkokBK50XaZCsO66uLxABcuzvncV8A3YiJLUgw==" + "resolved": "10.0.10", + "contentHash": "WRjSRBfv6A6UjgjO8EQuLe9xqdICpkQx1hACUziCw4B2uGL+2jVhkFLq/G7rxRr3MGvqLo9B+nNdfIJ/5CYN7A==" } } } From 2a2218bcf734a0cede2a315246ec42843e65d0a1 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 1 Aug 2026 12:49:47 +0100 Subject: [PATCH 05/12] small fix --- src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs b/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs index a76a9ed6..0042af7a 100644 --- a/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs +++ b/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs @@ -39,7 +39,7 @@ internal partial class RarCrcStream : RarStream ) { // NOTE: we use the last FileHeader in a multipart volume to check CRC - throw new InvalidFormatException("file crc mismatch"); + throw new InvalidFormatException("file crc mismatch: " + _key); } return result; From b91289d82c732e50c8554d051726027c889cd476 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 1 Aug 2026 13:25:08 +0100 Subject: [PATCH 06/12] add large file tests and GZip/Lzw factory fixes --- src/SharpCompress/Factories/GZipFactory.cs | 12 +- src/SharpCompress/Factories/LzwFactory.cs | 12 +- tests/SharpCompress.Test/LargeArchiveTests.cs | 187 ++++++++++++++++++ tests/TestArchives/Archives/Large/Large.7z | Bin 0 -> 10012 bytes tests/TestArchives/Archives/Large/Large.gz | Bin 0 -> 162850 bytes tests/TestArchives/Archives/Large/Large.rar | Bin 0 -> 3147 bytes .../TestArchives/Archives/Large/Large.tar.gz | Bin 0 -> 163062 bytes tests/TestArchives/Archives/Large/Large.zip | Bin 0 -> 162830 bytes tests/TestArchives/Archives/Large/README.md | 38 ++++ 9 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 tests/SharpCompress.Test/LargeArchiveTests.cs create mode 100644 tests/TestArchives/Archives/Large/Large.7z create mode 100644 tests/TestArchives/Archives/Large/Large.gz create mode 100644 tests/TestArchives/Archives/Large/Large.rar create mode 100644 tests/TestArchives/Archives/Large/Large.tar.gz create mode 100644 tests/TestArchives/Archives/Large/Large.zip create mode 100644 tests/TestArchives/Archives/Large/README.md diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index bda6ef1f..4649f0fc 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -151,13 +151,16 @@ public class GZipFactory CompressionContext.FromStream(sharpCompressStream).WithReaderOptions(options) ) ); - if (TarArchive.IsTarFile(testStream)) + var isTarArchive = TarArchive.IsTarFile(testStream); + + // The TAR probe can consume arbitrary compressed input before it rejects a stream. + sharpCompressStream.Rewind(); + sharpCompressStream.StopRecording(); + if (isTarArchive) { - sharpCompressStream.StopRecording(); reader = new TarReader(sharpCompressStream, options, CompressionType.GZip); return true; } - sharpCompressStream.StopRecording(); reader = OpenReader(sharpCompressStream, options); return true; } @@ -185,12 +188,13 @@ public class GZipFactory var tarReader = await new TarFactory() .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) .ConfigureAwait(false); + sharpCompressStream.Rewind(); + sharpCompressStream.StopRecording(); if (tarReader is not null) { return tarReader; } - sharpCompressStream.StopRecording(); return await OpenAsyncReader(sharpCompressStream, options, cancellationToken) .ConfigureAwait(false); } diff --git a/src/SharpCompress/Factories/LzwFactory.cs b/src/SharpCompress/Factories/LzwFactory.cs index bd5569d1..da475d8a 100644 --- a/src/SharpCompress/Factories/LzwFactory.cs +++ b/src/SharpCompress/Factories/LzwFactory.cs @@ -65,14 +65,17 @@ public class LzwFactory : Factory, IReaderFactory ) ) { - if (TarArchive.IsTarFile(testStream)) + var isTarArchive = TarArchive.IsTarFile(testStream); + + // The TAR probe can consume arbitrary compressed input before it rejects a stream. + sharpCompressStream.Rewind(); + sharpCompressStream.StopRecording(); + if (isTarArchive) { - sharpCompressStream.StopRecording(); reader = new TarReader(sharpCompressStream, options, CompressionType.Lzw); return true; } } - sharpCompressStream.StopRecording(); reader = OpenReader(sharpCompressStream, options); return true; } @@ -100,12 +103,13 @@ public class LzwFactory : Factory, IReaderFactory var tarReader = await new TarFactory() .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) .ConfigureAwait(false); + sharpCompressStream.Rewind(); + sharpCompressStream.StopRecording(); if (tarReader is not null) { return tarReader; } - sharpCompressStream.StopRecording(); return await OpenAsyncReader(sharpCompressStream, options, cancellationToken) .ConfigureAwait(false); } diff --git a/tests/SharpCompress.Test/LargeArchiveTests.cs b/tests/SharpCompress.Test/LargeArchiveTests.cs new file mode 100644 index 00000000..fc94c935 --- /dev/null +++ b/tests/SharpCompress.Test/LargeArchiveTests.cs @@ -0,0 +1,187 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using SharpCompress.Archives; +using SharpCompress.Crypto; +using SharpCompress.Readers; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test; + +[Collection(LargeArchiveCollection.Name)] +public class LargeArchiveTests : TestBase +{ + private const long LargeFileSize = 64L * 1024 * 1024; + private const uint LargeFileCrc = 0xF9081EB0; + private const int BufferSize = 64 * 1024; + + [Theory] + [InlineData("Large/Large.zip")] + [InlineData("Large/Large.tar")] + [InlineData("Large/Large.gz")] + [InlineData("Large/Large.rar")] + [InlineData("Large/Large.7z")] + public void OpenArchive_ShouldStreamLargeEntry(string fixtureName) + { + using var stream = File.OpenRead(GetArchiveFixturePath(fixtureName)); + using var archive = ArchiveFactory.OpenArchive(stream); + + VerifyArchive(archive); + } + + [Theory] + [InlineData("Large/Large.zip")] + [InlineData("Large/Large.tar")] + [InlineData("Large/Large.gz")] + [InlineData("Large/Large.rar")] + [InlineData("Large/Large.7z")] + public async Task OpenAsyncArchive_ShouldStreamLargeEntry(string fixtureName) + { + await using var stream = new AsyncOnlyStream( + File.OpenRead(await GetArchiveFixturePathAsync(fixtureName)) + ); + await using var archive = await ArchiveFactory.OpenAsyncArchive(stream); + + var entry = await GetSingleEntryAsync(archive); + await using var entryStream = await entry.OpenEntryStreamAsync(); + await VerifyContentAsync(entry.Key, entryStream); + } + + [Theory] + [InlineData("Large/Large.zip")] + [InlineData("Large/Large.tar")] + [InlineData("Large/Large.gz")] + [InlineData("Large/Large.rar")] + [InlineData("Large/Large.tar.gz")] + public void OpenReader_ShouldStreamLargeEntry(string fixtureName) + { + using var stream = File.OpenRead(GetFixturePath(fixtureName)); + using var reader = ReaderFactory.OpenReader(stream); + + VerifyReader(reader); + } + + [Theory] + [InlineData("Large/Large.zip")] + [InlineData("Large/Large.tar")] + [InlineData("Large/Large.gz")] + [InlineData("Large/Large.rar")] + [InlineData("Large/Large.tar.gz")] + public async Task OpenAsyncReader_ShouldStreamLargeEntry(string fixtureName) + { + await using var stream = new AsyncOnlyStream(File.OpenRead(GetFixturePath(fixtureName))); + await using var reader = await ReaderFactory.OpenAsyncReader(stream); + + Assert.True(await reader.MoveToNextEntryAsync()); + Assert.False(reader.Entry.IsDirectory); + await using var entryStream = await reader.OpenEntryStreamAsync(); + await VerifyContentAsync(reader.Entry.Key, entryStream); + Assert.False(await reader.MoveToNextEntryAsync()); + } + + private static string GetFixturePath(string fixtureName) => + Path.Combine(TEST_ARCHIVES_PATH, fixtureName); + + private string GetArchiveFixturePath(string fixtureName) => + fixtureName == "Large/Large.tar" ? MaterializeTarFixture() : GetFixturePath(fixtureName); + + private async Task GetArchiveFixturePathAsync(string fixtureName) => + fixtureName == "Large/Large.tar" + ? await MaterializeTarFixtureAsync() + : GetFixturePath(fixtureName); + + private string MaterializeTarFixture() + { + var tarPath = Path.Combine(SCRATCH_FILES_PATH, "Large.tar"); + using var compressedStream = File.OpenRead(GetFixturePath("Large/Large.tar.gz")); + using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress); + using var tarStream = File.Create(tarPath); + gzipStream.CopyTo(tarStream); + return tarPath; + } + + private async Task MaterializeTarFixtureAsync() + { + var tarPath = Path.Combine(SCRATCH_FILES_PATH, "Large.tar"); + using var compressedStream = File.OpenRead(GetFixturePath("Large/Large.tar.gz")); + using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress); + using var tarStream = File.Create(tarPath); + await gzipStream.CopyToAsync(tarStream); + return tarPath; + } + + private static void VerifyArchive(IArchive archive) + { + var entry = Assert.Single(archive.Entries); + + Assert.False(entry.IsDirectory); + using var entryStream = entry.OpenEntryStream(); + VerifyContent(entry.Key, entryStream); + } + + private static async Task GetSingleEntryAsync(IAsyncArchive archive) + { + IArchiveEntry? entry = null; + await foreach (var candidate in archive.EntriesAsync) + { + Assert.Null(entry); + entry = candidate; + } + + return entry ?? throw new InvalidOperationException("The archive contains no entries."); + } + + private static void VerifyReader(IReader reader) + { + Assert.True(reader.MoveToNextEntry()); + Assert.False(reader.Entry.IsDirectory); + using var entryStream = reader.OpenEntryStream(); + VerifyContent(reader.Entry.Key, entryStream); + Assert.False(reader.MoveToNextEntry()); + } + + private static void VerifyContent(string? key, Stream entryStream) + { + Assert.Equal("large.bin", key); + + using var crcStream = new Crc32Stream(Stream.Null); + var buffer = new byte[BufferSize]; + long length = 0; + int bytesRead; + while ((bytesRead = entryStream.Read(buffer, 0, buffer.Length)) > 0) + { + crcStream.Write(buffer, 0, bytesRead); + length += bytesRead; + } + + Assert.Equal(LargeFileSize, length); + Assert.Equal(LargeFileCrc, crcStream.Crc); + } + + private static async Task VerifyContentAsync(string? key, Stream entryStream) + { + Assert.Equal("large.bin", key); + + using var crcStream = new Crc32Stream(Stream.Null); + var buffer = new byte[BufferSize]; + long length = 0; + int bytesRead; + while ((bytesRead = await entryStream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await crcStream.WriteAsync(buffer, 0, bytesRead); + length += bytesRead; + } + + Assert.Equal(LargeFileSize, length); + Assert.Equal(LargeFileCrc, crcStream.Crc); + } +} + +[CollectionDefinition(LargeArchiveCollection.Name, DisableParallelization = true)] +public sealed class LargeArchiveCollection +{ + public const string Name = "Large archive fixtures"; +} diff --git a/tests/TestArchives/Archives/Large/Large.7z b/tests/TestArchives/Archives/Large/Large.7z new file mode 100644 index 0000000000000000000000000000000000000000..719ea18df023e2174e0eab883ff7aaf94708504b GIT binary patch literal 10012 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 F1ps&6m3ROE literal 0 HcmV?d00001 diff --git a/tests/TestArchives/Archives/Large/Large.rar b/tests/TestArchives/Archives/Large/Large.rar new file mode 100644 index 0000000000000000000000000000000000000000..6f0a65d484263ea26b0589e8bb2b542e390d6d8b GIT binary patch literal 3147 zcmWGaEK-zWXJlYm*eb}&$jZsU!N}ME0t}%urxvO+G5ww1(9poZ*3i(fpn+jYGZVu@ z?zjzd96uXe898$ji_%l|k}~r;S?_WiyM9#u8eqIH(M{jLv(bz5!;~%#g^7DRe>F>M zyjwr{daT~OfAx8B`|t1B`SIoD_UHTU=h@iV{jUG|@!!1raaR8&7#J9&kJk4vFfg$G zGhkp~5dU$2fq^0ZF#`hwMua<%=x literal 0 HcmV?d00001 diff --git a/tests/TestArchives/Archives/Large/Large.tar.gz b/tests/TestArchives/Archives/Large/Large.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..007fef083fe9e4d65cac12d5c3639a5659ff8303 GIT binary patch literal 163062 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 bj+TU@B_UNx!tIred7_sW+AA_}F|YstBaSne literal 0 HcmV?d00001 diff --git a/tests/TestArchives/Archives/Large/Large.zip b/tests/TestArchives/Archives/Large/Large.zip new file mode 100644 index 0000000000000000000000000000000000000000..5b07336fde030aff7517df489d4596cddafca01f GIT binary patch literal 162830 zcmWIWW@Zs#U}E54U|>)T*dWL8b5=DI0|NsC3nv2uLr!8*da7PhX5O2Vr+FI`L>M;g z4&KnMY{AI>M@8x+v+(xgjydaPnHhe}wz?nQdnwAQI7fBu@!Rt?E^m5XaL)4ioZ?Yp zGz>=5!Du!ZEeb|U!qJj&v?LrY2}eu9(UNeqBpfXXM@z!dl5n&n94!e)OTy8TaI_>G zEeS_U!qJj&v?LrY2}eu9(UNeqBpfXXM@z!dl5n&n94!e)OTy8TaI_>GEeS_U!qJj& zv?LrY2}eu9(UNeqBpfXXM@z!dl5n&n94!e)OTy8TaI_>GEeS_U!qJj&v?LrY2}eu9 z(UNeqBpfXXM@z!dl5n&n94!e)OTy8TaI_>GEeS_U!qJj&v?LrY2}eu9(UNeqBpfXX zM@z!dl8_oDp?r0b2k5{+hA02C-B;YJe!utoC@~raqv>EY8;lkOqb1>JNjO>(j+TU@ zCE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{p zmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)X zS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)n%aX9&)=~w07~rS>Ta`m- zY13#tj)v}NjW$}Njn-(RCE;jEI9d{pmV~1v;b=)XS`vJ zNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@ zCE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{p zmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)X zS`vGEeS_U!qJj&v?LrY2}iTRXh}F)5{{OHqb1>JNjO>(j+TU@CE;jE zI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v z;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1vAuUV7M1MX`&|!cLf9l_I zT&1N=qwzQzx}!DPXpJ^nqm7n?qb1>JNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJ zNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v z;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vGEeS_U!qJj&v?LrY2}eu9(UNeq zBpfXXM@z!dl5n&n94!e)OTy8TaI_>GEeS_U!qJj&v?LrY2}eu9(UNeqBpl5Kqb1>J zNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@ zCE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{p zmV~1v;b=)XS`vGEeS_U!qJj&v?LrY2}eu9(UNeq zBpfXXM@z!dl5n&n94!e)OTy8TaI_>GEeS_U!qJj&v?LrY2}eu9(UNeqBpfXXM@z!d zl5n&n94!e)OTy8TaI_>GEeS_U!qJj&v?LrY2}eu9(UNeqBpfXXM@z!dl5n&n94!e) zOTy8TaI_>GEeS_U!qJj&v?LrY2}eu9(UNeqBpfXXM@zy%QWDM+5AbGWl4BNOU}E54 zU|>)T*dWL8b5=DI0|NsC3nz$VU|?WmU|?9%2;$@<7Nw`^C1vIXc(byB1Q;0@8O#|N J7!HE9002S{jOhRX literal 0 HcmV?d00001 diff --git a/tests/TestArchives/Archives/Large/README.md b/tests/TestArchives/Archives/Large/README.md new file mode 100644 index 00000000..4992f31b --- /dev/null +++ b/tests/TestArchives/Archives/Large/README.md @@ -0,0 +1,38 @@ +# Large Test Archives + +Each fixture contains one `large.bin` entry with a 67,108,864-byte (64 MiB) +deterministic, repeated text pattern with a 2 KiB `0xFF` prefix. The entry CRC-32 is +`f9081eb0`. + +| Fixture | Archive API | Reader API | +| --- | --- | --- | +| `Large.zip` | Yes | Yes | +| Generated `Large.tar` | Yes | N/A | +| `Large.gz` | Yes | Yes | +| `Large.rar` | Yes | Yes | +| `Large.7z` | Yes | No | +| `Large.tar.gz` | No | Yes | + +The compressible payload keeps the compressed fixtures small while requiring a full +64 MiB decompression to validate each API. The Archive API test expands `Large.tar.gz` +to a scratch `Large.tar` before exercising raw TAR support, so the 64 MiB TAR file is +not committed. + +## Regenerating + +The fixtures were created with `zip`, `tar`, `gzip`, RAR 7.22, and 7-Zip. Run these +commands from a temporary directory after replacing `` with the repository root: + +```sh +yes "SharpCompress large fixture" | head -c 67108864 > large.bin +printf '\377%.0s' {1..2048} > prefix.bin +dd if=prefix.bin of=large.bin bs=2048 count=1 conv=notrunc +touch -t 202001010000 large.bin +mkdir -p /tests/TestArchives/Archives/Large +zip -X -9 -j /tests/TestArchives/Archives/Large/Large.zip large.bin +COPYFILE_DISABLE=1 tar -cf large.tar large.bin +gzip -9 -c large.bin > /tests/TestArchives/Archives/Large/Large.gz +gzip -n -9 -c large.tar > /tests/TestArchives/Archives/Large/Large.tar.gz +rar a -ma5 -m5 -ep /tests/TestArchives/Archives/Large/Large.rar large.bin +7z a -t7z -mx=9 /tests/TestArchives/Archives/Large/Large.7z large.bin +``` From d391cf21e15938731bbc2f68e225e1b9821bee59 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 1 Aug 2026 13:26:48 +0100 Subject: [PATCH 07/12] finalize check in --- tests/SharpCompress.Test/LargeArchiveTests.cs | 14 ++++++++------ tests/TestArchives/Archives/Large/README.md | 7 +++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/SharpCompress.Test/LargeArchiveTests.cs b/tests/SharpCompress.Test/LargeArchiveTests.cs index fc94c935..761abda6 100644 --- a/tests/SharpCompress.Test/LargeArchiveTests.cs +++ b/tests/SharpCompress.Test/LargeArchiveTests.cs @@ -26,7 +26,7 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.7z")] public void OpenArchive_ShouldStreamLargeEntry(string fixtureName) { - using var stream = File.OpenRead(GetArchiveFixturePath(fixtureName)); + using var stream = File.OpenRead(GetMaterializedFixturePath(fixtureName)); using var archive = ArchiveFactory.OpenArchive(stream); VerifyArchive(archive); @@ -41,7 +41,7 @@ public class LargeArchiveTests : TestBase public async Task OpenAsyncArchive_ShouldStreamLargeEntry(string fixtureName) { await using var stream = new AsyncOnlyStream( - File.OpenRead(await GetArchiveFixturePathAsync(fixtureName)) + File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) ); await using var archive = await ArchiveFactory.OpenAsyncArchive(stream); @@ -58,7 +58,7 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.tar.gz")] public void OpenReader_ShouldStreamLargeEntry(string fixtureName) { - using var stream = File.OpenRead(GetFixturePath(fixtureName)); + using var stream = File.OpenRead(GetMaterializedFixturePath(fixtureName)); using var reader = ReaderFactory.OpenReader(stream); VerifyReader(reader); @@ -72,7 +72,9 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.tar.gz")] public async Task OpenAsyncReader_ShouldStreamLargeEntry(string fixtureName) { - await using var stream = new AsyncOnlyStream(File.OpenRead(GetFixturePath(fixtureName))); + await using var stream = new AsyncOnlyStream( + File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) + ); await using var reader = await ReaderFactory.OpenAsyncReader(stream); Assert.True(await reader.MoveToNextEntryAsync()); @@ -85,10 +87,10 @@ public class LargeArchiveTests : TestBase private static string GetFixturePath(string fixtureName) => Path.Combine(TEST_ARCHIVES_PATH, fixtureName); - private string GetArchiveFixturePath(string fixtureName) => + private string GetMaterializedFixturePath(string fixtureName) => fixtureName == "Large/Large.tar" ? MaterializeTarFixture() : GetFixturePath(fixtureName); - private async Task GetArchiveFixturePathAsync(string fixtureName) => + private async Task GetMaterializedFixturePathAsync(string fixtureName) => fixtureName == "Large/Large.tar" ? await MaterializeTarFixtureAsync() : GetFixturePath(fixtureName); diff --git a/tests/TestArchives/Archives/Large/README.md b/tests/TestArchives/Archives/Large/README.md index 4992f31b..0797db5d 100644 --- a/tests/TestArchives/Archives/Large/README.md +++ b/tests/TestArchives/Archives/Large/README.md @@ -7,16 +7,15 @@ deterministic, repeated text pattern with a 2 KiB `0xFF` prefix. The entry CRC-3 | Fixture | Archive API | Reader API | | --- | --- | --- | | `Large.zip` | Yes | Yes | -| Generated `Large.tar` | Yes | N/A | +| Generated `Large.tar` | Yes | Yes | | `Large.gz` | Yes | Yes | | `Large.rar` | Yes | Yes | | `Large.7z` | Yes | No | | `Large.tar.gz` | No | Yes | The compressible payload keeps the compressed fixtures small while requiring a full -64 MiB decompression to validate each API. The Archive API test expands `Large.tar.gz` -to a scratch `Large.tar` before exercising raw TAR support, so the 64 MiB TAR file is -not committed. +64 MiB decompression to validate each API. The tests expand `Large.tar.gz` to a scratch +`Large.tar` before exercising raw TAR support, so the 64 MiB TAR file is not committed. ## Regenerating From 369ab91522f81e14efa90ee368687f2015fc1788 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 3 Aug 2026 16:01:58 +0100 Subject: [PATCH 08/12] fix windows build --- tests/SharpCompress.Test/LargeArchiveTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/SharpCompress.Test/LargeArchiveTests.cs b/tests/SharpCompress.Test/LargeArchiveTests.cs index 761abda6..18b427e2 100644 --- a/tests/SharpCompress.Test/LargeArchiveTests.cs +++ b/tests/SharpCompress.Test/LargeArchiveTests.cs @@ -40,13 +40,24 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.7z")] public async Task OpenAsyncArchive_ShouldStreamLargeEntry(string fixtureName) { +#if LEGACY_DOTNET + using var stream = new AsyncOnlyStream( + File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) + ); +#else await using var stream = new AsyncOnlyStream( File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) ); +#endif await using var archive = await ArchiveFactory.OpenAsyncArchive(stream); var entry = await GetSingleEntryAsync(archive); + +#if LEGACY_DOTNET + using var entryStream = await entry.OpenEntryStreamAsync(); +#else await using var entryStream = await entry.OpenEntryStreamAsync(); +#endif await VerifyContentAsync(entry.Key, entryStream); } @@ -72,14 +83,25 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.tar.gz")] public async Task OpenAsyncReader_ShouldStreamLargeEntry(string fixtureName) { +#if LEGACY_DOTNET + using var stream = new AsyncOnlyStream( + File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) + ); +#else await using var stream = new AsyncOnlyStream( File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) ); +#endif await using var reader = await ReaderFactory.OpenAsyncReader(stream); Assert.True(await reader.MoveToNextEntryAsync()); Assert.False(reader.Entry.IsDirectory); + +#if LEGACY_DOTNET + using var entryStream = await reader.OpenEntryStreamAsync(); +#else await using var entryStream = await reader.OpenEntryStreamAsync(); +#endif await VerifyContentAsync(reader.Entry.Key, entryStream); Assert.False(await reader.MoveToNextEntryAsync()); } From 4a71ec898bb06e9536a0c219f722dfa8a6f5b35b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 3 Aug 2026 16:38:27 +0100 Subject: [PATCH 09/12] fixes found by gh and sol --- src/SharpCompress/Factories/GZipFactory.cs | 19 ++++++++++--- src/SharpCompress/Factories/LzwFactory.cs | 18 +++++++++--- tests/SharpCompress.Test/LargeArchiveTests.cs | 16 +++++++++++ .../Tar/TarReaderAsyncTests.cs | 28 +++++++++++++++---- 4 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index 4649f0fc..7dfdd313 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -185,14 +185,25 @@ public class GZipFactory } sharpCompressStream.Rewind(); - var tarReader = await new TarFactory() - .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) + using var testStream = SharpCompressStream.CreateNonDisposing( + await options + .Providers.CreateDecompressStreamAsync( + CompressionType.GZip, + SharpCompressStream.CreateNonDisposing(sharpCompressStream), + CompressionContext.FromStream(sharpCompressStream).WithReaderOptions(options), + cancellationToken + ) + .ConfigureAwait(false) + ); + var isTarArchive = await TarArchive + .IsTarFileAsync(testStream, cancellationToken) .ConfigureAwait(false); + sharpCompressStream.Rewind(); sharpCompressStream.StopRecording(); - if (tarReader is not null) + if (isTarArchive) { - return tarReader; + return new TarReader(sharpCompressStream, options, CompressionType.GZip); } return await OpenAsyncReader(sharpCompressStream, options, cancellationToken) diff --git a/src/SharpCompress/Factories/LzwFactory.cs b/src/SharpCompress/Factories/LzwFactory.cs index da475d8a..098ec002 100644 --- a/src/SharpCompress/Factories/LzwFactory.cs +++ b/src/SharpCompress/Factories/LzwFactory.cs @@ -100,14 +100,24 @@ public class LzwFactory : Factory, IReaderFactory } sharpCompressStream.Rewind(); - var tarReader = await new TarFactory() - .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) + using var testStream = SharpCompressStream.CreateNonDisposing( + await options + .Providers.CreateDecompressStreamAsync( + CompressionType.Lzw, + SharpCompressStream.CreateNonDisposing(sharpCompressStream), + cancellationToken + ) + .ConfigureAwait(false) + ); + var isTarArchive = await TarArchive + .IsTarFileAsync(testStream, cancellationToken) .ConfigureAwait(false); + sharpCompressStream.Rewind(); sharpCompressStream.StopRecording(); - if (tarReader is not null) + if (isTarArchive) { - return tarReader; + return new TarReader(sharpCompressStream, options, CompressionType.Lzw); } return await OpenAsyncReader(sharpCompressStream, options, cancellationToken) diff --git a/tests/SharpCompress.Test/LargeArchiveTests.cs b/tests/SharpCompress.Test/LargeArchiveTests.cs index 761abda6..09090f6a 100644 --- a/tests/SharpCompress.Test/LargeArchiveTests.cs +++ b/tests/SharpCompress.Test/LargeArchiveTests.cs @@ -84,6 +84,22 @@ public class LargeArchiveTests : TestBase Assert.False(await reader.MoveToNextEntryAsync()); } + [Fact] + public async Task OpenAsyncReader_WithGZipExtensionHint_ShouldStreamLargeTarEntry() + { + using var file = File.OpenRead(GetFixturePath("Large/Large.tar.gz")); + await using var stream = new AsyncOnlyStream(new ForwardOnlyStream(file)); + var options = new ReaderOptions { ExtensionHint = "gz" }; + + await using var reader = await ReaderFactory.OpenAsyncReader(stream, options); + + Assert.True(await reader.MoveToNextEntryAsync()); + Assert.False(reader.Entry.IsDirectory); + await using var entryStream = await reader.OpenEntryStreamAsync(); + await VerifyContentAsync(reader.Entry.Key, entryStream); + Assert.False(await reader.MoveToNextEntryAsync()); + } + private static string GetFixturePath(string fixtureName) => Path.Combine(TEST_ARCHIVES_PATH, fixtureName); diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index 90bce78e..c329d9d2 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -47,22 +47,38 @@ public class TarReaderAsyncTests : ReaderTests await ReadAsync("Tar.tar.Z", CompressionType.Lzw); [Theory] - [InlineData("Tar.tar.gz", "gz", CompressionType.GZip)] - [InlineData("Tar.tar.Z", "z", CompressionType.Lzw)] + [InlineData("Tar.tar.gz", "gz", CompressionType.GZip, false)] + [InlineData("Tar.tar.gz", "gz", CompressionType.GZip, true)] + [InlineData("Tar.tar.Z", "z", CompressionType.Lzw, false)] + [InlineData("Tar.tar.Z", "z", CompressionType.Lzw, true)] public async ValueTask ReaderFactory_ExtensionHint_PreservesCompressedTarDetection_Async( string archiveName, string extensionHint, - CompressionType compressionType + CompressionType compressionType, + bool useForwardOnlyStream ) { - using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + Stream source = useForwardOnlyStream ? new ForwardOnlyStream(file) : file; + await using var stream = new AsyncOnlyStream(source); var options = ReaderOptions.ForExternalStream.WithExtensionHint(extensionHint); await using var reader = await ReaderFactory.OpenAsyncReader(stream, options); Assert.Equal(ArchiveType.Tar, reader.Type); - Assert.True(await reader.MoveToNextEntryAsync()); - Assert.Equal(compressionType, reader.Entry.CompressionType); + var entryCount = 0; + while (await reader.MoveToNextEntryAsync()) + { + entryCount++; + Assert.Equal(compressionType, reader.Entry.CompressionType); + if (!reader.Entry.IsDirectory) + { + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); + } + } + + Assert.True(entryCount > 0); + VerifyFiles(); } [Fact] From 62d1b5dcc82d32dcc4254f0d1601767e8a04a08b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 4 Aug 2026 09:11:26 +0100 Subject: [PATCH 10/12] Provide a better way to manage async disposable streams --- .../Archives/IArchiveEntryExtensions.cs | 9 ++--- src/SharpCompress/Common/EntryStream.Async.cs | 4 +-- src/SharpCompress/Common/EntryStream.cs | 2 +- src/SharpCompress/IO/AsyncDisposableStream.cs | 36 +++++++++++++++++++ src/SharpCompress/IO/AsyncDisposeScope.cs | 36 +++++++++++++++++++ .../IO/ConfiguredAsyncDisposeScope.cs | 19 ++++++++++ .../Polyfills/StreamExtensions.cs | 14 ++++++++ .../Readers/AbstractReader.Async.cs | 15 +------- .../Readers/IAsyncReaderExtensions.cs | 6 ---- .../BZip2/BZip2StreamAsyncTests.cs | 14 +------- .../GZip/GZipCrcExtractionTests.cs | 5 +-- tests/SharpCompress.Test/LargeArchiveTests.cs | 23 ++---------- .../Mocks/AsyncOnlyStream.cs | 3 +- .../Mocks/FlushOnDisposeStream.cs | 5 ++- .../Mocks/ForwardOnlyStream.cs | 7 ++-- tests/SharpCompress.Test/Mocks/TestStream.cs | 7 ++-- .../Mocks/ThrowOnFlushStream.cs | 3 +- .../Mocks/TruncatedStream.cs | 3 +- tests/SharpCompress.Test/ReaderTests.cs | 12 ++----- .../Tar/TarArchiveAsyncTests.cs | 11 ++---- .../Tar/TarReaderAsyncTests.cs | 5 --- .../Tar/TarWriterNonSeekableTests.cs | 7 ++-- .../SharpCompress.Test/Zip/Zip64AsyncTests.cs | 11 ------ .../Zip/ZipCrcExtractionTests.cs | 10 ++---- .../Zip/ZipReaderAsyncTests.cs | 8 ----- .../Zip/ZipWriterNonSeekableTests.cs | 7 ++-- 26 files changed, 141 insertions(+), 141 deletions(-) create mode 100644 src/SharpCompress/IO/AsyncDisposableStream.cs create mode 100644 src/SharpCompress/IO/AsyncDisposeScope.cs create mode 100644 src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index 502a419e..c0e58656 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -108,15 +108,12 @@ public static class IArchiveEntryExtensions throw new ExtractionException("Entry is a file directory and cannot be extracted."); } -#if LEGACY_DOTNET - using var entryStream = await archiveEntry + var entryStream = await archiveEntry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); -#else - await using var entryStream = await archiveEntry - .OpenEntryStreamAsync(cancellationToken) + await using var entryStreamScope = entryStream + .DisposeAsyncScope() .ConfigureAwait(false); -#endif var checkedStream = options is null ? entryStream : IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options); diff --git a/src/SharpCompress/Common/EntryStream.Async.cs b/src/SharpCompress/Common/EntryStream.Async.cs index a58afe70..b7112da7 100644 --- a/src/SharpCompress/Common/EntryStream.Async.cs +++ b/src/SharpCompress/Common/EntryStream.Async.cs @@ -17,7 +17,6 @@ public partial class EntryStream _completed = true; } -#if !LEGACY_DOTNET public override async ValueTask DisposeAsync() { if (_isDisposed) @@ -43,9 +42,8 @@ public partial class EntryStream } } await base.DisposeAsync().ConfigureAwait(false); - await _stream.DisposeAsync().ConfigureAwait(false); + await _stream.DisposeAsyncCompat().ConfigureAwait(false); } -#endif public override async Task ReadAsync( byte[] buffer, diff --git a/src/SharpCompress/Common/EntryStream.cs b/src/SharpCompress/Common/EntryStream.cs index b8ccf339..ed4e8fc6 100644 --- a/src/SharpCompress/Common/EntryStream.cs +++ b/src/SharpCompress/Common/EntryStream.cs @@ -8,7 +8,7 @@ using SharpCompress.Readers; namespace SharpCompress.Common; -public partial class EntryStream : Stream +public partial class EntryStream : AsyncDisposableStream { private readonly IReader _reader; private readonly Stream _stream; diff --git a/src/SharpCompress/IO/AsyncDisposableStream.cs b/src/SharpCompress/IO/AsyncDisposableStream.cs new file mode 100644 index 00000000..4c7fafbe --- /dev/null +++ b/src/SharpCompress/IO/AsyncDisposableStream.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +/// +/// A that is guaranteed to be asynchronously disposable on every target framework. +/// +/// +/// +/// On .NET Framework 4.8 and .NET Standard 2.0, has no DisposeAsync. +/// Microsoft.Bcl.AsyncInterfaces supplies the interface on those +/// targets but cannot retrofit it onto the BCL's , and C# will not accept an +/// extension method for the pattern - await using requires a reachable instance +/// DisposeAsync. Deriving from this class instead of therefore makes a type +/// usable with await using uniformly, with no conditional compilation at the call site. +/// +/// +/// The fallback below is the same behaviour as the BCL's own default , +/// so a derived type may call await base.DisposeAsync() unconditionally on any target. +/// +/// +public abstract class AsyncDisposableStream : Stream +#if NO_STREAM_DISPOSEASYNC + , IAsyncDisposable +#endif +{ +#if NO_STREAM_DISPOSEASYNC + public virtual ValueTask DisposeAsync() + { + Dispose(); + return default; + } +#endif +} diff --git a/src/SharpCompress/IO/AsyncDisposeScope.cs b/src/SharpCompress/IO/AsyncDisposeScope.cs new file mode 100644 index 00000000..d8167a6c --- /dev/null +++ b/src/SharpCompress/IO/AsyncDisposeScope.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +/// +/// Makes any resource usable with await using, disposing it asynchronously when the runtime type +/// supports it and synchronously otherwise. +/// +/// +/// Needed for locals whose static type is (or another type that +/// only sometimes has DisposeAsync), where await using cannot bind directly on +/// .NET Framework 4.8 / .NET Standard 2.0. Unlike a compile-time guard, this picks the asynchronous path +/// based on the runtime type, so a stream that really is asynchronously disposable is disposed that way on +/// every target framework. Prefer deriving from where the type is ours. +/// +internal readonly struct AsyncDisposeScope(IDisposable? resource) : IAsyncDisposable +{ + public ValueTask DisposeAsync() + { + if (resource is IAsyncDisposable asyncDisposable) + { + return asyncDisposable.DisposeAsync(); + } + + resource?.Dispose(); + return default; + } + + /// + /// Mirrors ConfiguredAsyncDisposable so await using can specify context capture without + /// boxing this struct through . + /// + public ConfiguredAsyncDisposeScope ConfigureAwait(bool continueOnCapturedContext) => + new(resource, continueOnCapturedContext); +} \ No newline at end of file diff --git a/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs b/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs new file mode 100644 index 00000000..f9d631c9 --- /dev/null +++ b/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +internal readonly struct ConfiguredAsyncDisposeScope(IDisposable? resource, bool continueOnCapturedContext) +{ + public ConfiguredValueTaskAwaitable DisposeAsync() + { + if (resource is IAsyncDisposable asyncDisposable) + { + return asyncDisposable.DisposeAsync().ConfigureAwait(continueOnCapturedContext); + } + + resource?.Dispose(); + return default(ValueTask).ConfigureAwait(continueOnCapturedContext); + } +} diff --git a/src/SharpCompress/Polyfills/StreamExtensions.cs b/src/SharpCompress/Polyfills/StreamExtensions.cs index c6e66b82..d4e80630 100644 --- a/src/SharpCompress/Polyfills/StreamExtensions.cs +++ b/src/SharpCompress/Polyfills/StreamExtensions.cs @@ -25,6 +25,20 @@ public static class StreamExtensions public void Skip() => stream.CopyTo(Stream.Null); + /// + /// Returns a scope that disposes this stream when awaited, asynchronously where the runtime type + /// supports it. Lets await using be written against a -typed local on + /// every target framework. + /// + internal AsyncDisposeScope DisposeAsyncScope() => new(stream); + + /// + /// Disposes this stream, asynchronously where the runtime type supports it. Use where the static + /// type is , which has no DisposeAsync on .NET Framework 4.8 / + /// .NET Standard 2.0. + /// + internal ValueTask DisposeAsyncCompat() => new AsyncDisposeScope(stream).DisposeAsync(); + public async ValueTask SkipAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/SharpCompress/Readers/AbstractReader.Async.cs b/src/SharpCompress/Readers/AbstractReader.Async.cs index 8628c0c2..969c73e6 100644 --- a/src/SharpCompress/Readers/AbstractReader.Async.cs +++ b/src/SharpCompress/Readers/AbstractReader.Async.cs @@ -104,13 +104,8 @@ public abstract partial class AbstractReader } } //don't know the size so we have to try to decompress to skip -#if LEGACY_DOTNET - using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); - await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); -#else await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); -#endif } public async ValueTask WriteEntryToAsync( @@ -139,19 +134,11 @@ public abstract partial class AbstractReader private async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken) { -#if LEGACY_DOTNET - using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); + await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); var sourceStream = WrapWithProgress(s, Entry); await sourceStream .CopyToAsync(writeStream, Options.BufferSize, cancellationToken) .ConfigureAwait(false); -#else - await using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); - var sourceStream = WrapWithProgress(s, Entry); - await sourceStream - .CopyToAsync(writeStream, Options.BufferSize, cancellationToken) - .ConfigureAwait(false); -#endif } public async ValueTask OpenEntryStreamAsync( diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs index 7515b257..5b4e667b 100644 --- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -108,15 +108,9 @@ public static class IAsyncReaderExtensions CancellationToken cancellationToken ) { -#if LEGACY_DOTNET - using var entryStream = await reader - .OpenEntryStreamAsync(cancellationToken) - .ConfigureAwait(false); -#else await using var entryStream = await reader .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); -#endif var checkedStream = IEntryExtensions.WrapWithChecksumValidation( reader.Entry, entryStream, diff --git a/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs b/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs index d5b2ef78..fc7e649c 100644 --- a/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs +++ b/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs @@ -151,19 +151,8 @@ public class BZip2StreamAsyncTests Assert.True(compressed.Length > 0); // Decompress and verify -#if LEGACY_DOTNET + // MemoryStream has nothing to dispose asynchronously using (var readStream = new MemoryStream(compressed)) - { - using ( - var bzip2Stream = await BZip2Stream.CreateAsync( - new AsyncOnlyStream(readStream), - SharpCompress.Compressors.CompressionMode.Decompress, - false - ) - ) - { -#else - await using (var readStream = new MemoryStream(compressed)) { await using ( var bzip2Stream = await BZip2Stream.CreateAsync( @@ -173,7 +162,6 @@ public class BZip2StreamAsyncTests ) ) { -#endif var result = new StringBuilder(); var buffer = new byte[256]; int bytesRead; diff --git a/tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs b/tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs index 2e98c962..acabcb4b 100644 --- a/tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs @@ -68,11 +68,8 @@ public class GZipCrcExtractionTests : TestBase [Fact] public async Task GZipArchive_WriteToFileAsync_Throws_On_Crc_Mismatch() { -#if LEGACY_DOTNET + // MemoryStream has nothing to dispose asynchronously using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true)); -#else - await using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true)); -#endif await using var archive = await GZipArchive.OpenAsyncArchive(stream); var entry = await archive.EntriesAsync.SingleAsync(); var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString()); diff --git a/tests/SharpCompress.Test/LargeArchiveTests.cs b/tests/SharpCompress.Test/LargeArchiveTests.cs index 18b427e2..50f1cfec 100644 --- a/tests/SharpCompress.Test/LargeArchiveTests.cs +++ b/tests/SharpCompress.Test/LargeArchiveTests.cs @@ -40,24 +40,15 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.7z")] public async Task OpenAsyncArchive_ShouldStreamLargeEntry(string fixtureName) { -#if LEGACY_DOTNET - using var stream = new AsyncOnlyStream( - File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) - ); -#else await using var stream = new AsyncOnlyStream( File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) ); -#endif await using var archive = await ArchiveFactory.OpenAsyncArchive(stream); var entry = await GetSingleEntryAsync(archive); -#if LEGACY_DOTNET - using var entryStream = await entry.OpenEntryStreamAsync(); -#else - await using var entryStream = await entry.OpenEntryStreamAsync(); -#endif + var entryStream = await entry.OpenEntryStreamAsync(); + await using var entryStreamScope = entryStream.DisposeAsyncScope(); await VerifyContentAsync(entry.Key, entryStream); } @@ -83,25 +74,15 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.tar.gz")] public async Task OpenAsyncReader_ShouldStreamLargeEntry(string fixtureName) { -#if LEGACY_DOTNET - using var stream = new AsyncOnlyStream( - File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) - ); -#else await using var stream = new AsyncOnlyStream( File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) ); -#endif await using var reader = await ReaderFactory.OpenAsyncReader(stream); Assert.True(await reader.MoveToNextEntryAsync()); Assert.False(reader.Entry.IsDirectory); -#if LEGACY_DOTNET - using var entryStream = await reader.OpenEntryStreamAsync(); -#else await using var entryStream = await reader.OpenEntryStreamAsync(); -#endif await VerifyContentAsync(reader.Entry.Key, entryStream); Assert.False(await reader.MoveToNextEntryAsync()); } diff --git a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs index 232f60ee..9dec5e5a 100644 --- a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs +++ b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs @@ -2,10 +2,11 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; -public class AsyncOnlyStream(Stream stream, bool disposeStream = true) : Stream +public class AsyncOnlyStream(Stream stream, bool disposeStream = true) : AsyncDisposableStream { private readonly Stream _stream = stream ?? throw new ArgumentNullException(nameof(stream)); diff --git a/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs b/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs index 63f20702..28dff313 100644 --- a/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs +++ b/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; @@ -8,7 +9,7 @@ namespace SharpCompress.Test.Mocks; // CryptoStream doesn't always trigger the Flush, so this class is used instead // See https://referencesource.microsoft.com/#mscorlib/system/security/cryptography/cryptostream.cs,141 -public class FlushOnDisposeStream(Stream innerStream) : Stream +public class FlushOnDisposeStream(Stream innerStream) : AsyncDisposableStream { public override bool CanRead => innerStream.CanRead; @@ -48,12 +49,10 @@ public class FlushOnDisposeStream(Stream innerStream) : Stream base.Dispose(disposing); } -#if !LEGACY_DOTNET public override async ValueTask DisposeAsync() { await innerStream.FlushAsync(); innerStream.Close(); await base.DisposeAsync(); } -#endif } diff --git a/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs b/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs index 32961ebc..f6b7ec8e 100644 --- a/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs +++ b/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; @@ -9,7 +10,7 @@ namespace SharpCompress.Test.Mocks; /// A forward-only stream wrapper that delegates directly to the underlying stream /// without any buffering. Supports reading and writing but not seeking. /// -public class ForwardOnlyStream : Stream +public class ForwardOnlyStream : AsyncDisposableStream { private readonly Stream _stream; private bool _isDisposed; @@ -142,17 +143,15 @@ public class ForwardOnlyStream : Stream } } -#if !LEGACY_DOTNET public override async ValueTask DisposeAsync() { if (!_isDisposed) { - await _stream.DisposeAsync(); + await _stream.DisposeAsyncCompat(); _isDisposed = true; } await base.DisposeAsync(); } -#endif private void ThrowIfDisposed() { diff --git a/tests/SharpCompress.Test/Mocks/TestStream.cs b/tests/SharpCompress.Test/Mocks/TestStream.cs index d3ff48a8..5061afc3 100644 --- a/tests/SharpCompress.Test/Mocks/TestStream.cs +++ b/tests/SharpCompress.Test/Mocks/TestStream.cs @@ -2,10 +2,11 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; -public class TestStream(Stream stream, bool read, bool write, bool seek) : Stream +public class TestStream(Stream stream, bool read, bool write, bool seek) : AsyncDisposableStream { public TestStream(Stream stream) : this(stream, stream.CanRead, stream.CanWrite, stream.CanSeek) { } @@ -50,14 +51,14 @@ public class TestStream(Stream stream, bool read, bool write, bool seek) : Strea Memory buffer, CancellationToken cancellationToken = default ) => stream.ReadAsync(buffer, cancellationToken); +#endif public override async ValueTask DisposeAsync() { await base.DisposeAsync(); - await stream.DisposeAsync(); + await stream.DisposeAsyncCompat(); IsDisposed = true; } -#endif public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin); diff --git a/tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs b/tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs index 2cf1a84c..911762a5 100644 --- a/tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs +++ b/tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; @@ -9,7 +10,7 @@ namespace SharpCompress.Test.Mocks; /// A stream wrapper that throws NotSupportedException on Flush() calls. /// This is used to test that archive iteration handles streams that don't support flushing. /// -public class ThrowOnFlushStream : Stream +public class ThrowOnFlushStream : AsyncDisposableStream { private readonly Stream inner; diff --git a/tests/SharpCompress.Test/Mocks/TruncatedStream.cs b/tests/SharpCompress.Test/Mocks/TruncatedStream.cs index 8699a430..55037f2e 100644 --- a/tests/SharpCompress.Test/Mocks/TruncatedStream.cs +++ b/tests/SharpCompress.Test/Mocks/TruncatedStream.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; @@ -7,7 +8,7 @@ namespace SharpCompress.Test.Mocks; /// A stream wrapper that truncates the underlying stream after reading a specified number of bytes. /// Used for testing error handling when streams end prematurely. /// -public class TruncatedStream : Stream +public class TruncatedStream : AsyncDisposableStream { private readonly Stream baseStream; private readonly long truncateAfterBytes; diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 4c9024c9..64469219 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -159,18 +159,12 @@ public abstract class ReaderTests : TestBase { using var file = File.OpenRead(testArchive); -#if !LEGACY_DOTNET - await using var protectedStream = SharpCompressStream.CreateNonDisposing( + // SharpCompressStream is not yet an AsyncDisposableStream, so scope its disposal + var protectedStream = SharpCompressStream.CreateNonDisposing( new ForwardOnlyStream(file, options.BufferSize) ); + await using var protectedStreamScope = protectedStream.DisposeAsyncScope(); await using var testStream = new TestStream(protectedStream); -#else - - using var protectedStream = SharpCompressStream.CreateNonDisposing( - new ForwardOnlyStream(file, options.BufferSize) - ); - using var testStream = new TestStream(protectedStream); -#endif await using ( var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(testStream), diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 3a1b95cc..dabc0fa2 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -187,10 +187,6 @@ public class TarArchiveAsyncTests : ArchiveTests } } } -#if LEGACY_DOTNET - //add a delay because old .net sucks on DisposeAsync - await Task.Delay(TimeSpan.FromSeconds(1)); -#endif } [Fact] @@ -338,11 +334,8 @@ public class TarArchiveAsyncTests : ArchiveTests { ++numberOfEntries; -#if LEGACY_DOTNET - using var tarEntryStream = await entry.OpenEntryStreamAsync(); -#else - await using var tarEntryStream = await entry.OpenEntryStreamAsync(); -#endif + var tarEntryStream = await entry.OpenEntryStreamAsync(); + await using var tarEntryStreamScope = tarEntryStream.DisposeAsyncScope(); using var testFileStream = new MemoryStream(); await tarEntryStream.CopyToAsync(testFileStream); Assert.Equal(testBytes.Length, testFileStream.Length); diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index 90bce78e..9307b7d3 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -337,13 +337,8 @@ public class TarReaderAsyncTests : ReaderTests Assert.True(await reader.MoveToNextEntryAsync()); Assert.Equal("inner.tar.gz", reader.Entry.Key); -#if !LEGACY_DOTNET await using var entryStream = await reader.OpenEntryStreamAsync(); await using var flushingStream = new FlushOnDisposeStream(entryStream); -#else - using var entryStream = await reader.OpenEntryStreamAsync(); - using var flushingStream = new FlushOnDisposeStream(entryStream); -#endif // Extract inner.tar.gz await using var innerReader = await ReaderFactory.OpenAsyncReader(flushingStream); diff --git a/tests/SharpCompress.Test/Tar/TarWriterNonSeekableTests.cs b/tests/SharpCompress.Test/Tar/TarWriterNonSeekableTests.cs index 454c20a6..218533db 100644 --- a/tests/SharpCompress.Test/Tar/TarWriterNonSeekableTests.cs +++ b/tests/SharpCompress.Test/Tar/TarWriterNonSeekableTests.cs @@ -89,11 +89,8 @@ public class TarWriterNonSeekableTests { var entry = fileEntries.Single(e => e.Key == name); using var extracted = new MemoryStream(); -#if LEGACY_DOTNET - using (var entryStream = await entry.OpenEntryStreamAsync()) -#else - await using (var entryStream = await entry.OpenEntryStreamAsync()) -#endif + var entryStream = await entry.OpenEntryStreamAsync(); + await using (entryStream.DisposeAsyncScope()) { await entryStream.CopyToAsync(extracted); } diff --git a/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs index 5124b6b0..626265cd 100644 --- a/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs @@ -198,11 +198,7 @@ public class Zip64AsyncTests : WriterTests count++; lastKey = rd.Entry.Key; -#if LEGACY_DOTNET - using var entryStream = await rd.OpenEntryStreamAsync(); -#else await using var entryStream = await rd.OpenEntryStreamAsync(); -#endif if (rd.Entry.Key == "small") { using var ms = new MemoryStream(); @@ -337,17 +333,10 @@ public class Zip64AsyncTests : WriterTests ); while (await rd.MoveToNextEntryAsync()) { -#if LEGACY_DOTNET - using (var entryStream = await rd.OpenEntryStreamAsync()) - { - await entryStream.SkipEntryAsync(); - } -#else await using (var entryStream = await rd.OpenEntryStreamAsync()) { await entryStream.SkipEntryAsync(); } -#endif count++; if (prev != null) { diff --git a/tests/SharpCompress.Test/Zip/ZipCrcExtractionTests.cs b/tests/SharpCompress.Test/Zip/ZipCrcExtractionTests.cs index cd0e610c..4a61b150 100644 --- a/tests/SharpCompress.Test/Zip/ZipCrcExtractionTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipCrcExtractionTests.cs @@ -122,11 +122,8 @@ public class ZipCrcExtractionTests : ArchiveTests using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false); using var archive = ZipArchive.OpenArchive(zipStream); var entry = archive.Entries.Single(e => !e.IsDirectory); -#if LEGACY_DOTNET + // MemoryStream has nothing to dispose asynchronously using var destination = new MemoryStream(); -#else - await using var destination = new MemoryStream(); -#endif var exception = await Assert.ThrowsAsync(async () => await entry.WriteToAsync(destination, new ExtractionOptions { CheckCrc = true }) @@ -141,11 +138,8 @@ public class ZipCrcExtractionTests : ArchiveTests using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false); using var archive = ZipArchive.OpenArchive(zipStream); var entry = archive.Entries.Single(e => !e.IsDirectory); -#if LEGACY_DOTNET + // MemoryStream has nothing to dispose asynchronously using var destination = new MemoryStream(); -#else - await using var destination = new MemoryStream(); -#endif await entry.WriteToAsync(destination, new ExtractionOptions { CheckCrc = false }); diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index 4c7cd627..78f70b28 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -315,11 +315,7 @@ public class ZipReaderAsyncTests : ReaderTests { if (!reader.Entry.IsDirectory) { -#if LEGACY_DOTNET - using var entryStream = await reader.OpenEntryStreamAsync(); -#else await using var entryStream = await reader.OpenEntryStreamAsync(); -#endif // Read some data var buffer = new byte[1024]; await entryStream.ReadAsync(buffer, 0, buffer.Length); @@ -342,11 +338,7 @@ public class ZipReaderAsyncTests : ReaderTests { if (!reader.Entry.IsDirectory) { -#if LEGACY_DOTNET - using var entryStream = await reader.OpenEntryStreamAsync(); -#else await using var entryStream = await reader.OpenEntryStreamAsync(); -#endif // Read some data var buffer = new byte[1024]; await entryStream.ReadAsync(buffer, 0, buffer.Length); diff --git a/tests/SharpCompress.Test/Zip/ZipWriterNonSeekableTests.cs b/tests/SharpCompress.Test/Zip/ZipWriterNonSeekableTests.cs index 4dd10b57..95cdcc56 100644 --- a/tests/SharpCompress.Test/Zip/ZipWriterNonSeekableTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipWriterNonSeekableTests.cs @@ -163,11 +163,8 @@ public class ZipWriterNonSeekableTests { var entry = archive.Entries.Single(e => e.Key == name); using var extracted = new MemoryStream(); -#if LEGACY_DOTNET - using (var entryStream = await entry.OpenEntryStreamAsync()) -#else - await using (var entryStream = await entry.OpenEntryStreamAsync()) -#endif + var entryStream = await entry.OpenEntryStreamAsync(); + await using (entryStream.DisposeAsyncScope()) { await entryStream.CopyToAsync(extracted); } From 693921ec730588fbf5940e02465465334aae6b7c Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 4 Aug 2026 09:12:00 +0100 Subject: [PATCH 11/12] fmt --- src/SharpCompress/IO/AsyncDisposeScope.cs | 2 +- src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/IO/AsyncDisposeScope.cs b/src/SharpCompress/IO/AsyncDisposeScope.cs index d8167a6c..9d4538e4 100644 --- a/src/SharpCompress/IO/AsyncDisposeScope.cs +++ b/src/SharpCompress/IO/AsyncDisposeScope.cs @@ -33,4 +33,4 @@ internal readonly struct AsyncDisposeScope(IDisposable? resource) : IAsyncDispos /// public ConfiguredAsyncDisposeScope ConfigureAwait(bool continueOnCapturedContext) => new(resource, continueOnCapturedContext); -} \ No newline at end of file +} diff --git a/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs b/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs index f9d631c9..9ee5e368 100644 --- a/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs +++ b/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs @@ -4,7 +4,10 @@ using System.Threading.Tasks; namespace SharpCompress.IO; -internal readonly struct ConfiguredAsyncDisposeScope(IDisposable? resource, bool continueOnCapturedContext) +internal readonly struct ConfiguredAsyncDisposeScope( + IDisposable? resource, + bool continueOnCapturedContext +) { public ConfiguredValueTaskAwaitable DisposeAsync() { From 20ea4a3e03a746a16919931e8ce447dda8393251 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 4 Aug 2026 09:27:48 +0100 Subject: [PATCH 12/12] Do need the csproj change --- src/SharpCompress/SharpCompress.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SharpCompress/SharpCompress.csproj b/src/SharpCompress/SharpCompress.csproj index 991eccb9..c57cb722 100644 --- a/src/SharpCompress/SharpCompress.csproj +++ b/src/SharpCompress/SharpCompress.csproj @@ -31,6 +31,9 @@ $(DefineConstants);LEGACY_DOTNET + + $(DefineConstants);NO_STREAM_DISPOSEASYNC + true true