From 0b3cd313e1bb32a5a3949566fd18d75d62c915d6 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 28 Nov 2025 14:45:16 +0000 Subject: [PATCH] Introduce AsyncStream for tests to use to prove they're using async versions. Knowns: detection and small reads aren't async --- ASYNC_ANALYSIS.md | 222 ++++++++++++++++++ SharpCompress.sln | 1 + .../Compressors/Xz/BinaryUtils.cs | 2 +- src/SharpCompress/Utility.cs | 14 +- .../Mocks/AsyncOnlyStream.cs | 167 +++++++++++++ .../Zip/ZipReaderAsyncTests.cs | 136 ++++++----- 6 files changed, 472 insertions(+), 70 deletions(-) create mode 100644 ASYNC_ANALYSIS.md create mode 100644 tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs diff --git a/ASYNC_ANALYSIS.md b/ASYNC_ANALYSIS.md new file mode 100644 index 00000000..1ff1ab42 --- /dev/null +++ b/ASYNC_ANALYSIS.md @@ -0,0 +1,222 @@ +# Async Method Analysis for SharpCompress Stream Implementations + +This document analyzes all Stream implementations in SharpCompress to identify which ones need async method implementations and which tests are missing async versions. + +## Stream Implementations Analysis + +### Criteria +- **Needs Async Read**: Has `Read()` implementation but no proper `ReadAsync()` override (or `ReadAsync` just wraps sync) +- **Needs Async Write**: Has `Write()` implementation but no proper `WriteAsync()` override (or `WriteAsync` just wraps sync) + +### ✅ Streams with Proper Async Implementations + +| Stream | Location | Read | Write | Notes | +|--------|----------|------|-------|-------| +| TarReadOnlySubStream | Common/Tar/TarReadOnlySubStream.cs | ✅ | N/A | Has proper ReadAsync with async stream delegation | +| EntryStream | Common/EntryStream.cs | ✅ | N/A | Has proper ReadAsync | +| BZip2Stream | Compressors/BZip2/BZip2Stream.cs | ✅ | ✅ | Delegates to inner stream async methods | +| XZStream | Compressors/Xz/XZStream.cs | ✅ | N/A | Has proper async implementation | +| LZipStream | Compressors/LZMA/LZipStream.cs | ✅ | ✅ | Delegates to inner stream async methods | +| LzmaStream | Compressors/LZMA/LzmaStream.cs | ✅ | ⚠️ | Read has proper async; Write wraps sync | +| AesDecoderStream | Compressors/LZMA/AesDecoderStream.cs | ✅ | N/A | Has proper async implementation | +| CrcBuilderStream | Compressors/LZMA/Utilites/CrcBuilderStream.cs | N/A | ✅ | Has proper async write | +| CrcCheckStream | Compressors/LZMA/Utilites/CrcCheckStream.cs | N/A | ⚠️ | WriteAsync wraps sync | +| DeflateStream | Compressors/Deflate/DeflateStream.cs | ✅ | ✅ | Delegates to base stream async | +| ZlibBaseStream | Compressors/Deflate/ZlibBaseStream.cs | ✅ | ✅ | Has proper async implementations | +| RarStream | Compressors/Rar/RarStream.cs | ✅ | ⚠️ | Read has async; Write wraps sync | +| RarCrcStream | Compressors/Rar/RarCrcStream.cs | ✅ | N/A | Extends RarStream with async | +| MultiVolumeReadOnlyStream | Compressors/Rar/MultiVolumeReadOnlyStream.cs | ✅ | N/A | Has proper async read | +| RarBLAKE2spStream | Compressors/Rar/RarBLAKE2spStream.cs | ✅ | N/A | Has proper async read | +| Deflate64Stream | Compressors/Deflate64/Deflate64Stream.cs | ✅ | N/A | Has proper async read | +| ADCStream | Compressors/ADC/ADCStream.cs | ✅ | N/A | Has proper async read | +| ReadOnlySubStream | IO/ReadOnlySubStream.cs | ✅ | N/A | Has proper async read | +| SharpCompressStream | IO/SharpCompressStream.cs | ✅ | ✅ | Has proper async implementations | + +--- + +## ❌ Streams Needing Async Implementation + +### High Priority - Has Read but no proper ReadAsync + +| Stream | Location | Issue | Complexity | +|--------|----------|-------|------------| +| **Crc32Stream** | Crypto/Crc32Stream.cs | Has `Write()` but no `WriteAsync` | Low - Just add CRC calculation around async write | +| **PkwareTraditionalCryptoStream** | Common/Zip/PkwareTraditionalCryptoStream.cs | Has `Read()` and `Write()` but no async versions | Medium - Crypto operations are synchronous, need to buffer async reads | +| **WinzipAesCryptoStream** | Common/Zip/WinzipAesCryptoStream.cs | Has `Read()` but no `ReadAsync` | Medium - Crypto operations are synchronous, need async stream reads | +| **CBZip2InputStream** | Compressors/BZip2/CBZip2InputStream.cs | `ReadAsync` wraps sync `ReadByte` loop | High - Core decompression is CPU-bound, but stream reads could be async | +| **CBZip2OutputStream** | Compressors/BZip2/CBZip2OutputStream.cs | `WriteAsync` wraps sync `WriteByte` loop | High - Core compression is CPU-bound | +| **Bcj2DecoderStream** | Compressors/LZMA/Bcj2DecoderStream.cs | `ReadAsync` wraps sync - uses iterator/state machine | High - Complex state machine with multiple streams | +| **PpmdStream** | Compressors/PPMd/PpmdStream.cs | No async at all | Medium - Depends on model decoding | +| **ShrinkStream** | Compressors/Shrink/ShrinkStream.cs | No async at all | Low - Single-shot decompression | +| **LzwStream** | Compressors/Lzw/LzwStream.cs | No async at all | Medium - Bit-level operations, but input reads could be async | +| **ExplodeStream** | Compressors/Explode/ExplodeStream.cs | No async at all | Medium - Similar to LzwStream | +| **ArcLzwStream** | Compressors/ArcLzw/ArcLzwStream.cs | No async at all | Low - Single-shot decompression | +| **BufferedSubStream** | IO/BufferedSubStream.cs | No async - uses sync cache refill | Medium - Cache operations could be async | + +### Streams Where Async May Not Be Beneficial + +| Stream | Location | Reason | +|--------|----------|--------| +| ZStandardStream | Compressors/ZStandard/ZStandardStream.cs | Extends ZstdSharp.DecompressionStream - async behavior depends on base class | +| ReadOnlyStream | Compressors/Xz/ReadOnlyStream.cs | Abstract base - no Read implementation | +| XZReadOnlyStream | Compressors/Xz/XZReadOnlyStream.cs | Abstract base - no Read implementation | +| DecoderStream2 | Compressors/LZMA/DecoderStream.cs | Abstract base - no Read implementation | + +--- + +## Detailed Analysis of Streams Needing Work + +### 1. Crc32Stream (Low Priority) +- **Location**: `src/SharpCompress/Crypto/Crc32Stream.cs` +- **Issue**: Has `Write()` (lines 83-87) but no `WriteAsync` +- **Solution**: Add `WriteAsync` that calls base stream's `WriteAsync` and then calculates CRC synchronously (CRC is fast) + +### 2. PkwareTraditionalCryptoStream (Medium Priority) +- **Location**: `src/SharpCompress/Common/Zip/PkwareTraditionalCryptoStream.cs` +- **Issue**: Has `Read()` (lines 69-86) and `Write()` (lines 88-113) but no async versions +- **Solution**: + - ReadAsync: Read from underlying stream asynchronously, then decrypt synchronously + - WriteAsync: Encrypt synchronously, then write asynchronously + +### 3. WinzipAesCryptoStream (Medium Priority) +- **Location**: `src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs` +- **Issue**: Has `Read()` (lines 106-123) but no `ReadAsync` +- **Solution**: Read from stream asynchronously, transform blocks synchronously (crypto is CPU-bound) + +### 4. CBZip2InputStream (High Complexity) +- **Location**: `src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs` +- **Issue**: `ReadAsync` (lines 1132-1152) just wraps sync `ReadByte` in a loop +- **Solution**: Would need significant refactoring to make the bit-reading operations async + +### 5. CBZip2OutputStream (High Complexity) +- **Location**: `src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs` +- **Issue**: `WriteAsync` (lines 2033-2046) wraps sync `WriteByte` loop +- **Solution**: Would need significant refactoring of compression pipeline + +### 6. Bcj2DecoderStream (High Complexity) +- **Location**: `src/SharpCompress/Compressors/LZMA/Bcj2DecoderStream.cs` +- **Issue**: Uses iterator pattern (`Run()`) - `ReadAsync` (lines 196-206) just wraps sync +- **Solution**: Would require rewriting the iterator pattern to support async + +### 7. PpmdStream (Medium Priority) +- **Location**: `src/SharpCompress/Compressors/PPMd/PpmdStream.cs` +- **Issue**: No async methods at all +- **Solution**: Add ReadAsync that reads from model decoder asynchronously + +### 8. ShrinkStream (Low Priority) +- **Location**: `src/SharpCompress/Compressors/Shrink/ShrinkStream.cs` +- **Issue**: No async - does single-shot decompression in Read +- **Solution**: Could add async wrapper, but single-shot nature limits benefit + +### 9. LzwStream (Medium Priority) +- **Location**: `src/SharpCompress/Compressors/Lzw/LzwStream.cs` +- **Issue**: No async methods +- **Solution**: Bit-level operations are sync, but underlying stream reads could be buffered async + +### 10. ExplodeStream (Medium Priority) +- **Location**: `src/SharpCompress/Compressors/Explode/ExplodeStream.cs` +- **Issue**: No async methods +- **Solution**: Similar to LzwStream - could async buffer reads from underlying stream + +### 11. ArcLzwStream (Low Priority) +- **Location**: `src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.cs` +- **Issue**: No async - single-shot decompression +- **Solution**: Limited benefit due to single-shot nature + +### 12. BufferedSubStream (Medium Priority) +- **Location**: `src/SharpCompress/IO/BufferedSubStream.cs` +- **Issue**: `RefillCache()` uses sync read +- **Solution**: Add async cache refill and ReadAsync + +--- + +## Test Analysis - Missing Async Versions + +### Tests WITH Async Versions (Good ✅) +| Sync Test | Async Test | +|-----------|------------| +| BZip2ReaderTests | BZip2StreamAsyncTests | +| TarArchiveTests | TarArchiveAsyncTests | +| TarReaderTests | TarReaderAsyncTests | +| TarWriterTests | TarWriterAsyncTests | +| ZipReaderTests | ZipReaderAsyncTests | +| ZipWriterTests | ZipWriterAsyncTests | +| ZipArchiveTests | ZipArchiveAsyncTests | +| Zip64Tests | Zip64AsyncTests | +| ZipMemoryArchiveWithCrcTests | ZipMemoryArchiveWithCrcAsyncTests | +| RarArchiveTests | RarArchiveAsyncTests | +| RarReaderTests | RarReaderAsyncTests | +| GZipArchiveTests | GZipArchiveAsyncTests | +| GZipReaderTests | GZipReaderAsyncTests | +| GZipWriterTests | GZipWriterAsyncTests | +| XZStreamTests | XZStreamAsyncTests | +| XZHeaderTests | XZHeaderAsyncTests | +| XZIndexTests | XZIndexAsyncTests | +| XZBlockTests | XZBlockAsyncTests | +| SharpCompressStreamTest | SharpCompressStreamAsyncTests | +| RewindableStreamTest | RewindableStreamAsyncTest | +| LzmaStreamTests | LzmaStreamAsyncTests | +| ZlibBaseStreamTests | ZLibBaseStreamAsyncTests | +| ADCTest | AdcAsyncTest | + +### Tests WITHOUT Async Versions (Need Work ❌) + +| Test File | Location | Priority | +|-----------|----------|----------| +| **SevenZipArchiveTests** | SevenZip/SevenZipArchiveTests.cs | High - 7z is commonly used | +| **ArcReaderTests** | Arc/ArcReaderTests.cs | Low - Less common format | +| **ArjReaderTests** | Arj/ArjReaderTests.cs | Low - Legacy format | +| **Crc32Tests** | Xz/Crc32Tests.cs | Low - Utility tests | +| **Crc64Tests** | Xz/Crc64Tests.cs | Low - Utility tests | +| **RarCRCTest** | Rar/RarCRCTest.cs | Low - Utility tests | +| **RarHeaderFactoryTest** | Rar/RarHeaderFactoryTest.cs | Low - Unit tests | +| **Lzma2Tests** | Xz/Filters/Lzma2Tests.cs | Medium - Filter tests | +| **BCJTests** | Xz/Filters/BCJTests.cs | Medium - Filter tests | +| **BranchExecTests** | Filters/BranchExecTests.cs | Low - Filter tests | +| **ExceptionHierarchyTests** | ExceptionHierarchyTests.cs | N/A - No I/O | +| **UtilityTests** | UtilityTests.cs | N/A - No I/O | +| **Zip64VersionConsistencyTests** | Zip/Zip64VersionConsistencyTests.cs | Low - Unit tests | + +### Directory-level Tests Missing Async + +| Test File | Has Sync | Needs Async | +|-----------|----------|-------------| +| TarArchiveDirectoryTests | ✅ | ❌ | +| TarWriterDirectoryTests | ✅ | ❌ | +| ZipArchiveDirectoryTests | ✅ | ❌ | +| ZipWriterDirectoryTests | ✅ | ❌ | +| GZipArchiveDirectoryTests | ✅ | ❌ | +| GZipWriterDirectoryTests | ✅ | ❌ | + +--- + +## Summary + +### Stream Implementation Work Required + +| Priority | Count | Examples | +|----------|-------|----------| +| **High** | 3 | CBZip2InputStream, CBZip2OutputStream, Bcj2DecoderStream | +| **Medium** | 6 | PkwareTraditionalCryptoStream, WinzipAesCryptoStream, PpmdStream, LzwStream, ExplodeStream, BufferedSubStream | +| **Low** | 4 | Crc32Stream, ShrinkStream, ArcLzwStream | + +### Test Work Required + +| Priority | Count | Examples | +|----------|-------|----------| +| **High** | 1 | SevenZipArchiveTests | +| **Medium** | 2 | Lzma2Tests, BCJTests | +| **Low** | 5 | ArcReaderTests, ArjReaderTests, Crc32Tests, etc. | +| **Directory Tests** | 6 | TarArchiveDirectoryTests, ZipArchiveDirectoryTests, etc. | + +--- + +## Recommendations + +1. **Start with Medium-Priority Streams**: PkwareTraditionalCryptoStream and WinzipAesCryptoStream are commonly used in ZIP files and have straightforward async patterns (async read → sync crypto). + +2. **Consider the ROI**: The BZip2 and Bcj2 streams are complex to make truly async because their core algorithms are CPU-bound. The benefit would primarily be in avoiding blocking on the underlying stream reads. + +3. **Test Coverage**: Prioritize SevenZipArchiveAsyncTests as 7z is a commonly used format. + +4. **Directory Tests**: Create async versions of directory tests to ensure async extraction to directories works correctly. diff --git a/SharpCompress.sln b/SharpCompress.sln index fc9b64b8..0245ff20 100644 --- a/SharpCompress.sln +++ b/SharpCompress.sln @@ -25,6 +25,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{CDB425 README.md = README.md FORMATS.md = FORMATS.md AGENTS.md = AGENTS.md + ASYNC_ANALYSIS.md = ASYNC_ANALYSIS.md EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCompress.Performance", "tests\SharpCompress.Performance\SharpCompress.Performance.csproj", "{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}" diff --git a/src/SharpCompress/Compressors/Xz/BinaryUtils.cs b/src/SharpCompress/Compressors/Xz/BinaryUtils.cs index 12cb2958..61b3eb85 100644 --- a/src/SharpCompress/Compressors/Xz/BinaryUtils.cs +++ b/src/SharpCompress/Compressors/Xz/BinaryUtils.cs @@ -38,7 +38,7 @@ public static class BinaryUtils ) { var bytes = new byte[4]; - var read = await stream.ReadFullyAsync(bytes, cancellationToken).ConfigureAwait(false); + var read = await stream.ReadFullyAsync(bytes, 0, 4, cancellationToken).ConfigureAwait(false); if (!read) { throw new EndOfStreamException(); diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index d781c79b..acd4b669 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -450,26 +450,30 @@ internal static class Utility public static async Task ReadFullyAsync( this Stream stream, byte[] buffer, + int? offset = null, + int? total = null, CancellationToken cancellationToken = default ) { - var total = 0; + offset ??= 0; + total ??= buffer.Length; + var count = 0; int read; while ( ( read = await stream - .ReadAsync(buffer, total, buffer.Length - total, cancellationToken) + .ReadAsync(buffer, offset.Value + count, total.Value - count, cancellationToken) .ConfigureAwait(false) ) > 0 ) { - total += read; - if (total >= buffer.Length) + count += read; + if (count >= total) { return true; } } - return (total >= buffer.Length); + return (count >= total); } public static string TrimNulls(this string source) => source.Replace('\0', ' ').Trim(); diff --git a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs new file mode 100644 index 00000000..ebda1124 --- /dev/null +++ b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs @@ -0,0 +1,167 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Test.Mocks; + +/// +/// A stream wrapper that only allows async read operations. +/// Throws InvalidOperationException on synchronous Read calls to ensure +/// async code paths are being used. +/// +public class AsyncOnlyStream : Stream +{ + private readonly Stream _stream; + private bool _isDisposed; + + public AsyncOnlyStream(Stream stream, bool throwOnSyncMethods = true) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + ThrowOnSyncMethods = throwOnSyncMethods; + } + + public bool ThrowOnSyncMethods { get; set; } + + public override bool CanRead => _stream.CanRead; + + public override bool CanSeek => _stream.CanSeek; + + public override bool CanWrite => _stream.CanWrite; + + public override long Length => _stream.Length; + + public override long Position + { + get => _stream.Position; + set => _stream.Position = value; + } + + public override void Flush() => _stream.Flush(); + + public override Task FlushAsync(CancellationToken cancellationToken) => + _stream.FlushAsync(cancellationToken); + + public override int Read(byte[] buffer, int offset, int count) + { + if (ThrowOnSyncMethods) + { + throw new InvalidOperationException( + "Synchronous Read is not allowed on AsyncOnlyStream. Use ReadAsync instead." + ); + } + return _stream.Read(buffer, offset, count); + } + + public override int ReadByte() + { + if (ThrowOnSyncMethods) + { + throw new InvalidOperationException( + "Synchronous ReadByte is not allowed on AsyncOnlyStream. Use ReadAsync instead." + ); + } + return _stream.ReadByte(); + } + + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) => _stream.ReadAsync(buffer, offset, count, cancellationToken); + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) => _stream.ReadAsync(buffer, cancellationToken); + + public override int Read(Span buffer) + { + if (ThrowOnSyncMethods) + { + throw new InvalidOperationException( + "Synchronous Read is not allowed on AsyncOnlyStream. Use ReadAsync instead." + ); + } + return _stream.Read(buffer); + } +#endif + + public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin); + + public override void SetLength(long value) => _stream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) + { + if (ThrowOnSyncMethods) + { + throw new InvalidOperationException( + "Synchronous Write is not allowed on AsyncOnlyStream. Use WriteAsync instead." + ); + } + _stream.Write(buffer, offset, count); + } + + public override void WriteByte(byte value) + { + if (ThrowOnSyncMethods) + { + throw new InvalidOperationException( + "Synchronous WriteByte is not allowed on AsyncOnlyStream. Use WriteAsync instead." + ); + } + _stream.WriteByte(value); + } + + public override Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) => _stream.WriteAsync(buffer, offset, count, cancellationToken); + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) => _stream.WriteAsync(buffer, cancellationToken); + + public override void Write(ReadOnlySpan buffer) + { + if (ThrowOnSyncMethods) + { + throw new InvalidOperationException( + "Synchronous Write is not allowed on AsyncOnlyStream. Use WriteAsync instead." + ); + } + _stream.Write(buffer); + } +#endif + + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + _isDisposed = true; + if (disposing) + { + _stream.Dispose(); + } + } + base.Dispose(disposing); + } + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override async ValueTask DisposeAsync() + { + if (!_isDisposed) + { + _isDisposed = true; + await _stream.DisposeAsync().ConfigureAwait(false); + } + await base.DisposeAsync().ConfigureAwait(false); + } +#endif +} diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index 2892be57..5093c163 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -13,12 +13,21 @@ public class ZipReaderAsyncTests : ReaderTests { public ZipReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true; + /// + /// Opens a file with an AsyncOnlyStream wrapper. + /// ThrowOnSyncMethods starts as false to allow synchronous reads during Open/format detection, + /// then should be set to true after opening to ensure async code paths are used. + /// + private static AsyncOnlyStream OpenAsyncOnlyFile(string path) => + new(new ForwardOnlyStream(File.OpenRead(path)), throwOnSyncMethods: false); + [Fact] public async Task Issue_269_Double_Skip_Async() { var path = Path.Combine(TEST_ARCHIVES_PATH, "PrePostHeaders.zip"); - using Stream stream = new ForwardOnlyStream(File.OpenRead(path)); + using var stream = OpenAsyncOnlyFile(path); using var reader = ReaderFactory.Open(stream); + stream.ThrowOnSyncMethods = true; var count = 0; while (await reader.MoveToNextEntryAsync()) { @@ -60,10 +69,11 @@ public class ZipReaderAsyncTests : ReaderTests [Fact] public async Task Zip_Deflate_Streamed_Skip_Async() { - using Stream stream = new ForwardOnlyStream( - File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) + using var stream = OpenAsyncOnlyFile( + Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip") ); using var reader = ReaderFactory.Open(stream); + stream.ThrowOnSyncMethods = true; var x = 0; while (await reader.MoveToNextEntryAsync()) { @@ -116,21 +126,20 @@ public class ZipReaderAsyncTests : ReaderTests [Fact] public async Task Zip_BZip2_PkwareEncryption_Read_Async() { - using ( - Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.pkware.zip")) - ) - using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" })) + using var stream = OpenAsyncOnlyFile( + Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.pkware.zip") + ); + using var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }); + stream.ThrowOnSyncMethods = true; + while (await reader.MoveToNextEntryAsync()) { - while (await reader.MoveToNextEntryAsync()) + if (!reader.Entry.IsDirectory) { - if (!reader.Entry.IsDirectory) - { - Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); - } + Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType); + await reader.WriteEntryToDirectoryAsync( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); } } VerifyFiles(); @@ -139,11 +148,13 @@ public class ZipReaderAsyncTests : ReaderTests [Fact] public async Task Zip_Reader_Disposal_Test_Async() { - using var stream = new TestStream( - File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) + using var innerStream = OpenAsyncOnlyFile( + Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip") ); + using var stream = new TestStream(innerStream); using (var reader = ReaderFactory.Open(stream)) { + innerStream.ThrowOnSyncMethods = true; while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) @@ -161,10 +172,12 @@ public class ZipReaderAsyncTests : ReaderTests [Fact] public async Task Zip_Reader_Disposal_Test2_Async() { - using var stream = new TestStream( - File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) + using var innerStream = OpenAsyncOnlyFile( + Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip") ); + using var stream = new TestStream(innerStream); var reader = ReaderFactory.Open(stream); + innerStream.ThrowOnSyncMethods = true; while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) @@ -182,38 +195,11 @@ public class ZipReaderAsyncTests : ReaderTests public async Task Zip_LZMA_WinzipAES_Read_Async() => await Assert.ThrowsAsync(async () => { - using ( - Stream stream = File.OpenRead( - Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.WinzipAES.zip") - ) - ) - using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" })) - { - while (await reader.MoveToNextEntryAsync()) - { - if (!reader.Entry.IsDirectory) - { - Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); - } - } - } - VerifyFiles(); - }); - - [Fact] - public async Task Zip_Deflate_WinzipAES_Read_Async() - { - using ( - Stream stream = File.OpenRead( - Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip") - ) - ) - using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" })) - { + using var stream = OpenAsyncOnlyFile( + Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.WinzipAES.zip") + ); + using var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }); + stream.ThrowOnSyncMethods = true; while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) @@ -225,6 +211,27 @@ public class ZipReaderAsyncTests : ReaderTests ); } } + VerifyFiles(); + }); + + [Fact] + public async Task Zip_Deflate_WinzipAES_Read_Async() + { + using var stream = OpenAsyncOnlyFile( + Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip") + ); + using var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }); + stream.ThrowOnSyncMethods = true; + while (await reader.MoveToNextEntryAsync()) + { + if (!reader.Entry.IsDirectory) + { + Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType); + await reader.WriteEntryToDirectoryAsync( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } } VerifyFiles(); } @@ -233,20 +240,21 @@ public class ZipReaderAsyncTests : ReaderTests public async Task Zip_Deflate_ZipCrypto_Read_Async() { var count = 0; - using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip"))) - using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" })) + using var stream = OpenAsyncOnlyFile( + Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip") + ); + using var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }); + stream.ThrowOnSyncMethods = true; + while (await reader.MoveToNextEntryAsync()) { - while (await reader.MoveToNextEntryAsync()) + if (!reader.Entry.IsDirectory) { - if (!reader.Entry.IsDirectory) - { - Assert.Equal(CompressionType.None, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); - count++; - } + Assert.Equal(CompressionType.None, reader.Entry.CompressionType); + await reader.WriteEntryToDirectoryAsync( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + count++; } } Assert.Equal(8, count);