From e58fcb23e5de31221fd048c239b17044900f6daa Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 13 Oct 2026 12:57:14 +0100 Subject: [PATCH] Preserve async disposal across all supported frameworks --- docs/API.md | 2 + .../Archives/GZip/GZipArchive.Async.cs | 31 ++++++----- .../Archives/IArchiveEntryExtensions.cs | 9 ++-- .../IWritableAsyncArchiveExtensions.cs | 7 +-- .../Archives/Tar/TarArchive.Async.cs | 22 +++----- .../Archives/Zip/ZipArchive.Async.cs | 22 +++----- .../Common/Rar/Headers/MarkHeader.Async.cs | 6 +-- .../SevenZip/SevenZipStreamsCompressor.cs | 8 --- .../Common/Tar/TarReadOnlySubStream.cs | 5 +- src/SharpCompress/Common/Volume.Async.cs | 7 +-- .../Common/Zip/WinzipAesCryptoStream.Async.cs | 4 +- .../Common/Zip/WinzipAesCryptoStream.cs | 3 +- .../Compressors/Deflate/ZlibStream.Async.cs | 9 +++- .../Compressors/Deflate/ZlibStream.cs | 3 ++ .../Compressors/PPMd/PpmdStream.cs | 4 -- .../IO/ProgressReportingStream.Async.cs | 4 +- .../IO/ProgressReportingStream.cs | 2 +- .../IO/SeekableSharpCompressStream.Async.cs | 11 ++-- .../IO/SharpCompressStream.Async.cs | 11 +++- src/SharpCompress/IO/SharpCompressStream.cs | 3 ++ .../Readers/AbstractReader.Async.cs | 8 --- .../Readers/IAsyncReaderExtensions.cs | 18 ++----- src/SharpCompress/Utility.Async.cs | 7 +-- .../Writers/IWriterExtensions.cs | 7 +-- .../Writers/Zip/ZipWriter.Async.cs | 4 -- .../AsyncDisposalCompatibilityTests.cs | 53 +++++++++++++++++++ .../Streams/WinzipAesCryptoStreamTests.cs | 10 ---- .../Tar/TarArchiveAsyncTests.cs | 5 +- .../Zip/ZipArchiveAsyncTests.cs | 5 +- 29 files changed, 141 insertions(+), 149 deletions(-) create mode 100644 tests/SharpCompress.Test/Streams/AsyncDisposalCompatibilityTests.cs diff --git a/docs/API.md b/docs/API.md index b6df93d9..295feeda 100644 --- a/docs/API.md +++ b/docs/API.md @@ -694,6 +694,8 @@ using (var archive = ZipArchive.OpenArchive("archive.zip", options)) ### Async Extract with Cancellation +Async archive and reader/writer operations dispose SharpCompress resources asynchronously on every target, including .NET Framework 4.8 and .NET Standard 2.0. `SharpCompressStream.DisposeAsync()` and `ZlibStream.DisposeAsync()` are available on every target; stream wrappers forward disposal to runtime `IAsyncDisposable` implementations where available, falling back to synchronous disposal only for streams that do not support it. + ```csharp var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromMinutes(5)); diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs index 3b6ee24d..032e33f3 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs @@ -25,10 +25,11 @@ public partial class GZipArchive CancellationToken cancellationToken = default ) { -#if !STREAM_DISPOSEASYNC +#if SYNC_ONLY using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); #else - await using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); + var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); + await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false); #endif await SaveToAsync(stream, new GZipWriterOptions(CompressionType.GZip), cancellationToken) .ConfigureAwait(false); @@ -47,24 +48,23 @@ public partial class GZipArchive { throw new InvalidFormatException("Only one entry is allowed in a GZip Archive"); } -#if !STREAM_DISPOSEASYNC - using var writer = new GZipWriter(stream, options); -#else await using var writer = new GZipWriter(stream, options); -#endif await foreach ( var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) ) { if (!entry.IsDirectory) { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry +#if SYNC_ONLY + using var entryStream = entry.OpenEntryStream(); #else - await using var entryStream = await entry -#endif + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); + await using var entryStreamScope = entryStream + .DisposeAsyncScope() + .ConfigureAwait(false); +#endif await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), @@ -77,13 +77,16 @@ public partial class GZipArchive } foreach (var entry in newEntries.Where(x => !x.IsDirectory)) { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry +#if SYNC_ONLY + using var entryStream = entry.OpenEntryStream(); #else - await using var entryStream = await entry -#endif + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); + await using var entryStreamScope = entryStream + .DisposeAsyncScope() + .ConfigureAwait(false); +#endif await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index 98ab3115..b4d858f2 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -77,13 +77,9 @@ public static partial class IArchiveEntryExtensions var entryStream = await archiveEntry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); -#if !STREAM_DISPOSEASYNC - using var entryStreamScope = entryStream; -#else await using var entryStreamScope = entryStream .DisposeAsyncScope() .ConfigureAwait(false); -#endif #endif var checkedStream = options is null ? entryStream @@ -168,10 +164,11 @@ public static partial class IArchiveEntryExtensions options, async (x, fm, ct) => { -#if !STREAM_DISPOSEASYNC +#if SYNC_ONLY using var fs = File.Open(x, fm); #else - await using var fs = File.Open(x, fm); + var fs = File.Open(x, fm); + await using var scope = fs.DisposeAsyncScope().ConfigureAwait(false); #endif await entry .WriteToAsync(fs, options.BufferSize, options, null, ct) diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index 7e32f1d2..d74b756b 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -83,11 +83,8 @@ public static class IWritableAsyncArchiveExtensions ) where TOptions : IWriterOptions { -#if !STREAM_DISPOSEASYNC - using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); -#else - await using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); -#endif + var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); + await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false); await writableArchive.SaveToAsync(stream, options, cancellationToken).ConfigureAwait(false); } } diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs index a318ab53..2c5e69e0 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -22,11 +22,7 @@ public partial class TarArchive CancellationToken cancellationToken = default ) { -#if !STREAM_DISPOSEASYNC - using var writer = new TarWriter(stream, options); -#else await using var writer = new TarWriter(stream, options); -#endif await foreach ( var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) ) @@ -43,13 +39,12 @@ public partial class TarArchive } else { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry -#else - await using var entryStream = await entry -#endif + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); + await using var entryStreamScope = entryStream + .DisposeAsyncScope() + .ConfigureAwait(false); await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), @@ -75,15 +70,12 @@ public partial class TarArchive } else { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); -#else - await using var entryStream = await entry - .OpenEntryStreamAsync(cancellationToken) + await using var entryStreamScope = entryStream + .DisposeAsyncScope() .ConfigureAwait(false); -#endif await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs index a65719a0..122112eb 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs @@ -84,11 +84,7 @@ public partial class ZipArchive CancellationToken cancellationToken = default ) { -#if !STREAM_DISPOSEASYNC - using var writer = new ZipWriter(stream, options); -#else await using var writer = new ZipWriter(stream, options); -#endif await foreach ( var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) ) @@ -105,13 +101,12 @@ public partial class ZipArchive } else { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry -#else - await using var entryStream = await entry -#endif + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); + await using var entryStreamScope = entryStream + .DisposeAsyncScope() + .ConfigureAwait(false); await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), @@ -135,13 +130,12 @@ public partial class ZipArchive } else { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry -#else - await using var entryStream = await entry -#endif + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); + await using var entryStreamScope = entryStream + .DisposeAsyncScope() + .ConfigureAwait(false); await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), diff --git a/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs b/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs index 5aae4c3c..52b0ad4f 100644 --- a/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs +++ b/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs @@ -122,11 +122,7 @@ internal partial class MarkHeader { if (!leaveStreamOpen) { -#if !STREAM_DISPOSEASYNC - stream.Dispose(); -#else - await stream.DisposeAsync().ConfigureAwait(false); -#endif + await stream.DisposeAsyncCompat().ConfigureAwait(false); } throw new InvalidFormatException("Error trying to read rar signature.", e); } diff --git a/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs index e769ea23..1b704e0c 100644 --- a/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs +++ b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs @@ -122,11 +122,7 @@ internal sealed class SevenZipStreamsCompressor(Stream outputStream) uint inputCrc2; long inputSize2; { -#if !STREAM_DISPOSEASYNC - using var lzma2Stream = new Lzma2EncoderStream( -#else await using var lzma2Stream = new Lzma2EncoderStream( -#endif outCrcStream, encoderProperties.DictionarySize, encoderProperties.NumFastBytes @@ -156,11 +152,7 @@ internal sealed class SevenZipStreamsCompressor(Stream outputStream) uint inputCrc; long inputSize; { -#if !STREAM_DISPOSEASYNC - using var lzmaStream = LzmaStream.Create(encoderProperties, false, outCrcStream); -#else await using var lzmaStream = LzmaStream.Create(encoderProperties, false, outCrcStream); -#endif properties = lzmaStream.Properties; (inputCrc, inputSize) = await CopyWithCrcAsync( diff --git a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs index ced9ea27..1d0608a2 100644 --- a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs +++ b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs @@ -1,10 +1,11 @@ using System; using System.IO; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Common.Tar; -internal class TarReadOnlySubStream : Stream +internal class TarReadOnlySubStream : AsyncDisposableStream { private readonly Stream _stream; @@ -34,7 +35,6 @@ internal class TarReadOnlySubStream : Stream base.Dispose(disposing); } -#if STREAM_DISPOSEASYNC public override async System.Threading.Tasks.ValueTask DisposeAsync() { if (_isDisposed) @@ -49,7 +49,6 @@ internal class TarReadOnlySubStream : Stream GC.SuppressFinalize(this); await base.DisposeAsync().ConfigureAwait(false); } -#endif private long BytesLeftToRead { get; set; } diff --git a/src/SharpCompress/Common/Volume.Async.cs b/src/SharpCompress/Common/Volume.Async.cs index 55d9b686..05984162 100644 --- a/src/SharpCompress/Common/Volume.Async.cs +++ b/src/SharpCompress/Common/Volume.Async.cs @@ -7,12 +7,7 @@ public abstract partial class Volume { public virtual async ValueTask DisposeAsync() { -#if !STREAM_DISPOSEASYNC - _actualStream.Dispose(); - await Task.CompletedTask.ConfigureAwait(false); -#else - await _actualStream.DisposeAsync().ConfigureAwait(false); -#endif + await _actualStream.DisposeAsyncCompat().ConfigureAwait(false); GC.SuppressFinalize(this); } } diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs index e9867989..242b8515 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs @@ -10,7 +10,6 @@ namespace SharpCompress.Common.Zip; internal partial class WinzipAesCryptoStream { -#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() { if (_isDisposed) @@ -31,11 +30,10 @@ internal partial class WinzipAesCryptoStream _authentication.Dispose(); _transform.Dispose(); _cipher.Dispose(); - await _stream.DisposeAsync().ConfigureAwait(false); + await _stream.DisposeAsyncCompat().ConfigureAwait(false); } await base.DisposeAsync().ConfigureAwait(false); } -#endif private async ValueTask VerifyAuthenticationAsync(CancellationToken cancellationToken) { diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs index 06b354af..b96ca1eb 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs @@ -3,10 +3,11 @@ using System.Buffers.Binary; using System.IO; using System.Runtime.ExceptionServices; using System.Security.Cryptography; +using SharpCompress.IO; namespace SharpCompress.Common.Zip; -internal partial class WinzipAesCryptoStream : Stream +internal partial class WinzipAesCryptoStream : AsyncDisposableStream { private const int BLOCK_SIZE_IN_BYTES = 16; private readonly Aes _cipher; diff --git a/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs b/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs index 8e433214..1021bbde 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs @@ -23,6 +23,9 @@ public partial class ZlibStream #if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() +#else + public virtual async ValueTask DisposeAsync() +#endif { if (_disposed) { @@ -33,9 +36,13 @@ public partial class ZlibStream { await _baseStream.DisposeAsync().ConfigureAwait(false); } +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); - } +#else + base.Dispose(); + GC.SuppressFinalize(this); #endif + } /// /// Read data from the stream. diff --git a/src/SharpCompress/Compressors/Deflate/ZlibStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibStream.cs index 580961d7..97c77538 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibStream.cs @@ -33,6 +33,9 @@ using SharpCompress.Common; namespace SharpCompress.Compressors.Deflate; public partial class ZlibStream : Stream +#if !STREAM_DISPOSEASYNC + , IAsyncDisposable +#endif { private readonly ZlibBaseStream _baseStream; private bool _disposed; diff --git a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs index d6a97a61..bcf39c90 100644 --- a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs +++ b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs @@ -160,11 +160,7 @@ public class PpmdStream : Stream, IAsyncDisposable } catch { -#if !STREAM_DISPOSEASYNC - instance.Dispose(); -#else await instance.DisposeAsync().ConfigureAwait(false); -#endif throw; } } diff --git a/src/SharpCompress/IO/ProgressReportingStream.Async.cs b/src/SharpCompress/IO/ProgressReportingStream.Async.cs index 449990f5..096c0a95 100644 --- a/src/SharpCompress/IO/ProgressReportingStream.Async.cs +++ b/src/SharpCompress/IO/ProgressReportingStream.Async.cs @@ -45,14 +45,12 @@ internal sealed partial class ProgressReportingStream } #endif -#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() { if (!_leaveOpen) { - await _baseStream.DisposeAsync().ConfigureAwait(false); + await _baseStream.DisposeAsyncCompat().ConfigureAwait(false); } await base.DisposeAsync().ConfigureAwait(false); } -#endif } diff --git a/src/SharpCompress/IO/ProgressReportingStream.cs b/src/SharpCompress/IO/ProgressReportingStream.cs index 8834a546..4eb73bab 100644 --- a/src/SharpCompress/IO/ProgressReportingStream.cs +++ b/src/SharpCompress/IO/ProgressReportingStream.cs @@ -8,7 +8,7 @@ namespace SharpCompress.IO; /// A stream wrapper that reports progress as data is read from the source. /// Used to track compression or extraction progress by wrapping the source stream. /// -internal sealed partial class ProgressReportingStream : Stream +internal sealed partial class ProgressReportingStream : AsyncDisposableStream { private readonly Stream _baseStream; private readonly IProgress _progress; diff --git a/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs b/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs index dd082d39..7bf2b650 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs @@ -30,12 +30,12 @@ internal sealed partial class SeekableSharpCompressStream ) => _stream.WriteAsync(buffer, cancellationToken); #endif -#if STREAM_DISPOSEASYNC - public override ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { if (_isDisposed) { - return base.DisposeAsync(); + await base.DisposeAsync().ConfigureAwait(false); + return; } if (ThrowOnDispose) { @@ -46,11 +46,10 @@ internal sealed partial class SeekableSharpCompressStream _isDisposed = true; if (!LeaveStreamOpen) { - _stream.Dispose(); + await _stream.DisposeAsyncCompat().ConfigureAwait(false); } - return base.DisposeAsync(); + await base.DisposeAsync().ConfigureAwait(false); } -#endif [Zomp.SyncMethodGenerator.CreateSyncVersion] public override Task WriteAsync( diff --git a/src/SharpCompress/IO/SharpCompressStream.Async.cs b/src/SharpCompress/IO/SharpCompressStream.Async.cs index 2b2057d7..47ba651e 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Async.cs @@ -253,6 +253,9 @@ public partial class SharpCompressStream #if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() +#else + public virtual async ValueTask DisposeAsync() +#endif { if (!isDisposed) { @@ -267,7 +270,7 @@ public partial class SharpCompressStream { if (!LeaveStreamOpen) { - await stream.DisposeAsync().ConfigureAwait(false); + await stream.DisposeAsyncCompat().ConfigureAwait(false); } } finally @@ -276,7 +279,11 @@ public partial class SharpCompressStream _ringBuffer = null; } } +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); - } +#else + base.Dispose(); + GC.SuppressFinalize(this); #endif + } } diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index 7a5b7777..d59c53c2 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -17,6 +17,9 @@ namespace SharpCompress.IO; /// stream and usage scenario. /// public partial class SharpCompressStream : Stream, IStreamStack +#if !STREAM_DISPOSEASYNC + , IAsyncDisposable +#endif { public virtual Stream BaseStream() => stream; diff --git a/src/SharpCompress/Readers/AbstractReader.Async.cs b/src/SharpCompress/Readers/AbstractReader.Async.cs index 7b75cac2..969c73e6 100644 --- a/src/SharpCompress/Readers/AbstractReader.Async.cs +++ b/src/SharpCompress/Readers/AbstractReader.Async.cs @@ -104,11 +104,7 @@ public abstract partial class AbstractReader } } //don't know the size so we have to try to decompress to skip -#if !STREAM_DISPOSEASYNC - using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); -#else await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); -#endif await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); } @@ -138,11 +134,7 @@ public abstract partial class AbstractReader private async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken) { -#if !STREAM_DISPOSEASYNC - using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); -#else await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); -#endif var sourceStream = WrapWithProgress(s, Entry); await sourceStream .CopyToAsync(writeStream, Options.BufferSize, cancellationToken) diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs index 12473db3..c962d116 100644 --- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -45,11 +45,8 @@ public static class IAsyncReaderExtensions options, async (x, fm, ct) => { -#if !STREAM_DISPOSEASYNC - using var fs = File.Open(x, fm); -#else - await using var fs = File.Open(x, fm); -#endif + var fs = File.Open(x, fm); + await using var scope = fs.DisposeAsyncScope().ConfigureAwait(false); await CopyEntryToAsync(reader, fs, options, ct).ConfigureAwait(false); }, cancellationToken @@ -87,11 +84,8 @@ public static class IAsyncReaderExtensions options, async (x, fm, ct) => { -#if !STREAM_DISPOSEASYNC - using var fs = File.Open(x, fm); -#else - await using var fs = File.Open(x, fm); -#endif + var fs = File.Open(x, fm); + await using var scope = fs.DisposeAsyncScope().ConfigureAwait(false); await CopyEntryToAsync(reader, fs, options, ct).ConfigureAwait(false); }, cancellationToken @@ -116,11 +110,7 @@ public static class IAsyncReaderExtensions CancellationToken cancellationToken ) { -#if !STREAM_DISPOSEASYNC - using var entryStream = await reader -#else await using var entryStream = await reader -#endif .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); var checkedStream = IEntryExtensions.WrapWithChecksumValidation( diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs index 7d1546c5..6e6debff 100644 --- a/src/SharpCompress/Utility.Async.cs +++ b/src/SharpCompress/Utility.Async.cs @@ -70,11 +70,8 @@ internal static partial class Utility ) { // Use ReadOnlySubStream to limit reading and leverage framework's CopyToAsync -#if !STREAM_DISPOSEASYNC - using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); -#else - await using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); -#endif + var limitedStream = new IO.ReadOnlySubStream(source, maxLength); + await using var scope = limitedStream.DisposeAsyncScope().ConfigureAwait(false); await limitedStream .CopyToAsync(destination, bufferSize ?? Constants.BufferSize, cancellationToken) .ConfigureAwait(false); diff --git a/src/SharpCompress/Writers/IWriterExtensions.cs b/src/SharpCompress/Writers/IWriterExtensions.cs index d6c94ea0..b1a082f9 100644 --- a/src/SharpCompress/Writers/IWriterExtensions.cs +++ b/src/SharpCompress/Writers/IWriterExtensions.cs @@ -77,11 +77,8 @@ public static class IWriterExtensions { throw new ArgumentException("Source does not exist: " + source.FullName); } -#if !STREAM_DISPOSEASYNC - using var stream = source.OpenRead(); -#else - await using var stream = source.OpenRead(); -#endif + var stream = source.OpenRead(); + await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false); await writer .WriteAsync(entryPath, stream, source.LastWriteTime, cancellationToken) .ConfigureAwait(false); diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs index 0f3bea53..ec0d9ca4 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs @@ -83,11 +83,7 @@ public partial class ZipWriter #if !SYNC_ONLY cancellationToken.ThrowIfCancellationRequested(); #endif -#if !STREAM_DISPOSEASYNC - using var output = await WriteToStreamAsync( -#else await using var output = await WriteToStreamAsync( -#endif entryPath, zipWriterEntryOptions, cancellationToken diff --git a/tests/SharpCompress.Test/Streams/AsyncDisposalCompatibilityTests.cs b/tests/SharpCompress.Test/Streams/AsyncDisposalCompatibilityTests.cs new file mode 100644 index 00000000..6407eda8 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/AsyncDisposalCompatibilityTests.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class AsyncDisposalCompatibilityTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async ValueTask SharpCompressStream_Forwards_Async_Disposal(bool seekable) + { + var source = new AsyncDisposeOnlyStream(seekable); + var wrapper = SharpCompressStream.Create(source, 128); + await ((IAsyncDisposable)wrapper).DisposeAsync().ConfigureAwait(false); + Assert.True(source.AsyncDisposed); + await ((IAsyncDisposable)wrapper).DisposeAsync().ConfigureAwait(false); + } + + [Fact] + public async ValueTask Stream_Disposal_Scope_Uses_Runtime_Async_Disposable() + { + var source = new AsyncDisposeOnlyStream(true); + Stream stream = source; + await using (stream.DisposeAsyncScope().ConfigureAwait(false)) { } + Assert.True(source.AsyncDisposed); + } + + private sealed class AsyncDisposeOnlyStream(bool seekable) : AsyncOnlyStream(new MemoryStream()) + { + public override bool CanSeek => seekable; + public bool AsyncDisposed { get; private set; } + + public override ValueTask DisposeAsync() + { + AsyncDisposed = true; + return base.DisposeAsync(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !AsyncDisposed) + { + throw new NotSupportedException("Synchronous disposal is not supported."); + } + base.Dispose(disposing); + } + } +} diff --git a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs index 861eaf20..463b49f0 100644 --- a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs @@ -25,12 +25,6 @@ public class WinzipAesCryptoStreamTests { foreach (var completion in new[] { "dispose", "retry", "dispose-async" }) { -#if LEGACY_DOTNET - if (completion == "dispose-async") - { - continue; - } -#endif yield return [length, tagBytesRead, corruption, completion]; } } @@ -82,12 +76,10 @@ public class WinzipAesCryptoStreamTests { stream.Dispose(); } -#if STREAM_DISPOSEASYNC else if (completion == "dispose-async") { await stream.DisposeAsync(); } -#endif else { Assert.Equal(0, await stream.ReadAsync(new byte[1], 0, 1, CancellationToken.None)); @@ -153,7 +145,6 @@ public class WinzipAesCryptoStreamTests stream.Dispose(); } -#if STREAM_DISPOSEASYNC [Fact] public async Task DisposeAsync_Verifies_Unread_Ciphertext() { @@ -171,7 +162,6 @@ public class WinzipAesCryptoStreamTests ); await stream.DisposeAsync(); } -#endif [Theory] [InlineData(false, 0)] diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index afa668ed..f31d6a84 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -286,7 +286,6 @@ public class TarArchiveAsyncTests : ArchiveTests Assert.Equal(0, opened.Length); } -#if STREAM_DISPOSEASYNC [Fact] public async ValueTask Tar_Save_New_Entry_Async_Only_Streams() { @@ -308,11 +307,11 @@ public class TarArchiveAsyncTests : ArchiveTests destination.Position = 0; await using var reopened = await TarArchive.OpenAsyncArchive(destination); var entry = await reopened.EntriesAsync.SingleAsync(); - await using var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); + var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); + await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false); using var reader = new StreamReader(stream); Assert.Equal("test", await reader.ReadToEndAsync()); } -#endif [Fact] public async ValueTask Tar_Random_Write_Add_Async() diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index be8c0bc8..710a4141 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -265,7 +265,6 @@ public class ZipArchiveAsyncTests : ArchiveTests Assert.Equal(0, opened.Length); } -#if STREAM_DISPOSEASYNC [Fact] public async ValueTask Zip_Save_New_Entry_Async_Only_Streams() { @@ -284,11 +283,11 @@ public class ZipArchiveAsyncTests : ArchiveTests destination.Position = 0; await using var reopened = await ZipArchive.OpenAsyncArchive(destination); var entry = await reopened.EntriesAsync.SingleAsync(); - await using var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); + var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); + await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false); using var reader = new StreamReader(stream); Assert.Equal("test", await reader.ReadToEndAsync()); } -#endif [Fact] public async ValueTask Zip_Deflate_Entry_Stream_Async()