diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs index 70b61bae2..031287ed1 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs @@ -36,11 +36,10 @@ internal IEnumerable ReadStreamHeader(Stream stream) throw new ArgumentException("Stream must be a SharpCompressStream", nameof(stream)); } } - SharpCompressStream rewindableStream = (SharpCompressStream)stream; + var rewindableStream = (SharpCompressStream)stream; while (true) { - ZipHeader? header; var reader = new BinaryReader(rewindableStream); uint headerBytes = 0; if ( @@ -155,7 +154,7 @@ internal IEnumerable ReadStreamHeader(Stream stream) } _lastEntryHeader = null; - header = ReadHeader(headerBytes, reader); + var header = ReadHeader(headerBytes, reader); if (header is null) { yield break; diff --git a/src/SharpCompress/Compressors/Deflate64/Deflate64Stream.cs b/src/SharpCompress/Compressors/Deflate64/Deflate64Stream.cs index 8ef83ec41..3ba9e618e 100644 --- a/src/SharpCompress/Compressors/Deflate64/Deflate64Stream.cs +++ b/src/SharpCompress/Compressors/Deflate64/Deflate64Stream.cs @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Zip; using SharpCompress.IO; @@ -39,7 +39,6 @@ void IStreamStack.SetPosition(long position) { } private const int DEFAULT_BUFFER_SIZE = 8192; private Stream _stream; - private CompressionMode _mode; private InflaterManaged _inflater; private byte[] _buffer; @@ -62,61 +61,23 @@ public Deflate64Stream(Stream stream, CompressionMode mode) throw new ArgumentException("Deflate64: input stream is not readable", nameof(stream)); } - InitializeInflater(stream, ZipCompressionMethod.Deflate64); -#if DEBUG_STREAMS - this.DebugConstruct(typeof(Deflate64Stream)); -#endif - } - - /// - /// Sets up this DeflateManagedStream to be used for Inflation/Decompression - /// - private void InitializeInflater( - Stream stream, - ZipCompressionMethod method = ZipCompressionMethod.Deflate - ) - { - Debug.Assert(stream != null); - Debug.Assert( - method == ZipCompressionMethod.Deflate || method == ZipCompressionMethod.Deflate64 - ); if (!stream.CanRead) { throw new ArgumentException("Deflate64: input stream is not readable", nameof(stream)); } - _inflater = new InflaterManaged(method == ZipCompressionMethod.Deflate64); + _inflater = new InflaterManaged(true); _stream = stream; - _mode = CompressionMode.Decompress; _buffer = new byte[DEFAULT_BUFFER_SIZE]; +#if DEBUG_STREAMS + this.DebugConstruct(typeof(Deflate64Stream)); +#endif } - public override bool CanRead - { - get - { - if (_stream is null) - { - return false; - } - - return (_mode == CompressionMode.Decompress && _stream.CanRead); - } - } - - public override bool CanWrite - { - get - { - if (_stream is null) - { - return false; - } + public override bool CanRead => _stream.CanRead; - return (_mode == CompressionMode.Compress && _stream.CanWrite); - } - } + public override bool CanWrite => false; public override bool CanSeek => false; @@ -138,7 +99,6 @@ public override void SetLength(long value) => public override int Read(byte[] array, int offset, int count) { - EnsureDecompressionMode(); ValidateParameters(array, offset, count); EnsureNotDisposed(); @@ -185,6 +145,106 @@ public override int Read(byte[] array, int offset, int count) return count - remainingCount; } + public override async Task ReadAsync( + byte[] array, + int offset, + int count, + CancellationToken cancellationToken + ) + { + ValidateParameters(array, offset, count); + EnsureNotDisposed(); + + int bytesRead; + var currentOffset = offset; + var remainingCount = count; + + while (true) + { + bytesRead = _inflater.Inflate(array, currentOffset, remainingCount); + currentOffset += bytesRead; + remainingCount -= bytesRead; + + if (remainingCount == 0) + { + break; + } + + if (_inflater.Finished()) + { + // if we finished decompressing, we can't have anything left in the outputwindow. + Debug.Assert( + _inflater.AvailableOutput == 0, + "We should have copied all stuff out!" + ); + break; + } + + var bytes = await _stream + .ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) + .ConfigureAwait(false); + if (bytes <= 0) + { + break; + } + else if (bytes > _buffer.Length) + { + // The stream is either malicious or poorly implemented and returned a number of + // bytes larger than the buffer supplied to it. + throw new InvalidFormatException("Deflate64: invalid data"); + } + + _inflater.SetInput(_buffer, 0, bytes); + } + + return count - remainingCount; + } + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + EnsureNotDisposed(); + + // InflaterManaged doesn't have a Span-based Inflate method, so we need to work with arrays + // For large buffers, we could rent from ArrayPool, but for simplicity we'll use the buffer's array if available + if ( + System.Runtime.InteropServices.MemoryMarshal.TryGetArray( + buffer, + out var arraySegment + ) + ) + { + // Fast path: the Memory is backed by an array + return await ReadAsync( + arraySegment.Array!, + arraySegment.Offset, + arraySegment.Count, + cancellationToken + ) + .ConfigureAwait(false); + } + else + { + // Slow path: rent a temporary array + var tempBuffer = System.Buffers.ArrayPool.Shared.Rent(buffer.Length); + try + { + var bytesRead = await ReadAsync(tempBuffer, 0, buffer.Length, cancellationToken) + .ConfigureAwait(false); + tempBuffer.AsMemory(0, bytesRead).CopyTo(buffer); + return bytesRead; + } + finally + { + System.Buffers.ArrayPool.Shared.Return(tempBuffer); + } + } + } +#endif + private void ValidateParameters(byte[] array, int offset, int count) { if (array is null) @@ -220,26 +280,6 @@ private void EnsureNotDisposed() private static void ThrowStreamClosedException() => throw new ObjectDisposedException(null, "Deflate64: stream has been disposed"); - private void EnsureDecompressionMode() - { - if (_mode != CompressionMode.Decompress) - { - ThrowCannotReadFromDeflateManagedStreamException(); - } - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowCannotReadFromDeflateManagedStreamException() => - throw new InvalidOperationException("Deflate64: cannot read from this stream"); - - private void EnsureCompressionMode() - { - if (_mode != CompressionMode.Compress) - { - ThrowCannotWriteToDeflateManagedStreamException(); - } - } - [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowCannotWriteToDeflateManagedStreamException() => throw new InvalidOperationException("Deflate64: cannot write to this stream"); @@ -281,20 +321,17 @@ protected override void Dispose(bool disposing) #endif if (disposing) { - _stream?.Dispose(); + _stream.Dispose(); } } finally { - _stream = null; - try { - _inflater?.Dispose(); + _inflater.Dispose(); } finally { - _inflater = null; base.Dispose(disposing); } } diff --git a/src/SharpCompress/IO/ReadOnlySubStream.cs b/src/SharpCompress/IO/ReadOnlySubStream.cs index ffcd55fd7..5e317c257 100644 --- a/src/SharpCompress/IO/ReadOnlySubStream.cs +++ b/src/SharpCompress/IO/ReadOnlySubStream.cs @@ -1,6 +1,8 @@ using System; using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.IO; @@ -93,6 +95,47 @@ public override int Read(Span buffer) } #endif + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (BytesLeftToRead < count) + { + count = (int)BytesLeftToRead; + } + var read = await Stream + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + if (read > 0) + { + BytesLeftToRead -= read; + _position += read; + } + return read; + } + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + var sliceLen = BytesLeftToRead < buffer.Length ? BytesLeftToRead : buffer.Length; + var read = await Stream + .ReadAsync(buffer.Slice(0, (int)sliceLen), cancellationToken) + .ConfigureAwait(false); + if (read > 0) + { + BytesLeftToRead -= read; + _position += read; + } + return read; + } +#endif + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index 4bc71031f..9277753d7 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -96,6 +96,33 @@ public bool MoveToNextEntry() return false; } + public async Task MoveToNextEntryAsync(CancellationToken cancellationToken = default) + { + if (_completed) + { + return false; + } + if (Cancelled) + { + throw new ReaderCancelledException("Reader has been cancelled."); + } + if (_entriesForCurrentReadStream is null) + { + return LoadStreamForReading(RequestInitialStream()); + } + if (!_wroteCurrentEntry) + { + await SkipEntryAsync(cancellationToken).ConfigureAwait(false); + } + _wroteCurrentEntry = false; + if (NextEntryForCurrentStream()) + { + return true; + } + _completed = true; + return false; + } + protected bool LoadStreamForReading(Stream stream) { _entriesForCurrentReadStream?.Dispose(); @@ -129,6 +156,14 @@ private void SkipEntry() } } + private async Task SkipEntryAsync(CancellationToken cancellationToken) + { + if (!Entry.IsDirectory) + { + await SkipAsync(cancellationToken).ConfigureAwait(false); + } + } + private void Skip() { var part = Entry.Parts.First(); @@ -151,6 +186,33 @@ private void Skip() s.SkipEntry(); } + private async Task SkipAsync(CancellationToken cancellationToken) + { + var part = Entry.Parts.First(); + + if (!Entry.IsSplitAfter && !Entry.IsSolid && Entry.CompressedSize > 0) + { + //not solid and has a known compressed size then we can skip raw bytes. + var rawStream = part.GetRawStream(); + + if (rawStream != null) + { + var bytesToAdvance = Entry.CompressedSize; + await rawStream.SkipAsync(bytesToAdvance, cancellationToken).ConfigureAwait(false); + part.Skipped = true; + return; + } + } + //don't know the size so we have to try to decompress to skip +#if NETFRAMEWORK || NETSTANDARD2_0 + using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); + await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); +#else + await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); + await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); +#endif + } + public void WriteEntryTo(Stream writableStream) { if (_wroteCurrentEntry) @@ -232,6 +294,19 @@ public EntryStream OpenEntryStream() return stream; } + public Task OpenEntryStreamAsync(CancellationToken cancellationToken = default) + { + if (_wroteCurrentEntry) + { + throw new ArgumentException( + "WriteEntryToAsync or OpenEntryStreamAsync can only be called once." + ); + } + var stream = GetEntryStream(); + _wroteCurrentEntry = true; + return Task.FromResult(stream); + } + /// /// Retains a reference to the entry stream, so we can check whether it completed later. /// diff --git a/src/SharpCompress/Readers/IReader.cs b/src/SharpCompress/Readers/IReader.cs index b66636c46..c2d7c3c0e 100644 --- a/src/SharpCompress/Readers/IReader.cs +++ b/src/SharpCompress/Readers/IReader.cs @@ -39,9 +39,23 @@ public interface IReader : IDisposable /// bool MoveToNextEntry(); + /// + /// Moves to the next entry asynchronously by reading more data from the underlying stream. This skips if data has not been read. + /// + /// + /// + Task MoveToNextEntryAsync(CancellationToken cancellationToken = default); + /// /// Opens the current entry as a stream that will decompress as it is read. /// Read the entire stream or use SkipEntry on EntryStream. /// EntryStream OpenEntryStream(); + + /// + /// Opens the current entry asynchronously as a stream that will decompress as it is read. + /// Read the entire stream or use SkipEntry on EntryStream. + /// + /// + Task OpenEntryStreamAsync(CancellationToken cancellationToken = default); } diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 904860ce8..9ebb16774 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -335,9 +335,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "B3etT5XQ2nlWkZGO2m/ytDYrOmSsQG1XNBaM6ZYlX5Ch/tDrMFadr0/mK6gjZwaQc55g+5+WZMw4Cz3m8VEF7g==" + "requested": "[8.0.17, )", + "resolved": "8.0.17", + "contentHash": "x5/y4l8AtshpBOrCZdlE4txw8K3e3s9meBFeZeR3l8hbbku2V7kK6ojhXvrbjg1rk3G+JqL1BI26gtgc1ZrdUw==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", diff --git a/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs b/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs index 4c279a3c8..064d20662 100644 --- a/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs +++ b/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; using SharpCompress.Readers; @@ -57,7 +59,7 @@ protected override void Dispose(bool disposing) public override bool CanRead => true; public override bool CanSeek => false; - public override bool CanWrite => false; + public override bool CanWrite => true; public override void Flush() { } @@ -72,10 +74,41 @@ public override long Position public override int Read(byte[] buffer, int offset, int count) => stream.Read(buffer, offset, count); + 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); +#endif + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => - throw new NotSupportedException(); + stream.Write(buffer, offset, count); + + 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); +#endif + + public override Task FlushAsync(CancellationToken cancellationToken) => + stream.FlushAsync(cancellationToken); } diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index b51fd8049..b919331d0 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -126,7 +126,7 @@ public async Task UseReaderAsync( CancellationToken cancellationToken = default ) { - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync(cancellationToken)) { if (!reader.Entry.IsDirectory) { diff --git a/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs new file mode 100644 index 000000000..223ad969a --- /dev/null +++ b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs @@ -0,0 +1,242 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using SharpCompress.Archives; +using SharpCompress.Common; +using SharpCompress.Common.Zip; +using SharpCompress.Compressors.Deflate; +using SharpCompress.Readers; +using SharpCompress.Readers.Zip; +using SharpCompress.Test.Mocks; +using SharpCompress.Writers; +using SharpCompress.Writers.Zip; +using Xunit; + +namespace SharpCompress.Test.Zip; + +public class Zip64AsyncTests : WriterTests +{ + public Zip64AsyncTests() + : base(ArchiveType.Zip) { } + + // 4GiB + 1 + private const long FOUR_GB_LIMIT = ((long)uint.MaxValue) + 1; + + //[Fact] + [Trait("format", "zip64")] + public async Task Zip64_Single_Large_File_Async() => + await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false); + + //[Fact] + [Trait("format", "zip64")] + public async Task Zip64_Two_Large_Files_Async() => + await RunSingleTestAsync(2, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false); + + [Fact] + [Trait("format", "zip64")] + public async Task Zip64_Two_Small_files_Async() => + // Multiple files, does not require zip64 + await RunSingleTestAsync(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: false); + + [Fact] + [Trait("format", "zip64")] + public async Task Zip64_Two_Small_files_stream_Async() => + await RunSingleTestAsync(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: true); + + [Fact] + [Trait("format", "zip64")] + public async Task Zip64_Two_Small_Files_Zip64_Async() => + // Multiple files, use zip64 even though it is not required + await RunSingleTestAsync(2, FOUR_GB_LIMIT / 2, setZip64: true, forwardOnly: false); + + [Fact] + [Trait("format", "zip64")] + public async Task Zip64_Single_Large_File_Fail_Async() + { + try + { + // One single file, should fail + await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: false, forwardOnly: false); + throw new InvalidOperationException("Test did not fail?"); + } + catch (NotSupportedException) { } + } + + [Fact] + [Trait("zip64", "true")] + public async Task Zip64_Single_Large_File_Zip64_Streaming_Fail_Async() + { + try + { + // One single file, should fail (fast) with zip64 + await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: true, forwardOnly: true); + throw new InvalidOperationException("Test did not fail?"); + } + catch (NotSupportedException) { } + } + + [Fact] + [Trait("zip64", "true")] + public async Task Zip64_Single_Large_File_Streaming_Fail_Async() + { + try + { + // One single file, should fail once the write discovers the problem + await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: false, forwardOnly: true); + throw new InvalidOperationException("Test did not fail?"); + } + catch (NotSupportedException) { } + } + + public async Task RunSingleTestAsync( + long files, + long filesize, + bool setZip64, + bool forwardOnly, + long writeChunkSize = 1024 * 1024, + string filename = "zip64-test-async.zip" + ) + { + filename = Path.Combine(SCRATCH2_FILES_PATH, filename); + + try + { + if (File.Exists(filename)) + { + File.Delete(filename); + } + + if (!File.Exists(filename)) + { + await CreateZipArchiveAsync( + filename, + files, + filesize, + writeChunkSize, + setZip64, + forwardOnly + ); + } + + var resForward = await ReadForwardOnlyAsync(filename); + if (resForward.Item1 != files) + { + throw new InvalidOperationException( + $"Incorrect number of items reported: {resForward.Item1}, should have been {files}" + ); + } + + if (resForward.Item2 != files * filesize) + { + throw new InvalidOperationException( + $"Incorrect combined size reported: {resForward.Item2}, should have been {files * filesize}" + ); + } + + var resArchive = ReadArchive(filename); + if (resArchive.Item1 != files) + { + throw new InvalidOperationException( + $"Incorrect number of items reported: {resArchive.Item1}, should have been {files}" + ); + } + + if (resArchive.Item2 != files * filesize) + { + throw new InvalidOperationException( + $"Incorrect number of items reported: {resArchive.Item2}, should have been {files * filesize}" + ); + } + } + finally + { + if (File.Exists(filename)) + { + File.Delete(filename); + } + } + } + + public async Task CreateZipArchiveAsync( + string filename, + long files, + long filesize, + long chunksize, + bool setZip64, + bool forwardOnly + ) + { + var data = new byte[chunksize]; + + // Use deflate for speed + var opts = new ZipWriterOptions(CompressionType.Deflate) { UseZip64 = setZip64 }; + + // Use no compression to ensure we hit the limits (actually inflates a bit, but seems better than using method==Store) + var eo = new ZipWriterEntryOptions { DeflateCompressionLevel = CompressionLevel.None }; + + using var zip = File.OpenWrite(filename); + using var st = forwardOnly ? (Stream)new ForwardOnlyStream(zip) : zip; + using var zipWriter = (ZipWriter)WriterFactory.Open(st, ArchiveType.Zip, opts); + for (var i = 0; i < files; i++) + { + using var str = zipWriter.WriteToStream(i.ToString(), eo); + var left = filesize; + while (left > 0) + { + var b = (int)Math.Min(left, data.Length); + // Use synchronous Write to match the sync version and avoid ForwardOnlyStream issues + await str.WriteAsync(data, 0, b); + left -= b; + } + } + } + + public async Task> ReadForwardOnlyAsync(string filename) + { + long count = 0; + long size = 0; + ZipEntry? prev = null; + using (var fs = File.OpenRead(filename)) + using (var rd = ZipReader.Open(fs, new ReaderOptions { LookForHeader = false })) + { + while (await rd.MoveToNextEntryAsync()) + { +#if NETFRAMEWORK || NETSTANDARD2_0 + using (var entryStream = await rd.OpenEntryStreamAsync()) + { + await entryStream.SkipEntryAsync(); + } +#else + await using (var entryStream = await rd.OpenEntryStreamAsync()) + { + await entryStream.SkipEntryAsync(); + } +#endif + count++; + if (prev != null) + { + size += prev.Size; + } + + prev = rd.Entry; + } + } + + if (prev != null) + { + size += prev.Size; + } + + return new Tuple(count, size); + } + + public Tuple ReadArchive(string filename) + { + using var archive = ArchiveFactory.Open(filename); + return new Tuple( + archive.Entries.Count(), + archive.Entries.Select(x => x.Size).Sum() + ); + } +} diff --git a/tests/SharpCompress.Test/Zip/Zip64Tests.cs b/tests/SharpCompress.Test/Zip/Zip64Tests.cs index a89b11287..e92c8d52c 100644 --- a/tests/SharpCompress.Test/Zip/Zip64Tests.cs +++ b/tests/SharpCompress.Test/Zip/Zip64Tests.cs @@ -22,31 +22,37 @@ public Zip64Tests() // 4GiB + 1 private const long FOUR_GB_LIMIT = ((long)uint.MaxValue) + 1; + //[Fact] [Trait("format", "zip64")] public void Zip64_Single_Large_File() => // One single file, requires zip64 RunSingleTest(1, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false); + //[Fact] [Trait("format", "zip64")] public void Zip64_Two_Large_Files() => // One single file, requires zip64 RunSingleTest(2, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false); + [Fact] [Trait("format", "zip64")] public void Zip64_Two_Small_files() => // Multiple files, does not require zip64 RunSingleTest(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: false); + [Fact] [Trait("format", "zip64")] public void Zip64_Two_Small_files_stream() => // Multiple files, does not require zip64, and works with streams RunSingleTest(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: true); + [Fact] [Trait("format", "zip64")] public void Zip64_Two_Small_Files_Zip64() => // Multiple files, use zip64 even though it is not required RunSingleTest(2, FOUR_GB_LIMIT / 2, setZip64: true, forwardOnly: false); + [Fact] [Trait("format", "zip64")] public void Zip64_Single_Large_File_Fail() { @@ -59,6 +65,7 @@ public void Zip64_Single_Large_File_Fail() catch (NotSupportedException) { } } + [Fact] [Trait("zip64", "true")] public void Zip64_Single_Large_File_Zip64_Streaming_Fail() { @@ -71,6 +78,7 @@ public void Zip64_Single_Large_File_Zip64_Streaming_Fail() catch (NotSupportedException) { } } + [Fact] [Trait("zip64", "true")] public void Zip64_Single_Large_File_Streaming_Fail() { diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs new file mode 100644 index 000000000..798c15b19 --- /dev/null +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -0,0 +1,196 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Archives; +using SharpCompress.Archives.Zip; +using SharpCompress.Common; +using SharpCompress.Writers; +using SharpCompress.Writers.Zip; +using Xunit; + +namespace SharpCompress.Test.Zip; + +public class ZipArchiveAsyncTests : ArchiveTests +{ + public ZipArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true; + + [Fact] + public async Task Zip_ZipX_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.zipx"); + + [Fact] + public async Task Zip_BZip2_Streamed_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.bzip2.dd.zip"); + + [Fact] + public async Task Zip_BZip2_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.bzip2.zip"); + + [Fact] + public async Task Zip_Deflate_Streamed2_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.deflate.dd-.zip"); + + [Fact] + public async Task Zip_Deflate_Streamed_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.deflate.dd.zip"); + + [Fact] + public async Task Zip_Deflate_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.deflate.zip"); + + [Fact] + public async Task Zip_Deflate64_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.deflate64.zip"); + + [Fact] + public async Task Zip_LZMA_Streamed_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.lzma.dd.zip"); + + [Fact] + public async Task Zip_LZMA_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.lzma.zip"); + + [Fact] + public async Task Zip_PPMd_Streamed_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.ppmd.dd.zip"); + + [Fact] + public async Task Zip_PPMd_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.ppmd.zip"); + + [Fact] + public async Task Zip_None_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.none.zip"); + + [Fact] + public async Task Zip_Zip64_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Zip.zip64.zip"); + + [Fact] + public async Task Zip_Shrink_ArchiveStreamRead_Async() + { + UseExtensionInsteadOfNameToVerify = true; + UseCaseInsensitiveToVerify = true; + await ArchiveStreamReadAsync("Zip.shrink.zip"); + } + + [Fact] + public async Task Zip_Implode_ArchiveStreamRead_Async() + { + UseExtensionInsteadOfNameToVerify = true; + UseCaseInsensitiveToVerify = true; + await ArchiveStreamReadAsync("Zip.implode.zip"); + } + + [Fact] + public async Task Zip_Reduce1_ArchiveStreamRead_Async() + { + UseExtensionInsteadOfNameToVerify = true; + UseCaseInsensitiveToVerify = true; + await ArchiveStreamReadAsync("Zip.reduce1.zip"); + } + + [Fact] + public async Task Zip_Reduce2_ArchiveStreamRead_Async() + { + UseExtensionInsteadOfNameToVerify = true; + UseCaseInsensitiveToVerify = true; + await ArchiveStreamReadAsync("Zip.reduce2.zip"); + } + + [Fact] + public async Task Zip_Reduce3_ArchiveStreamRead_Async() + { + UseExtensionInsteadOfNameToVerify = true; + UseCaseInsensitiveToVerify = true; + await ArchiveStreamReadAsync("Zip.reduce3.zip"); + } + + [Fact] + public async Task Zip_Reduce4_ArchiveStreamRead_Async() + { + UseExtensionInsteadOfNameToVerify = true; + UseCaseInsensitiveToVerify = true; + await ArchiveStreamReadAsync("Zip.reduce4.zip"); + } + + [Fact] + public async Task Zip_Random_Write_Remove_Async() + { + var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.mod.zip"); + var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.noEmptyDirs.zip"); + var modified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.mod.zip"); + + using (var archive = ZipArchive.Open(unmodified)) + { + var entry = archive.Entries.Single(x => + x.Key.NotNull().EndsWith("jpg", StringComparison.OrdinalIgnoreCase) + ); + archive.RemoveEntry(entry); + + WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate); + writerOptions.ArchiveEncoding.Default = Encoding.GetEncoding(866); + + await archive.SaveToAsync(scratchPath, writerOptions); + } + CompareArchivesByPath(modified, scratchPath, Encoding.GetEncoding(866)); + } + + [Fact] + public async Task Zip_Random_Write_Add_Async() + { + var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg"); + var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.mod.zip"); + var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.mod.zip"); + var modified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.noEmptyDirs.zip"); + + using (var archive = ZipArchive.Open(unmodified)) + { + archive.AddEntry("jpg\\test.jpg", jpg); + + WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate); + writerOptions.ArchiveEncoding.Default = Encoding.GetEncoding(866); + + await archive.SaveToAsync(scratchPath, writerOptions); + } + CompareArchivesByPath(modified, scratchPath, Encoding.GetEncoding(866)); + } + + [Fact] + public async Task Zip_Create_New_Async() + { + var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.noEmptyDirs.zip"); + var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.noEmptyDirs.zip"); + + using (var archive = ZipArchive.Create()) + { + archive.DeflateCompressionLevel = Compressors.Deflate.CompressionLevel.BestSpeed; + archive.AddAllFromDirectory(ORIGINAL_FILES_PATH); + + WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate); + writerOptions.ArchiveEncoding.Default = Encoding.UTF8; + + await archive.SaveToAsync(scratchPath, writerOptions); + } + CompareArchivesByPath(unmodified, scratchPath); + } + + [Fact] + public async Task Zip_Deflate_Entry_Stream_Async() + { + using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"))) + using (var archive = ZipArchive.Open(stream)) + { + foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + { + await entry.WriteToDirectoryAsync( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + } + VerifyFiles(); + } +} diff --git a/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs new file mode 100644 index 000000000..eae1b22e2 --- /dev/null +++ b/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using SharpCompress.Archives.Zip; +using SharpCompress.Common; +using SharpCompress.Compressors.Deflate; +using SharpCompress.Compressors.Xz; +using SharpCompress.Crypto; +using SharpCompress.Writers; +using SharpCompress.Writers.Zip; +using Xunit; + +namespace SharpCompress.Test.Zip; + +public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests +{ + public ZipTypesLevelsWithCrcRatioAsyncTests() => UseExtensionInsteadOfNameToVerify = true; + + [Theory] + [InlineData(CompressionType.Deflate, 1, 1, 0.11f)] // was 0.8f, actual 0.104 + [InlineData(CompressionType.Deflate, 3, 1, 0.08f)] // was 0.8f, actual 0.078 + [InlineData(CompressionType.Deflate, 6, 1, 0.05f)] // was 0.8f, actual ~0.042 + [InlineData(CompressionType.Deflate, 9, 1, 0.04f)] // was 0.7f, actual 0.038 + [InlineData(CompressionType.ZStandard, 1, 1, 0.025f)] // was 0.8f, actual 0.023 + [InlineData(CompressionType.ZStandard, 3, 1, 0.015f)] // was 0.7f, actual 0.013 + [InlineData(CompressionType.ZStandard, 9, 1, 0.006f)] // was 0.7f, actual 0.005 + [InlineData(CompressionType.ZStandard, 22, 1, 0.005f)] // was 0.7f, actual 0.004 + [InlineData(CompressionType.BZip2, 0, 1, 0.035f)] // was 0.8f, actual 0.033 + [InlineData(CompressionType.LZMA, 0, 1, 0.005f)] // was 0.8f, actual 0.004 + [InlineData(CompressionType.None, 0, 1, 1.001f)] // was 1.1f, actual 1.000 + [InlineData(CompressionType.Deflate, 6, 2, 0.045f)] // was 0.8f, actual 0.042 + [InlineData(CompressionType.ZStandard, 3, 2, 0.012f)] // was 0.7f, actual 0.010 + [InlineData(CompressionType.BZip2, 0, 2, 0.035f)] // was 0.8f, actual 0.032 + [InlineData(CompressionType.Deflate, 9, 3, 0.04f)] // was 0.7f, actual 0.038 + [InlineData(CompressionType.ZStandard, 9, 3, 0.003f)] // was 0.7f, actual 0.002 + public async Task Zip_Create_Archive_With_3_Files_Crc32_Test_Async( + CompressionType compressionType, + int compressionLevel, + int sizeMb, + float expectedRatio + ) + { + const int OneMiB = 1024 * 1024; + var baseSize = sizeMb * OneMiB; + + // Generate test content for files with sizes based on the sizeMb parameter + var file1Data = TestPseudoTextStream.Create(baseSize); + var file2Data = TestPseudoTextStream.Create(baseSize * 2); + var file3Data = TestPseudoTextStream.Create(baseSize * 3); + + var expectedFiles = new Dictionary + { + [$"file1_{sizeMb}MiB.txt"] = (file1Data, CalculateCrc32(file1Data)), + [$"data/file2_{sizeMb * 2}MiB.txt"] = (file2Data, CalculateCrc32(file2Data)), + [$"deep/nested/file3_{sizeMb * 3}MiB.txt"] = (file3Data, CalculateCrc32(file3Data)), + }; + + // Create zip archive in memory + using var zipStream = new MemoryStream(); + using (var writer = CreateWriterWithLevel(zipStream, compressionType, compressionLevel)) + { + await writer.WriteAsync($"file1_{sizeMb}MiB.txt", new MemoryStream(file1Data)); + await writer.WriteAsync($"data/file2_{sizeMb * 2}MiB.txt", new MemoryStream(file2Data)); + await writer.WriteAsync( + $"deep/nested/file3_{sizeMb * 3}MiB.txt", + new MemoryStream(file3Data) + ); + } + + // Calculate and output actual compression ratio + var originalSize = file1Data.Length + file2Data.Length + file3Data.Length; + var actualRatio = (double)zipStream.Length / originalSize; + //Debug.WriteLine($"Zip_Create_Archive_With_3_Files_Crc32_Test_Async: {compressionType} Level={compressionLevel} Size={sizeMb}MB Expected={expectedRatio:F3} Actual={actualRatio:F3}"); + + // Verify compression occurred (except for None compression type) + if (compressionType != CompressionType.None) + { + Assert.True( + zipStream.Length < originalSize, + $"Compression failed: compressed={zipStream.Length}, original={originalSize}" + ); + } + + // Verify compression ratio + VerifyCompressionRatio( + originalSize, + zipStream.Length, + expectedRatio, + $"{compressionType} level {compressionLevel}" + ); + + // Verify archive content and CRC32 + await VerifyArchiveContentAsync(zipStream, expectedFiles); + + // Verify compression type is correctly set + VerifyCompressionType(zipStream, compressionType); + } + + [Theory] + [InlineData(CompressionType.Deflate, 1, 4, 0.11f)] // was 0.8, actual 0.105 + [InlineData(CompressionType.Deflate, 3, 4, 0.08f)] // was 0.8, actual 0.077 + [InlineData(CompressionType.Deflate, 6, 4, 0.045f)] // was 0.8, actual 0.042 + [InlineData(CompressionType.Deflate, 9, 4, 0.04f)] // was 0.8, actual 0.037 + [InlineData(CompressionType.ZStandard, 1, 4, 0.025f)] // was 0.8, actual 0.022 + [InlineData(CompressionType.ZStandard, 3, 4, 0.012f)] // was 0.8, actual 0.010 + [InlineData(CompressionType.ZStandard, 9, 4, 0.003f)] // was 0.8, actual 0.002 + [InlineData(CompressionType.ZStandard, 22, 4, 0.003f)] // was 0.8, actual 0.002 + [InlineData(CompressionType.BZip2, 0, 4, 0.035f)] // was 0.8, actual 0.032 + [InlineData(CompressionType.LZMA, 0, 4, 0.003f)] // was 0.8, actual 0.002 + public async Task Zip_WriterFactory_Crc32_Test_Async( + CompressionType compressionType, + int compressionLevel, + int sizeMb, + float expectedRatio + ) + { + var fileSize = sizeMb * 1024 * 1024; + + var testData = TestPseudoTextStream.Create(fileSize); + var expectedCrc = CalculateCrc32(testData); + + // Create archive with specified compression level + using var zipStream = new MemoryStream(); + var writerOptions = new ZipWriterOptions(compressionType) + { + CompressionLevel = compressionLevel, + }; + + using (var writer = WriterFactory.Open(zipStream, ArchiveType.Zip, writerOptions)) + { + await writer.WriteAsync( + $"{compressionType}_level_{compressionLevel}_{sizeMb}MiB.txt", + new MemoryStream(testData) + ); + } + + // Calculate and output actual compression ratio + var actualRatio = (double)zipStream.Length / testData.Length; + //Debug.WriteLine($"Zip_WriterFactory_Crc32_Test_Async: {compressionType} Level={compressionLevel} Size={sizeMb}MB Expected={expectedRatio:F3} Actual={actualRatio:F3}"); + + VerifyCompressionRatio( + testData.Length, + zipStream.Length, + expectedRatio, + $"{compressionType} level {compressionLevel}" + ); + + // Verify the archive + zipStream.Position = 0; + using var archive = ZipArchive.Open(zipStream); + + var entry = archive.Entries.Single(e => !e.IsDirectory); + using var entryStream = entry.OpenEntryStream(); + using var extractedStream = new MemoryStream(); + await entryStream.CopyToAsync(extractedStream); + + var extractedData = extractedStream.ToArray(); + var actualCrc = CalculateCrc32(extractedData); + + Assert.Equal(compressionType, entry.CompressionType); + Assert.Equal(expectedCrc, actualCrc); + Assert.Equal(testData.Length, extractedData.Length); + Assert.Equal(testData, extractedData); + } + + [Theory] + [InlineData(CompressionType.Deflate, 1, 2, 0.11f)] // was 0.8, actual 0.104 + [InlineData(CompressionType.Deflate, 3, 2, 0.08f)] // was 0.8, actual 0.077 + [InlineData(CompressionType.Deflate, 6, 2, 0.045f)] // was 0.8, actual 0.042 + [InlineData(CompressionType.Deflate, 9, 2, 0.04f)] // was 0.7, actual 0.038 + [InlineData(CompressionType.ZStandard, 1, 2, 0.025f)] // was 0.8, actual 0.023 + [InlineData(CompressionType.ZStandard, 3, 2, 0.015f)] // was 0.7, actual 0.012 + [InlineData(CompressionType.ZStandard, 9, 2, 0.006f)] // was 0.7, actual 0.005 + [InlineData(CompressionType.ZStandard, 22, 2, 0.005f)] // was 0.7, actual 0.004 + [InlineData(CompressionType.BZip2, 0, 2, 0.035f)] // was 0.8, actual 0.032 + [InlineData(CompressionType.LZMA, 0, 2, 0.005f)] // was 0.8, actual 0.004 + public async Task Zip_ZipArchiveOpen_Crc32_Test_Async( + CompressionType compressionType, + int compressionLevel, + int sizeMb, + float expectedRatio + ) + { + var fileSize = sizeMb * 1024 * 1024; + + var testData = TestPseudoTextStream.Create(fileSize); + var expectedCrc = CalculateCrc32(testData); + + // Create archive with specified compression and level + using var zipStream = new MemoryStream(); + using (var writer = CreateWriterWithLevel(zipStream, compressionType, compressionLevel)) + { + await writer.WriteAsync( + $"{compressionType}_{compressionLevel}_{sizeMb}MiB.txt", + new MemoryStream(testData) + ); + } + + // Calculate and output actual compression ratio + var actualRatio = (double)zipStream.Length / testData.Length; + //Debug.WriteLine($"Zip_ZipArchiveOpen_Crc32_Test_Async: {compressionType} Level={compressionLevel} Size={sizeMb}MB Expected={expectedRatio:F3} Actual={actualRatio:F3}"); + + // Verify the archive + zipStream.Position = 0; + using var archive = ZipArchive.Open(zipStream); + + var entry = archive.Entries.Single(e => !e.IsDirectory); + using var entryStream = entry.OpenEntryStream(); + using var extractedStream = new MemoryStream(); + await entryStream.CopyToAsync(extractedStream); + + var extractedData = extractedStream.ToArray(); + var actualCrc = CalculateCrc32(extractedData); + + Assert.Equal(compressionType, entry.CompressionType); + Assert.Equal(expectedCrc, actualCrc); + Assert.Equal(testData.Length, extractedData.Length); + + // For smaller files, verify full content; for larger, spot check + if (testData.Length <= sizeMb * 2) + { + Assert.Equal(testData, extractedData); + } + else + { + VerifyDataSpotCheck(testData, extractedData); + } + + VerifyCompressionRatio( + testData.Length, + zipStream.Length, + expectedRatio, + $"{compressionType} Level {compressionLevel}" + ); + } + + // Helper method for async archive content verification + private async Task VerifyArchiveContentAsync( + MemoryStream zipStream, + Dictionary expectedFiles + ) + { + zipStream.Position = 0; + using var archive = ZipArchive.Open(zipStream); + + foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) + { + Assert.True( + expectedFiles.ContainsKey(entry.Key!), + $"Unexpected file in archive: {entry.Key}" + ); + + var expected = expectedFiles[entry.Key!]; + using var entryStream = entry.OpenEntryStream(); + using var extractedStream = new MemoryStream(); + await entryStream.CopyToAsync(extractedStream); + + var extractedData = extractedStream.ToArray(); + var actualCrc = CalculateCrc32(extractedData); + + Assert.Equal(expected.crc, actualCrc); + Assert.Equal(expected.data.Length, extractedData.Length); + + // For larger files, just spot check, for smaller verify full content + var expectedData = expected.data; + if (expectedData.Length <= 2 * 1024 * 1024) + { + Assert.Equal(expectedData, extractedData); + } + else + { + VerifyDataSpotCheck(expectedData, extractedData); + } + } + + Assert.Equal(expectedFiles.Count, archive.Entries.Count(e => !e.IsDirectory)); + } +} diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs new file mode 100644 index 000000000..2892be577 --- /dev/null +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -0,0 +1,254 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Readers; +using SharpCompress.Readers.Zip; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Zip; + +public class ZipReaderAsyncTests : ReaderTests +{ + public ZipReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true; + + [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 reader = ReaderFactory.Open(stream); + var count = 0; + while (await reader.MoveToNextEntryAsync()) + { + count++; + if (!reader.Entry.IsDirectory) + { + if (count % 2 != 0) + { + await reader.WriteEntryToAsync(Stream.Null); + } + } + } + } + + [Fact] + public async Task Zip_Zip64_Streamed_Read_Async() => + await ReadAsync("Zip.zip64.zip", CompressionType.Deflate); + + [Fact] + public async Task Zip_ZipX_Streamed_Read_Async() => + await ReadAsync("Zip.zipx", CompressionType.LZMA); + + [Fact] + public async Task Zip_BZip2_Streamed_Read_Async() => + await ReadAsync("Zip.bzip2.dd.zip", CompressionType.BZip2); + + [Fact] + public async Task Zip_BZip2_Read_Async() => + await ReadAsync("Zip.bzip2.zip", CompressionType.BZip2); + + [Fact] + public async Task Zip_Deflate_Streamed2_Read_Async() => + await ReadAsync("Zip.deflate.dd-.zip", CompressionType.Deflate); + + [Fact] + public async Task Zip_Deflate_Streamed_Read_Async() => + await ReadAsync("Zip.deflate.dd.zip", CompressionType.Deflate); + + [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 reader = ReaderFactory.Open(stream); + var x = 0; + while (await reader.MoveToNextEntryAsync()) + { + if (!reader.Entry.IsDirectory) + { + x++; + if (x % 2 == 0) + { + await reader.WriteEntryToDirectoryAsync( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + } + } + } + + [Fact] + public async Task Zip_Deflate_Read_Async() => + await ReadAsync("Zip.deflate.zip", CompressionType.Deflate); + + [Fact] + public async Task Zip_Deflate64_Read_Async() => + await ReadAsync("Zip.deflate64.zip", CompressionType.Deflate64); + + [Fact] + public async Task Zip_LZMA_Streamed_Read_Async() => + await ReadAsync("Zip.lzma.dd.zip", CompressionType.LZMA); + + [Fact] + public async Task Zip_LZMA_Read_Async() => + await ReadAsync("Zip.lzma.zip", CompressionType.LZMA); + + [Fact] + public async Task Zip_PPMd_Streamed_Read_Async() => + await ReadAsync("Zip.ppmd.dd.zip", CompressionType.PPMd); + + [Fact] + public async Task Zip_PPMd_Read_Async() => + await ReadAsync("Zip.ppmd.zip", CompressionType.PPMd); + + [Fact] + public async Task Zip_None_Read_Async() => + await ReadAsync("Zip.none.zip", CompressionType.None); + + [Fact] + public async Task Zip_Deflate_NoEmptyDirs_Read_Async() => + await ReadAsync("Zip.deflate.noEmptyDirs.zip", CompressionType.Deflate); + + [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" })) + { + while (await reader.MoveToNextEntryAsync()) + { + if (!reader.Entry.IsDirectory) + { + Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType); + await reader.WriteEntryToDirectoryAsync( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + } + } + VerifyFiles(); + } + + [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 reader = ReaderFactory.Open(stream)) + { + while (await reader.MoveToNextEntryAsync()) + { + if (!reader.Entry.IsDirectory) + { + await reader.WriteEntryToDirectoryAsync( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + } + } + Assert.True(stream.IsDisposed); + } + + [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")) + ); + var reader = ReaderFactory.Open(stream); + while (await reader.MoveToNextEntryAsync()) + { + if (!reader.Entry.IsDirectory) + { + await reader.WriteEntryToDirectoryAsync( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + } + Assert.False(stream.IsDisposed); + } + + [Fact] + 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" })) + { + 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_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" })) + { + while (await reader.MoveToNextEntryAsync()) + { + 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(8, count); + } +} diff --git a/tests/SharpCompress.Test/Zip/ZipWriterAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipWriterAsyncTests.cs new file mode 100644 index 000000000..bd37e9144 --- /dev/null +++ b/tests/SharpCompress.Test/Zip/ZipWriterAsyncTests.cs @@ -0,0 +1,67 @@ +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common; +using Xunit; + +namespace SharpCompress.Test.Zip; + +public class ZipWriterAsyncTests : WriterTests +{ + public ZipWriterAsyncTests() + : base(ArchiveType.Zip) { } + + [Fact] + public async Task Zip_Deflate_Write_Async() => + await WriteAsync( + CompressionType.Deflate, + "Zip.deflate.noEmptyDirs.zip", + "Zip.deflate.noEmptyDirs.zip", + Encoding.UTF8 + ); + + [Fact] + public async Task Zip_BZip2_Write_Async() => + await WriteAsync( + CompressionType.BZip2, + "Zip.bzip2.noEmptyDirs.zip", + "Zip.bzip2.noEmptyDirs.zip", + Encoding.UTF8 + ); + + [Fact] + public async Task Zip_None_Write_Async() => + await WriteAsync( + CompressionType.None, + "Zip.none.noEmptyDirs.zip", + "Zip.none.noEmptyDirs.zip", + Encoding.UTF8 + ); + + [Fact] + public async Task Zip_LZMA_Write_Async() => + await WriteAsync( + CompressionType.LZMA, + "Zip.lzma.noEmptyDirs.zip", + "Zip.lzma.noEmptyDirs.zip", + Encoding.UTF8 + ); + + [Fact] + public async Task Zip_PPMd_Write_Async() => + await WriteAsync( + CompressionType.PPMd, + "Zip.ppmd.noEmptyDirs.zip", + "Zip.ppmd.noEmptyDirs.zip", + Encoding.UTF8 + ); + + [Fact] + public async Task Zip_Rar_Write_Async() => + await Assert.ThrowsAsync(async () => + await WriteAsync( + CompressionType.Rar, + "Zip.ppmd.noEmptyDirs.zip", + "Zip.ppmd.noEmptyDirs.zip" + ) + ); +}