From 94716a5ba9eac76742c5ac4b9c23124f3cdf6389 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 4 Feb 2026 09:20:43 +0000 Subject: [PATCH 1/3] add sync over async dispose --- .../Archives/SevenZip/SevenZipArchive.cs | 6 +++--- src/SharpCompress/Common/EntryStream.cs | 13 +++++++++++-- src/SharpCompress/Common/Tar/TarFilePart.cs | 18 +++++++++++++++++- .../Common/Tar/TarHeaderFactory.Async.cs | 6 +++++- .../Common/Tar/TarHeaderFactory.cs | 2 +- .../Common/Tar/TarReadOnlySubStream.cs | 13 +++++++++++-- .../Readers/AbstractReader.Async.cs | 6 +++++- src/SharpCompress/Readers/AbstractReader.cs | 13 ++++++------- .../Readers/Rar/RarReader.Async.cs | 14 +++++++++++--- src/SharpCompress/Readers/Rar/RarReader.cs | 8 ++++---- 10 files changed, 74 insertions(+), 25 deletions(-) diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index e9b27d6a..e55ff474 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -153,12 +153,12 @@ public partial class SevenZipArchive : AbstractArchive GetCompressedStreamAsync(CancellationToken cancellationToken = default) + { + if (_seekableStream != null) + { + var useSyncOverAsync = false; +#if LEGACY_DOTNET + useSyncOverAsync = true; +#endif + _seekableStream.Position = Header.DataStartPosition ?? 0; + return new ValueTask(new TarReadOnlySubStream(_seekableStream, Header.Size, useSyncOverAsync)); + } + return new ValueTask(Header.PackedStream.NotNull()); + } + internal override Stream? GetRawStream() => null; } diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs index de6422aa..158f61ce 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs @@ -36,7 +36,11 @@ internal static partial class TarHeaderFactory break; case StreamingMode.Streaming: { - header.PackedStream = new TarReadOnlySubStream(stream, header.Size); + var useSyncOverAsync = false; + #if LEGACY_DOTNET + useSyncOverAsync = true; + #endif + header.PackedStream = new TarReadOnlySubStream(stream, header.Size, useSyncOverAsync); } break; default: diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs index c141bc8e..6c60adc0 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs @@ -37,7 +37,7 @@ internal static partial class TarHeaderFactory break; case StreamingMode.Streaming: { - header.PackedStream = new TarReadOnlySubStream(stream, header.Size); + header.PackedStream = new TarReadOnlySubStream(stream, header.Size, false); } break; default: diff --git a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs index c1db57be..b3a37500 100644 --- a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs +++ b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs @@ -6,13 +6,15 @@ namespace SharpCompress.Common.Tar; internal class TarReadOnlySubStream : Stream { private readonly Stream _stream; + private readonly bool _useSyncOverAsyncDispose; private bool _isDisposed; private long _amountRead; - public TarReadOnlySubStream(Stream stream, long bytesToRead) + public TarReadOnlySubStream(Stream stream, long bytesToRead, bool useSyncOverAsyncDispose) { _stream = stream; + _useSyncOverAsyncDispose = useSyncOverAsyncDispose; BytesLeftToRead = bytesToRead; #if DEBUG_STREAMS this.DebugConstruct(typeof(TarReadOnlySubStream)); @@ -41,7 +43,14 @@ internal class TarReadOnlySubStream : Stream if (bytesInLastBlock != 0) { - _stream.Skip(512 - bytesInLastBlock); + if (_useSyncOverAsyncDispose) + { + _stream.SkipAsync(512 - bytesInLastBlock).GetAwaiter().GetResult(); + } + else + { + _stream.Skip(512 - bytesInLastBlock); + } } } } diff --git a/src/SharpCompress/Readers/AbstractReader.Async.cs b/src/SharpCompress/Readers/AbstractReader.Async.cs index 958045e0..979f701d 100644 --- a/src/SharpCompress/Readers/AbstractReader.Async.cs +++ b/src/SharpCompress/Readers/AbstractReader.Async.cs @@ -178,7 +178,11 @@ public abstract partial class AbstractReader .Parts.First() .GetCompressedStreamAsync(cancellationToken) .ConfigureAwait(false); - return CreateEntryStream(stream); + var useSyncOverAsync = false; +#if LEGACY_DOTNET + useSyncOverAsync = true; +#endif + return CreateEntryStream(stream, useSyncOverAsync); } internal virtual ValueTask NextEntryForCurrentStreamAsync() => diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index e969955a..21823de1 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -35,8 +35,6 @@ public abstract partial class AbstractReader : IReader, IAsyncR public ArchiveType ArchiveType { get; } - protected bool IsAsync => _entriesForCurrentReadStreamAsync is not null; - /// /// Current volume that the current entry resides in /// @@ -248,19 +246,20 @@ public abstract partial class AbstractReader : IReader, IAsyncR { throw new ArgumentException("WriteEntryTo or OpenEntryStream can only be called once."); } - var stream = GetEntryStream(); + var stream = GetEntryStream(false); _wroteCurrentEntry = true; return stream; } + /// /// Retains a reference to the entry stream, so we can check whether it completed later. /// - protected EntryStream CreateEntryStream(Stream? decompressed) => - new(this, decompressed.NotNull()); + protected EntryStream CreateEntryStream(Stream? decompressed, bool useSyncOverAsyncDispose) => + new(this, decompressed.NotNull(), useSyncOverAsyncDispose); - protected virtual EntryStream GetEntryStream() => - CreateEntryStream(Entry.Parts.First().GetCompressedStream()); + protected virtual EntryStream GetEntryStream(bool useSyncOverAsyncDispose) => + CreateEntryStream(Entry.Parts.First().GetCompressedStream(), useSyncOverAsyncDispose); #endregion diff --git a/src/SharpCompress/Readers/Rar/RarReader.Async.cs b/src/SharpCompress/Readers/Rar/RarReader.Async.cs index 0e1943d2..0aafc998 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.Async.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.Async.cs @@ -27,6 +27,11 @@ public abstract partial class RarReader CancellationToken cancellationToken = default ) { + + var useSyncOverAsync = false; +#if LEGACY_DOTNET + useSyncOverAsync = true; +#endif if (Entry.IsRedir) { throw new InvalidOperationException("no stream for redirect entry"); @@ -40,7 +45,8 @@ public abstract partial class RarReader return CreateEntryStream( await RarCrcStream .CreateAsync(UnpackV1.Value, Entry.FileHeader, stream, cancellationToken) - .ConfigureAwait(false) + .ConfigureAwait(false), + useSyncOverAsync ); } @@ -49,14 +55,16 @@ public abstract partial class RarReader return CreateEntryStream( await RarBLAKE2spStream .CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken) - .ConfigureAwait(false) + .ConfigureAwait(false), + useSyncOverAsync ); } return CreateEntryStream( await RarCrcStream .CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken) - .ConfigureAwait(false) + .ConfigureAwait(false), + useSyncOverAsync ); } } diff --git a/src/SharpCompress/Readers/Rar/RarReader.cs b/src/SharpCompress/Readers/Rar/RarReader.cs index cd0aebc9..f7fb05ca 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.cs @@ -110,7 +110,7 @@ public abstract partial class RarReader : AbstractReader CreateFilePartEnumerableForCurrentEntry() => Entry.Parts; - protected override EntryStream GetEntryStream() + protected override EntryStream GetEntryStream(bool useSyncOverAsyncDispose) { if (Entry.IsRedir) { @@ -122,17 +122,17 @@ public abstract partial class RarReader : AbstractReader 5) { return CreateEntryStream( - RarBLAKE2spStream.Create(UnpackV2017.Value, Entry.FileHeader, stream) + RarBLAKE2spStream.Create(UnpackV2017.Value, Entry.FileHeader, stream), useSyncOverAsyncDispose ); } - return CreateEntryStream(RarCrcStream.Create(UnpackV2017.Value, Entry.FileHeader, stream)); + return CreateEntryStream(RarCrcStream.Create(UnpackV2017.Value, Entry.FileHeader, stream), useSyncOverAsyncDispose); } // GetEntryStreamAsync moved to RarReader.Async.cs From b93ed79ef3b1694351e2cc0ad519ceeccb20206f Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 4 Feb 2026 11:26:47 +0000 Subject: [PATCH 2/3] another sync over async --- .../Common/Zip/WinzipAesCryptoStream.cs | 37 +++++++++++++------ .../Common/Zip/ZipFilePart.Async.cs | 7 +++- src/SharpCompress/Common/Zip/ZipFilePart.cs | 3 +- .../Zip/ZipReaderAsyncTests.cs | 12 +++--- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs index 5ceba087..8ce891f9 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Buffers.Binary; using System.IO; using System.Security.Cryptography; @@ -18,15 +19,17 @@ internal partial class WinzipAesCryptoStream : Stream private bool _isFinalBlock; private long _totalBytesLeftToRead; private bool _isDisposed; + private bool _useSyncOverAsyncDispose; internal WinzipAesCryptoStream( Stream stream, WinzipAesEncryptionData winzipAesEncryptionData, - long length - ) + long length, + bool useSyncOverAsyncDispose) { _stream = stream; _totalBytesLeftToRead = length; + _useSyncOverAsyncDispose = useSyncOverAsyncDispose; #if DEBUG_STREAMS this.DebugConstruct(typeof(WinzipAesCryptoStream)); @@ -75,20 +78,30 @@ internal partial class WinzipAesCryptoStream : Stream if (disposing) { // Read out last 10 auth bytes - catch exceptions for async-only streams - try - { - Span ten = stackalloc byte[10]; - _stream.ReadFully(ten); - } - catch (NotSupportedException) - { - // Stream may be async-only, auth bytes will be skipped - // This is acceptable when the entire stream has been read - } + if (_useSyncOverAsyncDispose) + { + var ten = ArrayPool.Shared.Rent(10); + try { + _stream.ReadFullyAsync(ten, 0, 10).GetAwaiter().GetResult(); + } finally { + ArrayPool.Shared.Return(ten); + } + } + else + { + Span ten = stackalloc byte[10]; + _stream.ReadFully(ten); + } _stream.Dispose(); } } + private async Task ReadAuthBytesAsync() + { + byte[] authBytes = new byte[10]; + await _stream.ReadFullyAsync(authBytes, 0, 10).ConfigureAwait(false); + } + public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs index 61c6e8e2..dbb9b322 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs @@ -99,10 +99,15 @@ internal abstract partial class ZipFilePart { if (Header.WinzipAesEncryptionData != null) { + var useSyncOverAsync = false; +#if LEGACY_DOTNET + useSyncOverAsync = true; +#endif return new WinzipAesCryptoStream( plainStream, Header.WinzipAesEncryptionData, - Header.CompressedSize - 10 + Header.CompressedSize - 10, + useSyncOverAsync ); } return plainStream; diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.cs b/src/SharpCompress/Common/Zip/ZipFilePart.cs index 88822249..43eac529 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.cs @@ -270,7 +270,8 @@ internal abstract partial class ZipFilePart : FilePart return new WinzipAesCryptoStream( plainStream, Header.WinzipAesEncryptionData, - Header.CompressedSize - 10 + Header.CompressedSize - 10, + false ); } return plainStream; diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index c93552cf..ece913bf 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Archives; using SharpCompress.Common; using SharpCompress.IO; using SharpCompress.Readers; @@ -252,14 +253,14 @@ public class ZipReaderAsyncTests : ReaderTests File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip")) ) ) - using ( - IReader baseReader = ZipReader.OpenReader( + + await using ( + var reader = await ReaderFactory.OpenAsyncReader( stream, new ReaderOptions { Password = "test" } ) ) { - IAsyncReader reader = (IAsyncReader)baseReader; while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) @@ -284,14 +285,13 @@ public class ZipReaderAsyncTests : ReaderTests File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip")) ) ) - using ( - IReader baseReader = ZipReader.OpenReader( + await using ( + var reader = await ReaderFactory.OpenAsyncReader( stream, new ReaderOptions { Password = "test" } ) ) { - IAsyncReader reader = (IAsyncReader)baseReader; while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) From 7e8005a9d8aaae301a03521e1690d623fbea59f6 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 4 Feb 2026 11:27:03 +0000 Subject: [PATCH 3/3] fmt --- .../Archives/SevenZip/SevenZipArchive.cs | 3 ++- src/SharpCompress/Common/Tar/TarFilePart.cs | 8 +++++-- .../Common/Tar/TarHeaderFactory.Async.cs | 12 ++++++---- .../Common/Tar/TarHeaderFactory.cs | 6 ++++- .../Common/Tar/TarReadOnlySubStream.cs | 16 ++++++------- .../Common/Zip/WinzipAesCryptoStream.cs | 24 +++++++++++-------- src/SharpCompress/Readers/AbstractReader.cs | 1 - .../Readers/Rar/RarReader.Async.cs | 1 - src/SharpCompress/Readers/Rar/RarReader.cs | 13 +++++++--- 9 files changed, 53 insertions(+), 31 deletions(-) diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index e55ff474..3f766cd8 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -186,7 +186,8 @@ public partial class SevenZipArchive : AbstractArchive GetCompressedStreamAsync(CancellationToken cancellationToken = default) + internal override ValueTask GetCompressedStreamAsync( + CancellationToken cancellationToken = default + ) { if (_seekableStream != null) { @@ -39,7 +41,9 @@ internal sealed class TarFilePart : FilePart useSyncOverAsync = true; #endif _seekableStream.Position = Header.DataStartPosition ?? 0; - return new ValueTask(new TarReadOnlySubStream(_seekableStream, Header.Size, useSyncOverAsync)); + return new ValueTask( + new TarReadOnlySubStream(_seekableStream, Header.Size, useSyncOverAsync) + ); } return new ValueTask(Header.PackedStream.NotNull()); } diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs index 158f61ce..ad6a357e 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs @@ -37,10 +37,14 @@ internal static partial class TarHeaderFactory case StreamingMode.Streaming: { var useSyncOverAsync = false; - #if LEGACY_DOTNET - useSyncOverAsync = true; - #endif - header.PackedStream = new TarReadOnlySubStream(stream, header.Size, useSyncOverAsync); +#if LEGACY_DOTNET + useSyncOverAsync = true; +#endif + header.PackedStream = new TarReadOnlySubStream( + stream, + header.Size, + useSyncOverAsync + ); } break; default: diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs index 6c60adc0..bcd3a5e9 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs @@ -37,7 +37,11 @@ internal static partial class TarHeaderFactory break; case StreamingMode.Streaming: { - header.PackedStream = new TarReadOnlySubStream(stream, header.Size, false); + header.PackedStream = new TarReadOnlySubStream( + stream, + header.Size, + false + ); } break; default: diff --git a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs index b3a37500..7edbed2f 100644 --- a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs +++ b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs @@ -43,14 +43,14 @@ internal class TarReadOnlySubStream : Stream if (bytesInLastBlock != 0) { - if (_useSyncOverAsyncDispose) - { - _stream.SkipAsync(512 - bytesInLastBlock).GetAwaiter().GetResult(); - } - else - { - _stream.Skip(512 - bytesInLastBlock); - } + if (_useSyncOverAsyncDispose) + { + _stream.SkipAsync(512 - bytesInLastBlock).GetAwaiter().GetResult(); + } + else + { + _stream.Skip(512 - bytesInLastBlock); + } } } } diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs index 8ce891f9..5434c58b 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs @@ -25,7 +25,8 @@ internal partial class WinzipAesCryptoStream : Stream Stream stream, WinzipAesEncryptionData winzipAesEncryptionData, long length, - bool useSyncOverAsyncDispose) + bool useSyncOverAsyncDispose + ) { _stream = stream; _totalBytesLeftToRead = length; @@ -78,20 +79,23 @@ internal partial class WinzipAesCryptoStream : Stream if (disposing) { // Read out last 10 auth bytes - catch exceptions for async-only streams - if (_useSyncOverAsyncDispose) + if (_useSyncOverAsyncDispose) + { + var ten = ArrayPool.Shared.Rent(10); + try { - var ten = ArrayPool.Shared.Rent(10); - try { _stream.ReadFullyAsync(ten, 0, 10).GetAwaiter().GetResult(); - } finally { - ArrayPool.Shared.Return(ten); - } } - else + finally { - Span ten = stackalloc byte[10]; - _stream.ReadFully(ten); + ArrayPool.Shared.Return(ten); } + } + else + { + Span ten = stackalloc byte[10]; + _stream.ReadFully(ten); + } _stream.Dispose(); } } diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index 21823de1..46b595b0 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -251,7 +251,6 @@ public abstract partial class AbstractReader : IReader, IAsyncR return stream; } - /// /// Retains a reference to the entry stream, so we can check whether it completed later. /// diff --git a/src/SharpCompress/Readers/Rar/RarReader.Async.cs b/src/SharpCompress/Readers/Rar/RarReader.Async.cs index 0aafc998..9dc50f11 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.Async.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.Async.cs @@ -27,7 +27,6 @@ public abstract partial class RarReader CancellationToken cancellationToken = default ) { - var useSyncOverAsync = false; #if LEGACY_DOTNET useSyncOverAsync = true; diff --git a/src/SharpCompress/Readers/Rar/RarReader.cs b/src/SharpCompress/Readers/Rar/RarReader.cs index f7fb05ca..cae92ee1 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.cs @@ -122,17 +122,24 @@ public abstract partial class RarReader : AbstractReader 5) { return CreateEntryStream( - RarBLAKE2spStream.Create(UnpackV2017.Value, Entry.FileHeader, stream), useSyncOverAsyncDispose + RarBLAKE2spStream.Create(UnpackV2017.Value, Entry.FileHeader, stream), + useSyncOverAsyncDispose ); } - return CreateEntryStream(RarCrcStream.Create(UnpackV2017.Value, Entry.FileHeader, stream), useSyncOverAsyncDispose); + return CreateEntryStream( + RarCrcStream.Create(UnpackV2017.Value, Entry.FileHeader, stream), + useSyncOverAsyncDispose + ); } // GetEntryStreamAsync moved to RarReader.Async.cs