Preserve async disposal across all supported frameworks

This commit is contained in:
Adam Hathcock
2026-10-13 12:57:14 +01:00
parent 3bbe0cc93f
commit e58fcb23e5
29 changed files with 141 additions and 149 deletions

View File

@@ -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));

View File

@@ -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"),

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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"),

View File

@@ -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"),

View File

@@ -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);
}

View File

@@ -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(

View File

@@ -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; }

View File

@@ -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);
}
}

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -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
}
/// <summary>
/// Read data from the stream.

View File

@@ -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;

View File

@@ -160,11 +160,7 @@ public class PpmdStream : Stream, IAsyncDisposable
}
catch
{
#if !STREAM_DISPOSEASYNC
instance.Dispose();
#else
await instance.DisposeAsync().ConfigureAwait(false);
#endif
throw;
}
}

View File

@@ -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
}

View File

@@ -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.
/// </summary>
internal sealed partial class ProgressReportingStream : Stream
internal sealed partial class ProgressReportingStream : AsyncDisposableStream
{
private readonly Stream _baseStream;
private readonly IProgress<ProgressReport> _progress;

View File

@@ -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(

View File

@@ -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
}
}

View File

@@ -17,6 +17,9 @@ namespace SharpCompress.IO;
/// stream and usage scenario.
/// </remarks>
public partial class SharpCompressStream : Stream, IStreamStack
#if !STREAM_DISPOSEASYNC
, IAsyncDisposable
#endif
{
public virtual Stream BaseStream() => stream;

View File

@@ -104,11 +104,7 @@ public abstract partial class AbstractReader<TEntry, TVolume>
}
}
//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<TEntry, TVolume>
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)

View File

@@ -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(

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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)]

View File

@@ -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()

View File

@@ -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()