diff --git a/Directory.Packages.props b/Directory.Packages.props index 18d207e9..b9457b1d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ - + @@ -9,16 +9,16 @@ - + - + diff --git a/build/packages.lock.json b/build/packages.lock.json index cf4e18fe..6db93011 100644 --- a/build/packages.lock.json +++ b/build/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Bullseye": { "type": "Direct", - "requested": "[6.2.0, )", - "resolved": "6.2.0", - "contentHash": "YWQxe+pvudGSzW1QqnRUlr9XV0xtlNvMxorVaFPqoMH8qAYNxniMQKGrPSiQkgEy0ckn/TAq98E+DIwTSR7vbQ==" + "requested": "[6.3.0, )", + "resolved": "6.3.0", + "contentHash": "yK14DF1aGvyGxIDFwApU1+YJcnXRmuVhGdz7js7eVTBnLvl/4rg20jb43YDhFvifcx7tRD4iOzF6ykoLdvWNfw==" }, "Glob": { "type": "Direct", @@ -36,9 +36,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -54,9 +54,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", diff --git a/docs/API.md b/docs/API.md index 99cc48d2..295feeda 100644 --- a/docs/API.md +++ b/docs/API.md @@ -107,7 +107,7 @@ The stream overloads of `DetectArchive` and `InspectArchive` preserve the suppli Stored (compression method 0) comments are supported up to 16 MiB of unpacked data; larger or unknown-size comments throw `InvalidFormatException`. Comments using other compression methods are skipped without affecting entry extraction. -Truncated encrypted comment data throws `InvalidFormatException` on synchronous reads or `IncompleteArchiveException` on asynchronous reads. RAR5 KDF counts above 24 are rejected with `CryptographicException` before deriving encryption keys. +Truncated encrypted comment data throws `InvalidFormatException` on both synchronous and asynchronous reads. RAR5 KDF counts above 24 are rejected with `CryptographicException` before deriving encryption keys. ```csharp using var archive = RarArchive.OpenArchive( @@ -502,6 +502,8 @@ archive.SaveTo("output.zip", options); ### Extraction behavior +`ZipArchiveEntry.OpenEntryStreamAsync` and `TarArchiveEntry.OpenEntryStreamAsync` are virtual and support newly added writable entries as well as entries read from an archive. Opening a new writable entry checks cancellation, rewinds its supplied stream, and returns a non-owning wrapper. Disposing that wrapper leaves the supplied stream open; archive disposal follows the entry's `closeStream` setting. + ```csharp var options = new ExtractionOptions { @@ -692,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/docs/FORMATS.md b/docs/FORMATS.md index ab6c88f3..545706a9 100644 --- a/docs/FORMATS.md +++ b/docs/FORMATS.md @@ -45,7 +45,7 @@ - RAR5 encrypted headers and data accept KDF counts from 0 through 24 (the binary logarithm of the PBKDF2 iteration count), matching UnRAR's acceptance limit. Larger counts are rejected before key derivation. - RAR companion filenames use the same exact-match-first, case-insensitive fallback as ZIP, with ambiguous fallback matches rejected. -- Encrypted ciphertext must contain complete AES blocks. Truncation throws `InvalidFormatException` on synchronous reads or `IncompleteArchiveException` on asynchronous reads. +- Encrypted ciphertext must contain complete AES blocks. Truncation throws `InvalidFormatException` on both synchronous and asynchronous reads. ### 7Zip Format Notes diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs index 7b2cda51..032e33f3 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs @@ -25,7 +25,12 @@ public partial class GZipArchive CancellationToken cancellationToken = default ) { +#if SYNC_ONLY using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); +#else + 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); } @@ -50,9 +55,16 @@ public partial class GZipArchive { if (!entry.IsDirectory) { - using var entryStream = await entry +#if SYNC_ONLY + using var entryStream = entry.OpenEntryStream(); +#else + 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"), @@ -65,9 +77,16 @@ public partial class GZipArchive } foreach (var entry in newEntries.Where(x => !x.IsDirectory)) { - using var entryStream = await entry +#if SYNC_ONLY + using var entryStream = entry.OpenEntryStream(); +#else + 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 31b4596a..b4d858f2 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -164,7 +164,12 @@ public static partial class IArchiveEntryExtensions options, async (x, fm, ct) => { +#if SYNC_ONLY using var fs = File.Open(x, fm); +#else + var fs = File.Open(x, fm); + await using var scope = fs.DisposeAsyncScope().ConfigureAwait(false); +#endif await entry .WriteToAsync(fs, options.BufferSize, options, null, ct) .ConfigureAwait(false); diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index dcfee164..d74b756b 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -83,7 +83,8 @@ public static class IWritableAsyncArchiveExtensions ) where TOptions : IWriterOptions { - 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); 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 d8991d43..2c5e69e0 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -22,7 +22,7 @@ public partial class TarArchive CancellationToken cancellationToken = default ) { - using var writer = new TarWriter(stream, options); + await using var writer = new TarWriter(stream, options); await foreach ( var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) ) @@ -39,7 +39,12 @@ public partial class TarArchive } else { - using var entryStream = entry.OpenEntryStream(); + 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"), @@ -65,7 +70,12 @@ public partial class TarArchive } else { - using var entryStream = entry.OpenEntryStream(); + 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/Archives/Tar/TarArchiveEntry.cs b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs index c09d4755..88f832f6 100644 --- a/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs @@ -20,7 +20,7 @@ public class TarArchiveEntry : TarEntry, IArchiveEntry public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull(); - public async ValueTask OpenEntryStreamAsync( + public virtual async ValueTask OpenEntryStreamAsync( CancellationToken cancellationToken = default ) => ( diff --git a/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs b/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs index 84f8ff44..c1a81f93 100644 --- a/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs +++ b/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -82,6 +84,15 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv return SharpCompressStream.CreateNonDisposing(stream); } + public override ValueTask OpenEntryStreamAsync( + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + // Writable entries have no archive parts; opening only rewinds the supplied stream. + return new(OpenEntryStream()); + } + internal override void Close() { if (closeStream && stream is not null) diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs index 97c412e6..122112eb 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs @@ -84,7 +84,7 @@ public partial class ZipArchive CancellationToken cancellationToken = default ) { - using var writer = new ZipWriter(stream, options); + await using var writer = new ZipWriter(stream, options); await foreach ( var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) ) @@ -101,7 +101,12 @@ public partial class ZipArchive } else { - using var entryStream = entry.OpenEntryStream(); + 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"), @@ -125,7 +130,12 @@ public partial class ZipArchive } else { - using var entryStream = entry.OpenEntryStream(); + 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/Archives/Zip/ZipArchiveEntry.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs index 308727a6..063b457c 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs @@ -8,7 +8,7 @@ namespace SharpCompress.Archives.Zip; public partial class ZipArchiveEntry { - public async ValueTask OpenEntryStreamAsync( + public virtual async ValueTask OpenEntryStreamAsync( CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs b/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs index b74fea04..d6995e55 100644 --- a/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs +++ b/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -83,6 +85,15 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry return SharpCompressStream.CreateNonDisposing(stream); } + public override ValueTask OpenEntryStreamAsync( + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + // Writable entries have no archive parts; opening only rewinds the supplied stream. + return new(OpenEntryStream()); + } + internal override void Close() { if (closeStream && !isDisposed && stream is not null) diff --git a/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs b/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs index 77cf4476..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 LEGACY_DOTNET && !NETSTANDARD2_1 - 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/Tar/TarReadOnlySubStream.cs b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs index c4dc0b72..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 !LEGACY_DOTNET 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 3a07aece..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 LEGACY_DOTNET - _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 2ee0b2ea..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 !LEGACY_DOTNET 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) { @@ -44,17 +42,37 @@ internal partial class WinzipAesCryptoStream { return; } - _authRead = true; - var tag = new byte[10]; try { - await _stream - .ReadExactAsync(tag, 0, tag.Length, cancellationToken) - .ConfigureAwait(false); - ValidateAuthentication(tag); + while (_authenticationTagBytesRead < _authenticationTag.Length) + { + var read = await _stream + .ReadAsync( + _authenticationTag, + _authenticationTagBytesRead, + _authenticationTag.Length - _authenticationTagBytesRead, + cancellationToken + ) + .ConfigureAwait(false); + if (read == 0) + { + throw new IncompleteArchiveException( + "Unexpected end of WinZip AES authentication tag." + ); + } + _authenticationTagBytesRead += read; + } + ValidateAuthentication(_authenticationTag); + _authRead = true; + } + catch (OperationCanceledException) + { + // Cancellation is transient; keep the tag prefix so disposal can finish verification. + throw; } catch (Exception exception) { + _authRead = true; _authenticationFailure = ExceptionDispatchInfo.Capture(exception); throw; } diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs index 1ec17808..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; @@ -15,6 +16,10 @@ internal partial class WinzipAesCryptoStream : Stream private readonly ICryptoTransform _transform; private readonly HMACSHA1 _authentication; private bool _authRead; + + // Retain tag progress across canceled reads, including a subsequent synchronous disposal. + private readonly byte[] _authenticationTag = new byte[10]; + private int _authenticationTagBytesRead; private ExceptionDispatchInfo? _authenticationFailure; private int _nonce = 1; private byte[] _counterOut = new byte[BLOCK_SIZE_IN_BYTES]; @@ -83,7 +88,7 @@ internal partial class WinzipAesCryptoStream : Stream { if (!_authRead) { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC // Legacy Stream lacks DisposeAsync, including for async-only inputs. #pragma warning disable VSTHRD002 // Avoid problematic synchronous waits #pragma warning disable CA2012 @@ -118,15 +123,33 @@ internal partial class WinzipAesCryptoStream : Stream { return; } - _authRead = true; - var tag = new byte[10]; try { - _stream.ReadExact(tag, 0, tag.Length); - ValidateAuthentication(tag); + while (_authenticationTagBytesRead < _authenticationTag.Length) + { + var read = _stream.Read( + _authenticationTag, + _authenticationTagBytesRead, + _authenticationTag.Length - _authenticationTagBytesRead + ); + if (read == 0) + { + throw new IncompleteArchiveException( + "Unexpected end of WinZip AES authentication tag." + ); + } + _authenticationTagBytesRead += read; + } + ValidateAuthentication(_authenticationTag); + _authRead = true; + } + catch (OperationCanceledException) + { + throw; } catch (Exception exception) { + _authRead = true; _authenticationFailure = ExceptionDispatchInfo.Capture(exception); throw; } diff --git a/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs b/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs index 35cf9f1d..c7b33645 100644 --- a/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs +++ b/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs @@ -115,7 +115,7 @@ public sealed partial class BZip2Stream : IAsyncDisposable CancellationToken cancellationToken = default ) => await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -136,7 +136,7 @@ public sealed partial class BZip2Stream : IAsyncDisposable stream.Dispose(); } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #else await Task.CompletedTask.ConfigureAwait(false); diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs index c1053216..ab85c1ef 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs @@ -707,7 +707,7 @@ internal partial class CBZip2InputStream } else { - SetupNoRandPartA(); + await SetupNoRandPartAAsync(cancellationToken).ConfigureAwait(false); } } @@ -792,7 +792,7 @@ internal partial class CBZip2InputStream z ^= (char)((rNToGo == 1) ? 1 : 0); j2 = 0; currentState = RAND_PART_C_STATE; - SetupRandPartC(); + await SetupRandPartCAsync(cancellationToken).ConfigureAwait(false); } else { diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.Async.cs b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.Async.cs index d56e9244..6db68c8f 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.Async.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.Async.cs @@ -670,7 +670,7 @@ internal sealed partial class CBZip2OutputStream : IAsyncDisposable await bsStream.FlushAsync(cancellationToken).ConfigureAwait(false); } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -696,7 +696,7 @@ internal sealed partial class CBZip2OutputStream : IAsyncDisposable } bsStream = null; -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #else await Task.CompletedTask.ConfigureAwait(false); diff --git a/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs b/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs index ae6343cd..496adca2 100644 --- a/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs +++ b/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs @@ -8,7 +8,7 @@ namespace SharpCompress.Compressors.Deflate; public partial class DeflateStream { -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -22,7 +22,7 @@ public partial class DeflateStream } _disposed = true; } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #else await Task.CompletedTask.ConfigureAwait(false); diff --git a/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs b/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs index 4a70c519..db8bfbcc 100644 --- a/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs +++ b/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs @@ -183,7 +183,7 @@ public partial class GZipStream } #endif -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -200,7 +200,7 @@ public partial class GZipStream await BaseStream.DisposeAsync().ConfigureAwait(false); } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #endif } diff --git a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs index adb462b1..b574e74c 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs @@ -510,7 +510,7 @@ internal partial class ZlibBaseStream : Stream, IStreamStack } } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -521,7 +521,7 @@ internal partial class ZlibBaseStream : Stream, IStreamStack return; } isDisposed = true; -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #endif if (_stream is null) diff --git a/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs b/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs index 058bd28e..1021bbde 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs @@ -21,8 +21,11 @@ public partial class ZlibStream await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false); } -#if !LEGACY_DOTNET +#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/LZMA/Lzma2EncoderStream.cs b/src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs index 7440ff66..2790b7c5 100644 --- a/src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs +++ b/src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs @@ -200,7 +200,7 @@ internal sealed class Lzma2EncoderStream : Stream private static readonly byte[] _endMarker = [0x00]; -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -229,7 +229,7 @@ internal sealed class Lzma2EncoderStream : Stream } } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #endif } diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs index 1c87d5e1..0c23a259 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs @@ -454,7 +454,7 @@ public partial class LzmaStream } #endif -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -494,7 +494,7 @@ public partial class LzmaStream } } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #endif GC.SuppressFinalize(this); diff --git a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs index 744682c4..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 LEGACY_DOTNET && !NETSTANDARD2_1 - instance.Dispose(); -#else await instance.DisposeAsync().ConfigureAwait(false); -#endif throw; } } @@ -196,7 +192,7 @@ public class PpmdStream : Stream, IAsyncDisposable base.Dispose(disposing); } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -215,7 +211,7 @@ public class PpmdStream : Stream, IAsyncDisposable _modelH?.Dispose(); _modelH = null; -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #endif } diff --git a/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs b/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs index b136335d..de7b8fe9 100644 --- a/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs +++ b/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs @@ -36,7 +36,9 @@ public partial class XZFooter using (var reader = new BinaryReader(stream)) { BackwardSize = (reader.ReadLittleEndianUInt32() + 1) * 4; - StreamFlags = reader.ReadBytes(2); + StreamFlags = await reader + .ReadBytesAsync(2, cancellationToken: cancellationToken) + .ConfigureAwait(false); } var magBy = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false); if (!magBy.AsSpan().SequenceEqual(_magicBytes)) diff --git a/src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs b/src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs index 5f988e4c..d788d152 100644 --- a/src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs +++ b/src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs @@ -8,7 +8,7 @@ namespace SharpCompress.Compressors.ZStandard; public partial class CompressionStream { -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -16,7 +16,7 @@ public partial class CompressionStream { if (compressor == null) { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC Dispose(true); GC.SuppressFinalize(this); await Task.CompletedTask.ConfigureAwait(false); @@ -35,7 +35,7 @@ public partial class CompressionStream ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC Dispose(true); await Task.CompletedTask.ConfigureAwait(false); #else diff --git a/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs index 20490d91..c4161fd2 100644 --- a/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs +++ b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs @@ -203,7 +203,7 @@ public partial class DecompressionStream : Stream } } -#if LEGACY_DOTNET && !NETSTANDARD2_1 +#if !STREAM_DISPOSEASYNC public virtual ValueTask DisposeAsync() { try diff --git a/src/SharpCompress/IO/AsyncDisposableStream.cs b/src/SharpCompress/IO/AsyncDisposableStream.cs index 4c7fafbe..e528b4ce 100644 --- a/src/SharpCompress/IO/AsyncDisposableStream.cs +++ b/src/SharpCompress/IO/AsyncDisposableStream.cs @@ -22,11 +22,11 @@ namespace SharpCompress.IO; /// /// public abstract class AsyncDisposableStream : Stream -#if NO_STREAM_DISPOSEASYNC +#if !STREAM_DISPOSEASYNC , IAsyncDisposable #endif { -#if NO_STREAM_DISPOSEASYNC +#if !STREAM_DISPOSEASYNC public virtual ValueTask DisposeAsync() { Dispose(); diff --git a/src/SharpCompress/IO/ProgressReportingStream.Async.cs b/src/SharpCompress/IO/ProgressReportingStream.Async.cs index fd10398b..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 !LEGACY_DOTNET 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 1bac16de..7bf2b650 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs @@ -28,12 +28,14 @@ internal sealed partial class SeekableSharpCompressStream ReadOnlyMemory buffer, CancellationToken cancellationToken = default ) => _stream.WriteAsync(buffer, cancellationToken); +#endif - public override ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { if (_isDisposed) { - return base.DisposeAsync(); + await base.DisposeAsync().ConfigureAwait(false); + return; } if (ThrowOnDispose) { @@ -44,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 7b1f376d..47ba651e 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Async.cs @@ -251,8 +251,11 @@ public partial class SharpCompressStream } } -#if !LEGACY_DOTNET +#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/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs index 5b4e667b..c962d116 100644 --- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -45,7 +45,8 @@ public static class IAsyncReaderExtensions options, async (x, fm, ct) => { - using var fs = File.Open(x, fm); + var fs = File.Open(x, fm); + await using var scope = fs.DisposeAsyncScope().ConfigureAwait(false); await CopyEntryToAsync(reader, fs, options, ct).ConfigureAwait(false); }, cancellationToken @@ -83,7 +84,8 @@ public static class IAsyncReaderExtensions options, async (x, fm, ct) => { - using var fs = File.Open(x, fm); + var fs = File.Open(x, fm); + await using var scope = fs.DisposeAsyncScope().ConfigureAwait(false); await CopyEntryToAsync(reader, fs, options, ct).ConfigureAwait(false); }, cancellationToken diff --git a/src/SharpCompress/SharpCompress.csproj b/src/SharpCompress/SharpCompress.csproj index c57cb722..d5ba7f80 100644 --- a/src/SharpCompress/SharpCompress.csproj +++ b/src/SharpCompress/SharpCompress.csproj @@ -31,8 +31,8 @@ $(DefineConstants);LEGACY_DOTNET - - $(DefineConstants);NO_STREAM_DISPOSEASYNC + + $(DefineConstants);STREAM_DISPOSEASYNC true diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs index df323d69..6e6debff 100644 --- a/src/SharpCompress/Utility.Async.cs +++ b/src/SharpCompress/Utility.Async.cs @@ -70,7 +70,8 @@ internal static partial class Utility ) { // Use ReadOnlySubStream to limit reading and leverage framework's CopyToAsync - using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); + 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 a178dd5d..b1a082f9 100644 --- a/src/SharpCompress/Writers/IWriterExtensions.cs +++ b/src/SharpCompress/Writers/IWriterExtensions.cs @@ -77,7 +77,8 @@ public static class IWriterExtensions { throw new ArgumentException("Source does not exist: " + source.FullName); } - using var stream = source.OpenRead(); + 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/packages.lock.json b/src/SharpCompress/packages.lock.json index f4c3588e..6afa698b 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -33,9 +33,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -45,19 +45,20 @@ }, "System.Text.Encoding.CodePages": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "rt1G/LdKC6oc3xPyQhMn5vRTp9iTTt1BS9Dx/67TpW9KWgFJ39lyEz0y4CbDksx7iTZEfgG61UsQMoGEXEpF4A==", "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.ValueTuple": "4.6.2" } }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -118,6 +119,11 @@ "dependencies": { "System.Runtime.CompilerServices.Unsafe": "4.5.3" } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.6.2", + "contentHash": "yQgmjfFximrNm9LIV3mL6T5MzjeC+epeE5rl4hXxAlYmxby7RM1dPSkIKXk9HNkl6G54h2JHOmLD46+Pey+IRg==" } }, ".NETStandard,Version=v2.0": { @@ -152,9 +158,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "NETStandard.Library": { "type": "Direct", @@ -173,19 +179,19 @@ }, "System.Text.Encoding.CodePages": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "rt1G/LdKC6oc3xPyQhMn5vRTp9iTTt1BS9Dx/67TpW9KWgFJ39lyEz0y4CbDksx7iTZEfgG61UsQMoGEXEpF4A==", "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" } }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -282,9 +288,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -294,18 +300,18 @@ }, "System.Text.Encoding.CodePages": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "rt1G/LdKC6oc3xPyQhMn5vRTp9iTTt1BS9Dx/67TpW9KWgFJ39lyEz0y4CbDksx7iTZEfgG61UsQMoGEXEpF4A==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "System.Runtime.CompilerServices.Unsafe": "6.1.2" } }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -332,8 +338,8 @@ }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" } }, "net10.0": { @@ -365,9 +371,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -377,9 +383,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -428,9 +434,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -440,9 +446,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -497,9 +503,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -509,9 +515,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", diff --git a/tests/SharpCompress.AotSmoke/packages.lock.json b/tests/SharpCompress.AotSmoke/packages.lock.json index 882d8334..7fb4c275 100644 --- a/tests/SharpCompress.AotSmoke/packages.lock.json +++ b/tests/SharpCompress.AotSmoke/packages.lock.json @@ -36,9 +36,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -48,9 +48,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", diff --git a/tests/SharpCompress.Performance/packages.lock.json b/tests/SharpCompress.Performance/packages.lock.json index eb03c8cd..a3d81fee 100644 --- a/tests/SharpCompress.Performance/packages.lock.json +++ b/tests/SharpCompress.Performance/packages.lock.json @@ -52,9 +52,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -64,9 +64,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "BenchmarkDotNet.Annotations": { "type": "Transitive", diff --git a/tests/SharpCompress.Test/SharpCompress.Test.csproj b/tests/SharpCompress.Test/SharpCompress.Test.csproj index 68ff64ae..a8769f29 100644 --- a/tests/SharpCompress.Test/SharpCompress.Test.csproj +++ b/tests/SharpCompress.Test/SharpCompress.Test.csproj @@ -12,6 +12,9 @@ AnyCPU false + + $(DefineConstants);STREAM_DISPOSEASYNC + $(DefineConstants);WINDOWS 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/BufferPoolLifetimeTests.cs b/tests/SharpCompress.Test/Streams/BufferPoolLifetimeTests.cs index dd26ba01..a0569d92 100644 --- a/tests/SharpCompress.Test/Streams/BufferPoolLifetimeTests.cs +++ b/tests/SharpCompress.Test/Streams/BufferPoolLifetimeTests.cs @@ -58,7 +58,7 @@ public class BufferPoolLifetimeTests stream.Dispose(); } stream.Dispose(); -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC await stream.DisposeAsync(); #else await Task.CompletedTask; diff --git a/tests/SharpCompress.Test/Streams/SharpCompressLifetimeTests.cs b/tests/SharpCompress.Test/Streams/SharpCompressLifetimeTests.cs index df339154..b38ca2da 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressLifetimeTests.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressLifetimeTests.cs @@ -25,7 +25,7 @@ public class SharpCompressLifetimeTests Assert.NotNull(ring.GetValue(stream)); if (asyncDispose) { -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC await Assert.ThrowsAsync(async () => await stream.DisposeAsync()); #else Assert.Throws(() => stream.Dispose()); diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeAsyncTest.cs index 1c3b7d18..c2221d10 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeAsyncTest.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeAsyncTest.cs @@ -9,7 +9,7 @@ namespace SharpCompress.Test.Streams; public class SharpCompressStreamEdgeAsyncTest { -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC [Fact] public async ValueTask DisposeAsync_WithLeaveStreamOpenTrue_DoesNotDisposeUnderlying() diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorAsyncTest.cs index cf90a566..38020859 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorAsyncTest.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorAsyncTest.cs @@ -49,7 +49,7 @@ public class SharpCompressStreamErrorAsyncTest } } -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC [Fact] public async ValueTask DisposeAsync_WithThrowOnDisposeTrue_ThrowsInvalidOperation() { diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughAsyncTest.cs index 521bac6a..862bd442 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughAsyncTest.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughAsyncTest.cs @@ -84,7 +84,7 @@ public class SharpCompressStreamPassthroughAsyncTest Assert.Equal(3, ms.Length); } -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC [Fact] public async ValueTask CreateNonDisposing_DoesNotDisposeUnderlying_Async() { diff --git a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs index 8b879981..463b49f0 100644 --- a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs @@ -1,5 +1,6 @@ using System; using System.Buffers.Binary; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -14,6 +15,120 @@ namespace SharpCompress.Test.Streams; public class WinzipAesCryptoStreamTests { + public static IEnumerable AuthenticationCancellationCases() + { + foreach (var length in new[] { 0, 33 }) + { + foreach (var tagBytesRead in new[] { 0, 4 }) + { + foreach (var corruption in new[] { "none", "tag", "missing-tag" }) + { + foreach (var completion in new[] { "dispose", "retry", "dispose-async" }) + { + yield return [length, tagBytesRead, corruption, completion]; + } + } + } + } + } + + [Theory] + [MemberData(nameof(AuthenticationCancellationCases))] + public async Task Authentication_Resumes_After_Cancelled_Tag_Read( + int length, + int tagBytesRead, + string corruption, + string completion + ) + { + byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87]; + var data = CreateEncryptionData("password", salt); + var ciphertext = EncryptCtr(new byte[length], data.KeyBytes); +#pragma warning disable CA5350 // WinZip AES mandates HMAC-SHA1-80. + using var authentication = new HMACSHA1(data.IvBytes); +#pragma warning restore CA5350 + var bytes = ciphertext.Concat(authentication.ComputeHash(ciphertext).Take(10)).ToArray(); + if (corruption == "tag") + { + // Corrupt the prefix, which must survive cancellation and be verified on retry. + bytes[length] ^= 1; + } + else if (corruption == "missing-tag") + { + bytes = bytes.Take(bytes.Length - 1).ToArray(); + } + using var cancellation = new CancellationTokenSource(); + using var input = new CancelTagReadStream(bytes, length + tagBytesRead, cancellation); + using var stream = new WinzipAesCryptoStream(input, data, length); + await Assert.ThrowsAnyAsync(async () => + await stream.ReadAsync( + new byte[Math.Max(1, length)], + 0, + Math.Max(1, length), + cancellation.Token + ) + ); + Assert.Equal(length + tagBytesRead, input.Position); + + var error = await Record.ExceptionAsync(async () => + { + if (completion == "dispose") + { + stream.Dispose(); + } + else if (completion == "dispose-async") + { + await stream.DisposeAsync(); + } + else + { + Assert.Equal(0, await stream.ReadAsync(new byte[1], 0, 1, CancellationToken.None)); + } + }); + if (corruption == "none") + { + Assert.Null(error); + } + else + { + Assert.Equal( + corruption == "tag" + ? typeof(SharpCompress.Common.InvalidFormatException) + : typeof(SharpCompress.Common.IncompleteArchiveException), + error?.GetType() + ); + } + } + + private sealed class CancelTagReadStream( + byte[] bytes, + int cancelPosition, + CancellationTokenSource cancellation + ) : MemoryStream(bytes) + { + private bool cancelled; + + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (!cancelled) + { + if (Position == cancelPosition) + { + cancelled = true; + cancellation.Cancel(); + return Task.FromCanceled(cancellation.Token); + } + count = Math.Min(count, (int)(cancelPosition - Position)); + } + return base.ReadAsync(buffer, offset, count, cancellationToken); + } + } + [Fact] public void Dispose_Verifies_Unread_Ciphertext() { @@ -30,7 +145,6 @@ public class WinzipAesCryptoStreamTests stream.Dispose(); } -#if !LEGACY_DOTNET [Fact] public async Task DisposeAsync_Verifies_Unread_Ciphertext() { @@ -48,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 dabc0fa2..f31d6a84 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Archives.Tar; @@ -229,6 +230,89 @@ public class TarArchiveAsyncTests : ArchiveTests Assert.True(entryStream.IsDisposed); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async ValueTask Tar_New_Entry_OpenEntryStreamAsync_Rewinds_And_Preserves_Ownership( + bool closeStream + ) + { + var bytes = Encoding.UTF8.GetBytes("test"); + using var source = new TestStream(new MemoryStream(bytes)); + await using (var archive = await TarArchive.CreateAsyncArchive()) + { + IArchiveEntry entry = await archive.AddEntryAsync( + "test.txt", + source, + closeStream: closeStream, + size: source.Length + ); + for (var i = 0; i < 2; i++) + { + source.Position = source.Length; + using (var opened = await entry.OpenEntryStreamAsync(CancellationToken.None)) + { + using var destination = new MemoryStream(); + await opened.CopyToAsync(destination, 81920, CancellationToken.None); + Assert.Equal(bytes, destination.ToArray()); + } + Assert.False(source.IsDisposed); + } + } + Assert.Equal(closeStream, source.IsDisposed); + } + + [Fact] + public async ValueTask Tar_New_Entry_OpenEntryStreamAsync_Cancelled_Does_Not_Rewind() + { + using var source = new MemoryStream(Encoding.UTF8.GetBytes("test")); + await using var archive = await TarArchive.CreateAsyncArchive(); + var entry = await archive.AddEntryAsync("test.txt", source, size: source.Length); + source.Position = source.Length; + using var cancellation = new CancellationTokenSource(); + cancellation.Cancel(); + await Assert.ThrowsAsync(async () => + await entry.OpenEntryStreamAsync(cancellation.Token) + ); + Assert.Equal(source.Length, source.Position); + } + + [Fact] + public async ValueTask Tar_New_Directory_OpenEntryStreamAsync() + { + await using var archive = await TarArchive.CreateAsyncArchive(); + var entry = await archive.AddDirectoryEntryAsync("directory"); + using var opened = await entry.OpenEntryStreamAsync(CancellationToken.None); + Assert.Equal(0, opened.Length); + } + + [Fact] + public async ValueTask Tar_Save_New_Entry_Async_Only_Streams() + { + using var source = new AsyncOnlyStream(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + using var destination = new MemoryStream(); + using var output = new AsyncOnlyStream(destination, disposeStream: false); + await using (var archive = await TarArchive.CreateAsyncArchive()) + { + await archive.AddEntryAsync("test.txt", source, size: source.Length); + await archive.SaveToAsync( + output, + new TarWriterOptions(CompressionType.None, finalizeArchiveOnClose: true) + { + LeaveStreamOpen = true, + }, + CancellationToken.None + ); + } + destination.Position = 0; + await using var reopened = await TarArchive.OpenAsyncArchive(destination); + var entry = await reopened.EntriesAsync.SingleAsync(); + 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()); + } + [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 420ce481..710a4141 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using SharpCompress; using SharpCompress.Archives; @@ -208,6 +209,86 @@ public class ZipArchiveAsyncTests : ArchiveTests Assert.True(entryStream.IsDisposed); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async ValueTask Zip_New_Entry_OpenEntryStreamAsync_Rewinds_And_Preserves_Ownership( + bool closeStream + ) + { + var bytes = Encoding.UTF8.GetBytes("test"); + using var source = new TestStream(new MemoryStream(bytes)); + await using (var archive = await ZipArchive.CreateAsyncArchive()) + { + IArchiveEntry entry = await archive.AddEntryAsync( + "test.txt", + source, + closeStream: closeStream, + size: source.Length + ); + for (var i = 0; i < 2; i++) + { + source.Position = source.Length; + using (var opened = await entry.OpenEntryStreamAsync(CancellationToken.None)) + { + using var destination = new MemoryStream(); + await opened.CopyToAsync(destination, 81920, CancellationToken.None); + Assert.Equal(bytes, destination.ToArray()); + } + Assert.False(source.IsDisposed); + } + } + Assert.Equal(closeStream, source.IsDisposed); + } + + [Fact] + public async ValueTask Zip_New_Entry_OpenEntryStreamAsync_Cancelled_Does_Not_Rewind() + { + using var source = new MemoryStream(Encoding.UTF8.GetBytes("test")); + await using var archive = await ZipArchive.CreateAsyncArchive(); + var entry = await archive.AddEntryAsync("test.txt", source, size: source.Length); + source.Position = source.Length; + using var cancellation = new CancellationTokenSource(); + cancellation.Cancel(); + await Assert.ThrowsAsync(async () => + await entry.OpenEntryStreamAsync(cancellation.Token) + ); + Assert.Equal(source.Length, source.Position); + } + + [Fact] + public async ValueTask Zip_New_Directory_OpenEntryStreamAsync() + { + await using var archive = await ZipArchive.CreateAsyncArchive(); + var entry = await archive.AddDirectoryEntryAsync("directory"); + using var opened = await entry.OpenEntryStreamAsync(CancellationToken.None); + Assert.Equal(0, opened.Length); + } + + [Fact] + public async ValueTask Zip_Save_New_Entry_Async_Only_Streams() + { + using var source = new AsyncOnlyStream(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + using var destination = new MemoryStream(); + using var output = new AsyncOnlyStream(destination, disposeStream: false); + await using (var archive = await ZipArchive.CreateAsyncArchive()) + { + await archive.AddEntryAsync("test.txt", source, size: source.Length); + await archive.SaveToAsync( + output, + new ZipWriterOptions(CompressionType.Deflate) { LeaveStreamOpen = true }, + CancellationToken.None + ); + } + destination.Position = 0; + await using var reopened = await ZipArchive.OpenAsyncArchive(destination); + var entry = await reopened.EntriesAsync.SingleAsync(); + 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()); + } + [Fact] public async ValueTask Zip_Deflate_Entry_Stream_Async() { diff --git a/tests/SharpCompress.Test/packages.lock.json b/tests/SharpCompress.Test/packages.lock.json index db9a3aa7..90ade434 100644 --- a/tests/SharpCompress.Test/packages.lock.json +++ b/tests/SharpCompress.Test/packages.lock.json @@ -42,9 +42,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -72,9 +72,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.ApplicationInsights": { "type": "Transitive", @@ -236,6 +236,11 @@ "System.Runtime.CompilerServices.Unsafe": "4.5.3" } }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.6.2", + "contentHash": "yQgmjfFximrNm9LIV3mL6T5MzjeC+epeE5rl4hXxAlYmxby7RM1dPSkIKXk9HNkl6G54h2JHOmLD46+Pey+IRg==" + }, "xunit.analyzers": { "type": "Transitive", "resolved": "1.27.0", @@ -311,7 +316,7 @@ "type": "Project", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "[8.0.0, )", - "System.Text.Encoding.CodePages": "[8.0.0, )" + "System.Text.Encoding.CodePages": "[10.0.12, )" } }, "Microsoft.Bcl.AsyncInterfaces": { @@ -325,12 +330,13 @@ }, "System.Text.Encoding.CodePages": { "type": "CentralTransitive", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "rt1G/LdKC6oc3xPyQhMn5vRTp9iTTt1BS9Dx/67TpW9KWgFJ39lyEz0y4CbDksx7iTZEfgG61UsQMoGEXEpF4A==", "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.ValueTuple": "4.6.2" } } }, @@ -373,9 +379,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -400,9 +406,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.ApplicationInsights": { "type": "Transitive",