From 7c3c94ed7f534838d5ab6c2049a2233ca26c2b69 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 25 Nov 2025 14:44:03 +0000 Subject: [PATCH 01/46] Add ArcReaderAsync tests --- .../Arc/ArcReaderAsyncTests.cs | 26 +++++++++++++++++++ .../SharpCompress.Test/Arc/ArcReaderTests.cs | 8 ------ tests/SharpCompress.Test/ReaderTests.cs | 12 ++++++--- tests/SharpCompress.Test/WriterTests.cs | 6 ++--- 4 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs diff --git a/tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs b/tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs new file mode 100644 index 00000000..4aa69e34 --- /dev/null +++ b/tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using SharpCompress.Common; +using Xunit; + +namespace SharpCompress.Test.Arc; + +public class ArcReaderAsyncTests : ReaderTests +{ + public ArcReaderAsyncTests() + { + UseExtensionInsteadOfNameToVerify = true; + UseCaseInsensitiveToVerify = true; + } + + [Fact] + public async Task Arc_Uncompressed_Read_Async() => + await ReadAsync("Arc.uncompressed.arc", CompressionType.None); + + [Fact] + public async Task Arc_Squeezed_Read_Async() => + await ReadAsync("Arc.squeezed.arc"); + + [Fact] + public async Task Arc_Crunched_Read_Async() => + await ReadAsync("Arc.crunched.arc"); +} diff --git a/tests/SharpCompress.Test/Arc/ArcReaderTests.cs b/tests/SharpCompress.Test/Arc/ArcReaderTests.cs index 915e2ae1..5ab79f02 100644 --- a/tests/SharpCompress.Test/Arc/ArcReaderTests.cs +++ b/tests/SharpCompress.Test/Arc/ArcReaderTests.cs @@ -1,12 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using SharpCompress.Common; -using SharpCompress.Readers; -using SharpCompress.Readers.Arc; using Xunit; namespace SharpCompress.Test.Arc diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index cc5a75a7..86f1bf8b 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -112,7 +112,7 @@ public abstract class ReaderTests : TestBase protected async Task ReadAsync( string testArchive, - CompressionType expectedCompression, + CompressionType? expectedCompression = null, ReaderOptions? options = null, CancellationToken cancellationToken = default ) @@ -131,7 +131,7 @@ public abstract class ReaderTests : TestBase private async Task ReadImplAsync( string testArchive, - CompressionType expectedCompression, + CompressionType? expectedCompression, ReaderOptions options, CancellationToken cancellationToken = default ) @@ -158,7 +158,7 @@ public abstract class ReaderTests : TestBase public async Task UseReaderAsync( IReader reader, - CompressionType expectedCompression, + CompressionType? expectedCompression, CancellationToken cancellationToken = default ) { @@ -166,7 +166,11 @@ public abstract class ReaderTests : TestBase { if (!reader.Entry.IsDirectory) { - Assert.Equal(expectedCompression, reader.Entry.CompressionType); + if (expectedCompression.HasValue) + { + Assert.Equal(expectedCompression, reader.Entry.CompressionType); + } + await reader.WriteEntryToDirectoryAsync( SCRATCH_FILES_PATH, new ExtractionOptions { ExtractFullPath = true, Overwrite = true }, diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index 5212fab5..71633daf 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -91,10 +91,10 @@ public class WriterTests : TestBase SharpCompressStream.Create(stream, leaveOpen: true), readerOptions ); - reader.WriteAllToDirectory( + await reader.WriteAllToDirectoryAsync( SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true } - ); + new ExtractionOptions { ExtractFullPath = true }, + cancellationToken); } VerifyFiles(); } From 3bdaba46a91de3131ce443d87289e7d6b7ab48cb Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 25 Nov 2025 15:39:43 +0000 Subject: [PATCH 02/46] fmt --- tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs | 6 ++---- tests/SharpCompress.Test/WriterTests.cs | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs b/tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs index 4aa69e34..14eb7642 100644 --- a/tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs @@ -17,10 +17,8 @@ public class ArcReaderAsyncTests : ReaderTests await ReadAsync("Arc.uncompressed.arc", CompressionType.None); [Fact] - public async Task Arc_Squeezed_Read_Async() => - await ReadAsync("Arc.squeezed.arc"); + public async Task Arc_Squeezed_Read_Async() => await ReadAsync("Arc.squeezed.arc"); [Fact] - public async Task Arc_Crunched_Read_Async() => - await ReadAsync("Arc.crunched.arc"); + public async Task Arc_Crunched_Read_Async() => await ReadAsync("Arc.crunched.arc"); } diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index 71633daf..1d2d8a13 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -94,7 +94,8 @@ public class WriterTests : TestBase await reader.WriteAllToDirectoryAsync( SCRATCH_FILES_PATH, new ExtractionOptions { ExtractFullPath = true }, - cancellationToken); + cancellationToken + ); } VerifyFiles(); } From fb76bd82f2d9cde7ecc54a28b1739fa52929cf7f Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 26 Nov 2025 08:09:20 +0000 Subject: [PATCH 03/46] first commit of async reader --- src/SharpCompress/Common/AsyncBinaryReader.cs | 88 +++++++++++++++++++ .../Common/Zip/Headers/DirectoryEndHeader.cs | 2 +- .../Common/Zip/Headers/LocalEntryHeader.cs | 32 ++++--- .../Headers/Zip64DirectoryEndLocatorHeader.cs | 14 ++- .../Common/Zip/Headers/ZipFileEntry.cs | 13 +-- .../Common/Zip/Headers/ZipHeader.cs | 16 ++-- .../Common/Zip/SeekableZipHeaderFactory.cs | 23 ++--- .../Common/Zip/ZipHeaderFactory.cs | 5 +- .../SharpCompress.Test/Arc/ArcReaderTests.cs | 1 + 9 files changed, 134 insertions(+), 60 deletions(-) create mode 100644 src/SharpCompress/Common/AsyncBinaryReader.cs diff --git a/src/SharpCompress/Common/AsyncBinaryReader.cs b/src/SharpCompress/Common/AsyncBinaryReader.cs new file mode 100644 index 00000000..2600f319 --- /dev/null +++ b/src/SharpCompress/Common/AsyncBinaryReader.cs @@ -0,0 +1,88 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Common +{ + public sealed class AsyncBinaryReader(Stream stream, bool leaveOpen = false) : IDisposable + { + private readonly Stream _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + private readonly byte[] _buffer = new byte[8]; + private bool _disposed; + + public Stream BaseStream => _stream; + + public async ValueTask ReadByteAsync(CancellationToken ct = default) + { + await ReadExactAsync(_buffer, 0, 1, ct).ConfigureAwait(false); + return _buffer[0]; + } + + public async ValueTask ReadUInt16Async(CancellationToken ct = default) + { + await ReadExactAsync(_buffer, 0, 2, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadUInt16LittleEndian(_buffer); + } + + public async ValueTask ReadUInt32Async(CancellationToken ct = default) + { + await ReadExactAsync(_buffer, 0, 4, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadUInt32LittleEndian(_buffer); + } + public async ValueTask ReadUInt64Async(CancellationToken ct = default) + { + await ReadExactAsync(_buffer, 0, 8, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadUInt64LittleEndian(_buffer); + } + + public async ValueTask ReadBytesAsync(int count, CancellationToken ct = default) + { + var result = new byte[count]; + await ReadExactAsync(result, 0, count, ct).ConfigureAwait(false); + return result; + } + + private async ValueTask ReadExactAsync(byte[] destination, int offset, int length, CancellationToken ct) + { + var read = 0; + while (read < length) + { + var n = await _stream.ReadAsync(destination, offset + read, length - read, ct).ConfigureAwait(false); + if (n == 0) + { + throw new EndOfStreamException(); + } + + read += n; + } + } + + public void Dispose() + { + if (_disposed || leaveOpen) + { + _disposed = true; + return; + } + + _disposed = true; + _stream.Dispose(); + } + +#if NET6_0_OR_GREATER + public ValueTask DisposeAsync() + { + if (_disposed || leaveOpen) + { + _disposed = true; + return default; + } + + _disposed = true; + return _stream.DisposeAsync(); + } +#endif + } +} diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs index 2e54a6dd..71502c0d 100644 --- a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs @@ -7,7 +7,7 @@ internal class DirectoryEndHeader : ZipHeader public DirectoryEndHeader() : base(ZipHeaderType.DirectoryEnd) { } - internal override void Read(BinaryReader reader) + internal override void Read(AsyncBinaryReader reader) { VolumeNumber = reader.ReadUInt16(); FirstVolumeWithDirectory = reader.ReadUInt16(); diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs index 1e3dc62d..c1ce5a6d 100644 --- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs @@ -1,27 +1,25 @@ using System.IO; using System.Linq; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class LocalEntryHeader : ZipFileEntry +internal class LocalEntryHeader(ArchiveEncoding archiveEncoding) : ZipFileEntry(ZipHeaderType.LocalEntry, archiveEncoding) { - public LocalEntryHeader(ArchiveEncoding archiveEncoding) - : base(ZipHeaderType.LocalEntry, archiveEncoding) { } - - internal override void Read(BinaryReader reader) + internal override async ValueTask Read(AsyncBinaryReader reader) { - Version = reader.ReadUInt16(); - Flags = (HeaderFlags)reader.ReadUInt16(); - CompressionMethod = (ZipCompressionMethod)reader.ReadUInt16(); - OriginalLastModifiedTime = LastModifiedTime = reader.ReadUInt16(); - OriginalLastModifiedDate = LastModifiedDate = reader.ReadUInt16(); - Crc = reader.ReadUInt32(); - CompressedSize = reader.ReadUInt32(); - UncompressedSize = reader.ReadUInt32(); - var nameLength = reader.ReadUInt16(); - var extraLength = reader.ReadUInt16(); - var name = reader.ReadBytes(nameLength); - var extra = reader.ReadBytes(extraLength); + Version = await reader.ReadUInt16Async(); + Flags = (HeaderFlags)await reader.ReadUInt16Async(); + CompressionMethod = (ZipCompressionMethod)await reader.ReadUInt16Async(); + OriginalLastModifiedTime = LastModifiedTime = await reader.ReadUInt16Async(); + OriginalLastModifiedDate = LastModifiedDate = await reader.ReadUInt16Async(); + Crc = await reader.ReadUInt32Async(); + CompressedSize = await reader.ReadUInt32Async(); + UncompressedSize = await reader.ReadUInt32Async(); + var nameLength = await reader.ReadUInt16Async(); + var extraLength = await reader.ReadUInt16Async(); + var name = await reader.ReadBytesAsync(nameLength); + var extra = await reader.ReadBytesAsync(extraLength); // According to .ZIP File Format Specification // diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs index 3020d377..6b44b219 100644 --- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs @@ -1,17 +1,15 @@ using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class Zip64DirectoryEndLocatorHeader : ZipHeader +internal class Zip64DirectoryEndLocatorHeader() : ZipHeader(ZipHeaderType.Zip64DirectoryEndLocator) { - public Zip64DirectoryEndLocatorHeader() - : base(ZipHeaderType.Zip64DirectoryEndLocator) { } - - internal override void Read(BinaryReader reader) + internal override async ValueTask Read(AsyncBinaryReader reader) { - FirstVolumeWithDirectory = reader.ReadUInt32(); - RelativeOffsetOfTheEndOfDirectoryRecord = (long)reader.ReadUInt64(); - TotalNumberOfVolumes = reader.ReadUInt32(); + FirstVolumeWithDirectory = await reader.ReadUInt32Async(); + RelativeOffsetOfTheEndOfDirectoryRecord = (long)await reader.ReadUInt64Async(); + TotalNumberOfVolumes = await reader.ReadUInt32Async(); } public uint FirstVolumeWithDirectory { get; private set; } diff --git a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs index f37690d6..5d7f1c32 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs @@ -5,15 +5,8 @@ using System.IO; namespace SharpCompress.Common.Zip.Headers; -internal abstract class ZipFileEntry : ZipHeader +internal abstract class ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding) : ZipHeader(type) { - protected ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding) - : base(type) - { - Extra = new List(); - ArchiveEncoding = archiveEncoding; - } - internal bool IsDirectory { get @@ -30,7 +23,7 @@ internal abstract class ZipFileEntry : ZipHeader internal Stream? PackedStream { get; set; } - internal ArchiveEncoding ArchiveEncoding { get; } + internal ArchiveEncoding ArchiveEncoding { get; } = archiveEncoding; internal string? Name { get; set; } @@ -44,7 +37,7 @@ internal abstract class ZipFileEntry : ZipHeader internal long UncompressedSize { get; set; } - internal List Extra { get; set; } + internal List Extra { get; set; } = new(); public string? Password { get; set; } diff --git a/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs b/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs index 36d40a82..ad1714fa 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs @@ -1,18 +1,12 @@ -using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal abstract class ZipHeader +internal abstract class ZipHeader(ZipHeaderType type) { - protected ZipHeader(ZipHeaderType type) - { - ZipHeaderType = type; - HasData = true; - } + internal ZipHeaderType ZipHeaderType { get; } = type; - internal ZipHeaderType ZipHeaderType { get; } + internal abstract ValueTask Read(AsyncBinaryReader reader); - internal abstract void Read(BinaryReader reader); - - internal bool HasData { get; set; } + internal bool HasData { get; set; } = true; } diff --git a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs index 005f6480..be32f8a8 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using SharpCompress.Common.Zip.Headers; using SharpCompress.IO; @@ -18,11 +19,11 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory internal SeekableZipHeaderFactory(string? password, ArchiveEncoding archiveEncoding) : base(StreamingMode.Seekable, password, archiveEncoding) { } - internal IEnumerable ReadSeekableHeader(Stream stream) + internal async IAsyncEnumerable ReadSeekableHeader(Stream stream) { - var reader = new BinaryReader(stream); + var reader = new AsyncBinaryReader(stream); - SeekBackToHeader(stream, reader); + await SeekBackToHeader(stream, reader); var eocd_location = stream.Position; var entry = new DirectoryEndHeader(); @@ -34,24 +35,24 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory // ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR should be before the EOCD stream.Seek(eocd_location - ZIP64_EOCD_LENGTH - 4, SeekOrigin.Begin); - var zip64_locator = reader.ReadUInt32(); + int zip64_locator = await reader.ReadUInt16Async(); if (zip64_locator != ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR) { throw new ArchiveException("Failed to locate the Zip64 Directory Locator"); } var zip64Locator = new Zip64DirectoryEndLocatorHeader(); - zip64Locator.Read(reader); + await zip64Locator.Read(reader); stream.Seek(zip64Locator.RelativeOffsetOfTheEndOfDirectoryRecord, SeekOrigin.Begin); - var zip64Signature = reader.ReadUInt32(); + var zip64Signature = await reader.ReadUInt32Async(); if (zip64Signature != ZIP64_END_OF_CENTRAL_DIRECTORY) { throw new ArchiveException("Failed to locate the Zip64 Header"); } var zip64Entry = new Zip64DirectoryEndHeader(); - zip64Entry.Read(reader); + await zip64Entry.Read(reader); stream.Seek(zip64Entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin); } else @@ -63,8 +64,8 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory while (true) { stream.Position = position; - var signature = reader.ReadUInt32(); - var nextHeader = ReadHeader(signature, reader, _zip64); + var signature = await reader.ReadUInt32Async(); + var nextHeader = await ReadHeader(signature, reader, _zip64); position = stream.Position; if (nextHeader is null) @@ -98,7 +99,7 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory return true; } - private static void SeekBackToHeader(Stream stream, BinaryReader reader) + private static async ValueTask SeekBackToHeader(Stream stream, AsyncBinaryReader reader) { // Minimum EOCD length if (stream.Length < MINIMUM_EOCD_LENGTH) @@ -117,7 +118,7 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory stream.Seek(-len, SeekOrigin.End); - var seek = reader.ReadBytes(len); + var seek = await reader.ReadBytesAsync(len); // Search in reverse Array.Reverse(seek); diff --git a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs index 45869ff6..f461050f 100644 --- a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Threading.Tasks; using SharpCompress.Common.Zip.Headers; using SharpCompress.IO; @@ -34,14 +35,14 @@ internal class ZipHeaderFactory _archiveEncoding = archiveEncoding; } - protected ZipHeader? ReadHeader(uint headerBytes, BinaryReader reader, bool zip64 = false) + protected async ValueTask ReadHeader(uint headerBytes, AsyncBinaryReader reader, bool zip64 = false) { switch (headerBytes) { case ENTRY_HEADER_BYTES: { var entryHeader = new LocalEntryHeader(_archiveEncoding); - entryHeader.Read(reader); + await entryHeader.Read(reader); LoadHeader(entryHeader, reader.BaseStream); _lastEntryHeader = entryHeader; diff --git a/tests/SharpCompress.Test/Arc/ArcReaderTests.cs b/tests/SharpCompress.Test/Arc/ArcReaderTests.cs index 5ab79f02..54f169c3 100644 --- a/tests/SharpCompress.Test/Arc/ArcReaderTests.cs +++ b/tests/SharpCompress.Test/Arc/ArcReaderTests.cs @@ -1,3 +1,4 @@ +using System; using SharpCompress.Common; using Xunit; From 8415a19912bd4f40c27d7118e4222cded707c02d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:15:28 +0000 Subject: [PATCH 04/46] Initial plan From 39a0b4ce78fb57296eb4c3f6f945ab6b5b44191c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:23:55 +0000 Subject: [PATCH 05/46] Use BufferedStream for async reading in AsyncBinaryReader Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Common/AsyncBinaryReader.cs | 71 +++++++++++++++---- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/src/SharpCompress/Common/AsyncBinaryReader.cs b/src/SharpCompress/Common/AsyncBinaryReader.cs index 2600f319..a6a7bb9c 100644 --- a/src/SharpCompress/Common/AsyncBinaryReader.cs +++ b/src/SharpCompress/Common/AsyncBinaryReader.cs @@ -6,12 +6,31 @@ using System.Threading.Tasks; namespace SharpCompress.Common { - public sealed class AsyncBinaryReader(Stream stream, bool leaveOpen = false) : IDisposable + public sealed class AsyncBinaryReader : IDisposable { - private readonly Stream _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + private readonly Stream _stream; + private readonly Stream _originalStream; + private readonly bool _leaveOpen; private readonly byte[] _buffer = new byte[8]; private bool _disposed; + public AsyncBinaryReader(Stream stream, bool leaveOpen = false, int bufferSize = 4096) + { + _originalStream = stream ?? throw new ArgumentNullException(nameof(stream)); + _leaveOpen = leaveOpen; + + // Wrap the stream with BufferedStream if it's not already a buffered stream + // This enables efficient async reading with internal buffering + if (stream is BufferedStream || stream is IO.SharpCompressStream) + { + _stream = stream; + } + else + { + _stream = new BufferedStream(stream, bufferSize); + } + } + public Stream BaseStream => _stream; public async ValueTask ReadByteAsync(CancellationToken ct = default) @@ -31,6 +50,7 @@ namespace SharpCompress.Common await ReadExactAsync(_buffer, 0, 4, ct).ConfigureAwait(false); return BinaryPrimitives.ReadUInt32LittleEndian(_buffer); } + public async ValueTask ReadUInt64Async(CancellationToken ct = default) { await ReadExactAsync(_buffer, 0, 8, ct).ConfigureAwait(false); @@ -44,12 +64,19 @@ namespace SharpCompress.Common return result; } - private async ValueTask ReadExactAsync(byte[] destination, int offset, int length, CancellationToken ct) + private async ValueTask ReadExactAsync( + byte[] destination, + int offset, + int length, + CancellationToken ct + ) { var read = 0; while (read < length) { - var n = await _stream.ReadAsync(destination, offset + read, length - read, ct).ConfigureAwait(false); + var n = await _stream + .ReadAsync(destination, offset + read, length - read, ct) + .ConfigureAwait(false); if (n == 0) { throw new EndOfStreamException(); @@ -61,27 +88,47 @@ namespace SharpCompress.Common public void Dispose() { - if (_disposed || leaveOpen) + if (_disposed) { - _disposed = true; return; } _disposed = true; - _stream.Dispose(); + + // Dispose the buffered stream if we created it + if (_stream != _originalStream) + { + _stream.Dispose(); + } + + // Dispose the original stream if we own it + if (!_leaveOpen) + { + _originalStream.Dispose(); + } } #if NET6_0_OR_GREATER - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { - if (_disposed || leaveOpen) + if (_disposed) { - _disposed = true; - return default; + return; } _disposed = true; - return _stream.DisposeAsync(); + + // Dispose the buffered stream if we created it + if (_stream != _originalStream) + { + await _stream.DisposeAsync().ConfigureAwait(false); + } + + // Dispose the original stream if we own it + if (!_leaveOpen) + { + await _originalStream.DisposeAsync().ConfigureAwait(false); + } } #endif } From ec31cb9987e44cf1fac2393447b5c1b4f01b4585 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:47:31 +0000 Subject: [PATCH 06/46] Fix Zip headers to support both sync and async reading Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Archives/Zip/ZipArchive.cs | 6 +- .../Common/Zip/Headers/DirectoryEndHeader.cs | 15 ++- .../Zip/Headers/DirectoryEntryHeader.cs | 33 +++++- .../Common/Zip/Headers/IgnoreHeader.cs | 3 + .../Common/Zip/Headers/LocalEntryHeader.cs | 27 ++++- .../Common/Zip/Headers/SplitHeader.cs | 4 + .../Zip/Headers/Zip64DirectoryEndHeader.cs | 20 ++++ .../Headers/Zip64DirectoryEndLocatorHeader.cs | 9 +- .../Common/Zip/Headers/ZipFileEntry.cs | 3 +- .../Common/Zip/Headers/ZipHeader.cs | 2 + .../Common/Zip/SeekableZipHeaderFactory.cs | 108 +++++++++++++++++- .../Common/Zip/ZipHeaderFactory.cs | 78 ++++++++++++- 12 files changed, 298 insertions(+), 10 deletions(-) diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 57db85c2..ae1a58e5 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -199,7 +199,7 @@ public class ZipArchive : AbstractWritableArchive if (stream.CanSeek) //could be multipart. Test for central directory - might not be z64 safe { var z = new SeekableZipHeaderFactory(password, new ArchiveEncoding()); - var x = z.ReadSeekableHeader(stream).FirstOrDefault(); + var x = z.ReadSeekableHeader(stream, useSync: true).FirstOrDefault(); return x?.ZipHeaderType == ZipHeaderType.DirectoryEntry; } else @@ -254,7 +254,9 @@ public class ZipArchive : AbstractWritableArchive protected override IEnumerable LoadEntries(IEnumerable volumes) { var vols = volumes.ToArray(); - foreach (var h in headerFactory.NotNull().ReadSeekableHeader(vols.Last().Stream)) + foreach ( + var h in headerFactory.NotNull().ReadSeekableHeader(vols.Last().Stream, useSync: true) + ) { if (h != null) { diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs index 71502c0d..7d35f3ea 100644 --- a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -7,7 +8,7 @@ internal class DirectoryEndHeader : ZipHeader public DirectoryEndHeader() : base(ZipHeaderType.DirectoryEnd) { } - internal override void Read(AsyncBinaryReader reader) + internal override void Read(BinaryReader reader) { VolumeNumber = reader.ReadUInt16(); FirstVolumeWithDirectory = reader.ReadUInt16(); @@ -19,6 +20,18 @@ internal class DirectoryEndHeader : ZipHeader Comment = reader.ReadBytes(CommentLength); } + internal override async ValueTask Read(AsyncBinaryReader reader) + { + VolumeNumber = await reader.ReadUInt16Async(); + FirstVolumeWithDirectory = await reader.ReadUInt16Async(); + TotalNumberOfEntriesInDisk = await reader.ReadUInt16Async(); + TotalNumberOfEntries = await reader.ReadUInt16Async(); + DirectorySize = await reader.ReadUInt32Async(); + DirectoryStartOffsetRelativeToDisk = await reader.ReadUInt32Async(); + CommentLength = await reader.ReadUInt16Async(); + Comment = await reader.ReadBytesAsync(CommentLength); + } + public ushort VolumeNumber { get; private set; } public ushort FirstVolumeWithDirectory { get; private set; } diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs index 8cf4f4ad..b446423a 100644 --- a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs @@ -1,5 +1,6 @@ using System.IO; using System.Linq; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -31,7 +32,37 @@ internal class DirectoryEntryHeader : ZipFileEntry var extra = reader.ReadBytes(extraLength); var comment = reader.ReadBytes(commentLength); - // According to .ZIP File Format Specification + ProcessReadData(name, extra, comment); + } + + internal override async ValueTask Read(AsyncBinaryReader reader) + { + Version = await reader.ReadUInt16Async(); + VersionNeededToExtract = await reader.ReadUInt16Async(); + Flags = (HeaderFlags)await reader.ReadUInt16Async(); + CompressionMethod = (ZipCompressionMethod)await reader.ReadUInt16Async(); + OriginalLastModifiedTime = LastModifiedTime = await reader.ReadUInt16Async(); + OriginalLastModifiedDate = LastModifiedDate = await reader.ReadUInt16Async(); + Crc = await reader.ReadUInt32Async(); + CompressedSize = await reader.ReadUInt32Async(); + UncompressedSize = await reader.ReadUInt32Async(); + var nameLength = await reader.ReadUInt16Async(); + var extraLength = await reader.ReadUInt16Async(); + var commentLength = await reader.ReadUInt16Async(); + DiskNumberStart = await reader.ReadUInt16Async(); + InternalFileAttributes = await reader.ReadUInt16Async(); + ExternalFileAttributes = await reader.ReadUInt32Async(); + RelativeOffsetOfEntryHeader = await reader.ReadUInt32Async(); + + var name = await reader.ReadBytesAsync(nameLength); + var extra = await reader.ReadBytesAsync(extraLength); + var comment = await reader.ReadBytesAsync(commentLength); + + ProcessReadData(name, extra, comment); + } + + private void ProcessReadData(byte[] name, byte[] extra, byte[] comment) + { // // For example: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT // diff --git a/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs b/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs index 5a587a7b..9c648baf 100644 --- a/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -8,4 +9,6 @@ internal class IgnoreHeader : ZipHeader : base(type) { } internal override void Read(BinaryReader reader) { } + + internal override ValueTask Read(AsyncBinaryReader reader) => default; } diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs index c1ce5a6d..1df74e57 100644 --- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs @@ -4,8 +4,27 @@ using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class LocalEntryHeader(ArchiveEncoding archiveEncoding) : ZipFileEntry(ZipHeaderType.LocalEntry, archiveEncoding) +internal class LocalEntryHeader(ArchiveEncoding archiveEncoding) + : ZipFileEntry(ZipHeaderType.LocalEntry, archiveEncoding) { + internal override void Read(BinaryReader reader) + { + Version = reader.ReadUInt16(); + Flags = (HeaderFlags)reader.ReadUInt16(); + CompressionMethod = (ZipCompressionMethod)reader.ReadUInt16(); + OriginalLastModifiedTime = LastModifiedTime = reader.ReadUInt16(); + OriginalLastModifiedDate = LastModifiedDate = reader.ReadUInt16(); + Crc = reader.ReadUInt32(); + CompressedSize = reader.ReadUInt32(); + UncompressedSize = reader.ReadUInt32(); + var nameLength = reader.ReadUInt16(); + var extraLength = reader.ReadUInt16(); + var name = reader.ReadBytes(nameLength); + var extra = reader.ReadBytes(extraLength); + + ProcessReadData(name, extra); + } + internal override async ValueTask Read(AsyncBinaryReader reader) { Version = await reader.ReadUInt16Async(); @@ -21,7 +40,11 @@ internal class LocalEntryHeader(ArchiveEncoding archiveEncoding) : ZipFileEntry( var name = await reader.ReadBytesAsync(nameLength); var extra = await reader.ReadBytesAsync(extraLength); - // According to .ZIP File Format Specification + ProcessReadData(name, extra); + } + + private void ProcessReadData(byte[] name, byte[] extra) + { // // For example: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT // diff --git a/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs b/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs index 4151a6cb..29aaabaa 100644 --- a/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -9,4 +10,7 @@ internal class SplitHeader : ZipHeader : base(ZipHeaderType.Split) { } internal override void Read(BinaryReader reader) => throw new NotImplementedException(); + + internal override ValueTask Read(AsyncBinaryReader reader) => + throw new NotImplementedException(); } diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs index a74b4d1f..b15b6f16 100644 --- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -26,6 +27,25 @@ internal class Zip64DirectoryEndHeader : ZipHeader ); } + internal override async ValueTask Read(AsyncBinaryReader reader) + { + SizeOfDirectoryEndRecord = (long)await reader.ReadUInt64Async(); + VersionMadeBy = await reader.ReadUInt16Async(); + VersionNeededToExtract = await reader.ReadUInt16Async(); + VolumeNumber = await reader.ReadUInt32Async(); + FirstVolumeWithDirectory = await reader.ReadUInt32Async(); + TotalNumberOfEntriesInDisk = (long)await reader.ReadUInt64Async(); + TotalNumberOfEntries = (long)await reader.ReadUInt64Async(); + DirectorySize = (long)await reader.ReadUInt64Async(); + DirectoryStartOffsetRelativeToDisk = (long)await reader.ReadUInt64Async(); + DataSector = await reader.ReadBytesAsync( + (int)( + SizeOfDirectoryEndRecord + - SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS + ) + ); + } + private const int SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS = 44; public long SizeOfDirectoryEndRecord { get; private set; } diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs index 6b44b219..8326be99 100644 --- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs @@ -5,7 +5,14 @@ namespace SharpCompress.Common.Zip.Headers; internal class Zip64DirectoryEndLocatorHeader() : ZipHeader(ZipHeaderType.Zip64DirectoryEndLocator) { - internal override async ValueTask Read(AsyncBinaryReader reader) + internal override void Read(BinaryReader reader) + { + FirstVolumeWithDirectory = reader.ReadUInt32(); + RelativeOffsetOfTheEndOfDirectoryRecord = (long)reader.ReadUInt64(); + TotalNumberOfVolumes = reader.ReadUInt32(); + } + + internal override async ValueTask Read(AsyncBinaryReader reader) { FirstVolumeWithDirectory = await reader.ReadUInt32Async(); RelativeOffsetOfTheEndOfDirectoryRecord = (long)await reader.ReadUInt64Async(); diff --git a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs index 5d7f1c32..0f361c9c 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs @@ -5,7 +5,8 @@ using System.IO; namespace SharpCompress.Common.Zip.Headers; -internal abstract class ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding) : ZipHeader(type) +internal abstract class ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding) + : ZipHeader(type) { internal bool IsDirectory { diff --git a/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs b/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs index ad1714fa..9ce1caa3 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -6,6 +7,7 @@ internal abstract class ZipHeader(ZipHeaderType type) { internal ZipHeaderType ZipHeaderType { get; } = type; + internal abstract void Read(BinaryReader reader); internal abstract ValueTask Read(AsyncBinaryReader reader); internal bool HasData { get; set; } = true; diff --git a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs index be32f8a8..edb182da 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs @@ -27,7 +27,7 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory var eocd_location = stream.Position; var entry = new DirectoryEndHeader(); - entry.Read(reader); + await entry.Read(reader); if (entry.IsZip64) { @@ -86,6 +86,73 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory } } + internal IEnumerable ReadSeekableHeader(Stream stream, bool useSync) + { + var reader = new BinaryReader(stream); + + SeekBackToHeader(stream, reader); + + var eocd_location = stream.Position; + var entry = new DirectoryEndHeader(); + entry.Read(reader); + + if (entry.IsZip64) + { + _zip64 = true; + + // ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR should be before the EOCD + stream.Seek(eocd_location - ZIP64_EOCD_LENGTH - 4, SeekOrigin.Begin); + var zip64_locator = reader.ReadUInt32(); + if (zip64_locator != ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR) + { + throw new ArchiveException("Failed to locate the Zip64 Directory Locator"); + } + + var zip64Locator = new Zip64DirectoryEndLocatorHeader(); + zip64Locator.Read(reader); + + stream.Seek(zip64Locator.RelativeOffsetOfTheEndOfDirectoryRecord, SeekOrigin.Begin); + var zip64Signature = reader.ReadUInt32(); + if (zip64Signature != ZIP64_END_OF_CENTRAL_DIRECTORY) + { + throw new ArchiveException("Failed to locate the Zip64 Header"); + } + + var zip64Entry = new Zip64DirectoryEndHeader(); + zip64Entry.Read(reader); + stream.Seek(zip64Entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin); + } + else + { + stream.Seek(entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin); + } + + var position = stream.Position; + while (true) + { + stream.Position = position; + var signature = reader.ReadUInt32(); + var nextHeader = ReadHeader(signature, reader, _zip64); + position = stream.Position; + + if (nextHeader is null) + { + yield break; + } + + if (nextHeader is DirectoryEntryHeader entryHeader) + { + //entry could be zero bytes so we need to know that. + entryHeader.HasData = entryHeader.CompressedSize != 0; + yield return entryHeader; + } + else if (nextHeader is DirectoryEndHeader endHeader) + { + yield return endHeader; + } + } + } + private static bool IsMatch(byte[] haystack, int position, byte[] needle) { for (var i = 0; i < needle.Length; i++) @@ -138,6 +205,45 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory throw new ArchiveException("Failed to locate the Zip Header"); } + private static void SeekBackToHeader(Stream stream, BinaryReader reader) + { + // Minimum EOCD length + if (stream.Length < MINIMUM_EOCD_LENGTH) + { + throw new ArchiveException( + "Could not find Zip file Directory at the end of the file. File may be corrupted." + ); + } + + var len = + stream.Length < MAX_SEARCH_LENGTH_FOR_EOCD + ? (int)stream.Length + : MAX_SEARCH_LENGTH_FOR_EOCD; + // We search for marker in reverse to find the first occurance + byte[] needle = { 0x06, 0x05, 0x4b, 0x50 }; + + stream.Seek(-len, SeekOrigin.End); + + var seek = reader.ReadBytes(len); + + // Search in reverse + Array.Reverse(seek); + + // don't exclude the minimum eocd region, otherwise you fail to locate the header in empty zip files + var max_search_area = len; // - MINIMUM_EOCD_LENGTH; + + for (var pos_from_end = 0; pos_from_end < max_search_area; ++pos_from_end) + { + if (IsMatch(seek, pos_from_end, needle)) + { + stream.Seek(-pos_from_end, SeekOrigin.End); + return; + } + } + + throw new ArchiveException("Failed to locate the Zip Header"); + } + internal LocalEntryHeader GetLocalHeader( Stream stream, DirectoryEntryHeader directoryEntryHeader diff --git a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs index f461050f..e085a0de 100644 --- a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs @@ -35,7 +35,11 @@ internal class ZipHeaderFactory _archiveEncoding = archiveEncoding; } - protected async ValueTask ReadHeader(uint headerBytes, AsyncBinaryReader reader, bool zip64 = false) + protected async ValueTask ReadHeader( + uint headerBytes, + AsyncBinaryReader reader, + bool zip64 = false + ) { switch (headerBytes) { @@ -49,6 +53,78 @@ internal class ZipHeaderFactory return entryHeader; } case DIRECTORY_START_HEADER_BYTES: + { + var entry = new DirectoryEntryHeader(_archiveEncoding); + await entry.Read(reader); + return entry; + } + case POST_DATA_DESCRIPTOR: + { + if ( + _lastEntryHeader != null + && FlagUtility.HasFlag( + _lastEntryHeader.NotNull().Flags, + HeaderFlags.UsePostDataDescriptor + ) + ) + { + _lastEntryHeader.Crc = await reader.ReadUInt32Async(); + _lastEntryHeader.CompressedSize = zip64 + ? (long)await reader.ReadUInt64Async() + : await reader.ReadUInt32Async(); + _lastEntryHeader.UncompressedSize = zip64 + ? (long)await reader.ReadUInt64Async() + : await reader.ReadUInt32Async(); + } + else + { + await reader.ReadBytesAsync(zip64 ? 20 : 12); + } + return null; + } + case DIGITAL_SIGNATURE: + return null; + case DIRECTORY_END_HEADER_BYTES: + { + var entry = new DirectoryEndHeader(); + await entry.Read(reader); + return entry; + } + case SPLIT_ARCHIVE_HEADER_BYTES: + { + return new SplitHeader(); + } + case ZIP64_END_OF_CENTRAL_DIRECTORY: + { + var entry = new Zip64DirectoryEndHeader(); + await entry.Read(reader); + return entry; + } + case ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR: + { + var entry = new Zip64DirectoryEndLocatorHeader(); + await entry.Read(reader); + return entry; + } + default: + return null; + } + } + + protected ZipHeader? ReadHeader(uint headerBytes, BinaryReader reader, bool zip64 = false) + { + switch (headerBytes) + { + case ENTRY_HEADER_BYTES: + { + var entryHeader = new LocalEntryHeader(_archiveEncoding); + entryHeader.Read(reader); + LoadHeader(entryHeader, reader.BaseStream); + + _lastEntryHeader = entryHeader; + return entryHeader; + } + case DIRECTORY_START_HEADER_BYTES: { var entry = new DirectoryEntryHeader(_archiveEncoding); entry.Read(reader); From 6e0e20ba6ea53d7fa0117f4aebdbd13741862994 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:50:41 +0000 Subject: [PATCH 07/46] Fix zip64_locator to use ReadUInt32Async instead of ReadUInt16Async Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs index edb182da..170950b2 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs @@ -35,7 +35,7 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory // ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR should be before the EOCD stream.Seek(eocd_location - ZIP64_EOCD_LENGTH - 4, SeekOrigin.Begin); - int zip64_locator = await reader.ReadUInt16Async(); + uint zip64_locator = await reader.ReadUInt32Async(); if (zip64_locator != ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR) { throw new ArchiveException("Failed to locate the Zip64 Directory Locator"); From 44b7955d85daa172c5cb8393ec6b76ff56ca84dc Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 31 Dec 2025 14:43:15 +0000 Subject: [PATCH 08/46] reader tests --- .../Mocks/AsyncOnlyStream.cs | 60 +++++++++++++++++++ .../Zip/ZipReaderAsyncTests.cs | 14 +++-- 2 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs diff --git a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs new file mode 100644 index 00000000..4be47432 --- /dev/null +++ b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Test.Mocks; + +public class AsyncOnlyStream : Stream +{ + private readonly Stream _stream; + + public AsyncOnlyStream(Stream stream) + { + _stream = stream; + // Console.WriteLine("AsyncOnlyStream created"); + } + + 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 int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("Synchronous Read is not supported"); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _stream.ReadAsync(buffer, offset, count, cancellationToken); + } + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + return _stream.ReadAsync(buffer, cancellationToken); + } +#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) => _stream.Write(buffer, offset, count); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _stream.Dispose(); + } + base.Dispose(disposing); + } +} + diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index 2892be57..45d5acbb 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -1,7 +1,9 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; +using SharpCompress.IO; using SharpCompress.Readers; using SharpCompress.Readers.Zip; using SharpCompress.Test.Mocks; @@ -162,7 +164,7 @@ public class ZipReaderAsyncTests : ReaderTests public async Task Zip_Reader_Disposal_Test2_Async() { using var stream = new TestStream( - File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) + new AsyncOnlyStream(File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"))) ); var reader = ReaderFactory.Open(stream); while (await reader.MoveToNextEntryAsync()) @@ -183,9 +185,9 @@ public class ZipReaderAsyncTests : ReaderTests await Assert.ThrowsAsync(async () => { using ( - Stream stream = File.OpenRead( + Stream stream = new AsyncOnlyStream(File.OpenRead( Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.WinzipAES.zip") - ) + )) ) using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" })) { @@ -208,9 +210,9 @@ public class ZipReaderAsyncTests : ReaderTests public async Task Zip_Deflate_WinzipAES_Read_Async() { using ( - Stream stream = File.OpenRead( + Stream stream = new AsyncOnlyStream(File.OpenRead( Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip") - ) + )) ) using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" })) { @@ -233,7 +235,7 @@ 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 (Stream stream = new AsyncOnlyStream(File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip")))) using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" })) { while (await reader.MoveToNextEntryAsync()) From 8533b09091bf439cf17f28a7d971d776689d2b68 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 31 Dec 2025 14:53:55 +0000 Subject: [PATCH 09/46] start of implementing zip reading async --- src/SharpCompress/Common/FilePart.cs | 6 + .../Common/Zip/Headers/ZipFileEntry.cs | 20 ++ .../Common/Zip/StreamingZipFilePart.cs | 24 ++ src/SharpCompress/Common/Zip/ZipFilePart.cs | 218 ++++++++++++++++++ src/SharpCompress/Readers/AbstractReader.cs | 15 +- src/SharpCompress/Utility.cs | 27 +++ .../Mocks/AsyncOnlyStream.cs | 18 +- .../Zip/ZipReaderAsyncTests.cs | 22 +- 8 files changed, 334 insertions(+), 16 deletions(-) diff --git a/src/SharpCompress/Common/FilePart.cs b/src/SharpCompress/Common/FilePart.cs index 54e3c9f9..4af7ab75 100644 --- a/src/SharpCompress/Common/FilePart.cs +++ b/src/SharpCompress/Common/FilePart.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Common; @@ -14,4 +16,8 @@ public abstract class FilePart internal abstract Stream? GetCompressedStream(); internal abstract Stream? GetRawStream(); internal bool Skipped { get; set; } + + internal virtual Task GetCompressedStreamAsync( + CancellationToken cancellationToken = default + ) => Task.FromResult(GetCompressedStream()); } diff --git a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs index 0f361c9c..a1ed2028 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs @@ -2,6 +2,8 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -57,6 +59,24 @@ internal abstract class ZipFileEntry(ZipHeaderType type, ArchiveEncoding archive return encryptionData; } + internal async Task ComposeEncryptionDataAsync( + Stream archiveStream, + CancellationToken cancellationToken = default + ) + { + if (archiveStream is null) + { + throw new ArgumentNullException(nameof(archiveStream)); + } + + var buffer = new byte[12]; + await archiveStream.ReadFullyAsync(buffer, 0, 12, cancellationToken).ConfigureAwait(false); + + var encryptionData = PkwareTraditionalEncryptionData.ForRead(Password!, this, buffer); + + return encryptionData; + } + internal WinzipAesEncryptionData? WinzipAesEncryptionData { get; set; } /// diff --git a/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs b/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs index 5464a9cc..986d5efc 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common.Zip.Headers; using SharpCompress.Compressors.Deflate; using SharpCompress.IO; @@ -31,6 +33,28 @@ internal sealed class StreamingZipFilePart : ZipFilePart return _decompressionStream; } + internal override async Task GetCompressedStreamAsync( + CancellationToken cancellationToken = default + ) + { + if (!Header.HasData) + { + return Stream.Null; + } + _decompressionStream = await CreateDecompressionStreamAsync( + await GetCryptoStreamAsync(CreateBaseStream(), cancellationToken) + .ConfigureAwait(false), + Header.CompressionMethod, + cancellationToken + ) + .ConfigureAwait(false); + if (LeaveStreamOpen) + { + return SharpCompressStream.Create(_decompressionStream, leaveOpen: true); + } + return _decompressionStream; + } + internal BinaryReader FixStreamedFileLocation(ref SharpCompressStream rewindableStream) { if (Header.IsDirectory) diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.cs b/src/SharpCompress/Common/Zip/ZipFilePart.cs index 16eb8e1a..ca3881ae 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.cs @@ -2,6 +2,8 @@ using System; using System.Buffers.Binary; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common.Zip.Headers; using SharpCompress.Compressors; using SharpCompress.Compressors.BZip2; @@ -264,4 +266,220 @@ internal abstract class ZipFilePart : FilePart } return plainStream; } + + internal override async Task GetCompressedStreamAsync( + CancellationToken cancellationToken = default + ) + { + if (!Header.HasData) + { + return Stream.Null; + } + var decompressionStream = await CreateDecompressionStreamAsync( + await GetCryptoStreamAsync(CreateBaseStream(), cancellationToken) + .ConfigureAwait(false), + Header.CompressionMethod, + cancellationToken + ) + .ConfigureAwait(false); + if (LeaveStreamOpen) + { + return SharpCompressStream.Create(decompressionStream, leaveOpen: true); + } + return decompressionStream; + } + + protected async Task GetCryptoStreamAsync( + Stream plainStream, + CancellationToken cancellationToken = default + ) + { + var isFileEncrypted = FlagUtility.HasFlag(Header.Flags, HeaderFlags.Encrypted); + + if (Header.CompressedSize == 0 && isFileEncrypted) + { + throw new NotSupportedException("Cannot encrypt file with unknown size at start."); + } + + if ( + ( + Header.CompressedSize == 0 + && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor) + ) || Header.IsZip64 + ) + { + plainStream = SharpCompressStream.Create(plainStream, leaveOpen: true); //make sure AES doesn't close + } + else + { + plainStream = new ReadOnlySubStream(plainStream, Header.CompressedSize); //make sure AES doesn't close + } + + if (isFileEncrypted) + { + switch (Header.CompressionMethod) + { + case ZipCompressionMethod.None: + case ZipCompressionMethod.Shrink: + case ZipCompressionMethod.Reduce1: + case ZipCompressionMethod.Reduce2: + case ZipCompressionMethod.Reduce3: + case ZipCompressionMethod.Reduce4: + case ZipCompressionMethod.Deflate: + case ZipCompressionMethod.Deflate64: + case ZipCompressionMethod.BZip2: + case ZipCompressionMethod.LZMA: + case ZipCompressionMethod.PPMd: + { + return new PkwareTraditionalCryptoStream( + plainStream, + await Header + .ComposeEncryptionDataAsync(plainStream, cancellationToken) + .ConfigureAwait(false), + CryptoMode.Decrypt + ); + } + + case ZipCompressionMethod.WinzipAes: + { + if (Header.WinzipAesEncryptionData != null) + { + return new WinzipAesCryptoStream( + plainStream, + Header.WinzipAesEncryptionData, + Header.CompressedSize - 10 + ); + } + return plainStream; + } + + default: + { + throw new InvalidOperationException("Header.CompressionMethod is invalid"); + } + } + } + return plainStream; + } + + protected async Task CreateDecompressionStreamAsync( + Stream stream, + ZipCompressionMethod method, + CancellationToken cancellationToken = default + ) + { + switch (method) + { + case ZipCompressionMethod.None: + { + if (Header.CompressedSize is 0) + { + return new DataDescriptorStream(stream); + } + + return stream; + } + case ZipCompressionMethod.Shrink: + { + return new ShrinkStream( + stream, + CompressionMode.Decompress, + Header.CompressedSize, + Header.UncompressedSize + ); + } + case ZipCompressionMethod.Reduce1: + { + return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 1); + } + case ZipCompressionMethod.Reduce2: + { + return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 2); + } + case ZipCompressionMethod.Reduce3: + { + return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 3); + } + case ZipCompressionMethod.Reduce4: + { + return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 4); + } + case ZipCompressionMethod.Explode: + { + return new ExplodeStream( + stream, + Header.CompressedSize, + Header.UncompressedSize, + Header.Flags + ); + } + + case ZipCompressionMethod.Deflate: + { + return new DeflateStream(stream, CompressionMode.Decompress); + } + case ZipCompressionMethod.Deflate64: + { + return new Deflate64Stream(stream, CompressionMode.Decompress); + } + case ZipCompressionMethod.BZip2: + { + return new BZip2Stream(stream, CompressionMode.Decompress, false); + } + case ZipCompressionMethod.LZMA: + { + if (FlagUtility.HasFlag(Header.Flags, HeaderFlags.Encrypted)) + { + throw new NotSupportedException("LZMA with pkware encryption."); + } + var buffer = new byte[4]; + await stream.ReadFullyAsync(buffer, 0, 4, cancellationToken).ConfigureAwait(false); + var version = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(0, 2)); + var propsSize = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(2, 2)); + var props = new byte[propsSize]; + await stream + .ReadFullyAsync(props, 0, propsSize, cancellationToken) + .ConfigureAwait(false); + return new LzmaStream( + props, + stream, + Header.CompressedSize > 0 ? Header.CompressedSize - 4 - props.Length : -1, + FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) + ? -1 + : Header.UncompressedSize + ); + } + case ZipCompressionMethod.Xz: + { + return new XZStream(stream); + } + case ZipCompressionMethod.ZStandard: + { + return new DecompressionStream(stream); + } + case ZipCompressionMethod.PPMd: + { + var props = new byte[2]; + await stream.ReadFullyAsync(props, 0, 2, cancellationToken).ConfigureAwait(false); + return new PpmdStream(new PpmdProperties(props), stream, false); + } + case ZipCompressionMethod.WinzipAes: + { + var data = Header.Extra.SingleOrDefault(x => x.Type == ExtraDataType.WinZipAes); + if (data is null) + { + throw new InvalidFormatException("No Winzip AES extra data found."); + } + if (data.Length != 7) + { + throw new InvalidFormatException("Winzip data length is not 7."); + } + throw new NotSupportedException("WinzipAes isn't supported for streaming"); + } + default: + { + throw new NotSupportedException("CompressionMethod: " + Header.CompressionMethod); + } + } + } } diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index cd37bb5f..bd35a6e1 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -268,11 +268,11 @@ public abstract class AbstractReader : IReader internal async Task WriteAsync(Stream writeStream, CancellationToken cancellationToken) { #if NETFRAMEWORK || NETSTANDARD2_0 - using Stream s = OpenEntryStream(); + using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); var sourceStream = WrapWithProgress(s, Entry); await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false); #else - await using Stream s = OpenEntryStream(); + await using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); var sourceStream = WrapWithProgress(s, Entry); await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false); #endif @@ -347,9 +347,16 @@ public abstract class AbstractReader : IReader protected virtual EntryStream GetEntryStream() => CreateEntryStream(Entry.Parts.First().GetCompressedStream()); - protected virtual Task GetEntryStreamAsync( + protected virtual async Task GetEntryStreamAsync( CancellationToken cancellationToken = default - ) => Task.FromResult(GetEntryStream()); + ) + { + var stream = await Entry + .Parts.First() + .GetCompressedStreamAsync(cancellationToken) + .ConfigureAwait(false); + return CreateEntryStream(stream); + } #endregion diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index 6c627602..0c648f12 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -406,6 +406,33 @@ internal static class Utility return (total >= buffer.Length); } + public static async Task ReadFullyAsync( + this Stream stream, + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var total = 0; + int read; + while ( + ( + read = await stream + .ReadAsync(buffer, offset + total, count - total, cancellationToken) + .ConfigureAwait(false) + ) > 0 + ) + { + total += read; + if (total >= count) + { + return true; + } + } + return (total >= count); + } + 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 index 4be47432..d0b363e0 100644 --- a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs +++ b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs @@ -32,21 +32,32 @@ public class AsyncOnlyStream : Stream throw new NotSupportedException("Synchronous Read is not supported"); } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) { return _stream.ReadAsync(buffer, offset, count, cancellationToken); } #if !NETFRAMEWORK && !NETSTANDARD2_0 - public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + public override ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) { return _stream.ReadAsync(buffer, cancellationToken); } #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) => _stream.Write(buffer, offset, count); + + public override void Write(byte[] buffer, int offset, int count) => + _stream.Write(buffer, offset, count); protected override void Dispose(bool disposing) { @@ -57,4 +68,3 @@ public class AsyncOnlyStream : Stream base.Dispose(disposing); } } - diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index 45d5acbb..5acb3a41 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -164,7 +164,9 @@ public class ZipReaderAsyncTests : ReaderTests public async Task Zip_Reader_Disposal_Test2_Async() { using var stream = new TestStream( - new AsyncOnlyStream(File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"))) + new AsyncOnlyStream( + File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) + ) ); var reader = ReaderFactory.Open(stream); while (await reader.MoveToNextEntryAsync()) @@ -185,9 +187,9 @@ public class ZipReaderAsyncTests : ReaderTests await Assert.ThrowsAsync(async () => { using ( - Stream stream = new AsyncOnlyStream(File.OpenRead( - Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.WinzipAES.zip") - )) + Stream stream = new AsyncOnlyStream( + File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.WinzipAES.zip")) + ) ) using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" })) { @@ -210,9 +212,9 @@ public class ZipReaderAsyncTests : ReaderTests public async Task Zip_Deflate_WinzipAES_Read_Async() { using ( - Stream stream = new AsyncOnlyStream(File.OpenRead( - Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip") - )) + Stream stream = new AsyncOnlyStream( + File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip")) + ) ) using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" })) { @@ -235,7 +237,11 @@ public class ZipReaderAsyncTests : ReaderTests public async Task Zip_Deflate_ZipCrypto_Read_Async() { var count = 0; - using (Stream stream = new AsyncOnlyStream(File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip")))) + using ( + Stream stream = new AsyncOnlyStream( + File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip")) + ) + ) using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" })) { while (await reader.MoveToNextEntryAsync()) From d04830ba909e9682ed8e690b019dd8f58485dc01 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 2 Jan 2026 17:52:18 +0000 Subject: [PATCH 10/46] Add some OpenAsync --- src/SharpCompress/Archives/ArchiveFactory.cs | 126 ++++++++++++++++++ .../Archives/AutoArchiveFactory.cs | 14 ++ .../Archives/GZip/GZipArchive.cs | 64 +++++++++ src/SharpCompress/Archives/IArchiveFactory.cs | 26 ++++ .../Archives/IMultiArchiveFactory.cs | 26 ++++ src/SharpCompress/Archives/Rar/RarArchive.cs | 66 +++++++++ .../Archives/SevenZip/SevenZipArchive.cs | 64 +++++++++ src/SharpCompress/Archives/Tar/TarArchive.cs | 64 +++++++++ src/SharpCompress/Archives/Zip/ZipArchive.cs | 64 +++++++++ src/SharpCompress/Factories/ArcFactory.cs | 11 ++ src/SharpCompress/Factories/ArjFactory.cs | 11 ++ src/SharpCompress/Factories/GZipFactory.cs | 52 ++++++++ src/SharpCompress/Factories/RarFactory.cs | 41 ++++++ .../Factories/SevenZipFactory.cs | 30 +++++ src/SharpCompress/Factories/TarFactory.cs | 52 ++++++++ src/SharpCompress/Factories/ZipFactory.cs | 52 ++++++++ src/SharpCompress/Readers/IReaderFactory.cs | 15 +++ src/SharpCompress/Readers/ReaderFactory.cs | 104 +++++++++++++++ src/SharpCompress/Writers/IWriterFactory.cs | 8 ++ src/SharpCompress/Writers/WriterFactory.cs | 31 +++++ 20 files changed, 921 insertions(+) diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 94368ece..2a901900 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Factories; using SharpCompress.IO; @@ -24,6 +26,27 @@ public static class ArchiveFactory return FindFactory(stream).Open(stream, readerOptions); } + /// + /// Opens an Archive for random access asynchronously + /// + /// + /// + /// + /// + public static async Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + readerOptions ??= new ReaderOptions(); + stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize); + var factory = FindFactory(stream); + return await factory + .OpenAsync(stream, readerOptions, cancellationToken) + .ConfigureAwait(false); + } + public static IWritableArchive Create(ArchiveType type) { var factory = Factory @@ -49,6 +72,22 @@ public static class ArchiveFactory return Open(new FileInfo(filePath), options); } + /// + /// Opens an Archive from a filepath asynchronously. + /// + /// + /// + /// + public static Task OpenAsync( + string filePath, + ReaderOptions? options = null, + CancellationToken cancellationToken = default + ) + { + filePath.NotNullOrEmpty(nameof(filePath)); + return OpenAsync(new FileInfo(filePath), options, cancellationToken); + } + /// /// Constructor with a FileInfo object to an existing file. /// @@ -61,6 +100,24 @@ public static class ArchiveFactory return FindFactory(fileInfo).Open(fileInfo, options); } + /// + /// Opens an Archive from a FileInfo object asynchronously. + /// + /// + /// + /// + public static async Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? options = null, + CancellationToken cancellationToken = default + ) + { + options ??= new ReaderOptions { LeaveStreamOpen = false }; + + var factory = FindFactory(fileInfo); + return await factory.OpenAsync(fileInfo, options, cancellationToken).ConfigureAwait(false); + } + /// /// Constructor with IEnumerable FileInfo objects, multi and split support. /// @@ -87,6 +144,40 @@ public static class ArchiveFactory return FindFactory(fileInfo).Open(filesArray, options); } + /// + /// Opens a multi-part archive from files asynchronously. + /// + /// + /// + /// + public static async Task OpenAsync( + IEnumerable fileInfos, + ReaderOptions? options = null, + CancellationToken cancellationToken = default + ) + { + fileInfos.NotNull(nameof(fileInfos)); + var filesArray = fileInfos.ToArray(); + if (filesArray.Length == 0) + { + throw new InvalidOperationException("No files to open"); + } + + var fileInfo = filesArray[0]; + if (filesArray.Length == 1) + { + return await OpenAsync(fileInfo, options, cancellationToken).ConfigureAwait(false); + } + + fileInfo.NotNull(nameof(fileInfo)); + options ??= new ReaderOptions { LeaveStreamOpen = false }; + + var factory = FindFactory(fileInfo); + return await factory + .OpenAsync(filesArray, options, cancellationToken) + .ConfigureAwait(false); + } + /// /// Constructor with IEnumerable FileInfo objects, multi and split support. /// @@ -113,6 +204,41 @@ public static class ArchiveFactory return FindFactory(firstStream).Open(streamsArray, options); } + /// + /// Opens a multi-part archive from streams asynchronously. + /// + /// + /// + /// + public static async Task OpenAsync( + IEnumerable streams, + ReaderOptions? options = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + streams.NotNull(nameof(streams)); + var streamsArray = streams.ToArray(); + if (streamsArray.Length == 0) + { + throw new InvalidOperationException("No streams"); + } + + var firstStream = streamsArray[0]; + if (streamsArray.Length == 1) + { + return await OpenAsync(firstStream, options, cancellationToken).ConfigureAwait(false); + } + + firstStream.NotNull(nameof(firstStream)); + options ??= new ReaderOptions(); + + var factory = FindFactory(firstStream); + return await factory + .OpenAsync(streamsArray, options, cancellationToken) + .ConfigureAwait(false); + } + /// /// Extract to specific directory, retaining filename /// diff --git a/src/SharpCompress/Archives/AutoArchiveFactory.cs b/src/SharpCompress/Archives/AutoArchiveFactory.cs index 78313df5..de07c25e 100644 --- a/src/SharpCompress/Archives/AutoArchiveFactory.cs +++ b/src/SharpCompress/Archives/AutoArchiveFactory.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.Readers; @@ -25,6 +27,18 @@ class AutoArchiveFactory : IArchiveFactory public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) => ArchiveFactory.Open(stream, readerOptions); + public Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => ArchiveFactory.OpenAsync(stream, readerOptions, cancellationToken); + public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) => ArchiveFactory.Open(fileInfo, readerOptions); + + public Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => ArchiveFactory.OpenAsync(fileInfo, readerOptions, cancellationToken); } diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index 34e4c648..4871ebb5 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -102,6 +102,70 @@ public class GZipArchive : AbstractWritableArchive ); } + /// + /// Opens a GZipArchive asynchronously from a stream. + /// + /// + /// + /// + public static async Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(stream, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a GZipArchive asynchronously from a FileInfo. + /// + /// + /// + /// + public static async Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(fileInfo, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a GZipArchive asynchronously from multiple streams. + /// + /// + /// + /// + public static async Task OpenAsync( + IReadOnlyList streams, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(streams, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a GZipArchive asynchronously from multiple FileInfo objects. + /// + /// + /// + /// + public static async Task OpenAsync( + IReadOnlyList fileInfos, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(fileInfos, readerOptions)).ConfigureAwait(false); + } + public static GZipArchive Create() => new(); /// diff --git a/src/SharpCompress/Archives/IArchiveFactory.cs b/src/SharpCompress/Archives/IArchiveFactory.cs index 370e5c9f..2ebf5064 100644 --- a/src/SharpCompress/Archives/IArchiveFactory.cs +++ b/src/SharpCompress/Archives/IArchiveFactory.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Factories; using SharpCompress.Readers; @@ -26,10 +28,34 @@ public interface IArchiveFactory : IFactory /// reading options. IArchive Open(Stream stream, ReaderOptions? readerOptions = null); + /// + /// Opens an Archive for random access asynchronously. + /// + /// An open, readable and seekable stream. + /// reading options. + /// Cancellation token. + Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ); + /// /// Constructor with a FileInfo object to an existing file. /// /// the file to open. /// reading options. IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null); + + /// + /// Opens an Archive from a FileInfo object asynchronously. + /// + /// the file to open. + /// reading options. + /// Cancellation token. + Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ); } diff --git a/src/SharpCompress/Archives/IMultiArchiveFactory.cs b/src/SharpCompress/Archives/IMultiArchiveFactory.cs index c26b649f..d736bf84 100644 --- a/src/SharpCompress/Archives/IMultiArchiveFactory.cs +++ b/src/SharpCompress/Archives/IMultiArchiveFactory.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Factories; using SharpCompress.Readers; @@ -27,10 +29,34 @@ public interface IMultiArchiveFactory : IFactory /// reading options. IArchive Open(IReadOnlyList streams, ReaderOptions? readerOptions = null); + /// + /// Opens a multi-part archive from streams asynchronously. + /// + /// + /// reading options. + /// Cancellation token. + Task OpenAsync( + IReadOnlyList streams, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ); + /// /// Constructor with IEnumerable Stream objects, multi and split support. /// /// /// reading options. IArchive Open(IReadOnlyList fileInfos, ReaderOptions? readerOptions = null); + + /// + /// Opens a multi-part archive from files asynchronously. + /// + /// + /// reading options. + /// Cancellation token. + Task OpenAsync( + IReadOnlyList fileInfos, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ); } diff --git a/src/SharpCompress/Archives/Rar/RarArchive.cs b/src/SharpCompress/Archives/Rar/RarArchive.cs index 9acfdccc..9fa8e2a1 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Rar; using SharpCompress.Common.Rar.Headers; @@ -181,6 +183,70 @@ public class RarArchive : AbstractArchive ); } + /// + /// Opens a RarArchive asynchronously from a stream. + /// + /// + /// + /// + public static async Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(stream, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a RarArchive asynchronously from a FileInfo. + /// + /// + /// + /// + public static async Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(fileInfo, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a RarArchive asynchronously from multiple streams. + /// + /// + /// + /// + public static async Task OpenAsync( + IReadOnlyList streams, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(streams, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a RarArchive asynchronously from multiple FileInfo objects. + /// + /// + /// + /// + public static async Task OpenAsync( + IReadOnlyList fileInfos, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(fileInfos, readerOptions)).ConfigureAwait(false); + } + public static bool IsRarFile(string filePath) => IsRarFile(new FileInfo(filePath)); public static bool IsRarFile(FileInfo fileInfo) diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index d9b5ba1a..a3041c7d 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -105,6 +105,70 @@ public class SevenZipArchive : AbstractArchive + /// Opens a SevenZipArchive asynchronously from a stream. + /// + /// + /// + /// + public static async Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(stream, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a SevenZipArchive asynchronously from a FileInfo. + /// + /// + /// + /// + public static async Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(fileInfo, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a SevenZipArchive asynchronously from multiple streams. + /// + /// + /// + /// + public static async Task OpenAsync( + IReadOnlyList streams, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(streams, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a SevenZipArchive asynchronously from multiple FileInfo objects. + /// + /// + /// + /// + public static async Task OpenAsync( + IReadOnlyList fileInfos, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(fileInfos, readerOptions)).ConfigureAwait(false); + } + /// /// Constructor with a SourceStream able to handle FileInfo and Streams. /// diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index 2754fd9b..11a7c0cc 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -103,6 +103,70 @@ public class TarArchive : AbstractWritableArchive ); } + /// + /// Opens a TarArchive asynchronously from a stream. + /// + /// + /// + /// + public static async Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(stream, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a TarArchive asynchronously from a FileInfo. + /// + /// + /// + /// + public static async Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(fileInfo, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a TarArchive asynchronously from multiple streams. + /// + /// + /// + /// + public static async Task OpenAsync( + IReadOnlyList streams, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(streams, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a TarArchive asynchronously from multiple FileInfo objects. + /// + /// + /// + /// + public static async Task OpenAsync( + IReadOnlyList fileInfos, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(fileInfos, readerOptions)).ConfigureAwait(false); + } + public static bool IsTarFile(string filePath) => IsTarFile(new FileInfo(filePath)); public static bool IsTarFile(FileInfo fileInfo) diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index ae1a58e5..1395505c 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -124,6 +124,70 @@ public class ZipArchive : AbstractWritableArchive ); } + /// + /// Opens a ZipArchive asynchronously from a stream. + /// + /// + /// + /// + public static async Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(stream, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a ZipArchive asynchronously from a FileInfo. + /// + /// + /// + /// + public static async Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(fileInfo, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a ZipArchive asynchronously from multiple streams. + /// + /// + /// + /// + public static async Task OpenAsync( + IReadOnlyList streams, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(streams, readerOptions)).ConfigureAwait(false); + } + + /// + /// Opens a ZipArchive asynchronously from multiple FileInfo objects. + /// + /// + /// + /// + public static async Task OpenAsync( + IReadOnlyList fileInfos, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(fileInfos, readerOptions)).ConfigureAwait(false); + } + public static bool IsZipFile( string filePath, string? password = null, diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index b5180afa..18065afd 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Readers; @@ -42,5 +43,15 @@ namespace SharpCompress.Factories public IReader OpenReader(Stream stream, ReaderOptions? options) => ArcReader.Open(stream, options); + + public async Task OpenReaderAsync( + Stream stream, + ReaderOptions? options, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + } } } diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs index 7499946b..b0b120f3 100644 --- a/src/SharpCompress/Factories/ArjFactory.cs +++ b/src/SharpCompress/Factories/ArjFactory.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.Common; using SharpCompress.Common.Arj.Headers; @@ -38,5 +39,15 @@ namespace SharpCompress.Factories public IReader OpenReader(Stream stream, ReaderOptions? options) => ArjReader.Open(stream, options); + + public async Task OpenReaderAsync( + Stream stream, + ReaderOptions? options, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + } } } diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index 17f344cf..923991b7 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Archives.GZip; using SharpCompress.Archives.Tar; @@ -54,10 +56,24 @@ public class GZipFactory public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) => GZipArchive.Open(stream, readerOptions); + /// + public Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => GZipArchive.OpenAsync(stream, readerOptions, cancellationToken); + /// public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) => GZipArchive.Open(fileInfo, readerOptions); + /// + public Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => GZipArchive.OpenAsync(fileInfo, readerOptions, cancellationToken); + #endregion #region IMultiArchiveFactory @@ -66,10 +82,24 @@ public class GZipFactory public IArchive Open(IReadOnlyList streams, ReaderOptions? readerOptions = null) => GZipArchive.Open(streams, readerOptions); + /// + public Task OpenAsync( + IReadOnlyList streams, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => GZipArchive.OpenAsync(streams, readerOptions, cancellationToken); + /// public IArchive Open(IReadOnlyList fileInfos, ReaderOptions? readerOptions = null) => GZipArchive.Open(fileInfos, readerOptions); + /// + public Task OpenAsync( + IReadOnlyList fileInfos, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => GZipArchive.OpenAsync(fileInfos, readerOptions, cancellationToken); + #endregion #region IReaderFactory @@ -108,6 +138,17 @@ public class GZipFactory public IReader OpenReader(Stream stream, ReaderOptions? options) => GZipReader.Open(stream, options); + /// + public async Task OpenReaderAsync( + Stream stream, + ReaderOptions? options, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + } + #endregion #region IWriterFactory @@ -122,6 +163,17 @@ public class GZipFactory return new GZipWriter(stream, new GZipWriterOptions(writerOptions)); } + /// + public async Task OpenAsync( + Stream stream, + WriterOptions writerOptions, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(stream, writerOptions)).ConfigureAwait(false); + } + #endregion #region IWriteableArchiveFactory diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index 61099905..2986e61b 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Common; @@ -47,10 +49,24 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) => RarArchive.Open(stream, readerOptions); + /// + public Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => RarArchive.OpenAsync(stream, readerOptions, cancellationToken); + /// public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) => RarArchive.Open(fileInfo, readerOptions); + /// + public Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => RarArchive.OpenAsync(fileInfo, readerOptions, cancellationToken); + #endregion #region IMultiArchiveFactory @@ -59,10 +75,24 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade public IArchive Open(IReadOnlyList streams, ReaderOptions? readerOptions = null) => RarArchive.Open(streams, readerOptions); + /// + public Task OpenAsync( + IReadOnlyList streams, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => RarArchive.OpenAsync(streams, readerOptions, cancellationToken); + /// public IArchive Open(IReadOnlyList fileInfos, ReaderOptions? readerOptions = null) => RarArchive.Open(fileInfos, readerOptions); + /// + public Task OpenAsync( + IReadOnlyList fileInfos, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => RarArchive.OpenAsync(fileInfos, readerOptions, cancellationToken); + #endregion #region IReaderFactory @@ -71,5 +101,16 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade public IReader OpenReader(Stream stream, ReaderOptions? options) => RarReader.Open(stream, options); + /// + public async Task OpenReaderAsync( + Stream stream, + ReaderOptions? options, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + } + #endregion } diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index 18dedbfd..a9c74fae 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Archives.SevenZip; using SharpCompress.Common; @@ -42,10 +44,24 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) => SevenZipArchive.Open(stream, readerOptions); + /// + public Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => SevenZipArchive.OpenAsync(stream, readerOptions, cancellationToken); + /// public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) => SevenZipArchive.Open(fileInfo, readerOptions); + /// + public Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => SevenZipArchive.OpenAsync(fileInfo, readerOptions, cancellationToken); + #endregion #region IMultiArchiveFactory @@ -54,10 +70,24 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory public IArchive Open(IReadOnlyList streams, ReaderOptions? readerOptions = null) => SevenZipArchive.Open(streams, readerOptions); + /// + public Task OpenAsync( + IReadOnlyList streams, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => SevenZipArchive.OpenAsync(streams, readerOptions, cancellationToken); + /// public IArchive Open(IReadOnlyList fileInfos, ReaderOptions? readerOptions = null) => SevenZipArchive.Open(fileInfos, readerOptions); + /// + public Task OpenAsync( + IReadOnlyList fileInfos, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => SevenZipArchive.OpenAsync(fileInfos, readerOptions, cancellationToken); + #endregion #region reader diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index d32020fd..240b835d 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Archives.Tar; using SharpCompress.Common; @@ -67,10 +69,24 @@ public class TarFactory public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) => TarArchive.Open(stream, readerOptions); + /// + public Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => TarArchive.OpenAsync(stream, readerOptions, cancellationToken); + /// public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) => TarArchive.Open(fileInfo, readerOptions); + /// + public Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => TarArchive.OpenAsync(fileInfo, readerOptions, cancellationToken); + #endregion #region IMultiArchiveFactory @@ -79,10 +95,24 @@ public class TarFactory public IArchive Open(IReadOnlyList streams, ReaderOptions? readerOptions = null) => TarArchive.Open(streams, readerOptions); + /// + public Task OpenAsync( + IReadOnlyList streams, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => TarArchive.OpenAsync(streams, readerOptions, cancellationToken); + /// public IArchive Open(IReadOnlyList fileInfos, ReaderOptions? readerOptions = null) => TarArchive.Open(fileInfos, readerOptions); + /// + public Task OpenAsync( + IReadOnlyList fileInfos, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => TarArchive.OpenAsync(fileInfos, readerOptions, cancellationToken); + #endregion #region IReaderFactory @@ -234,6 +264,17 @@ public class TarFactory public IReader OpenReader(Stream stream, ReaderOptions? options) => TarReader.Open(stream, options); + /// + public async Task OpenReaderAsync( + Stream stream, + ReaderOptions? options, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + } + #endregion #region IWriterFactory @@ -242,6 +283,17 @@ public class TarFactory public IWriter Open(Stream stream, WriterOptions writerOptions) => new TarWriter(stream, new TarWriterOptions(writerOptions)); + /// + public async Task OpenAsync( + Stream stream, + WriterOptions writerOptions, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(stream, writerOptions)).ConfigureAwait(false); + } + #endregion #region IWriteableArchiveFactory diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index 5c2fcad8..30f4b49b 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Archives.Zip; using SharpCompress.Common; @@ -91,10 +93,24 @@ public class ZipFactory public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) => ZipArchive.Open(stream, readerOptions); + /// + public Task OpenAsync( + Stream stream, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => ZipArchive.OpenAsync(stream, readerOptions, cancellationToken); + /// public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) => ZipArchive.Open(fileInfo, readerOptions); + /// + public Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => ZipArchive.OpenAsync(fileInfo, readerOptions, cancellationToken); + #endregion #region IMultiArchiveFactory @@ -103,10 +119,24 @@ public class ZipFactory public IArchive Open(IReadOnlyList streams, ReaderOptions? readerOptions = null) => ZipArchive.Open(streams, readerOptions); + /// + public Task OpenAsync( + IReadOnlyList streams, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => ZipArchive.OpenAsync(streams, readerOptions, cancellationToken); + /// public IArchive Open(IReadOnlyList fileInfos, ReaderOptions? readerOptions = null) => ZipArchive.Open(fileInfos, readerOptions); + /// + public Task OpenAsync( + IReadOnlyList fileInfos, + ReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) => ZipArchive.OpenAsync(fileInfos, readerOptions, cancellationToken); + #endregion #region IReaderFactory @@ -115,6 +145,17 @@ public class ZipFactory public IReader OpenReader(Stream stream, ReaderOptions? options) => ZipReader.Open(stream, options); + /// + public async Task OpenReaderAsync( + Stream stream, + ReaderOptions? options, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + } + #endregion #region IWriterFactory @@ -123,6 +164,17 @@ public class ZipFactory public IWriter Open(Stream stream, WriterOptions writerOptions) => new ZipWriter(stream, new ZipWriterOptions(writerOptions)); + /// + public async Task OpenAsync( + Stream stream, + WriterOptions writerOptions, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return await Task.FromResult(Open(stream, writerOptions)).ConfigureAwait(false); + } + #endregion #region IWriteableArchiveFactory diff --git a/src/SharpCompress/Readers/IReaderFactory.cs b/src/SharpCompress/Readers/IReaderFactory.cs index 08c190a3..dd95f187 100644 --- a/src/SharpCompress/Readers/IReaderFactory.cs +++ b/src/SharpCompress/Readers/IReaderFactory.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Readers; @@ -11,4 +13,17 @@ public interface IReaderFactory : Factories.IFactory /// /// IReader OpenReader(Stream stream, ReaderOptions? options); + + /// + /// Opens a Reader asynchronously for Non-seeking usage + /// + /// + /// + /// + /// + Task OpenReaderAsync( + Stream stream, + ReaderOptions? options, + CancellationToken cancellationToken = default + ); } diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 80995e41..6809cfa4 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Factories; using SharpCompress.IO; @@ -15,12 +17,46 @@ public static class ReaderFactory return Open(new FileInfo(filePath), options); } + /// + /// Opens a Reader from a filepath asynchronously + /// + /// + /// + /// + /// + public static Task OpenAsync( + string filePath, + ReaderOptions? options = null, + CancellationToken cancellationToken = default + ) + { + filePath.NotNullOrEmpty(nameof(filePath)); + return OpenAsync(new FileInfo(filePath), options, cancellationToken); + } + public static IReader Open(FileInfo fileInfo, ReaderOptions? options = null) { options ??= new ReaderOptions { LeaveStreamOpen = false }; return Open(fileInfo.OpenRead(), options); } + /// + /// Opens a Reader from a FileInfo asynchronously + /// + /// + /// + /// + /// + public static Task OpenAsync( + FileInfo fileInfo, + ReaderOptions? options = null, + CancellationToken cancellationToken = default + ) + { + options ??= new ReaderOptions { LeaveStreamOpen = false }; + return OpenAsync(fileInfo.OpenRead(), options, cancellationToken); + } + /// /// Opens a Reader for Non-seeking usage /// @@ -73,4 +109,72 @@ public static class ReaderFactory "Cannot determine compressed stream type. Supported Reader Formats: Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard" ); } + + /// + /// Opens a Reader for Non-seeking usage asynchronously + /// + /// + /// + /// + /// + public static async Task OpenAsync( + Stream stream, + ReaderOptions? options = null, + CancellationToken cancellationToken = default + ) + { + stream.NotNull(nameof(stream)); + options ??= new ReaderOptions() { LeaveStreamOpen = false }; + + var bStream = new SharpCompressStream(stream, bufferSize: options.BufferSize); + + long pos = ((IStreamStack)bStream).GetPosition(); + + var factories = Factories.Factory.Factories.OfType(); + + Factory? testedFactory = null; + + if (!string.IsNullOrWhiteSpace(options.ExtensionHint)) + { + testedFactory = factories.FirstOrDefault(a => + a.GetSupportedExtensions() + .Contains(options.ExtensionHint, StringComparer.CurrentCultureIgnoreCase) + ); + if (testedFactory is IReaderFactory readerFactory) + { + ((IStreamStack)bStream).StackSeek(pos); + if (testedFactory.IsArchive(bStream, options.Password, options.BufferSize)) + { + ((IStreamStack)bStream).StackSeek(pos); + return await readerFactory + .OpenReaderAsync(bStream, options, cancellationToken) + .ConfigureAwait(false); + } + } + ((IStreamStack)bStream).StackSeek(pos); + } + + foreach (var factory in factories) + { + if (testedFactory == factory) + { + continue; // Already tested above + } + ((IStreamStack)bStream).StackSeek(pos); + if ( + factory is IReaderFactory readerFactory + && factory.IsArchive(bStream, options.Password, options.BufferSize) + ) + { + ((IStreamStack)bStream).StackSeek(pos); + return await readerFactory + .OpenReaderAsync(bStream, options, cancellationToken) + .ConfigureAwait(false); + } + } + + throw new InvalidFormatException( + "Cannot determine compressed stream type. Supported Reader Formats: Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard" + ); + } } diff --git a/src/SharpCompress/Writers/IWriterFactory.cs b/src/SharpCompress/Writers/IWriterFactory.cs index 094a5553..059dbe59 100644 --- a/src/SharpCompress/Writers/IWriterFactory.cs +++ b/src/SharpCompress/Writers/IWriterFactory.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Factories; namespace SharpCompress.Writers; @@ -6,4 +8,10 @@ namespace SharpCompress.Writers; public interface IWriterFactory : IFactory { IWriter Open(Stream stream, WriterOptions writerOptions); + + Task OpenAsync( + Stream stream, + WriterOptions writerOptions, + CancellationToken cancellationToken = default + ); } diff --git a/src/SharpCompress/Writers/WriterFactory.cs b/src/SharpCompress/Writers/WriterFactory.cs index 50e1fdfc..518e9d10 100644 --- a/src/SharpCompress/Writers/WriterFactory.cs +++ b/src/SharpCompress/Writers/WriterFactory.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; namespace SharpCompress.Writers; @@ -20,4 +22,33 @@ public static class WriterFactory throw new NotSupportedException("Archive Type does not have a Writer: " + archiveType); } + + /// + /// Opens a Writer asynchronously. + /// + /// The stream to write to. + /// The archive type. + /// Writer options. + /// Cancellation token. + /// A task that returns an IWriter. + public static async Task OpenAsync( + Stream stream, + ArchiveType archiveType, + WriterOptions writerOptions, + CancellationToken cancellationToken = default + ) + { + var factory = Factories + .Factory.Factories.OfType() + .FirstOrDefault(item => item.KnownArchiveType == archiveType); + + if (factory != null) + { + return await factory + .OpenAsync(stream, writerOptions, cancellationToken) + .ConfigureAwait(false); + } + + throw new NotSupportedException("Archive Type does not have a Writer: " + archiveType); + } } From ea02d310961cfbd7f9230438d4bd364eb4130f2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:10:54 +0000 Subject: [PATCH 11/46] Add IsArchiveAsync overloads for Zip and GZip factories - Added IsArchiveAsync interface method to IFactory - Implemented async versions of IsZipFile, IsZipMulti, IsGZipFile - Updated ZipFactory and GZipFactory to override IsArchiveAsync - Updated ReaderFactory.OpenAsync to use IsArchiveAsync - Fixed Zip_Reader_Disposal_Test2_Async to use ReaderFactory.OpenAsync - Fixed TestStream to properly forward ReadAsync calls - Removed BufferedStream wrapping from AsyncBinaryReader as it uses sync Read - Added default implementation in Factory base class Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Archives/AutoArchiveFactory.cs | 7 ++ .../Archives/GZip/GZipArchive.cs | 22 +++++ src/SharpCompress/Archives/Zip/ZipArchive.cs | 86 +++++++++++++++++++ src/SharpCompress/Common/AsyncBinaryReader.cs | 26 +----- src/SharpCompress/Factories/Factory.cs | 14 +++ src/SharpCompress/Factories/GZipFactory.cs | 8 ++ src/SharpCompress/Factories/IFactory.cs | 16 ++++ src/SharpCompress/Factories/ZipFactory.cs | 43 ++++++++++ src/SharpCompress/Readers/ReaderFactory.cs | 16 +++- tests/SharpCompress.Test/Mocks/TestStream.cs | 19 +++- .../Zip/ZipReaderAsyncTests.cs | 2 +- 11 files changed, 233 insertions(+), 26 deletions(-) diff --git a/src/SharpCompress/Archives/AutoArchiveFactory.cs b/src/SharpCompress/Archives/AutoArchiveFactory.cs index de07c25e..2f78e8f6 100644 --- a/src/SharpCompress/Archives/AutoArchiveFactory.cs +++ b/src/SharpCompress/Archives/AutoArchiveFactory.cs @@ -22,6 +22,13 @@ class AutoArchiveFactory : IArchiveFactory int bufferSize = ReaderOptions.DefaultBufferSize ) => throw new NotSupportedException(); + public Task IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize, + CancellationToken cancellationToken = default + ) => throw new NotSupportedException(); + public FileInfo? GetFilePart(int index, FileInfo part1) => throw new NotSupportedException(); public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) => diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index 4871ebb5..9ecab946 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -231,6 +231,28 @@ public class GZipArchive : AbstractWritableArchive return true; } + public static async Task IsGZipFileAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + // read the header on the first read + byte[] header = new byte[10]; + + // workitem 8501: handle edge case (decompress empty stream) + if (!await stream.ReadFullyAsync(header, cancellationToken).ConfigureAwait(false)) + { + return false; + } + + if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8) + { + return false; + } + + return true; + } + internal GZipArchive() : base(ArchiveType.GZip) { } diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 1395505c..1a87df5e 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -283,6 +283,92 @@ public class ZipArchive : AbstractWritableArchive } } + public static async Task IsZipFileAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null); + try + { + if (stream is not SharpCompressStream) + { + stream = new SharpCompressStream(stream, bufferSize: bufferSize); + } + + var header = headerFactory + .ReadStreamHeader(stream) + .FirstOrDefault(x => x.ZipHeaderType != ZipHeaderType.Split); + if (header is null) + { + return false; + } + return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType); + } + catch (CryptographicException) + { + return true; + } + catch + { + return false; + } + } + + public static async Task IsZipMultiAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null); + try + { + if (stream is not SharpCompressStream) + { + stream = new SharpCompressStream(stream, bufferSize: bufferSize); + } + + var header = headerFactory + .ReadStreamHeader(stream) + .FirstOrDefault(x => x.ZipHeaderType != ZipHeaderType.Split); + if (header is null) + { + if (stream.CanSeek) //could be multipart. Test for central directory - might not be z64 safe + { + var z = new SeekableZipHeaderFactory(password, new ArchiveEncoding()); + ZipHeader? x = null; + await foreach ( + var h in z.ReadSeekableHeader(stream).WithCancellation(cancellationToken) + ) + { + x = h; + break; + } + return x?.ZipHeaderType == ZipHeaderType.DirectoryEntry; + } + else + { + return false; + } + } + return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType); + } + catch (CryptographicException) + { + return true; + } + catch + { + return false; + } + } + protected override IEnumerable LoadVolumes(SourceStream stream) { stream.LoadAllParts(); //request all streams diff --git a/src/SharpCompress/Common/AsyncBinaryReader.cs b/src/SharpCompress/Common/AsyncBinaryReader.cs index a6a7bb9c..2a6eb92c 100644 --- a/src/SharpCompress/Common/AsyncBinaryReader.cs +++ b/src/SharpCompress/Common/AsyncBinaryReader.cs @@ -19,16 +19,10 @@ namespace SharpCompress.Common _originalStream = stream ?? throw new ArgumentNullException(nameof(stream)); _leaveOpen = leaveOpen; - // Wrap the stream with BufferedStream if it's not already a buffered stream - // This enables efficient async reading with internal buffering - if (stream is BufferedStream || stream is IO.SharpCompressStream) - { - _stream = stream; - } - else - { - _stream = new BufferedStream(stream, bufferSize); - } + // Use the stream directly without wrapping in BufferedStream + // BufferedStream uses synchronous Read internally which doesn't work with async-only streams + // SharpCompress uses SharpCompressStream for buffering which supports true async reads + _stream = stream; } public Stream BaseStream => _stream; @@ -95,12 +89,6 @@ namespace SharpCompress.Common _disposed = true; - // Dispose the buffered stream if we created it - if (_stream != _originalStream) - { - _stream.Dispose(); - } - // Dispose the original stream if we own it if (!_leaveOpen) { @@ -118,12 +106,6 @@ namespace SharpCompress.Common _disposed = true; - // Dispose the buffered stream if we created it - if (_stream != _originalStream) - { - await _stream.DisposeAsync().ConfigureAwait(false); - } - // Dispose the original stream if we own it if (!_leaveOpen) { diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index dba20177..b4db6506 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.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; using SharpCompress.Readers; @@ -56,6 +58,18 @@ public abstract class Factory : IFactory int bufferSize = ReaderOptions.DefaultBufferSize ); + /// + public virtual Task IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(IsArchive(stream, password, bufferSize)); + } + /// public virtual FileInfo? GetFilePart(int index, FileInfo part1) => null; diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index 923991b7..f6797b30 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -48,6 +48,14 @@ public class GZipFactory int bufferSize = ReaderOptions.DefaultBufferSize ) => GZipArchive.IsGZipFile(stream); + /// + public override Task IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize, + CancellationToken cancellationToken = default + ) => GZipArchive.IsGZipFileAsync(stream, cancellationToken); + #endregion #region IArchiveFactory diff --git a/src/SharpCompress/Factories/IFactory.cs b/src/SharpCompress/Factories/IFactory.cs index 63d5eeec..47200cb7 100644 --- a/src/SharpCompress/Factories/IFactory.cs +++ b/src/SharpCompress/Factories/IFactory.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Readers; namespace SharpCompress.Factories; @@ -42,6 +44,20 @@ public interface IFactory int bufferSize = ReaderOptions.DefaultBufferSize ); + /// + /// Returns true if the stream represents an archive of the format defined by this type asynchronously. + /// + /// A stream, pointing to the beginning of the archive. + /// optional password + /// buffer size for reading + /// cancellation token + Task IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize, + CancellationToken cancellationToken = default + ); + /// /// From a passed in archive (zip, rar, 7z, 001), return all parts. /// diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index 30f4b49b..21de0c5a 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -81,6 +81,49 @@ public class ZipFactory return false; } + /// + public override async Task IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + var startPosition = stream.CanSeek ? stream.Position : -1; + + // probe for single volume zip + + if (stream is not SharpCompressStream) // wrap to provide buffer bef + { + stream = new SharpCompressStream(stream, bufferSize: bufferSize); + } + + if (await ZipArchive.IsZipFileAsync(stream, password, bufferSize, cancellationToken)) + { + return true; + } + + // probe for a multipart zip + + if (!stream.CanSeek) + { + return false; + } + + stream.Position = startPosition; + + //test the zip (last) file of a multipart zip + if (await ZipArchive.IsZipMultiAsync(stream, password, bufferSize, cancellationToken)) + { + return true; + } + + stream.Position = startPosition; + + return false; + } + /// public override FileInfo? GetFilePart(int index, FileInfo part1) => ZipArchiveVolumeFactory.GetFilePart(index, part1); diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 6809cfa4..9fc0cc29 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -143,7 +143,14 @@ public static class ReaderFactory if (testedFactory is IReaderFactory readerFactory) { ((IStreamStack)bStream).StackSeek(pos); - if (testedFactory.IsArchive(bStream, options.Password, options.BufferSize)) + if ( + await testedFactory.IsArchiveAsync( + bStream, + options.Password, + options.BufferSize, + cancellationToken + ) + ) { ((IStreamStack)bStream).StackSeek(pos); return await readerFactory @@ -163,7 +170,12 @@ public static class ReaderFactory ((IStreamStack)bStream).StackSeek(pos); if ( factory is IReaderFactory readerFactory - && factory.IsArchive(bStream, options.Password, options.BufferSize) + && await factory.IsArchiveAsync( + bStream, + options.Password, + options.BufferSize, + cancellationToken + ) ) { ((IStreamStack)bStream).StackSeek(pos); diff --git a/tests/SharpCompress.Test/Mocks/TestStream.cs b/tests/SharpCompress.Test/Mocks/TestStream.cs index da7d65cc..37e1e808 100644 --- a/tests/SharpCompress.Test/Mocks/TestStream.cs +++ b/tests/SharpCompress.Test/Mocks/TestStream.cs @@ -1,4 +1,7 @@ -using System.IO; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Test.Mocks; @@ -35,6 +38,20 @@ public class TestStream(Stream stream, bool read, bool write, bool seek) : Strea 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) => stream.Seek(offset, origin); public override void SetLength(long value) => stream.SetLength(value); diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index 5acb3a41..fbb5ee3a 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -168,7 +168,7 @@ public class ZipReaderAsyncTests : ReaderTests File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) ) ); - var reader = ReaderFactory.Open(stream); + var reader = await ReaderFactory.OpenAsync(stream); while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) From 54640548ed191980a09e0eb34834ec52a74e460e Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 3 Jan 2026 15:47:10 +0000 Subject: [PATCH 12/46] Consolidate reads --- .../Compressors/Xz/MultiByteIntegers.cs | 37 +- .../Polyfills/BinaryReaderExtensions.cs | 58 +++ .../Polyfills/StreamExtensions.cs | 155 ++++--- src/SharpCompress/Utility.cs | 384 ++++++++---------- tests/SharpCompress.Test/UtilityTests.cs | 92 +++++ 5 files changed, 421 insertions(+), 305 deletions(-) create mode 100644 src/SharpCompress/Polyfills/BinaryReaderExtensions.cs diff --git a/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs index f5613d66..6f7a863b 100644 --- a/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs +++ b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs @@ -58,7 +58,7 @@ internal static class MultiByteIntegers MaxBytes = 9; } - var LastByte = await ReadByteAsync(reader, cancellationToken).ConfigureAwait(false); + var LastByte = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); var Output = (ulong)LastByte & 0x7F; var i = 0; @@ -69,7 +69,7 @@ internal static class MultiByteIntegers throw new InvalidFormatException(); } - LastByte = await ReadByteAsync(reader, cancellationToken).ConfigureAwait(false); + LastByte = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); if (LastByte == 0) { throw new InvalidFormatException(); @@ -79,37 +79,4 @@ internal static class MultiByteIntegers } return Output; } - - public static async Task ReadByteAsync( - this BinaryReader reader, - CancellationToken cancellationToken = default - ) - { - var buffer = new byte[1]; - var bytesRead = await reader - .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - if (bytesRead != 1) - { - throw new EndOfStreamException(); - } - return buffer[0]; - } - - public static async Task ReadBytesAsync( - this BinaryReader reader, - int count, - CancellationToken cancellationToken = default - ) - { - var buffer = new byte[count]; - var bytesRead = await reader - .BaseStream.ReadAsync(buffer, 0, count, cancellationToken) - .ConfigureAwait(false); - if (bytesRead != count) - { - throw new EndOfStreamException(); - } - return buffer; - } } diff --git a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs new file mode 100644 index 00000000..a030d4bc --- /dev/null +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -0,0 +1,58 @@ +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress; + +public static class BinaryReaderExtensions +{ + extension(BinaryReader reader) + { + public async Task ReadByteAsync(CancellationToken cancellationToken = default) + { + var buffer = ArrayPool.Shared.Rent(1); + try + { + var bytesRead = await reader + .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + if (bytesRead != 1) + { + throw new EndOfStreamException(); + } + + return buffer[0]; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public async Task ReadBytesAsync( + int count, + CancellationToken cancellationToken = default + ) + { + var buffer = ArrayPool.Shared.Rent(count); + try + { + var bytesRead = await reader + .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + if (bytesRead != count) + { + throw new EndOfStreamException(); + } + var bytes = new byte[count]; + System.Buffer.BlockCopy(buffer, 0, bytes, 0, count); + return bytes; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } +} diff --git a/src/SharpCompress/Polyfills/StreamExtensions.cs b/src/SharpCompress/Polyfills/StreamExtensions.cs index f00b274a..bd41ac75 100644 --- a/src/SharpCompress/Polyfills/StreamExtensions.cs +++ b/src/SharpCompress/Polyfills/StreamExtensions.cs @@ -1,5 +1,3 @@ -#if NETFRAMEWORK || NETSTANDARD2_0 - using System; using System.Buffers; using System.IO; @@ -8,63 +6,112 @@ using System.Threading.Tasks; namespace SharpCompress; -internal static class StreamExtensions +public static class StreamExtensions { - internal static int Read(this Stream stream, Span buffer) + extension(Stream stream) { - var temp = ArrayPool.Shared.Rent(buffer.Length); - - try + public void Skip(long advanceAmount) { - var read = stream.Read(temp, 0, buffer.Length); - - temp.AsSpan(0, read).CopyTo(buffer); - - return read; - } - finally - { - ArrayPool.Shared.Return(temp); - } - } - - internal static void Write(this Stream stream, ReadOnlySpan buffer) - { - var temp = ArrayPool.Shared.Rent(buffer.Length); - - buffer.CopyTo(temp); - - try - { - stream.Write(temp, 0, buffer.Length); - } - finally - { - ArrayPool.Shared.Return(temp); - } - } - - internal static async Task ReadExactlyAsync( - this Stream stream, - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - var totalRead = 0; - while (totalRead < count) - { - var read = await stream - .ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken) - .ConfigureAwait(false); - if (read == 0) + if (stream.CanSeek) { - throw new EndOfStreamException(); + stream.Position += advanceAmount; + return; + } + + using var buffer = MemoryPool.Shared.Rent(Utility.TEMP_BUFFER_SIZE); + while (advanceAmount > 0) + { + var toRead = (int)Math.Min(buffer.Memory.Length, advanceAmount); + var read = stream.Read(buffer.Memory.Slice(0, toRead).Span); + if (read <= 0) + { + break; + } + advanceAmount -= read; + } + } + + public void Skip() + { + using var buffer = MemoryPool.Shared.Rent(Utility.TEMP_BUFFER_SIZE); + while (stream.Read(buffer.Memory.Span) > 0) { } + } + + public async Task SkipAsync(CancellationToken cancellationToken = default) + { + var array = ArrayPool.Shared.Rent(Utility.TEMP_BUFFER_SIZE); + try + { + while (true) + { + var read = await stream + .ReadAsync(array, 0, array.Length, cancellationToken) + .ConfigureAwait(false); + if (read <= 0) + { + break; + } + } + } + finally + { + ArrayPool.Shared.Return(array); + } + } + + internal int Read(Span buffer) + { + var temp = ArrayPool.Shared.Rent(buffer.Length); + + try + { + var read = stream.Read(temp, 0, buffer.Length); + + temp.AsSpan(0, read).CopyTo(buffer); + + return read; + } + finally + { + ArrayPool.Shared.Return(temp); + } + } + + internal void Write(ReadOnlySpan buffer) + { + var temp = ArrayPool.Shared.Rent(buffer.Length); + + buffer.CopyTo(temp); + + try + { + stream.Write(temp, 0, buffer.Length); + } + finally + { + ArrayPool.Shared.Return(temp); + } + } + + internal async Task ReadExactlyAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + var totalRead = 0; + while (totalRead < count) + { + var read = await stream + .ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken) + .ConfigureAwait(false); + if (read == 0) + { + throw new EndOfStreamException(); + } + totalRead += read; } - totalRead += read; } } } - -#endif diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index 0c648f12..ea0faa2a 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -12,7 +12,7 @@ namespace SharpCompress; internal static class Utility { //80kb is a good industry standard temporary buffer size - private const int TEMP_BUFFER_SIZE = 81920; + internal const int TEMP_BUFFER_SIZE = 81920; private static readonly HashSet invalidChars = new(Path.GetInvalidFileNameChars()); public static ReadOnlyCollection ToReadOnly(this IList items) => new(items); @@ -63,58 +63,6 @@ internal static class Utility yield return item; } - public static void Skip(this Stream source, long advanceAmount) - { - if (source.CanSeek) - { - source.Position += advanceAmount; - return; - } - - using var buffer = MemoryPool.Shared.Rent(TEMP_BUFFER_SIZE); - while (advanceAmount > 0) - { - var toRead = (int)Math.Min(buffer.Memory.Length, advanceAmount); - var read = source.Read(buffer.Memory.Slice(0, toRead).Span); - if (read <= 0) - { - break; - } - advanceAmount -= read; - } - } - - public static void Skip(this Stream source) - { - using var buffer = MemoryPool.Shared.Rent(TEMP_BUFFER_SIZE); - while (source.Read(buffer.Memory.Span) > 0) { } - } - - public static async Task SkipAsync( - this Stream source, - CancellationToken cancellationToken = default - ) - { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try - { - while (true) - { - var read = await source - .ReadAsync(array, 0, array.Length, cancellationToken) - .ConfigureAwait(false); - if (read <= 0) - { - break; - } - } - } - finally - { - ArrayPool.Shared.Return(array); - } - } - public static DateTime DosDateToDateTime(ushort iDate, ushort iTime) { var year = (iDate / 512) + 1980; @@ -181,83 +129,85 @@ internal static class Utility return sTime.AddSeconds(unixtime); } - public static long TransferTo(this Stream source, Stream destination, long maxLength) + extension(Stream source) { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try + public long TransferTo(Stream destination, long maxLength) { - var maxReadSize = array.Length; - long total = 0; - var remaining = maxLength; - if (remaining < maxReadSize) + var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); + try { - maxReadSize = (int)remaining; - } - while (ReadTransferBlock(source, array, maxReadSize, out var count)) - { - destination.Write(array, 0, count); - total += count; - if (remaining - count < 0) - { - break; - } - remaining -= count; + var maxReadSize = array.Length; + long total = 0; + var remaining = maxLength; if (remaining < maxReadSize) { maxReadSize = (int)remaining; } + while (ReadTransferBlock(source, array, maxReadSize, out var count)) + { + destination.Write(array, 0, count); + total += count; + if (remaining - count < 0) + { + break; + } + remaining -= count; + if (remaining < maxReadSize) + { + maxReadSize = (int)remaining; + } + } + return total; + } + finally + { + ArrayPool.Shared.Return(array); } - return total; } - finally - { - ArrayPool.Shared.Return(array); - } - } - public static async Task TransferToAsync( - this Stream source, - Stream destination, - long maxLength, - CancellationToken cancellationToken = default - ) - { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try + public async Task TransferToAsync( + Stream destination, + long maxLength, + CancellationToken cancellationToken = default + ) { - var maxReadSize = array.Length; - long total = 0; - var remaining = maxLength; - if (remaining < maxReadSize) + var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); + try { - maxReadSize = (int)remaining; - } - while ( - await ReadTransferBlockAsync(source, array, maxReadSize, cancellationToken) - .ConfigureAwait(false) - is var (success, count) - && success - ) - { - await destination - .WriteAsync(array, 0, count, cancellationToken) - .ConfigureAwait(false); - total += count; - if (remaining - count < 0) - { - break; - } - remaining -= count; + var maxReadSize = array.Length; + long total = 0; + var remaining = maxLength; if (remaining < maxReadSize) { maxReadSize = (int)remaining; } + while ( + await ReadTransferBlockAsync(source, array, maxReadSize, cancellationToken) + .ConfigureAwait(false) + is var (success, count) + && success + ) + { + await destination + .WriteAsync(array, 0, count, cancellationToken) + .ConfigureAwait(false); + total += count; + if (remaining - count < 0) + { + break; + } + remaining -= count; + if (remaining < maxReadSize) + { + maxReadSize = (int)remaining; + } + } + return total; + } + finally + { + ArrayPool.Shared.Return(array); } - return total; - } - finally - { - ArrayPool.Shared.Return(array); } } @@ -288,37 +238,119 @@ internal static class Utility return (count != 0, count); } - public static async Task SkipAsync( - this Stream source, - long advanceAmount, - CancellationToken cancellationToken = default - ) + extension(Stream source) { - if (source.CanSeek) + public async Task SkipAsync( + long advanceAmount, + CancellationToken cancellationToken = default + ) { - source.Position += advanceAmount; - return; - } - - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try - { - while (advanceAmount > 0) + if (source.CanSeek) { - var toRead = (int)Math.Min(array.Length, advanceAmount); - var read = await source - .ReadAsync(array, 0, toRead, cancellationToken) - .ConfigureAwait(false); - if (read <= 0) + source.Position += advanceAmount; + return; + } + + var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); + try + { + while (advanceAmount > 0) { - break; + var toRead = (int)Math.Min(array.Length, advanceAmount); + var read = await source + .ReadAsync(array, 0, toRead, cancellationToken) + .ConfigureAwait(false); + if (read <= 0) + { + break; + } + advanceAmount -= read; } - advanceAmount -= read; + } + finally + { + ArrayPool.Shared.Return(array); } } - finally + + public bool ReadFully(byte[] buffer) { - ArrayPool.Shared.Return(array); + var total = 0; + int read; + while ((read = source.Read(buffer, total, buffer.Length - total)) > 0) + { + total += read; + if (total >= buffer.Length) + { + return true; + } + } + return (total >= buffer.Length); + } + + public bool ReadFully(Span buffer) + { + var total = 0; + int read; + while ((read = source.Read(buffer.Slice(total, buffer.Length - total))) > 0) + { + total += read; + if (total >= buffer.Length) + { + return true; + } + } + return (total >= buffer.Length); + } + + public async Task ReadFullyAsync( + byte[] buffer, + CancellationToken cancellationToken = default + ) + { + var total = 0; + int read; + while ( + ( + read = await source + .ReadAsync(buffer, total, buffer.Length - total, cancellationToken) + .ConfigureAwait(false) + ) > 0 + ) + { + total += read; + if (total >= buffer.Length) + { + return true; + } + } + return (total >= buffer.Length); + } + + public async Task ReadFullyAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var total = 0; + int read; + while ( + ( + read = await source + .ReadAsync(buffer, offset + total, count - total, cancellationToken) + .ConfigureAwait(false) + ) > 0 + ) + { + total += read; + if (total >= count) + { + return true; + } + } + return (total >= count); } } @@ -350,89 +382,9 @@ internal static class Utility } } #else - public static bool ReadFully(this Stream stream, byte[] buffer) - { - var total = 0; - int read; - while ((read = stream.Read(buffer, total, buffer.Length - total)) > 0) - { - total += read; - if (total >= buffer.Length) - { - return true; - } - } - return (total >= buffer.Length); - } - public static bool ReadFully(this Stream stream, Span buffer) - { - var total = 0; - int read; - while ((read = stream.Read(buffer.Slice(total, buffer.Length - total))) > 0) - { - total += read; - if (total >= buffer.Length) - { - return true; - } - } - return (total >= buffer.Length); - } #endif - public static async Task ReadFullyAsync( - this Stream stream, - byte[] buffer, - CancellationToken cancellationToken = default - ) - { - var total = 0; - int read; - while ( - ( - read = await stream - .ReadAsync(buffer, total, buffer.Length - total, cancellationToken) - .ConfigureAwait(false) - ) > 0 - ) - { - total += read; - if (total >= buffer.Length) - { - return true; - } - } - return (total >= buffer.Length); - } - - public static async Task ReadFullyAsync( - this Stream stream, - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) - { - var total = 0; - int read; - while ( - ( - read = await stream - .ReadAsync(buffer, offset + total, count - total, cancellationToken) - .ConfigureAwait(false) - ) > 0 - ) - { - total += read; - if (total >= count) - { - return true; - } - } - return (total >= count); - } - public static string TrimNulls(this string source) => source.Replace('\0', ' ').Trim(); /// diff --git a/tests/SharpCompress.Test/UtilityTests.cs b/tests/SharpCompress.Test/UtilityTests.cs index cbd57304..d533e85c 100644 --- a/tests/SharpCompress.Test/UtilityTests.cs +++ b/tests/SharpCompress.Test/UtilityTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using Xunit; namespace SharpCompress.Test; @@ -157,6 +158,97 @@ public class UtilityTests #endregion + #region ReadByteAsync Tests + + [Fact] + public async Task ReadByteAsync_ReadsOneByte() + { + var data = new byte[] { 42, 1, 2, 3 }; + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + var result = await reader.ReadByteAsync(); + + Assert.Equal(42, result); + Assert.Equal(1, stream.Position); + } + + [Fact] + public async Task ReadByteAsync_EmptyStream_ThrowsEndOfStreamException() + { + using var stream = new MemoryStream(); + using var reader = new BinaryReader(stream); + + await Assert.ThrowsAsync(async () => await reader.ReadByteAsync()); + } + + [Fact] + public async Task ReadByteAsync_MultipleReads_ReadsSequentially() + { + var data = new byte[] { 1, 2, 3 }; + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + var first = await reader.ReadByteAsync(); + var second = await reader.ReadByteAsync(); + var third = await reader.ReadByteAsync(); + + Assert.Equal(1, first); + Assert.Equal(2, second); + Assert.Equal(3, third); + } + + #endregion + + #region ReadBytesAsync Tests + + [Fact] + public async Task ReadBytesAsync_ReadsExactlyRequiredBytes() + { + var data = new byte[] { 1, 2, 3, 4, 5 }; + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + var result = await reader.ReadBytesAsync(3); + + Assert.Equal(new byte[] { 1, 2, 3 }, result); + Assert.Equal(3, stream.Position); + } + + [Fact] + public async Task ReadBytesAsync_NotEnoughData_ThrowsEndOfStreamException() + { + var data = new byte[] { 1, 2, 3 }; + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + await Assert.ThrowsAsync(async () => await reader.ReadBytesAsync(5)); + } + + [Fact] + public async Task ReadBytesAsync_EmptyStream_ThrowsEndOfStreamException() + { + using var stream = new MemoryStream(); + using var reader = new BinaryReader(stream); + + await Assert.ThrowsAsync(async () => await reader.ReadBytesAsync(1)); + } + + [Fact] + public async Task ReadBytesAsync_ZeroBytes_ReturnsEmptyArray() + { + var data = new byte[] { 1, 2, 3 }; + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + var result = await reader.ReadBytesAsync(0); + + Assert.Empty(result); + Assert.Equal(0, stream.Position); + } + + #endregion + #region Skip Tests [Fact] From 1a71c01fd4734741997cc6ae18ec6bd9fe45914b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:58:27 +0000 Subject: [PATCH 13/46] Consolidate ReadExact and ReadFully methods into Utility.cs Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Common/AsyncBinaryReader.cs | 32 +---- .../Compressors/LZMA/Utilites/Utils.cs | 35 ------ .../Polyfills/BinaryReaderExtensions.cs | 45 ++----- .../Polyfills/StreamExtensions.cs | 19 +-- src/SharpCompress/Utility.cs | 115 +++++++++++++++--- 5 files changed, 117 insertions(+), 129 deletions(-) diff --git a/src/SharpCompress/Common/AsyncBinaryReader.cs b/src/SharpCompress/Common/AsyncBinaryReader.cs index 2a6eb92c..51da5d5c 100644 --- a/src/SharpCompress/Common/AsyncBinaryReader.cs +++ b/src/SharpCompress/Common/AsyncBinaryReader.cs @@ -29,57 +29,35 @@ namespace SharpCompress.Common public async ValueTask ReadByteAsync(CancellationToken ct = default) { - await ReadExactAsync(_buffer, 0, 1, ct).ConfigureAwait(false); + await _stream.ReadExactAsync(_buffer, 0, 1, ct).ConfigureAwait(false); return _buffer[0]; } public async ValueTask ReadUInt16Async(CancellationToken ct = default) { - await ReadExactAsync(_buffer, 0, 2, ct).ConfigureAwait(false); + await _stream.ReadExactAsync(_buffer, 0, 2, ct).ConfigureAwait(false); return BinaryPrimitives.ReadUInt16LittleEndian(_buffer); } public async ValueTask ReadUInt32Async(CancellationToken ct = default) { - await ReadExactAsync(_buffer, 0, 4, ct).ConfigureAwait(false); + await _stream.ReadExactAsync(_buffer, 0, 4, ct).ConfigureAwait(false); return BinaryPrimitives.ReadUInt32LittleEndian(_buffer); } public async ValueTask ReadUInt64Async(CancellationToken ct = default) { - await ReadExactAsync(_buffer, 0, 8, ct).ConfigureAwait(false); + await _stream.ReadExactAsync(_buffer, 0, 8, ct).ConfigureAwait(false); return BinaryPrimitives.ReadUInt64LittleEndian(_buffer); } public async ValueTask ReadBytesAsync(int count, CancellationToken ct = default) { var result = new byte[count]; - await ReadExactAsync(result, 0, count, ct).ConfigureAwait(false); + await _stream.ReadExactAsync(result, 0, count, ct).ConfigureAwait(false); return result; } - private async ValueTask ReadExactAsync( - byte[] destination, - int offset, - int length, - CancellationToken ct - ) - { - var read = 0; - while (read < length) - { - var n = await _stream - .ReadAsync(destination, offset + read, length - read, ct) - .ConfigureAwait(false); - if (n == 0) - { - throw new EndOfStreamException(); - } - - read += n; - } - } - public void Dispose() { if (_disposed) diff --git a/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs b/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs index 19b0f374..b57cd53f 100644 --- a/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs +++ b/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs @@ -53,39 +53,4 @@ internal static class Utils throw new InvalidOperationException("Assertion failed."); } } - - public static void ReadExact(this Stream stream, byte[] buffer, int offset, int length) - { - if (stream is null) - { - throw new ArgumentNullException(nameof(stream)); - } - - if (buffer is null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (length < 0 || length > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - while (length > 0) - { - var fetched = stream.Read(buffer, offset, length); - if (fetched <= 0) - { - throw new EndOfStreamException(); - } - - offset += fetched; - length -= fetched; - } - } } diff --git a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs index a030d4bc..d34771cf 100644 --- a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -11,23 +11,11 @@ public static class BinaryReaderExtensions { public async Task ReadByteAsync(CancellationToken cancellationToken = default) { - var buffer = ArrayPool.Shared.Rent(1); - try - { - var bytesRead = await reader - .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - if (bytesRead != 1) - { - throw new EndOfStreamException(); - } - - return buffer[0]; - } - finally - { - ArrayPool.Shared.Return(buffer); - } + var buffer = new byte[1]; + await reader + .BaseStream.ReadExactAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + return buffer[0]; } public async Task ReadBytesAsync( @@ -35,24 +23,11 @@ public static class BinaryReaderExtensions CancellationToken cancellationToken = default ) { - var buffer = ArrayPool.Shared.Rent(count); - try - { - var bytesRead = await reader - .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - if (bytesRead != count) - { - throw new EndOfStreamException(); - } - var bytes = new byte[count]; - System.Buffer.BlockCopy(buffer, 0, bytes, 0, count); - return bytes; - } - finally - { - ArrayPool.Shared.Return(buffer); - } + var bytes = new byte[count]; + await reader + .BaseStream.ReadExactAsync(bytes, 0, count, cancellationToken) + .ConfigureAwait(false); + return bytes; } } } diff --git a/src/SharpCompress/Polyfills/StreamExtensions.cs b/src/SharpCompress/Polyfills/StreamExtensions.cs index bd41ac75..ab617e95 100644 --- a/src/SharpCompress/Polyfills/StreamExtensions.cs +++ b/src/SharpCompress/Polyfills/StreamExtensions.cs @@ -98,20 +98,9 @@ public static class StreamExtensions int offset, int count, CancellationToken cancellationToken - ) - { - var totalRead = 0; - while (totalRead < count) - { - var read = await stream - .ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken) - .ConfigureAwait(false); - if (read == 0) - { - throw new EndOfStreamException(); - } - totalRead += read; - } - } + ) => + await stream + .ReadExactAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); } } diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index ea0faa2a..4db9d340 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -273,6 +273,33 @@ internal static class Utility } } +#if NET60_OR_GREATER + public bool ReadFully(byte[] buffer) + { + try + { + source.ReadExactly(buffer); + return true; + } + catch (EndOfStreamException) + { + return false; + } + } + + public bool ReadFully(Span buffer) + { + try + { + source.ReadExactly(buffer); + return true; + } + catch (EndOfStreamException) + { + return false; + } + } +#else public bool ReadFully(byte[] buffer) { var total = 0; @@ -302,6 +329,7 @@ internal static class Utility } return (total >= buffer.Length); } +#endif public async Task ReadFullyAsync( byte[] buffer, @@ -354,36 +382,89 @@ internal static class Utility } } -#if NET60_OR_GREATER - - public static bool ReadFully(this Stream stream, byte[] buffer) + /// + /// Read exactly the requested number of bytes from a stream. Throws EndOfStreamException if not enough data is available. + /// + public static void ReadExact(this Stream stream, byte[] buffer, int offset, int length) { - try + if (stream is null) { - stream.ReadExactly(buffer); - return true; + throw new ArgumentNullException(nameof(stream)); } - catch (EndOfStreamException) + + if (buffer is null) { - return false; + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (length < 0 || length > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + while (length > 0) + { + var fetched = stream.Read(buffer, offset, length); + if (fetched <= 0) + { + throw new EndOfStreamException(); + } + + offset += fetched; + length -= fetched; } } - public static bool ReadFully(this Stream stream, Span buffer) + /// + /// Read exactly the requested number of bytes from a stream asynchronously. Throws EndOfStreamException if not enough data is available. + /// + public static async Task ReadExactAsync( + this Stream stream, + byte[] buffer, + int offset, + int length, + CancellationToken cancellationToken = default + ) { - try + if (stream is null) { - stream.ReadExactly(buffer); - return true; + throw new ArgumentNullException(nameof(stream)); } - catch (EndOfStreamException) + + if (buffer is null) { - return false; + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (length < 0 || length > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + while (length > 0) + { + var fetched = await stream + .ReadAsync(buffer, offset, length, cancellationToken) + .ConfigureAwait(false); + if (fetched <= 0) + { + throw new EndOfStreamException(); + } + + offset += fetched; + length -= fetched; } } -#else - -#endif public static string TrimNulls(this string source) => source.Replace('\0', ' ').Trim(); From 05642cbdc6dbce6983fdf32b3329f3db1e296024 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 17:12:43 +0000 Subject: [PATCH 14/46] Use ArrayPool for temporary buffers in BinaryReaderExtensions Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Polyfills/BinaryReaderExtensions.cs | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs index d34771cf..dbf17c25 100644 --- a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -11,11 +11,18 @@ public static class BinaryReaderExtensions { public async Task ReadByteAsync(CancellationToken cancellationToken = default) { - var buffer = new byte[1]; - await reader - .BaseStream.ReadExactAsync(buffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - return buffer[0]; + var buffer = ArrayPool.Shared.Rent(1); + try + { + await reader + .BaseStream.ReadExactAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + return buffer[0]; + } + finally + { + ArrayPool.Shared.Return(buffer); + } } public async Task ReadBytesAsync( @@ -23,11 +30,20 @@ public static class BinaryReaderExtensions CancellationToken cancellationToken = default ) { - var bytes = new byte[count]; - await reader - .BaseStream.ReadExactAsync(bytes, 0, count, cancellationToken) - .ConfigureAwait(false); - return bytes; + var buffer = ArrayPool.Shared.Rent(count); + try + { + await reader + .BaseStream.ReadExactAsync(buffer, 0, count, cancellationToken) + .ConfigureAwait(false); + var bytes = new byte[count]; + System.Array.Copy(buffer, 0, bytes, 0, count); + return bytes; + } + finally + { + ArrayPool.Shared.Return(buffer); + } } } } From 372ecb77d0a92e599a8735eee5fa6d3ec1d4f876 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 17:17:59 +0000 Subject: [PATCH 15/46] Use threshold-based ArrayPool strategy for BinaryReaderExtensions Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Polyfills/BinaryReaderExtensions.cs | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs index dbf17c25..d3c517a7 100644 --- a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Buffers; using System.IO; using System.Threading; @@ -11,18 +12,11 @@ public static class BinaryReaderExtensions { public async Task ReadByteAsync(CancellationToken cancellationToken = default) { - var buffer = ArrayPool.Shared.Rent(1); - try - { - await reader - .BaseStream.ReadExactAsync(buffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - return buffer[0]; - } - finally - { - ArrayPool.Shared.Return(buffer); - } + var buffer = new byte[1]; + await reader + .BaseStream.ReadExactAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + return buffer[0]; } public async Task ReadBytesAsync( @@ -30,6 +24,18 @@ public static class BinaryReaderExtensions CancellationToken cancellationToken = default ) { + // For small allocations, direct allocation is more efficient than pooling + // due to ArrayPool overhead and the need to copy data to return array + if (count <= 256) + { + var bytes = new byte[count]; + await reader + .BaseStream.ReadExactAsync(bytes, 0, count, cancellationToken) + .ConfigureAwait(false); + return bytes; + } + + // For larger allocations, use ArrayPool to reduce GC pressure var buffer = ArrayPool.Shared.Rent(count); try { @@ -37,7 +43,7 @@ public static class BinaryReaderExtensions .BaseStream.ReadExactAsync(buffer, 0, count, cancellationToken) .ConfigureAwait(false); var bytes = new byte[count]; - System.Array.Copy(buffer, 0, bytes, 0, count); + Array.Copy(buffer, 0, bytes, 0, count); return bytes; } finally From 77015224f6be8a6524c604bb8810953fa14780ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 17:19:55 +0000 Subject: [PATCH 16/46] Add input validation for ReadBytesAsync count parameter Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Polyfills/BinaryReaderExtensions.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs index d3c517a7..da4b3eea 100644 --- a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -24,6 +24,16 @@ public static class BinaryReaderExtensions CancellationToken cancellationToken = default ) { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Count must be non-negative."); + } + + if (count == 0) + { + return Array.Empty(); + } + // For small allocations, direct allocation is more efficient than pooling // due to ArrayPool overhead and the need to copy data to return array if (count <= 256) From 9bd86f64c927e474c29b70185c8b80af667f62a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 18:41:02 +0000 Subject: [PATCH 17/46] Replace manual TransferTo implementation with Stream.CopyTo framework methods Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Utility.cs | 104 ++++------------------------------- 1 file changed, 10 insertions(+), 94 deletions(-) diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index 4db9d340..c8c7a73c 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -133,36 +133,10 @@ internal static class Utility { public long TransferTo(Stream destination, long maxLength) { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try - { - var maxReadSize = array.Length; - long total = 0; - var remaining = maxLength; - if (remaining < maxReadSize) - { - maxReadSize = (int)remaining; - } - while (ReadTransferBlock(source, array, maxReadSize, out var count)) - { - destination.Write(array, 0, count); - total += count; - if (remaining - count < 0) - { - break; - } - remaining -= count; - if (remaining < maxReadSize) - { - maxReadSize = (int)remaining; - } - } - return total; - } - finally - { - ArrayPool.Shared.Return(array); - } + // Use ReadOnlySubStream to limit reading and leverage framework's CopyTo + using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); + limitedStream.CopyTo(destination, TEMP_BUFFER_SIZE); + return limitedStream.Position; } public async Task TransferToAsync( @@ -171,73 +145,15 @@ internal static class Utility CancellationToken cancellationToken = default ) { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try - { - var maxReadSize = array.Length; - long total = 0; - var remaining = maxLength; - if (remaining < maxReadSize) - { - maxReadSize = (int)remaining; - } - while ( - await ReadTransferBlockAsync(source, array, maxReadSize, cancellationToken) - .ConfigureAwait(false) - is var (success, count) - && success - ) - { - await destination - .WriteAsync(array, 0, count, cancellationToken) - .ConfigureAwait(false); - total += count; - if (remaining - count < 0) - { - break; - } - remaining -= count; - if (remaining < maxReadSize) - { - maxReadSize = (int)remaining; - } - } - return total; - } - finally - { - ArrayPool.Shared.Return(array); - } + // Use ReadOnlySubStream to limit reading and leverage framework's CopyToAsync + using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); + await limitedStream + .CopyToAsync(destination, TEMP_BUFFER_SIZE, cancellationToken) + .ConfigureAwait(false); + return limitedStream.Position; } } - private static bool ReadTransferBlock(Stream source, byte[] array, int maxSize, out int count) - { - var size = maxSize; - if (maxSize > array.Length) - { - size = array.Length; - } - count = source.Read(array, 0, size); - return count != 0; - } - - private static async Task<(bool success, int count)> ReadTransferBlockAsync( - Stream source, - byte[] array, - int maxSize, - CancellationToken cancellationToken - ) - { - var size = maxSize; - if (maxSize > array.Length) - { - size = array.Length; - } - var count = await source.ReadAsync(array, 0, size, cancellationToken).ConfigureAwait(false); - return (count != 0, count); - } - extension(Stream source) { public async Task SkipAsync( From 39d85ff4f602ac292a9a72bae1936411c0d4895c Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 7 Jan 2026 12:18:14 +0000 Subject: [PATCH 18/46] conflicts from merge --- src/SharpCompress/Factories/AceFactory.cs | 7 ++++ .../Polyfills/StreamExtensions.cs | 41 +++---------------- 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/src/SharpCompress/Factories/AceFactory.cs b/src/SharpCompress/Factories/AceFactory.cs index 5b80ae24..fe8d8f9c 100644 --- a/src/SharpCompress/Factories/AceFactory.cs +++ b/src/SharpCompress/Factories/AceFactory.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.Common; using SharpCompress.Common.Ace.Headers; @@ -33,5 +34,11 @@ namespace SharpCompress.Factories public IReader OpenReader(Stream stream, ReaderOptions? options) => AceReader.Open(stream, options); + + public Task OpenReaderAsync( + Stream stream, + ReaderOptions? options, + CancellationToken cancellationToken = default + ) => Task.FromResult(OpenReader(stream, options)); } } diff --git a/src/SharpCompress/Polyfills/StreamExtensions.cs b/src/SharpCompress/Polyfills/StreamExtensions.cs index ab617e95..d9e6a3ea 100644 --- a/src/SharpCompress/Polyfills/StreamExtensions.cs +++ b/src/SharpCompress/Polyfills/StreamExtensions.cs @@ -18,45 +18,16 @@ public static class StreamExtensions return; } - using var buffer = MemoryPool.Shared.Rent(Utility.TEMP_BUFFER_SIZE); - while (advanceAmount > 0) - { - var toRead = (int)Math.Min(buffer.Memory.Length, advanceAmount); - var read = stream.Read(buffer.Memory.Slice(0, toRead).Span); - if (read <= 0) - { - break; - } - advanceAmount -= read; - } + using var readOnlySubStream = new IO.ReadOnlySubStream(stream, advanceAmount); + readOnlySubStream.CopyTo(Stream.Null); } - public void Skip() - { - using var buffer = MemoryPool.Shared.Rent(Utility.TEMP_BUFFER_SIZE); - while (stream.Read(buffer.Memory.Span) > 0) { } - } + public void Skip() => stream.CopyTo(Stream.Null); - public async Task SkipAsync(CancellationToken cancellationToken = default) + public Task SkipAsync(CancellationToken cancellationToken = default) { - var array = ArrayPool.Shared.Rent(Utility.TEMP_BUFFER_SIZE); - try - { - while (true) - { - var read = await stream - .ReadAsync(array, 0, array.Length, cancellationToken) - .ConfigureAwait(false); - if (read <= 0) - { - break; - } - } - } - finally - { - ArrayPool.Shared.Return(array); - } + cancellationToken.ThrowIfCancellationRequested(); + return stream.CopyToAsync(Stream.Null); } internal int Read(Span buffer) From c3fd42057a26bc0d37c4b2434e9332bd9dfd8473 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 7 Jan 2026 14:47:20 +0000 Subject: [PATCH 19/46] Pass more Zip tests --- src/SharpCompress/Archives/Zip/ZipArchive.cs | 7 +- .../Common/Zip/StreamingZipHeaderFactory.cs | 330 ++++++++++++++++++ .../Common/Zip/ZipHeaderFactory.cs | 81 ++++- .../Polyfills/AsyncEnumerableExtensions.cs | 32 ++ src/SharpCompress/Readers/AbstractReader.cs | 90 ++++- src/SharpCompress/Readers/Zip/ZipReader.cs | 101 ++++++ 6 files changed, 632 insertions(+), 9 deletions(-) create mode 100644 src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 1a87df5e..7ce8a116 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -299,9 +299,10 @@ public class ZipArchive : AbstractWritableArchive stream = new SharpCompressStream(stream, bufferSize: bufferSize); } - var header = headerFactory - .ReadStreamHeader(stream) - .FirstOrDefault(x => x.ZipHeaderType != ZipHeaderType.Split); + var header = await headerFactory + .ReadStreamHeaderAsync(stream) + .Where(x => x.ZipHeaderType != ZipHeaderType.Split) + .FirstOrDefaultAsync(); if (header is null) { return false; diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs index 031287ed..eafab136 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs @@ -2,6 +2,9 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; using SharpCompress.Common.Zip.Headers; using SharpCompress.IO; @@ -200,4 +203,331 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory yield return header; } } + + /// + /// Reads ZIP headers asynchronously for streams that do not support synchronous reads. + /// + internal IAsyncEnumerable ReadStreamHeaderAsync(Stream stream) => + new StreamHeaderAsyncEnumerable(this, stream); + + /// + /// Invokes the shared async header parsing logic on the base factory. + /// + private ValueTask ReadHeaderAsyncInternal( + uint headerBytes, + AsyncBinaryReader reader + ) => ReadHeader(headerBytes, reader); + + /// + /// Exposes the last parsed local entry header to the async enumerator so it can handle streaming data descriptors. + /// + private LocalEntryHeader? LastEntryHeader + { + get => _lastEntryHeader; + set => _lastEntryHeader = value; + } + + /// + /// Produces an async enumerator for streaming ZIP headers. + /// + private sealed class StreamHeaderAsyncEnumerable : IAsyncEnumerable + { + private readonly StreamingZipHeaderFactory _headerFactory; + private readonly Stream _stream; + + public StreamHeaderAsyncEnumerable(StreamingZipHeaderFactory headerFactory, Stream stream) + { + _headerFactory = headerFactory; + _stream = stream; + } + + public IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default + ) => new StreamHeaderAsyncEnumerator(_headerFactory, _stream, cancellationToken); + } + + /// + /// Async implementation of using to avoid sync reads. + /// + private sealed class StreamHeaderAsyncEnumerator : IAsyncEnumerator, IDisposable + { + private readonly StreamingZipHeaderFactory _headerFactory; + private readonly SharpCompressStream _rewindableStream; + private readonly AsyncBinaryReader _reader; + private readonly CancellationToken _cancellationToken; + private bool _completed; + + public StreamHeaderAsyncEnumerator( + StreamingZipHeaderFactory headerFactory, + Stream stream, + CancellationToken cancellationToken + ) + { + _headerFactory = headerFactory; + _rewindableStream = EnsureSharpCompressStream(stream); + _reader = new AsyncBinaryReader(_rewindableStream, leaveOpen: true); + _cancellationToken = cancellationToken; + } + + private ZipHeader? _current; + + public ZipHeader Current => + _current ?? throw new InvalidOperationException("No current header is available."); + + /// + /// Advances to the next ZIP header in the stream, honoring streaming data descriptors where applicable. + /// + public async ValueTask MoveNextAsync() + { + if (_completed) + { + return false; + } + + while (true) + { + _cancellationToken.ThrowIfCancellationRequested(); + + uint headerBytes; + var lastEntryHeader = _headerFactory.LastEntryHeader; + if ( + lastEntryHeader != null + && FlagUtility.HasFlag(lastEntryHeader.Flags, HeaderFlags.UsePostDataDescriptor) + ) + { + if (lastEntryHeader.Part is null) + { + continue; + } + + var pos = _rewindableStream.CanSeek ? (long?)_rewindableStream.Position : null; + + var crc = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + if (crc == POST_DATA_DESCRIPTOR) + { + crc = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + } + lastEntryHeader.Crc = crc; + + //attempt 32bit read + ulong compressedSize = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + ulong uncompressedSize = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + headerBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + + //check for zip64 sentinel or unexpected header + bool isSentinel = + compressedSize == 0xFFFFFFFF || uncompressedSize == 0xFFFFFFFF; + bool isHeader = headerBytes == 0x04034b50 || headerBytes == 0x02014b50; + + if (!isHeader && !isSentinel) + { + //reshuffle into 64-bit values + compressedSize = (uncompressedSize << 32) | compressedSize; + uncompressedSize = + ((ulong)headerBytes << 32) + | await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + headerBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + } + else if (isSentinel) + { + //standards-compliant zip64 descriptor + compressedSize = await _reader + .ReadUInt64Async(_cancellationToken) + .ConfigureAwait(false); + uncompressedSize = await _reader + .ReadUInt64Async(_cancellationToken) + .ConfigureAwait(false); + } + + lastEntryHeader.CompressedSize = (long)compressedSize; + lastEntryHeader.UncompressedSize = (long)uncompressedSize; + + if (pos.HasValue) + { + lastEntryHeader.DataStartPosition = pos - lastEntryHeader.CompressedSize; + } + } + else if (lastEntryHeader != null && lastEntryHeader.IsZip64) + { + if (lastEntryHeader.Part is null) + { + continue; + } + + var pos = _rewindableStream.CanSeek ? (long?)_rewindableStream.Position : null; + + headerBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + + _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // version + _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // flags + _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // compressionMethod + _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedDate + _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedTime + + var crc = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + + if (crc == POST_DATA_DESCRIPTOR) + { + crc = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + } + lastEntryHeader.Crc = crc; + + // The DataDescriptor can be either 64bit or 32bit + var compressedSize = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + var uncompressedSize = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + + // Check if we have header or 64bit DataDescriptor + var testHeader = !(headerBytes == 0x04034b50 || headerBytes == 0x02014b50); + + var test64Bit = ((long)uncompressedSize << 32) | compressedSize; + if (test64Bit == lastEntryHeader.CompressedSize && testHeader) + { + lastEntryHeader.UncompressedSize = + ( + (long) + await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false) << 32 + ) | headerBytes; + headerBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + } + else + { + lastEntryHeader.UncompressedSize = uncompressedSize; + } + + if (pos.HasValue) + { + lastEntryHeader.DataStartPosition = pos - lastEntryHeader.CompressedSize; + + // 4 = First 4 bytes of the entry header (i.e. 50 4B 03 04) + _rewindableStream.Position = pos.Value + 4; + } + } + else + { + headerBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + } + + _headerFactory.LastEntryHeader = null; + var header = await _headerFactory + .ReadHeaderAsyncInternal(headerBytes, _reader) + .ConfigureAwait(false); + if (header is null) + { + _completed = true; + return false; + } + + //entry could be zero bytes so we need to know that. + if (header.ZipHeaderType == ZipHeaderType.LocalEntry) + { + var localHeader = (LocalEntryHeader)header; + var directoryHeader = _headerFactory._entries?.FirstOrDefault(entry => + entry.Key == localHeader.Name + && localHeader.CompressedSize == 0 + && localHeader.UncompressedSize == 0 + && localHeader.Crc == 0 + && localHeader.IsDirectory == false + ); + + if (directoryHeader != null) + { + localHeader.UncompressedSize = directoryHeader.Size; + localHeader.CompressedSize = directoryHeader.CompressedSize; + localHeader.Crc = (uint)directoryHeader.Crc; + } + + // If we have CompressedSize, there is data to be read + if (localHeader.CompressedSize > 0) + { + header.HasData = true; + } // Check if zip is streaming ( Length is 0 and is declared in PostDataDescriptor ) + else if (localHeader.Flags.HasFlag(HeaderFlags.UsePostDataDescriptor)) + { + var nextHeaderBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + ((IStreamStack)_rewindableStream).Rewind(sizeof(uint)); + + // Check if next data is PostDataDescriptor, streamed file with 0 length + header.HasData = !IsHeader(nextHeaderBytes); + } + else // We are not streaming and compressed size is 0, we have no data + { + header.HasData = false; + } + } + + _current = header; + return true; + } + } + + public ValueTask DisposeAsync() + { + Dispose(); + return default; + } + + /// + /// Disposes the underlying reader (without closing the archive stream). + /// + public void Dispose() + { + _reader.Dispose(); + } + + /// + /// Ensures the stream is a so header parsing can use rewind/buffer helpers. + /// + private static SharpCompressStream EnsureSharpCompressStream(Stream stream) + { + if (stream is SharpCompressStream sharpCompressStream) + { + return sharpCompressStream; + } + + // Ensure the stream is already a SharpCompressStream so the buffer/size is set. + // The original code wrapped this with RewindableStream; use SharpCompressStream so we can get the buffer size. + if (stream is SourceStream src) + { + return new SharpCompressStream( + stream, + src.ReaderOptions.LeaveStreamOpen, + bufferSize: src.ReaderOptions.BufferSize + ); + } + + throw new ArgumentException("Stream must be a SharpCompressStream", nameof(stream)); + } + } } diff --git a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs index e085a0de..d25f1025 100644 --- a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Threading.Tasks; +using SharpCompress; using SharpCompress.Common.Zip.Headers; using SharpCompress.IO; @@ -47,7 +48,7 @@ internal class ZipHeaderFactory { var entryHeader = new LocalEntryHeader(_archiveEncoding); await entryHeader.Read(reader); - LoadHeader(entryHeader, reader.BaseStream); + await LoadHeaderAsync(entryHeader, reader.BaseStream).ConfigureAwait(false); _lastEntryHeader = entryHeader; return entryHeader; @@ -282,4 +283,82 @@ internal class ZipHeaderFactory //} } + + /// + /// Loads encryption metadata and stream positioning for a header using async reads where needed. + /// + private async ValueTask LoadHeaderAsync(ZipFileEntry entryHeader, Stream stream) + { + if (FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.Encrypted)) + { + if ( + !entryHeader.IsDirectory + && entryHeader.CompressedSize == 0 + && FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.UsePostDataDescriptor) + ) + { + throw new NotSupportedException( + "SharpCompress cannot currently read non-seekable Zip Streams with encrypted data that has been written in a non-seekable manner." + ); + } + + if (_password is null) + { + throw new CryptographicException("No password supplied for encrypted zip."); + } + + entryHeader.Password = _password; + + if (entryHeader.CompressionMethod == ZipCompressionMethod.WinzipAes) + { + var data = entryHeader.Extra.SingleOrDefault(x => + x.Type == ExtraDataType.WinZipAes + ); + if (data != null) + { + var keySize = (WinzipAesKeySize)data.DataBytes[4]; + + var salt = new byte[WinzipAesEncryptionData.KeyLengthInBytes(keySize) / 2]; + var passwordVerifyValue = new byte[2]; + await stream.ReadExactAsync(salt, 0, salt.Length).ConfigureAwait(false); + await stream.ReadExactAsync(passwordVerifyValue, 0, 2).ConfigureAwait(false); + + entryHeader.WinzipAesEncryptionData = new WinzipAesEncryptionData( + keySize, + salt, + passwordVerifyValue, + _password + ); + + entryHeader.CompressedSize -= (uint)(salt.Length + 2); + } + } + } + + if (entryHeader.IsDirectory) + { + return; + } + + switch (_mode) + { + case StreamingMode.Seekable: + { + entryHeader.DataStartPosition = stream.Position; + stream.Position += entryHeader.CompressedSize; + break; + } + + case StreamingMode.Streaming: + { + entryHeader.PackedStream = stream; + break; + } + + default: + { + throw new InvalidFormatException("Invalid StreamingMode"); + } + } + } } diff --git a/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs b/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs new file mode 100644 index 00000000..f1379cbb --- /dev/null +++ b/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SharpCompress; + +public static class AsyncEnumerableExtensions +{ + extension(IAsyncEnumerable source) + { + public async IAsyncEnumerable Where(Func predicate) + { + await foreach (var item in source) + { + if (predicate(item)) + { + yield return item; + } + } + } + + public async ValueTask FirstOrDefaultAsync() + { + await foreach (var item in source) + { + return item; // Returns the very first item found + } + + return default; // Returns null/default if the stream is empty + } + } +} diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index bd35a6e1..01340f24 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -18,6 +18,11 @@ public abstract class AbstractReader : IReader { private bool _completed; private IEnumerator? _entriesForCurrentReadStream; + + /// + /// Holds the async entry enumerator when the reader is operating in an async-only mode. + /// + private IAsyncEnumerator? _asyncEntriesForCurrentReadStream; private bool _wroteCurrentEntry; internal AbstractReader(ReaderOptions options, ArchiveType archiveType) @@ -36,15 +41,22 @@ public abstract class AbstractReader : IReader public abstract TVolume? Volume { get; } /// - /// Current file entry + /// Current file entry (from either sync or async enumeration). /// - public TEntry Entry => _entriesForCurrentReadStream.NotNull().Current; + public TEntry Entry => + _entriesForCurrentReadStream?.Current + ?? _asyncEntriesForCurrentReadStream?.Current + ?? throw new InvalidOperationException("No current entry is available."); #region IDisposable Members public virtual void Dispose() { _entriesForCurrentReadStream?.Dispose(); + if (_asyncEntriesForCurrentReadStream is IDisposable disposable) + { + disposable.Dispose(); + } Volume?.Dispose(); } @@ -67,6 +79,12 @@ public abstract class AbstractReader : IReader public bool MoveToNextEntry() { + if (_asyncEntriesForCurrentReadStream is not null) + { + throw new InvalidOperationException( + $"{nameof(MoveToNextEntry)} cannot be used after {nameof(MoveToNextEntryAsync)} has been used." + ); + } if (_completed) { return false; @@ -102,16 +120,17 @@ public abstract class AbstractReader : IReader { throw new ReaderCancelledException("Reader has been cancelled."); } - if (_entriesForCurrentReadStream is null) + if (_entriesForCurrentReadStream is null && _asyncEntriesForCurrentReadStream is null) { - return LoadStreamForReading(RequestInitialStream()); + return await LoadStreamForReadingAsync(RequestInitialStream(), cancellationToken) + .ConfigureAwait(false); } if (!_wroteCurrentEntry) { await SkipEntryAsync(cancellationToken).ConfigureAwait(false); } _wroteCurrentEntry = false; - if (NextEntryForCurrentStream()) + if (await NextEntryForCurrentStreamAsync(cancellationToken).ConfigureAwait(false)) { return true; } @@ -121,6 +140,12 @@ public abstract class AbstractReader : IReader protected bool LoadStreamForReading(Stream stream) { + if (_asyncEntriesForCurrentReadStream is not null) + { + throw new InvalidOperationException( + $"{nameof(LoadStreamForReading)} cannot be used after {nameof(LoadStreamForReadingAsync)} has been used." + ); + } _entriesForCurrentReadStream?.Dispose(); if (stream is null || !stream.CanRead) { @@ -134,14 +159,69 @@ public abstract class AbstractReader : IReader return _entriesForCurrentReadStream.MoveNext(); } + /// + /// Loads the stream for reading entries asynchronously, using an async entry enumerator when available. + /// + protected async Task LoadStreamForReadingAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + // Always reset the previous async enumerator so that a new stream can be loaded cleanly. + if (_asyncEntriesForCurrentReadStream is IDisposable disposable) + { + disposable.Dispose(); + } + _asyncEntriesForCurrentReadStream = null; + + if (stream is null || !stream.CanRead) + { + throw new MultipartStreamRequiredException( + "File is split into multiple archives: '" + + Entry.Key + + "'. A new readable stream is required. Use Cancel if it was intended." + ); + } + + var entriesAsync = GetEntriesAsync(stream); + if (entriesAsync is null) + { + _entriesForCurrentReadStream = GetEntries(stream).GetEnumerator(); + return _entriesForCurrentReadStream.MoveNext(); + } + + _asyncEntriesForCurrentReadStream = entriesAsync.GetAsyncEnumerator(cancellationToken); + return await _asyncEntriesForCurrentReadStream.MoveNextAsync().ConfigureAwait(false); + } + protected virtual Stream RequestInitialStream() => Volume.NotNull("Volume isn't loaded.").Stream; internal virtual bool NextEntryForCurrentStream() => _entriesForCurrentReadStream.NotNull().MoveNext(); + /// + /// Moves the current async enumerator to the next entry. + /// + internal virtual ValueTask NextEntryForCurrentStreamAsync( + CancellationToken cancellationToken = default + ) + { + if (_asyncEntriesForCurrentReadStream is not null) + { + return _asyncEntriesForCurrentReadStream.MoveNextAsync(); + } + + return new ValueTask(NextEntryForCurrentStream()); + } + protected abstract IEnumerable GetEntries(Stream stream); + /// + /// Optionally returns an async entry sequence for formats that support true async header parsing. + /// + protected virtual IAsyncEnumerable? GetEntriesAsync(Stream stream) => null; + #region Entry Skip/Write private void SkipEntry() diff --git a/src/SharpCompress/Readers/Zip/ZipReader.cs b/src/SharpCompress/Readers/Zip/ZipReader.cs index 3a257845..673d6ec7 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.cs @@ -1,5 +1,8 @@ +using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Zip; using SharpCompress.Common.Zip.Headers; @@ -91,4 +94,102 @@ public class ZipReader : AbstractReader } } } + + /// + /// Returns entries asynchronously for streams that only support async reads. + /// + protected override IAsyncEnumerable? GetEntriesAsync(Stream stream) => + new ZipEntryAsyncEnumerable(_headerFactory, stream); + + /// + /// Adapts an async header sequence into an async entry sequence. + /// + private sealed class ZipEntryAsyncEnumerable : IAsyncEnumerable + { + private readonly StreamingZipHeaderFactory _headerFactory; + private readonly Stream _stream; + + public ZipEntryAsyncEnumerable(StreamingZipHeaderFactory headerFactory, Stream stream) + { + _headerFactory = headerFactory; + _stream = stream; + } + + public IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default + ) => new ZipEntryAsyncEnumerator(_headerFactory, _stream, cancellationToken); + } + + /// + /// Yields entries from streaming ZIP headers without requiring synchronous stream reads. + /// + private sealed class ZipEntryAsyncEnumerator : IAsyncEnumerator, IDisposable + { + private readonly Stream _stream; + private readonly IAsyncEnumerator _headerEnumerator; + private ZipEntry? _current; + + public ZipEntryAsyncEnumerator( + StreamingZipHeaderFactory headerFactory, + Stream stream, + CancellationToken cancellationToken + ) + { + _stream = stream; + _headerEnumerator = headerFactory + .ReadStreamHeaderAsync(stream) + .GetAsyncEnumerator(cancellationToken); + } + + public ZipEntry Current => + _current ?? throw new InvalidOperationException("No current entry is available."); + + /// + /// Advances to the next non-directory entry-relevant header and materializes a . + /// + public async ValueTask MoveNextAsync() + { + while (await _headerEnumerator.MoveNextAsync().ConfigureAwait(false)) + { + var header = _headerEnumerator.Current; + switch (header.ZipHeaderType) + { + case ZipHeaderType.LocalEntry: + _current = new ZipEntry( + new StreamingZipFilePart((LocalEntryHeader)header, _stream) + ); + return true; + case ZipHeaderType.DirectoryEntry: + // DirectoryEntry headers are intentionally skipped in streaming mode. + break; + case ZipHeaderType.DirectoryEnd: + _current = null; + return false; + } + } + + _current = null; + return false; + } + + /// + /// Disposes the underlying header enumerator. + /// + public ValueTask DisposeAsync() + { + Dispose(); + return default; + } + + /// + /// Disposes the underlying header enumerator. + /// + public void Dispose() + { + if (_headerEnumerator is IDisposable disposable) + { + disposable.Dispose(); + } + } + } } From b9792ca491e900ec3f90b0e9f2fa3a79f7d74f35 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 7 Jan 2026 14:54:32 +0000 Subject: [PATCH 20/46] fix async zip decompression --- src/SharpCompress/Common/Zip/ZipFilePart.cs | 26 ++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.cs b/src/SharpCompress/Common/Zip/ZipFilePart.cs index ca3881ae..b800d77c 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.cs @@ -470,11 +470,35 @@ internal abstract class ZipFilePart : FilePart { throw new InvalidFormatException("No Winzip AES extra data found."); } + if (data.Length != 7) { throw new InvalidFormatException("Winzip data length is not 7."); } - throw new NotSupportedException("WinzipAes isn't supported for streaming"); + + var compressedMethod = BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes); + + if (compressedMethod != 0x01 && compressedMethod != 0x02) + { + throw new InvalidFormatException( + "Unexpected vendor version number for WinZip AES metadata" + ); + } + + var vendorId = BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(2)); + if (vendorId != 0x4541) + { + throw new InvalidFormatException( + "Unexpected vendor ID for WinZip AES metadata" + ); + } + + return await CreateDecompressionStreamAsync( + stream, + (ZipCompressionMethod) + BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(5)), + cancellationToken + ); } default: { From ac0716ddeb606c38c7de9eb950556204bf64e78a Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 7 Jan 2026 15:01:04 +0000 Subject: [PATCH 21/46] write testing --- src/SharpCompress/Archives/ArchiveFactory.cs | 44 +++++++++++++++++++- tests/SharpCompress.Test/ArchiveTests.cs | 3 +- tests/SharpCompress.Test/WriterTests.cs | 3 +- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 2a901900..06bc7434 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -41,7 +41,7 @@ public static class ArchiveFactory { readerOptions ??= new ReaderOptions(); stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize); - var factory = FindFactory(stream); + var factory = await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); return await factory .OpenAsync(stream, readerOptions, cancellationToken) .ConfigureAwait(false); @@ -114,7 +114,7 @@ public static class ArchiveFactory { options ??= new ReaderOptions { LeaveStreamOpen = false }; - var factory = FindFactory(fileInfo); + var factory = await FindFactoryAsync(fileInfo, cancellationToken).ConfigureAwait(false); return await factory.OpenAsync(fileInfo, options, cancellationToken).ConfigureAwait(false); } @@ -292,6 +292,46 @@ public static class ArchiveFactory ); } + private static ValueTask FindFactoryAsync(FileInfo finfo, CancellationToken cancellationToken ) + where T : IFactory + { + finfo.NotNull(nameof(finfo)); + using Stream stream = finfo.OpenRead(); + return FindFactoryAsync(stream, cancellationToken); + } + + private static async ValueTask FindFactoryAsync(Stream stream, CancellationToken cancellationToken ) + where T : IFactory + { + stream.NotNull(nameof(stream)); + if (!stream.CanRead || !stream.CanSeek) + { + throw new ArgumentException("Stream should be readable and seekable"); + } + + var factories = Factory.Factories.OfType(); + + var startPosition = stream.Position; + + foreach (var factory in factories) + { + stream.Seek(startPosition, SeekOrigin.Begin); + + if (await factory.IsArchiveAsync(stream, cancellationToken: cancellationToken)) + { + stream.Seek(startPosition, SeekOrigin.Begin); + + return factory; + } + } + + var extensions = string.Join(", ", factories.Select(item => item.Name)); + + throw new InvalidOperationException( + $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" + ); + } + public static bool IsArchive( string filePath, out ArchiveType? type, diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 916c9e2c..44332eef 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -9,6 +9,7 @@ using SharpCompress.Compressors.Xz; using SharpCompress.Crypto; using SharpCompress.IO; using SharpCompress.Readers; +using SharpCompress.Test.Mocks; using SharpCompress.Writers; using SharpCompress.Writers.Zip; using Xunit; @@ -599,7 +600,7 @@ public class ArchiveTests : ReaderTests throwOnDispose: true ) ) - using (var archive = archiveFactory.Open(stream, readerOptions)) + using (var archive = await archiveFactory.OpenAsync(new AsyncOnlyStream(stream), readerOptions)) { try { diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index 1d2d8a13..b09cfee4 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; using SharpCompress.Readers; +using SharpCompress.Test.Mocks; using SharpCompress.Writers; namespace SharpCompress.Test; @@ -62,7 +63,7 @@ public class WriterTests : TestBase CancellationToken cancellationToken = default ) { - using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive))) + using (Stream stream = new AsyncOnlyStream(File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive)))) { var writerOptions = new WriterOptions(compressionType) { LeaveStreamOpen = true }; From 60d42ca9c3b7581d33050f217e85ecbf4ec5d621 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 7 Jan 2026 16:38:48 +0000 Subject: [PATCH 22/46] fmt --- src/SharpCompress/Archives/ArchiveFactory.cs | 16 ++++++++++++---- tests/SharpCompress.Test/ArchiveTests.cs | 7 ++++++- tests/SharpCompress.Test/WriterTests.cs | 6 +++++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 06bc7434..82af74ef 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -41,7 +41,8 @@ public static class ArchiveFactory { readerOptions ??= new ReaderOptions(); stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize); - var factory = await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + var factory = await FindFactoryAsync(stream, cancellationToken) + .ConfigureAwait(false); return await factory .OpenAsync(stream, readerOptions, cancellationToken) .ConfigureAwait(false); @@ -114,7 +115,8 @@ public static class ArchiveFactory { options ??= new ReaderOptions { LeaveStreamOpen = false }; - var factory = await FindFactoryAsync(fileInfo, cancellationToken).ConfigureAwait(false); + var factory = await FindFactoryAsync(fileInfo, cancellationToken) + .ConfigureAwait(false); return await factory.OpenAsync(fileInfo, options, cancellationToken).ConfigureAwait(false); } @@ -292,7 +294,10 @@ public static class ArchiveFactory ); } - private static ValueTask FindFactoryAsync(FileInfo finfo, CancellationToken cancellationToken ) + private static ValueTask FindFactoryAsync( + FileInfo finfo, + CancellationToken cancellationToken + ) where T : IFactory { finfo.NotNull(nameof(finfo)); @@ -300,7 +305,10 @@ public static class ArchiveFactory return FindFactoryAsync(stream, cancellationToken); } - private static async ValueTask FindFactoryAsync(Stream stream, CancellationToken cancellationToken ) + private static async ValueTask FindFactoryAsync( + Stream stream, + CancellationToken cancellationToken + ) where T : IFactory { stream.NotNull(nameof(stream)); diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 44332eef..2eea8838 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -600,7 +600,12 @@ public class ArchiveTests : ReaderTests throwOnDispose: true ) ) - using (var archive = await archiveFactory.OpenAsync(new AsyncOnlyStream(stream), readerOptions)) + using ( + var archive = await archiveFactory.OpenAsync( + new AsyncOnlyStream(stream), + readerOptions + ) + ) { try { diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index b09cfee4..410e69f7 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -63,7 +63,11 @@ public class WriterTests : TestBase CancellationToken cancellationToken = default ) { - using (Stream stream = new AsyncOnlyStream(File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive)))) + using ( + Stream stream = new AsyncOnlyStream( + File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive)) + ) + ) { var writerOptions = new WriterOptions(compressionType) { LeaveStreamOpen = true }; From 541fd136d5f9d9ca32c1251573237f453eeb236d Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 09:14:46 +0000 Subject: [PATCH 23/46] IArchiveAsync --- src/SharpCompress/Archives/AbstractArchive.cs | 85 +++++++++++++--- src/SharpCompress/Archives/IArchive.cs | 38 ++++++++ .../LazyAsyncReadOnlyCollection.cs | 96 +++++++++++++++++++ .../Polyfills/AsyncEnumerableExtensions.cs | 42 ++++++++ tests/SharpCompress.Test/ArchiveTests.cs | 2 +- 5 files changed, 249 insertions(+), 14 deletions(-) create mode 100644 src/SharpCompress/LazyAsyncReadOnlyCollection.cs diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index 672382ff..ff1ce04d 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -1,14 +1,13 @@ -using System; using System.Collections.Generic; -using System.IO; using System.Linq; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; using SharpCompress.Readers; namespace SharpCompress.Archives; -public abstract class AbstractArchive : IArchive +public abstract class AbstractArchive : IArchive, IArchiveAsync where TEntry : IArchiveEntry where TVolume : IVolume { @@ -26,6 +25,8 @@ public abstract class AbstractArchive : IArchive _sourceStream = sourceStream; _lazyVolumes = new LazyReadOnlyCollection(LoadVolumes(_sourceStream)); _lazyEntries = new LazyReadOnlyCollection(LoadEntries(Volumes)); + _lazyVolumesAsync = new LazyAsyncReadOnlyCollection(LoadVolumesAsync(_sourceStream)); + _lazyEntriesAsync = new LazyAsyncReadOnlyCollection(LoadEntriesAsync(_lazyVolumesAsync)); } internal AbstractArchive(ArchiveType type) @@ -34,24 +35,16 @@ public abstract class AbstractArchive : IArchive ReaderOptions = new(); _lazyVolumes = new LazyReadOnlyCollection(Enumerable.Empty()); _lazyEntries = new LazyReadOnlyCollection(Enumerable.Empty()); + _lazyVolumesAsync = new LazyAsyncReadOnlyCollection(AsyncEnumerableEx.Empty()); + _lazyEntriesAsync = new LazyAsyncReadOnlyCollection(AsyncEnumerableEx.Empty()); } public ArchiveType Type { get; } - private static Stream CheckStreams(Stream stream) - { - if (!stream.CanSeek || !stream.CanRead) - { - throw new ArchiveException("Archive streams must be Readable and Seekable"); - } - return stream; - } - /// /// Returns an ReadOnlyCollection of all the RarArchiveEntries across the one or many parts of the RarArchive. /// public virtual ICollection Entries => _lazyEntries; - /// /// Returns an ReadOnlyCollection of all the RarArchiveVolumes across the one or many parts of the RarArchive. /// @@ -72,6 +65,9 @@ public abstract class AbstractArchive : IArchive protected abstract IEnumerable LoadVolumes(SourceStream sourceStream); protected abstract IEnumerable LoadEntries(IEnumerable volumes); + + protected abstract IAsyncEnumerable LoadVolumesAsync(SourceStream sourceStream); + protected abstract IAsyncEnumerable LoadEntriesAsync(IAsyncEnumerable volumes); IEnumerable IArchive.Entries => Entries.Cast(); IEnumerable IArchive.Volumes => _lazyVolumes.Cast(); @@ -140,4 +136,67 @@ public abstract class AbstractArchive : IArchive return Entries.All(x => x.IsComplete); } } + + #region Async Support + + private readonly LazyAsyncReadOnlyCollection _lazyVolumesAsync; + private readonly LazyAsyncReadOnlyCollection _lazyEntriesAsync; + + + public virtual async ValueTask DisposeAsync() + { + if (!_disposed) + { + await foreach (var v in _lazyVolumesAsync) + { + v.Dispose(); + } + foreach (var v in _lazyEntriesAsync.GetLoaded().Cast()) + { + v.Close(); + } + _sourceStream?.Dispose(); + + _disposed = true; + } + } + + private async ValueTask EnsureEntriesLoadedAsync() + { + await _lazyEntriesAsync.EnsureFullyLoaded(); + await _lazyVolumesAsync.EnsureFullyLoaded(); + } + + public virtual IAsyncEnumerable EntriesAsync => _lazyEntriesAsync; + IAsyncEnumerable IArchiveAsync.EntriesAsync => EntriesAsync.Cast(); + + public IAsyncEnumerable VolumesAsync => _lazyVolumesAsync.Cast(); + public async ValueTask ExtractAllEntriesAsync() + { + if (!IsSolid && Type != ArchiveType.SevenZip) + { + throw new SharpCompressException( + "ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)." + ); + } + await EnsureEntriesLoadedAsync(); + return await CreateReaderForSolidExtractionAsync(); + } + + + protected abstract ValueTask CreateReaderForSolidExtractionAsync(); + + public virtual ValueTask IsSolidAsync() => new (false); + + public async ValueTask IsCompleteAsync() + { + await EnsureEntriesLoadedAsync(); + return await EntriesAsync.All(x => x.IsComplete); + } + + public async ValueTask TotalSizeAsync() => await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.CompressedSize); + + public async ValueTask TotalUncompressSizeAsync() => await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.Size); + + #endregion } diff --git a/src/SharpCompress/Archives/IArchive.cs b/src/SharpCompress/Archives/IArchive.cs index 3ed7490d..dacb5907 100644 --- a/src/SharpCompress/Archives/IArchive.cs +++ b/src/SharpCompress/Archives/IArchive.cs @@ -1,10 +1,48 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Readers; namespace SharpCompress.Archives; +public interface IArchiveAsync : IAsyncDisposable +{ + IAsyncEnumerable EntriesAsync { get; } + IAsyncEnumerable VolumesAsync { get; } + + ArchiveType Type { get; } + + /// + /// Use this method to extract all entries in an archive in order. + /// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be + /// extracted sequentially for the best performance. + /// + ValueTask ExtractAllEntriesAsync(); + + /// + /// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files). + /// Rar Archives can be SOLID while all 7Zip archives are considered SOLID. + /// + ValueTask IsSolidAsync(); + + /// + /// This checks to see if all the known entries have IsComplete = true + /// + ValueTask IsCompleteAsync(); + + /// + /// The total size of the files compressed in the archive. + /// + ValueTask TotalSizeAsync(); + + /// + /// The total size of the files as uncompressed in the archive. + /// + ValueTask TotalUncompressSizeAsync(); +} + + public interface IArchive : IDisposable { IEnumerable Entries { get; } diff --git a/src/SharpCompress/LazyAsyncReadOnlyCollection.cs b/src/SharpCompress/LazyAsyncReadOnlyCollection.cs new file mode 100644 index 00000000..499edfb9 --- /dev/null +++ b/src/SharpCompress/LazyAsyncReadOnlyCollection.cs @@ -0,0 +1,96 @@ +#nullable disable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress; + +internal sealed class LazyAsyncReadOnlyCollection(IAsyncEnumerable source) : IAsyncEnumerable +{ + private readonly List backing = new(); + private readonly IAsyncEnumerator source = source.GetAsyncEnumerator(); + private bool fullyLoaded; + + private class LazyLoader(LazyAsyncReadOnlyCollection lazyReadOnlyCollection, CancellationToken cancellationToken) : IAsyncEnumerator + { + private bool disposed; + private int index = -1; + + public ValueTask DisposeAsync() + { + if (!disposed) + { + disposed = true; + } + return default; + } + + public async ValueTask MoveNextAsync() + { + cancellationToken.ThrowIfCancellationRequested(); + if (index + 1 < lazyReadOnlyCollection.backing.Count) + { + index++; + return true; + } + if (!lazyReadOnlyCollection.fullyLoaded && await lazyReadOnlyCollection.source.MoveNextAsync()) + { + lazyReadOnlyCollection.backing.Add(lazyReadOnlyCollection.source.Current); + index++; + return true; + } + lazyReadOnlyCollection.fullyLoaded = true; + return false; + } + + #region IEnumerator Members + + public T Current => lazyReadOnlyCollection.backing[index]; + + #endregion + + #region IDisposable Members + + public void Dispose() + { + if (!disposed) + { + disposed = true; + } + } + + #endregion + + } + + internal async ValueTask EnsureFullyLoaded() + { + if (!fullyLoaded) + { + var loader = new LazyLoader(this, CancellationToken.None); + while (await loader.MoveNextAsync()) + { + // Intentionally empty + } + fullyLoaded = true; + } + } + + internal IEnumerable GetLoaded() => backing; + + #region ICollection Members + + public void Add(T item) => throw new NotSupportedException(); + + public void Clear() => throw new NotSupportedException(); + + public bool IsReadOnly => true; + + public bool Remove(T item) => throw new NotSupportedException(); + + #endregion + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => new LazyLoader(this, cancellationToken); +} diff --git a/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs b/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs index f1379cbb..74db33b4 100644 --- a/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs +++ b/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs @@ -1,13 +1,45 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace SharpCompress; +public static class AsyncEnumerableEx +{ + public static async IAsyncEnumerable Empty() + where T : notnull + { + await Task.CompletedTask; + yield break; + } +} + public static class AsyncEnumerableExtensions { extension(IAsyncEnumerable source) + where T : notnull { + public async IAsyncEnumerable Cast() + where TResult : class + { + await foreach (var item in source) + { + yield return (item as TResult).NotNull(); + } + } + public async ValueTask All(Func predicate) + { + await foreach (var item in source) + { + if (!predicate(item)) + { + return false; + } + } + + return true; + } public async IAsyncEnumerable Where(Func predicate) { await foreach (var item in source) @@ -28,5 +60,15 @@ public static class AsyncEnumerableExtensions return default; // Returns null/default if the stream is empty } + + public async ValueTask Aggregate(TAccumulate seed, Func func) + { + TAccumulate result = seed; + await foreach (var element in source) + { + result = func(result, element); + } + return result; + } } } diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 2eea8838..03128e9e 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -609,7 +609,7 @@ public class ArchiveTests : ReaderTests { try { - foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + await foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { await entry.WriteToDirectoryAsync( SCRATCH_FILES_PATH, From 0f37cbfd0bafd45af1c18d085134f7851ba8c90e Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 09:39:04 +0000 Subject: [PATCH 24/46] archive async path uses new async interface --- src/SharpCompress/Archives/AbstractArchive.cs | 14 ++++++++++--- src/SharpCompress/Archives/ArchiveFactory.cs | 10 ++++----- .../Archives/AutoArchiveFactory.cs | 4 ++-- .../Archives/GZip/GZipArchive.cs | 8 +++---- src/SharpCompress/Archives/IArchiveFactory.cs | 4 ++-- .../Archives/IMultiArchiveFactory.cs | 4 ++-- src/SharpCompress/Archives/Rar/RarArchive.cs | 8 +++---- .../Archives/SevenZip/SevenZipArchive.cs | 8 +++---- src/SharpCompress/Archives/Tar/TarArchive.cs | 8 +++---- src/SharpCompress/Archives/Zip/ZipArchive.cs | 8 +++---- src/SharpCompress/Factories/GZipFactory.cs | 8 +++---- src/SharpCompress/Factories/RarFactory.cs | 8 +++---- .../Factories/SevenZipFactory.cs | 8 +++---- src/SharpCompress/Factories/TarFactory.cs | 8 +++---- src/SharpCompress/Factories/ZipFactory.cs | 8 +++---- .../Polyfills/AsyncEnumerableExtensions.cs | 21 +++++++++++++++++++ tests/SharpCompress.Test/ArchiveTests.cs | 4 ++-- 17 files changed, 85 insertions(+), 56 deletions(-) diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index ff1ce04d..18a60f84 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -66,8 +66,15 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync protected abstract IEnumerable LoadEntries(IEnumerable volumes); - protected abstract IAsyncEnumerable LoadVolumesAsync(SourceStream sourceStream); - protected abstract IAsyncEnumerable LoadEntriesAsync(IAsyncEnumerable volumes); + protected virtual IAsyncEnumerable LoadVolumesAsync(SourceStream sourceStream) => LoadVolumes(sourceStream).ToAsyncEnumerable(); + + protected virtual async IAsyncEnumerable LoadEntriesAsync(IAsyncEnumerable volumes) + { + foreach (var item in LoadEntries(await volumes.ToListAsync()) ) + { + yield return item; + } + } IEnumerable IArchive.Entries => Entries.Cast(); IEnumerable IArchive.Volumes => _lazyVolumes.Cast(); @@ -184,7 +191,8 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync } - protected abstract ValueTask CreateReaderForSolidExtractionAsync(); + protected virtual ValueTask CreateReaderForSolidExtractionAsync() => + new (CreateReaderForSolidExtraction()); public virtual ValueTask IsSolidAsync() => new (false); diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 82af74ef..a1c6d5ba 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -33,7 +33,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -79,7 +79,7 @@ public static class ArchiveFactory /// /// /// - public static Task OpenAsync( + public static Task OpenAsync( string filePath, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -107,7 +107,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( FileInfo fileInfo, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -152,7 +152,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IEnumerable fileInfos, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -212,7 +212,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IEnumerable streams, ReaderOptions? options = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/AutoArchiveFactory.cs b/src/SharpCompress/Archives/AutoArchiveFactory.cs index 2f78e8f6..472dc4bb 100644 --- a/src/SharpCompress/Archives/AutoArchiveFactory.cs +++ b/src/SharpCompress/Archives/AutoArchiveFactory.cs @@ -34,7 +34,7 @@ class AutoArchiveFactory : IArchiveFactory public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) => ArchiveFactory.Open(stream, readerOptions); - public Task OpenAsync( + public Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -43,7 +43,7 @@ class AutoArchiveFactory : IArchiveFactory public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) => ArchiveFactory.Open(fileInfo, readerOptions); - public Task OpenAsync( + public Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index 9ecab946..f67f68ac 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -108,7 +108,7 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -124,7 +124,7 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -140,7 +140,7 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -156,7 +156,7 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/IArchiveFactory.cs b/src/SharpCompress/Archives/IArchiveFactory.cs index 2ebf5064..c456cd60 100644 --- a/src/SharpCompress/Archives/IArchiveFactory.cs +++ b/src/SharpCompress/Archives/IArchiveFactory.cs @@ -34,7 +34,7 @@ public interface IArchiveFactory : IFactory /// An open, readable and seekable stream. /// reading options. /// Cancellation token. - Task OpenAsync( + Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -53,7 +53,7 @@ public interface IArchiveFactory : IFactory /// the file to open. /// reading options. /// Cancellation token. - Task OpenAsync( + Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/IMultiArchiveFactory.cs b/src/SharpCompress/Archives/IMultiArchiveFactory.cs index d736bf84..313dc8af 100644 --- a/src/SharpCompress/Archives/IMultiArchiveFactory.cs +++ b/src/SharpCompress/Archives/IMultiArchiveFactory.cs @@ -35,7 +35,7 @@ public interface IMultiArchiveFactory : IFactory /// /// reading options. /// Cancellation token. - Task OpenAsync( + Task OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -54,7 +54,7 @@ public interface IMultiArchiveFactory : IFactory /// /// reading options. /// Cancellation token. - Task OpenAsync( + Task OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/Rar/RarArchive.cs b/src/SharpCompress/Archives/Rar/RarArchive.cs index 9fa8e2a1..dccbf8d2 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.cs @@ -189,7 +189,7 @@ public class RarArchive : AbstractArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -205,7 +205,7 @@ public class RarArchive : AbstractArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -221,7 +221,7 @@ public class RarArchive : AbstractArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -237,7 +237,7 @@ public class RarArchive : AbstractArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index a3041c7d..24c567f6 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -111,7 +111,7 @@ public class SevenZipArchive : AbstractArchive /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -127,7 +127,7 @@ public class SevenZipArchive : AbstractArchive /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -143,7 +143,7 @@ public class SevenZipArchive : AbstractArchive /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -159,7 +159,7 @@ public class SevenZipArchive : AbstractArchive /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index 11a7c0cc..ac5ad110 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -109,7 +109,7 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -125,7 +125,7 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -141,7 +141,7 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -157,7 +157,7 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 7ce8a116..e4c3f273 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -130,7 +130,7 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -146,7 +146,7 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -162,7 +162,7 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -178,7 +178,7 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index f6797b30..34085df3 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -65,7 +65,7 @@ public class GZipFactory GZipArchive.Open(stream, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -76,7 +76,7 @@ public class GZipFactory GZipArchive.Open(fileInfo, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -91,7 +91,7 @@ public class GZipFactory GZipArchive.Open(streams, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -102,7 +102,7 @@ public class GZipFactory GZipArchive.Open(fileInfos, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index 2986e61b..db1b725d 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -50,7 +50,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(stream, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -61,7 +61,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(fileInfo, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -76,7 +76,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(streams, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -87,7 +87,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(fileInfos, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index a9c74fae..73c08fa0 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -45,7 +45,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(stream, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -56,7 +56,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(fileInfo, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -71,7 +71,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(streams, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -82,7 +82,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(fileInfos, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 240b835d..57295699 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -70,7 +70,7 @@ public class TarFactory TarArchive.Open(stream, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -81,7 +81,7 @@ public class TarFactory TarArchive.Open(fileInfo, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -96,7 +96,7 @@ public class TarFactory TarArchive.Open(streams, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -107,7 +107,7 @@ public class TarFactory TarArchive.Open(fileInfos, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index 21de0c5a..756bf31f 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -137,7 +137,7 @@ public class ZipFactory ZipArchive.Open(stream, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -148,7 +148,7 @@ public class ZipFactory ZipArchive.Open(fileInfo, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -163,7 +163,7 @@ public class ZipFactory ZipArchive.Open(streams, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -174,7 +174,7 @@ public class ZipFactory ZipArchive.Open(fileInfos, readerOptions); /// - public Task OpenAsync( + public Task OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs b/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs index 74db33b4..f4c07df5 100644 --- a/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs +++ b/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs @@ -15,11 +15,32 @@ public static class AsyncEnumerableEx } } +public static class EnumerableExtensions +{ + public static async IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source) + { + await Task.CompletedTask; + foreach (var item in source) + { + yield return item; + } + } +} + public static class AsyncEnumerableExtensions { extension(IAsyncEnumerable source) where T : notnull { + public async ValueTask> ToListAsync() + { + var list = new List(); + await foreach (var item in source) + { + list.Add(item); + } + return list; + } public async IAsyncEnumerable Cast() where TResult : class { diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 03128e9e..c3ff9d6d 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -600,7 +600,7 @@ public class ArchiveTests : ReaderTests throwOnDispose: true ) ) - using ( + await using ( var archive = await archiveFactory.OpenAsync( new AsyncOnlyStream(stream), readerOptions @@ -609,7 +609,7 @@ public class ArchiveTests : ReaderTests { try { - await foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)) { await entry.WriteToDirectoryAsync( SCRATCH_FILES_PATH, From 60e5220bd06d5d592a5580ca8993684559239c8f Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 09:41:48 +0000 Subject: [PATCH 25/46] fmt --- src/SharpCompress/Archives/AbstractArchive.cs | 46 ++++++++++++------- src/SharpCompress/Archives/IArchive.cs | 1 - .../LazyAsyncReadOnlyCollection.cs | 17 +++++-- .../Polyfills/AsyncEnumerableExtensions.cs | 14 ++++-- tests/SharpCompress.Test/ArchiveTests.cs | 4 +- 5 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index 18a60f84..14bf9caa 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -25,8 +25,12 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync _sourceStream = sourceStream; _lazyVolumes = new LazyReadOnlyCollection(LoadVolumes(_sourceStream)); _lazyEntries = new LazyReadOnlyCollection(LoadEntries(Volumes)); - _lazyVolumesAsync = new LazyAsyncReadOnlyCollection(LoadVolumesAsync(_sourceStream)); - _lazyEntriesAsync = new LazyAsyncReadOnlyCollection(LoadEntriesAsync(_lazyVolumesAsync)); + _lazyVolumesAsync = new LazyAsyncReadOnlyCollection( + LoadVolumesAsync(_sourceStream) + ); + _lazyEntriesAsync = new LazyAsyncReadOnlyCollection( + LoadEntriesAsync(_lazyVolumesAsync) + ); } internal AbstractArchive(ArchiveType type) @@ -35,8 +39,12 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync ReaderOptions = new(); _lazyVolumes = new LazyReadOnlyCollection(Enumerable.Empty()); _lazyEntries = new LazyReadOnlyCollection(Enumerable.Empty()); - _lazyVolumesAsync = new LazyAsyncReadOnlyCollection(AsyncEnumerableEx.Empty()); - _lazyEntriesAsync = new LazyAsyncReadOnlyCollection(AsyncEnumerableEx.Empty()); + _lazyVolumesAsync = new LazyAsyncReadOnlyCollection( + AsyncEnumerableEx.Empty() + ); + _lazyEntriesAsync = new LazyAsyncReadOnlyCollection( + AsyncEnumerableEx.Empty() + ); } public ArchiveType Type { get; } @@ -45,6 +53,7 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync /// Returns an ReadOnlyCollection of all the RarArchiveEntries across the one or many parts of the RarArchive. /// public virtual ICollection Entries => _lazyEntries; + /// /// Returns an ReadOnlyCollection of all the RarArchiveVolumes across the one or many parts of the RarArchive. /// @@ -65,16 +74,19 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync protected abstract IEnumerable LoadVolumes(SourceStream sourceStream); protected abstract IEnumerable LoadEntries(IEnumerable volumes); + protected virtual IAsyncEnumerable LoadVolumesAsync(SourceStream sourceStream) => + LoadVolumes(sourceStream).ToAsyncEnumerable(); - protected virtual IAsyncEnumerable LoadVolumesAsync(SourceStream sourceStream) => LoadVolumes(sourceStream).ToAsyncEnumerable(); - - protected virtual async IAsyncEnumerable LoadEntriesAsync(IAsyncEnumerable volumes) + protected virtual async IAsyncEnumerable LoadEntriesAsync( + IAsyncEnumerable volumes + ) { - foreach (var item in LoadEntries(await volumes.ToListAsync()) ) + foreach (var item in LoadEntries(await volumes.ToListAsync())) { yield return item; } } + IEnumerable IArchive.Entries => Entries.Cast(); IEnumerable IArchive.Volumes => _lazyVolumes.Cast(); @@ -149,14 +161,13 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync private readonly LazyAsyncReadOnlyCollection _lazyVolumesAsync; private readonly LazyAsyncReadOnlyCollection _lazyEntriesAsync; - public virtual async ValueTask DisposeAsync() { if (!_disposed) { await foreach (var v in _lazyVolumesAsync) { - v.Dispose(); + v.Dispose(); } foreach (var v in _lazyEntriesAsync.GetLoaded().Cast()) { @@ -175,9 +186,11 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync } public virtual IAsyncEnumerable EntriesAsync => _lazyEntriesAsync; - IAsyncEnumerable IArchiveAsync.EntriesAsync => EntriesAsync.Cast(); + IAsyncEnumerable IArchiveAsync.EntriesAsync => + EntriesAsync.Cast(); public IAsyncEnumerable VolumesAsync => _lazyVolumesAsync.Cast(); + public async ValueTask ExtractAllEntriesAsync() { if (!IsSolid && Type != ArchiveType.SevenZip) @@ -190,11 +203,10 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync return await CreateReaderForSolidExtractionAsync(); } - protected virtual ValueTask CreateReaderForSolidExtractionAsync() => - new (CreateReaderForSolidExtraction()); + new(CreateReaderForSolidExtraction()); - public virtual ValueTask IsSolidAsync() => new (false); + public virtual ValueTask IsSolidAsync() => new(false); public async ValueTask IsCompleteAsync() { @@ -202,9 +214,11 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync return await EntriesAsync.All(x => x.IsComplete); } - public async ValueTask TotalSizeAsync() => await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.CompressedSize); + public async ValueTask TotalSizeAsync() => + await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.CompressedSize); - public async ValueTask TotalUncompressSizeAsync() => await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.Size); + public async ValueTask TotalUncompressSizeAsync() => + await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.Size); #endregion } diff --git a/src/SharpCompress/Archives/IArchive.cs b/src/SharpCompress/Archives/IArchive.cs index dacb5907..f123d984 100644 --- a/src/SharpCompress/Archives/IArchive.cs +++ b/src/SharpCompress/Archives/IArchive.cs @@ -42,7 +42,6 @@ public interface IArchiveAsync : IAsyncDisposable ValueTask TotalUncompressSizeAsync(); } - public interface IArchive : IDisposable { IEnumerable Entries { get; } diff --git a/src/SharpCompress/LazyAsyncReadOnlyCollection.cs b/src/SharpCompress/LazyAsyncReadOnlyCollection.cs index 499edfb9..85caf611 100644 --- a/src/SharpCompress/LazyAsyncReadOnlyCollection.cs +++ b/src/SharpCompress/LazyAsyncReadOnlyCollection.cs @@ -7,13 +7,17 @@ using System.Threading.Tasks; namespace SharpCompress; -internal sealed class LazyAsyncReadOnlyCollection(IAsyncEnumerable source) : IAsyncEnumerable +internal sealed class LazyAsyncReadOnlyCollection(IAsyncEnumerable source) + : IAsyncEnumerable { private readonly List backing = new(); private readonly IAsyncEnumerator source = source.GetAsyncEnumerator(); private bool fullyLoaded; - private class LazyLoader(LazyAsyncReadOnlyCollection lazyReadOnlyCollection, CancellationToken cancellationToken) : IAsyncEnumerator + private class LazyLoader( + LazyAsyncReadOnlyCollection lazyReadOnlyCollection, + CancellationToken cancellationToken + ) : IAsyncEnumerator { private bool disposed; private int index = -1; @@ -35,7 +39,10 @@ internal sealed class LazyAsyncReadOnlyCollection(IAsyncEnumerable source) index++; return true; } - if (!lazyReadOnlyCollection.fullyLoaded && await lazyReadOnlyCollection.source.MoveNextAsync()) + if ( + !lazyReadOnlyCollection.fullyLoaded + && await lazyReadOnlyCollection.source.MoveNextAsync() + ) { lazyReadOnlyCollection.backing.Add(lazyReadOnlyCollection.source.Current); index++; @@ -62,7 +69,6 @@ internal sealed class LazyAsyncReadOnlyCollection(IAsyncEnumerable source) } #endregion - } internal async ValueTask EnsureFullyLoaded() @@ -92,5 +98,6 @@ internal sealed class LazyAsyncReadOnlyCollection(IAsyncEnumerable source) #endregion - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => new LazyLoader(this, cancellationToken); + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => + new LazyLoader(this, cancellationToken); } diff --git a/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs b/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs index f4c07df5..785d4c32 100644 --- a/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs +++ b/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs @@ -10,8 +10,8 @@ public static class AsyncEnumerableEx public static async IAsyncEnumerable Empty() where T : notnull { - await Task.CompletedTask; - yield break; + await Task.CompletedTask; + yield break; } } @@ -41,6 +41,7 @@ public static class AsyncEnumerableExtensions } return list; } + public async IAsyncEnumerable Cast() where TResult : class { @@ -49,6 +50,7 @@ public static class AsyncEnumerableExtensions yield return (item as TResult).NotNull(); } } + public async ValueTask All(Func predicate) { await foreach (var item in source) @@ -61,6 +63,7 @@ public static class AsyncEnumerableExtensions return true; } + public async IAsyncEnumerable Where(Func predicate) { await foreach (var item in source) @@ -82,10 +85,13 @@ public static class AsyncEnumerableExtensions return default; // Returns null/default if the stream is empty } - public async ValueTask Aggregate(TAccumulate seed, Func func) + public async ValueTask Aggregate( + TAccumulate seed, + Func func + ) { TAccumulate result = seed; - await foreach (var element in source) + await foreach (var element in source) { result = func(result, element); } diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index c3ff9d6d..3001b4bd 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -609,7 +609,9 @@ public class ArchiveTests : ReaderTests { try { - await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)) + await foreach ( + var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory) + ) { await entry.WriteToDirectoryAsync( SCRATCH_FILES_PATH, From 8e42296c3a6454e9a5f91446cd040cef64dde2ee Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 10:22:53 +0000 Subject: [PATCH 26/46] switch Task to ValueTask --- .../Archives/AutoArchiveFactory.cs | 12 +-- .../Archives/GZip/GZipArchive.cs | 18 ++-- .../Archives/IArchiveAsyncExtensions.cs | 100 ++++++++++++++++++ .../Archives/IArchiveExtensions.cs | 86 --------------- src/SharpCompress/Archives/IArchiveFactory.cs | 4 +- .../Archives/IMultiArchiveFactory.cs | 4 +- src/SharpCompress/Archives/Rar/RarArchive.cs | 16 +-- .../Archives/SevenZip/SevenZipArchive.cs | 16 +-- src/SharpCompress/Archives/Tar/TarArchive.cs | 16 +-- src/SharpCompress/Archives/Zip/ZipArchive.cs | 76 +++++++++++-- .../Archives/Zip/ZipArchiveEntry.cs | 12 ++- .../Common/Zip/SeekableZipFilePart.cs | 17 +++ .../Common/Zip/SeekableZipHeaderFactory.cs | 100 +++++++++++++++++- src/SharpCompress/Factories/AceFactory.cs | 4 +- src/SharpCompress/Factories/ArcFactory.cs | 4 +- src/SharpCompress/Factories/ArjFactory.cs | 4 +- src/SharpCompress/Factories/Factory.cs | 4 +- src/SharpCompress/Factories/GZipFactory.cs | 18 ++-- src/SharpCompress/Factories/IFactory.cs | 2 +- src/SharpCompress/Factories/RarFactory.cs | 12 +-- .../Factories/SevenZipFactory.cs | 8 +- src/SharpCompress/Factories/TarFactory.cs | 16 +-- src/SharpCompress/Factories/ZipFactory.cs | 18 ++-- src/SharpCompress/Readers/IReaderFactory.cs | 2 +- src/SharpCompress/Writers/IWriterFactory.cs | 2 +- tests/SharpCompress.Test/ExtractAll.cs | 2 +- .../Zip/ZipArchiveAsyncTests.cs | 13 +-- 27 files changed, 387 insertions(+), 199 deletions(-) create mode 100644 src/SharpCompress/Archives/IArchiveAsyncExtensions.cs diff --git a/src/SharpCompress/Archives/AutoArchiveFactory.cs b/src/SharpCompress/Archives/AutoArchiveFactory.cs index 472dc4bb..0c36d1c0 100644 --- a/src/SharpCompress/Archives/AutoArchiveFactory.cs +++ b/src/SharpCompress/Archives/AutoArchiveFactory.cs @@ -8,7 +8,7 @@ using SharpCompress.Readers; namespace SharpCompress.Archives; -class AutoArchiveFactory : IArchiveFactory +internal class AutoArchiveFactory : IArchiveFactory { public string Name => nameof(AutoArchiveFactory); @@ -22,7 +22,7 @@ class AutoArchiveFactory : IArchiveFactory int bufferSize = ReaderOptions.DefaultBufferSize ) => throw new NotSupportedException(); - public Task IsArchiveAsync( + public ValueTask IsArchiveAsync( Stream stream, string? password = null, int bufferSize = ReaderOptions.DefaultBufferSize, @@ -34,18 +34,18 @@ class AutoArchiveFactory : IArchiveFactory public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) => ArchiveFactory.Open(stream, readerOptions); - public Task OpenAsync( + public async ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default - ) => ArchiveFactory.OpenAsync(stream, readerOptions, cancellationToken); + ) => await ArchiveFactory.OpenAsync(stream, readerOptions, cancellationToken); public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) => ArchiveFactory.Open(fileInfo, readerOptions); - public Task OpenAsync( + public async ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default - ) => ArchiveFactory.OpenAsync(fileInfo, readerOptions, cancellationToken); + ) => await ArchiveFactory.OpenAsync(fileInfo, readerOptions, cancellationToken); } diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index f67f68ac..6895cfac 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -108,14 +108,14 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(stream, readerOptions)).ConfigureAwait(false); + return new(Open(stream, readerOptions)); } /// @@ -124,14 +124,14 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(fileInfo, readerOptions)).ConfigureAwait(false); + return new(Open(fileInfo, readerOptions)); } /// @@ -140,14 +140,14 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(streams, readerOptions)).ConfigureAwait(false); + return new(Open(streams, readerOptions)); } /// @@ -156,14 +156,14 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(fileInfos, readerOptions)).ConfigureAwait(false); + return new(Open(fileInfos, readerOptions)); } public static GZipArchive Create() => new(); @@ -231,7 +231,7 @@ public class GZipArchive : AbstractWritableArchive return true; } - public static async Task IsGZipFileAsync( + public static async ValueTask IsGZipFileAsync( Stream stream, CancellationToken cancellationToken = default ) diff --git a/src/SharpCompress/Archives/IArchiveAsyncExtensions.cs b/src/SharpCompress/Archives/IArchiveAsyncExtensions.cs new file mode 100644 index 00000000..ca3db1cf --- /dev/null +++ b/src/SharpCompress/Archives/IArchiveAsyncExtensions.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Readers; + +namespace SharpCompress.Archives; + +public static class IArchiveAsyncExtensions +{ + /// The archive to extract. + extension(IArchiveAsync archive) + { + /// + /// Extract to specific directory asynchronously with progress reporting and cancellation support + /// + /// The folder to extract into. + /// Extraction options. + /// Optional progress reporter for tracking extraction progress. + /// Optional cancellation token. + public async Task WriteToDirectoryAsync( + string destinationDirectory, + ExtractionOptions? options = null, + IProgress? progress = null, + CancellationToken cancellationToken = default + ) + { + // For solid archives (Rar, 7Zip), use the optimized reader-based approach + if (await archive.IsSolidAsync() || archive.Type == ArchiveType.SevenZip) + { + using var reader = await archive.ExtractAllEntriesAsync(); + await reader.WriteAllToDirectoryAsync( + destinationDirectory, + options, + cancellationToken + ); + } + else + { + // For non-solid archives, extract entries directly + await archive.WriteToDirectoryAsyncInternal( + destinationDirectory, + options, + progress, + cancellationToken + ); + } + } + + private async Task WriteToDirectoryAsyncInternal( + string destinationDirectory, + ExtractionOptions? options, + IProgress? progress, + CancellationToken cancellationToken + ) + { + // Prepare for progress reporting + var totalBytes = await archive.TotalUncompressSizeAsync(); + var bytesRead = 0L; + + // Tracking for created directories. + var seenDirectories = new HashSet(); + + // Extract + await foreach (var entry in archive.EntriesAsync.WithCancellation(cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (entry.IsDirectory) + { + var dirPath = Path.Combine( + destinationDirectory, + entry.Key.NotNull("Entry Key is null") + ); + if ( + Path.GetDirectoryName(dirPath + "/") is { } parentDirectory + && seenDirectories.Add(dirPath) + ) + { + Directory.CreateDirectory(parentDirectory); + } + continue; + } + + // Use the entry's WriteToDirectoryAsync method which respects ExtractionOptions + await entry + .WriteToDirectoryAsync(destinationDirectory, options, cancellationToken) + .ConfigureAwait(false); + + // Update progress + bytesRead += entry.Size; + progress?.Report( + new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes) + ); + } + } + } +} diff --git a/src/SharpCompress/Archives/IArchiveExtensions.cs b/src/SharpCompress/Archives/IArchiveExtensions.cs index 0d39c6e2..c1d2ac98 100644 --- a/src/SharpCompress/Archives/IArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveExtensions.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Readers; @@ -80,89 +78,5 @@ public static class IArchiveExtensions ); } } - - /// - /// Extract to specific directory asynchronously with progress reporting and cancellation support - /// - /// The folder to extract into. - /// Extraction options. - /// Optional progress reporter for tracking extraction progress. - /// Optional cancellation token. - public async Task WriteToDirectoryAsync( - string destinationDirectory, - ExtractionOptions? options = null, - IProgress? progress = null, - CancellationToken cancellationToken = default - ) - { - // For solid archives (Rar, 7Zip), use the optimized reader-based approach - if (archive.IsSolid || archive.Type == ArchiveType.SevenZip) - { - using var reader = archive.ExtractAllEntries(); - await reader.WriteAllToDirectoryAsync( - destinationDirectory, - options, - cancellationToken - ); - } - else - { - // For non-solid archives, extract entries directly - await archive.WriteToDirectoryAsyncInternal( - destinationDirectory, - options, - progress, - cancellationToken - ); - } - } - - private async Task WriteToDirectoryAsyncInternal( - string destinationDirectory, - ExtractionOptions? options, - IProgress? progress, - CancellationToken cancellationToken - ) - { - // Prepare for progress reporting - var totalBytes = archive.TotalUncompressSize; - var bytesRead = 0L; - - // Tracking for created directories. - var seenDirectories = new HashSet(); - - // Extract - foreach (var entry in archive.Entries) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (entry.IsDirectory) - { - var dirPath = Path.Combine( - destinationDirectory, - entry.Key.NotNull("Entry Key is null") - ); - if ( - Path.GetDirectoryName(dirPath + "/") is { } parentDirectory - && seenDirectories.Add(dirPath) - ) - { - Directory.CreateDirectory(parentDirectory); - } - continue; - } - - // Use the entry's WriteToDirectoryAsync method which respects ExtractionOptions - await entry - .WriteToDirectoryAsync(destinationDirectory, options, cancellationToken) - .ConfigureAwait(false); - - // Update progress - bytesRead += entry.Size; - progress?.Report( - new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes) - ); - } - } } } diff --git a/src/SharpCompress/Archives/IArchiveFactory.cs b/src/SharpCompress/Archives/IArchiveFactory.cs index c456cd60..40e25d00 100644 --- a/src/SharpCompress/Archives/IArchiveFactory.cs +++ b/src/SharpCompress/Archives/IArchiveFactory.cs @@ -34,7 +34,7 @@ public interface IArchiveFactory : IFactory /// An open, readable and seekable stream. /// reading options. /// Cancellation token. - Task OpenAsync( + ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -53,7 +53,7 @@ public interface IArchiveFactory : IFactory /// the file to open. /// reading options. /// Cancellation token. - Task OpenAsync( + ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/IMultiArchiveFactory.cs b/src/SharpCompress/Archives/IMultiArchiveFactory.cs index 313dc8af..2c96ef53 100644 --- a/src/SharpCompress/Archives/IMultiArchiveFactory.cs +++ b/src/SharpCompress/Archives/IMultiArchiveFactory.cs @@ -35,7 +35,7 @@ public interface IMultiArchiveFactory : IFactory /// /// reading options. /// Cancellation token. - Task OpenAsync( + ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -54,7 +54,7 @@ public interface IMultiArchiveFactory : IFactory /// /// reading options. /// Cancellation token. - Task OpenAsync( + ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/Rar/RarArchive.cs b/src/SharpCompress/Archives/Rar/RarArchive.cs index dccbf8d2..45e97f93 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.cs @@ -189,14 +189,14 @@ public class RarArchive : AbstractArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(stream, readerOptions)).ConfigureAwait(false); + return new(Open(stream, readerOptions)); } /// @@ -205,14 +205,14 @@ public class RarArchive : AbstractArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(fileInfo, readerOptions)).ConfigureAwait(false); + return new(Open(fileInfo, readerOptions)); } /// @@ -221,14 +221,14 @@ public class RarArchive : AbstractArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(streams, readerOptions)).ConfigureAwait(false); + return new(Open(streams, readerOptions)); } /// @@ -237,14 +237,14 @@ public class RarArchive : AbstractArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(fileInfos, readerOptions)).ConfigureAwait(false); + return new(Open(fileInfos, readerOptions)); } public static bool IsRarFile(string filePath) => IsRarFile(new FileInfo(filePath)); diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index 24c567f6..f73621e6 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -111,14 +111,14 @@ public class SevenZipArchive : AbstractArchive /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(stream, readerOptions)).ConfigureAwait(false); + return new(Open(stream, readerOptions)); } /// @@ -127,14 +127,14 @@ public class SevenZipArchive : AbstractArchive /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(fileInfo, readerOptions)).ConfigureAwait(false); + return new(Open(fileInfo, readerOptions)); } /// @@ -143,14 +143,14 @@ public class SevenZipArchive : AbstractArchive /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(streams, readerOptions)).ConfigureAwait(false); + return new(Open(streams, readerOptions)); } /// @@ -159,14 +159,14 @@ public class SevenZipArchive : AbstractArchive /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(fileInfos, readerOptions)).ConfigureAwait(false); + return new(Open(fileInfos, readerOptions)); } /// diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index ac5ad110..9423c915 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -109,14 +109,14 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(stream, readerOptions)).ConfigureAwait(false); + return new(Open(stream, readerOptions)); } /// @@ -125,14 +125,14 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(fileInfo, readerOptions)).ConfigureAwait(false); + return new(Open(fileInfo, readerOptions)); } /// @@ -141,14 +141,14 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(streams, readerOptions)).ConfigureAwait(false); + return new(Open(streams, readerOptions)); } /// @@ -157,14 +157,14 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(fileInfos, readerOptions)).ConfigureAwait(false); + return new(Open(fileInfos, readerOptions)); } public static bool IsTarFile(string filePath) => IsTarFile(new FileInfo(filePath)); diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index e4c3f273..328aa7e0 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -130,14 +130,14 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(stream, readerOptions)).ConfigureAwait(false); + return new(Open(stream, readerOptions)); } /// @@ -146,14 +146,14 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(fileInfo, readerOptions)).ConfigureAwait(false); + return new(Open(fileInfo, readerOptions)); } /// @@ -162,14 +162,14 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(streams, readerOptions)).ConfigureAwait(false); + return new(Open(streams, readerOptions)); } /// @@ -178,14 +178,14 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static async Task OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(fileInfos, readerOptions)).ConfigureAwait(false); + return new(Open(fileInfos, readerOptions)); } public static bool IsZipFile( @@ -283,7 +283,7 @@ public class ZipArchive : AbstractWritableArchive } } - public static async Task IsZipFileAsync( + public static async ValueTask IsZipFileAsync( Stream stream, string? password = null, int bufferSize = ReaderOptions.DefaultBufferSize, @@ -319,7 +319,7 @@ public class ZipArchive : AbstractWritableArchive } } - public static async Task IsZipMultiAsync( + public static async ValueTask IsZipMultiAsync( Stream stream, string? password = null, int bufferSize = ReaderOptions.DefaultBufferSize, @@ -345,7 +345,8 @@ public class ZipArchive : AbstractWritableArchive var z = new SeekableZipHeaderFactory(password, new ArchiveEncoding()); ZipHeader? x = null; await foreach ( - var h in z.ReadSeekableHeader(stream).WithCancellation(cancellationToken) + var h in z.ReadSeekableHeaderAsync(stream) + .WithCancellation(cancellationToken) ) { x = h; @@ -451,6 +452,59 @@ public class ZipArchive : AbstractWritableArchive } } + protected override async IAsyncEnumerable LoadEntriesAsync( + IAsyncEnumerable volumes + ) + { + var vols = await volumes.ToListAsync(); + var volsArray = vols.ToArray(); + + await foreach ( + var h in headerFactory.NotNull().ReadSeekableHeaderAsync(volsArray.Last().Stream) + ) + { + if (h != null) + { + switch (h.ZipHeaderType) + { + case ZipHeaderType.DirectoryEntry: + { + var deh = (DirectoryEntryHeader)h; + Stream s; + if ( + deh.RelativeOffsetOfEntryHeader + deh.CompressedSize + > volsArray[deh.DiskNumberStart].Stream.Length + ) + { + var v = volsArray.Skip(deh.DiskNumberStart).ToArray(); + s = new SourceStream( + v[0].Stream, + i => i < v.Length ? v[i].Stream : null, + new ReaderOptions() { LeaveStreamOpen = true } + ); + } + else + { + s = volsArray[deh.DiskNumberStart].Stream; + } + + yield return new ZipArchiveEntry( + this, + new SeekableZipFilePart(headerFactory.NotNull(), deh, s) + ); + } + break; + case ZipHeaderType.DirectoryEnd: + { + var bytes = ((DirectoryEndHeader)h).Comment ?? Array.Empty(); + volsArray.Last().Comment = ReaderOptions.ArchiveEncoding.Decode(bytes); + yield break; + } + } + } + } + } + public void SaveTo(Stream stream) => SaveTo(stream, new WriterOptions(CompressionType.Deflate)); protected override void SaveTo( diff --git a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs index a6baf34b..81d419f2 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs @@ -13,9 +13,17 @@ public class ZipArchiveEntry : ZipEntry, IArchiveEntry public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull(); - public virtual Task OpenEntryStreamAsync( + public virtual async Task OpenEntryStreamAsync( CancellationToken cancellationToken = default - ) => Task.FromResult(OpenEntryStream()); + ) + { + var part = Parts.Single(); + if (part is SeekableZipFilePart seekablePart) + { + return (await seekablePart.GetCompressedStreamAsync(cancellationToken)).NotNull(); + } + return OpenEntryStream(); + } #region IArchiveEntry Members diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs index e7572711..f2a7d9de 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common.Zip.Headers; namespace SharpCompress.Common.Zip; @@ -25,9 +27,24 @@ internal class SeekableZipFilePart : ZipFilePart return base.GetCompressedStream(); } + internal override async Task GetCompressedStreamAsync( + CancellationToken cancellationToken = default + ) + { + if (!_isLocalHeaderLoaded) + { + await LoadLocalHeaderAsync(cancellationToken); + _isLocalHeaderLoaded = true; + } + return await base.GetCompressedStreamAsync(cancellationToken); + } + private void LoadLocalHeader() => Header = _headerFactory.GetLocalHeader(BaseStream, (DirectoryEntryHeader)Header); + private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default) => + Header = await _headerFactory.GetLocalHeaderAsync(BaseStream, (DirectoryEntryHeader)Header); + protected override Stream CreateBaseStream() { BaseStream.Position = Header.DataStartPosition.NotNull(); diff --git a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs index 170950b2..38441c27 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs @@ -19,11 +19,11 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory internal SeekableZipHeaderFactory(string? password, ArchiveEncoding archiveEncoding) : base(StreamingMode.Seekable, password, archiveEncoding) { } - internal async IAsyncEnumerable ReadSeekableHeader(Stream stream) + internal async IAsyncEnumerable ReadSeekableHeaderAsync(Stream stream) { var reader = new AsyncBinaryReader(stream); - await SeekBackToHeader(stream, reader); + await SeekBackToHeaderAsync(stream, reader); var eocd_location = stream.Position; var entry = new DirectoryEndHeader(); @@ -153,6 +153,73 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory } } + internal async IAsyncEnumerable ReadSeekableHeaderAsync(Stream stream, bool useSync) + { + var reader = new AsyncBinaryReader(stream); + + await SeekBackToHeaderAsync(stream, reader); + + var eocd_location = stream.Position; + var entry = new DirectoryEndHeader(); + await entry.Read(reader); + + if (entry.IsZip64) + { + _zip64 = true; + + // ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR should be before the EOCD + stream.Seek(eocd_location - ZIP64_EOCD_LENGTH - 4, SeekOrigin.Begin); + var zip64_locator = await reader.ReadUInt32Async(); + if (zip64_locator != ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR) + { + throw new ArchiveException("Failed to locate the Zip64 Directory Locator"); + } + + var zip64Locator = new Zip64DirectoryEndLocatorHeader(); + await zip64Locator.Read(reader); + + stream.Seek(zip64Locator.RelativeOffsetOfTheEndOfDirectoryRecord, SeekOrigin.Begin); + var zip64Signature = await reader.ReadUInt32Async(); + if (zip64Signature != ZIP64_END_OF_CENTRAL_DIRECTORY) + { + throw new ArchiveException("Failed to locate the Zip64 Header"); + } + + var zip64Entry = new Zip64DirectoryEndHeader(); + await zip64Entry.Read(reader); + stream.Seek(zip64Entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin); + } + else + { + stream.Seek(entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin); + } + + var position = stream.Position; + while (true) + { + stream.Position = position; + var signature = await reader.ReadUInt32Async(); + var nextHeader = await ReadHeader(signature, reader, _zip64); + position = stream.Position; + + if (nextHeader is null) + { + yield break; + } + + if (nextHeader is DirectoryEntryHeader entryHeader) + { + //entry could be zero bytes so we need to know that. + entryHeader.HasData = entryHeader.CompressedSize != 0; + yield return entryHeader; + } + else if (nextHeader is DirectoryEndHeader endHeader) + { + yield return endHeader; + } + } + } + private static bool IsMatch(byte[] haystack, int position, byte[] needle) { for (var i = 0; i < needle.Length; i++) @@ -166,7 +233,7 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory return true; } - private static async ValueTask SeekBackToHeader(Stream stream, AsyncBinaryReader reader) + private static async ValueTask SeekBackToHeaderAsync(Stream stream, AsyncBinaryReader reader) { // Minimum EOCD length if (stream.Length < MINIMUM_EOCD_LENGTH) @@ -270,4 +337,31 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory } return localEntryHeader; } + + internal async ValueTask GetLocalHeaderAsync( + Stream stream, + DirectoryEntryHeader directoryEntryHeader + ) + { + stream.Seek(directoryEntryHeader.RelativeOffsetOfEntryHeader, SeekOrigin.Begin); + var reader = new AsyncBinaryReader(stream); + var signature = await reader.ReadUInt32Async(); + if (await ReadHeader(signature, reader, _zip64) is not LocalEntryHeader localEntryHeader) + { + throw new InvalidOperationException(); + } + + // populate fields only known from the DirectoryEntryHeader + localEntryHeader.HasData = directoryEntryHeader.HasData; + localEntryHeader.ExternalFileAttributes = directoryEntryHeader.ExternalFileAttributes; + localEntryHeader.Comment = directoryEntryHeader.Comment; + + if (FlagUtility.HasFlag(localEntryHeader.Flags, HeaderFlags.UsePostDataDescriptor)) + { + localEntryHeader.Crc = directoryEntryHeader.Crc; + localEntryHeader.CompressedSize = directoryEntryHeader.CompressedSize; + localEntryHeader.UncompressedSize = directoryEntryHeader.UncompressedSize; + } + return localEntryHeader; + } } diff --git a/src/SharpCompress/Factories/AceFactory.cs b/src/SharpCompress/Factories/AceFactory.cs index fe8d8f9c..02a6e489 100644 --- a/src/SharpCompress/Factories/AceFactory.cs +++ b/src/SharpCompress/Factories/AceFactory.cs @@ -35,10 +35,10 @@ namespace SharpCompress.Factories public IReader OpenReader(Stream stream, ReaderOptions? options) => AceReader.Open(stream, options); - public Task OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default - ) => Task.FromResult(OpenReader(stream, options)); + ) => new(OpenReader(stream, options)); } } diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index 18065afd..f497509a 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -44,14 +44,14 @@ namespace SharpCompress.Factories public IReader OpenReader(Stream stream, ReaderOptions? options) => ArcReader.Open(stream, options); - public async Task OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + return new(OpenReader(stream, options)); } } } diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs index 420e6ae6..0f69bcab 100644 --- a/src/SharpCompress/Factories/ArjFactory.cs +++ b/src/SharpCompress/Factories/ArjFactory.cs @@ -35,14 +35,14 @@ namespace SharpCompress.Factories public IReader OpenReader(Stream stream, ReaderOptions? options) => ArjReader.Open(stream, options); - public async Task OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + return new(OpenReader(stream, options)); } } } diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 45e801b8..7662314c 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -60,7 +60,7 @@ public abstract class Factory : IFactory ); /// - public virtual Task IsArchiveAsync( + public virtual ValueTask IsArchiveAsync( Stream stream, string? password = null, int bufferSize = ReaderOptions.DefaultBufferSize, @@ -68,7 +68,7 @@ public abstract class Factory : IFactory ) { cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult(IsArchive(stream, password, bufferSize)); + return new(IsArchive(stream, password, bufferSize)); } /// diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index 34085df3..7fc14d75 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -49,7 +49,7 @@ public class GZipFactory ) => GZipArchive.IsGZipFile(stream); /// - public override Task IsArchiveAsync( + public override ValueTask IsArchiveAsync( Stream stream, string? password = null, int bufferSize = ReaderOptions.DefaultBufferSize, @@ -65,7 +65,7 @@ public class GZipFactory GZipArchive.Open(stream, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -76,7 +76,7 @@ public class GZipFactory GZipArchive.Open(fileInfo, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -91,7 +91,7 @@ public class GZipFactory GZipArchive.Open(streams, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -102,7 +102,7 @@ public class GZipFactory GZipArchive.Open(fileInfos, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -147,14 +147,14 @@ public class GZipFactory GZipReader.Open(stream, options); /// - public async Task OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + return new(OpenReader(stream, options)); } #endregion @@ -172,14 +172,14 @@ public class GZipFactory } /// - public async Task OpenAsync( + public ValueTask OpenAsync( Stream stream, WriterOptions writerOptions, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(stream, writerOptions)).ConfigureAwait(false); + return new(Open(stream, writerOptions)); } #endregion diff --git a/src/SharpCompress/Factories/IFactory.cs b/src/SharpCompress/Factories/IFactory.cs index 47200cb7..a9dd4f5a 100644 --- a/src/SharpCompress/Factories/IFactory.cs +++ b/src/SharpCompress/Factories/IFactory.cs @@ -51,7 +51,7 @@ public interface IFactory /// optional password /// buffer size for reading /// cancellation token - Task IsArchiveAsync( + ValueTask IsArchiveAsync( Stream stream, string? password = null, int bufferSize = ReaderOptions.DefaultBufferSize, diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index db1b725d..0180fefb 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -50,7 +50,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(stream, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -61,7 +61,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(fileInfo, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -76,7 +76,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(streams, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -87,7 +87,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(fileInfos, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -102,14 +102,14 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarReader.Open(stream, options); /// - public async Task OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + return new(OpenReader(stream, options)); } #endregion diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index 73c08fa0..5a4be49e 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -45,7 +45,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(stream, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -56,7 +56,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(fileInfo, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -71,7 +71,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(streams, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -82,7 +82,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(fileInfos, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 57295699..4adccf8b 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -70,7 +70,7 @@ public class TarFactory TarArchive.Open(stream, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -81,7 +81,7 @@ public class TarFactory TarArchive.Open(fileInfo, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -96,7 +96,7 @@ public class TarFactory TarArchive.Open(streams, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -107,7 +107,7 @@ public class TarFactory TarArchive.Open(fileInfos, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -265,14 +265,14 @@ public class TarFactory TarReader.Open(stream, options); /// - public async Task OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + return new(OpenReader(stream, options)); } #endregion @@ -284,14 +284,14 @@ public class TarFactory new TarWriter(stream, new TarWriterOptions(writerOptions)); /// - public async Task OpenAsync( + public ValueTask OpenAsync( Stream stream, WriterOptions writerOptions, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(stream, writerOptions)).ConfigureAwait(false); + return new(Open(stream, writerOptions)); } #endregion diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index 756bf31f..f8950b44 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -82,7 +82,7 @@ public class ZipFactory } /// - public override async Task IsArchiveAsync( + public override async ValueTask IsArchiveAsync( Stream stream, string? password = null, int bufferSize = ReaderOptions.DefaultBufferSize, @@ -137,7 +137,7 @@ public class ZipFactory ZipArchive.Open(stream, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -148,7 +148,7 @@ public class ZipFactory ZipArchive.Open(fileInfo, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -163,7 +163,7 @@ public class ZipFactory ZipArchive.Open(streams, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -174,7 +174,7 @@ public class ZipFactory ZipArchive.Open(fileInfos, readerOptions); /// - public Task OpenAsync( + public ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -189,14 +189,14 @@ public class ZipFactory ZipReader.Open(stream, options); /// - public async Task OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(OpenReader(stream, options)).ConfigureAwait(false); + return new(OpenReader(stream, options)); } #endregion @@ -208,14 +208,14 @@ public class ZipFactory new ZipWriter(stream, new ZipWriterOptions(writerOptions)); /// - public async Task OpenAsync( + public ValueTask OpenAsync( Stream stream, WriterOptions writerOptions, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return await Task.FromResult(Open(stream, writerOptions)).ConfigureAwait(false); + return new(Open(stream, writerOptions)); } #endregion diff --git a/src/SharpCompress/Readers/IReaderFactory.cs b/src/SharpCompress/Readers/IReaderFactory.cs index dd95f187..4b311fde 100644 --- a/src/SharpCompress/Readers/IReaderFactory.cs +++ b/src/SharpCompress/Readers/IReaderFactory.cs @@ -21,7 +21,7 @@ public interface IReaderFactory : Factories.IFactory /// /// /// - Task OpenReaderAsync( + ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Writers/IWriterFactory.cs b/src/SharpCompress/Writers/IWriterFactory.cs index 059dbe59..f933e819 100644 --- a/src/SharpCompress/Writers/IWriterFactory.cs +++ b/src/SharpCompress/Writers/IWriterFactory.cs @@ -9,7 +9,7 @@ public interface IWriterFactory : IFactory { IWriter Open(Stream stream, WriterOptions writerOptions); - Task OpenAsync( + ValueTask OpenAsync( Stream stream, WriterOptions writerOptions, CancellationToken cancellationToken = default diff --git a/tests/SharpCompress.Test/ExtractAll.cs b/tests/SharpCompress.Test/ExtractAll.cs index 3e8b7d7a..f45a7486 100644 --- a/tests/SharpCompress.Test/ExtractAll.cs +++ b/tests/SharpCompress.Test/ExtractAll.cs @@ -23,7 +23,7 @@ public class ExtractAllTests : TestBase var testArchive = Path.Combine(TEST_ARCHIVES_PATH, archivePath); var options = new ExtractionOptions() { ExtractFullPath = true, Overwrite = true }; - using var archive = ArchiveFactory.Open(testArchive); + await using var archive = await ArchiveFactory.OpenAsync(testArchive); await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH, options); } diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index cd93a3c1..7ee07db4 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -7,6 +7,7 @@ using SharpCompress.Archives; using SharpCompress.Archives.Zip; using SharpCompress.Common; using SharpCompress.Compressors.Deflate; +using SharpCompress.Test.Mocks; using SharpCompress.Writers; using SharpCompress.Writers.Zip; using Xunit; @@ -118,7 +119,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Random_Write_Remove_Async() + public async Task Zip_Random_Write_Remove_Sync() { var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.mod.zip"); var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.noEmptyDirs.zip"); @@ -140,7 +141,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Random_Write_Add_Async() + public async Task Zip_Random_Write_Add_Sync() { var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg"); var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.mod.zip"); @@ -182,9 +183,9 @@ public class ZipArchiveAsyncTests : ArchiveTests 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)) + await using (var archive = await ZipArchive.OpenAsync(new AsyncOnlyStream(stream))) { - foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)) { await entry.WriteToDirectoryAsync( SCRATCH_FILES_PATH, @@ -199,7 +200,7 @@ public class ZipArchiveAsyncTests : ArchiveTests public async Task Zip_Deflate_Archive_WriteToDirectoryAsync() { using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"))) - using (var archive = ZipArchive.Open(stream)) + await using (var archive = await ZipArchive.OpenAsync(new AsyncOnlyStream(stream))) { await archive.WriteToDirectoryAsync( SCRATCH_FILES_PATH, @@ -216,7 +217,7 @@ public class ZipArchiveAsyncTests : ArchiveTests var progress = new Progress(report => progressReports.Add(report)); using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"))) - using (var archive = ZipArchive.Open(stream)) + await using (var archive = await ZipArchive.OpenAsync(new AsyncOnlyStream(stream))) { await archive.WriteToDirectoryAsync( SCRATCH_FILES_PATH, From 406b198e0e4e43e972576f27891aeed9f7c04b81 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 10:24:33 +0000 Subject: [PATCH 27/46] can't dispose before returning --- src/SharpCompress/Archives/ArchiveFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index a1c6d5ba..245940f2 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -294,7 +294,7 @@ public static class ArchiveFactory ); } - private static ValueTask FindFactoryAsync( + private static async ValueTask FindFactoryAsync( FileInfo finfo, CancellationToken cancellationToken ) @@ -302,7 +302,7 @@ public static class ArchiveFactory { finfo.NotNull(nameof(finfo)); using Stream stream = finfo.OpenRead(); - return FindFactoryAsync(stream, cancellationToken); + return await FindFactoryAsync(stream, cancellationToken); } private static async ValueTask FindFactoryAsync( From 7aec98d65273e1976a00b48abfa8077e2fe0eee0 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 11:28:15 +0000 Subject: [PATCH 28/46] read async interface for reader --- src/SharpCompress/Archives/AbstractArchive.cs | 6 +- .../Archives/GZip/GZipArchive.cs | 7 ++ src/SharpCompress/Archives/IArchive.cs | 2 +- .../Archives/IArchiveAsyncExtensions.cs | 2 +- src/SharpCompress/Archives/Rar/RarArchive.cs | 8 +- .../Archives/SevenZip/SevenZipArchive.cs | 3 + src/SharpCompress/Archives/Tar/TarArchive.cs | 7 ++ src/SharpCompress/Archives/Zip/ZipArchive.cs | 7 ++ src/SharpCompress/Factories/AceFactory.cs | 15 +-- src/SharpCompress/Factories/ArcFactory.cs | 14 +-- src/SharpCompress/Factories/ArjFactory.cs | 14 +-- src/SharpCompress/Factories/Factory.cs | 36 ++++++++ src/SharpCompress/Factories/GZipFactory.cs | 10 +- src/SharpCompress/Factories/RarFactory.cs | 10 +- .../Factories/SevenZipFactory.cs | 6 ++ src/SharpCompress/Factories/TarFactory.cs | 10 +- .../Factories/ZStandardFactory.cs | 6 ++ src/SharpCompress/Factories/ZipFactory.cs | 10 +- src/SharpCompress/Readers/AbstractReader.cs | 92 +++++++++---------- src/SharpCompress/Readers/IReader.cs | 34 ++++--- .../Readers/IReaderAsyncExtensions.cs | 69 ++++++++++++++ .../Readers/IReaderExtensions.cs | 59 ------------ src/SharpCompress/Readers/IReaderFactory.cs | 12 +-- src/SharpCompress/Readers/ReaderFactory.cs | 13 +-- src/SharpCompress/Readers/Zip/ZipReader.cs | 2 +- tests/SharpCompress.Test/GZip/AsyncTests.cs | 12 ++- .../GZip/GZipReaderAsyncTests.cs | 6 +- .../SharpCompress.Test/ProgressReportTests.cs | 11 ++- .../Rar/RarArchiveAsyncTests.cs | 10 +- .../Rar/RarReaderAsyncTests.cs | 31 +++++-- tests/SharpCompress.Test/ReaderTests.cs | 10 +- .../Tar/TarReaderAsyncTests.cs | 24 +++-- tests/SharpCompress.Test/WriterTests.cs | 7 +- .../Zip/ZipReaderAsyncTests.cs | 8 +- 34 files changed, 360 insertions(+), 213 deletions(-) create mode 100644 src/SharpCompress/Readers/IReaderAsyncExtensions.cs diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index 14bf9caa..b66d7845 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -133,6 +133,7 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync } protected abstract IReader CreateReaderForSolidExtraction(); + protected abstract ValueTask CreateReaderForSolidExtractionAsync(); /// /// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files). @@ -191,7 +192,7 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync public IAsyncEnumerable VolumesAsync => _lazyVolumesAsync.Cast(); - public async ValueTask ExtractAllEntriesAsync() + public async ValueTask ExtractAllEntriesAsync() { if (!IsSolid && Type != ArchiveType.SevenZip) { @@ -203,9 +204,6 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync return await CreateReaderForSolidExtractionAsync(); } - protected virtual ValueTask CreateReaderForSolidExtractionAsync() => - new(CreateReaderForSolidExtraction()); - public virtual ValueTask IsSolidAsync() => new(false); public async ValueTask IsCompleteAsync() diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index 6895cfac..82d9c53d 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -336,4 +336,11 @@ public class GZipArchive : AbstractWritableArchive stream.Position = 0; return GZipReader.Open(stream); } + + protected override ValueTask CreateReaderForSolidExtractionAsync() + { + var stream = Volumes.Single().Stream; + stream.Position = 0; + return new(GZipReader.Open(stream)); + } } diff --git a/src/SharpCompress/Archives/IArchive.cs b/src/SharpCompress/Archives/IArchive.cs index f123d984..9f5b78a9 100644 --- a/src/SharpCompress/Archives/IArchive.cs +++ b/src/SharpCompress/Archives/IArchive.cs @@ -18,7 +18,7 @@ public interface IArchiveAsync : IAsyncDisposable /// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be /// extracted sequentially for the best performance. /// - ValueTask ExtractAllEntriesAsync(); + ValueTask ExtractAllEntriesAsync(); /// /// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files). diff --git a/src/SharpCompress/Archives/IArchiveAsyncExtensions.cs b/src/SharpCompress/Archives/IArchiveAsyncExtensions.cs index ca3db1cf..95a1617f 100644 --- a/src/SharpCompress/Archives/IArchiveAsyncExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveAsyncExtensions.cs @@ -30,7 +30,7 @@ public static class IArchiveAsyncExtensions // For solid archives (Rar, 7Zip), use the optimized reader-based approach if (await archive.IsSolidAsync() || archive.Type == ArchiveType.SevenZip) { - using var reader = await archive.ExtractAllEntriesAsync(); + await using var reader = await archive.ExtractAllEntriesAsync(); await reader.WriteAllToDirectoryAsync( destinationDirectory, options, diff --git a/src/SharpCompress/Archives/Rar/RarArchive.cs b/src/SharpCompress/Archives/Rar/RarArchive.cs index 45e97f93..400551b0 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.cs @@ -67,7 +67,13 @@ public class RarArchive : AbstractArchive return new StreamRarArchiveVolume(sourceStream, ReaderOptions, i++).AsEnumerable(); } - protected override IReader CreateReaderForSolidExtraction() + protected override IReader CreateReaderForSolidExtraction() => + CreateReaderForSolidExtractionInternal(); + + protected override ValueTask CreateReaderForSolidExtractionAsync() => + new(CreateReaderForSolidExtractionInternal()); + + private RarReader CreateReaderForSolidExtractionInternal() { if (this.IsMultipartVolume()) { diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index f73621e6..54c59538 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -265,6 +265,9 @@ public class SevenZipArchive : AbstractArchive new SevenZipReader(ReaderOptions, this); + protected override ValueTask CreateReaderForSolidExtractionAsync() => + new(new SevenZipReader(ReaderOptions, this)); + public override bool IsSolid => Entries .Where(x => !x.IsDirectory) diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index 9423c915..195d7ee4 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -366,4 +366,11 @@ public class TarArchive : AbstractWritableArchive stream.Position = 0; return TarReader.Open(stream); } + + protected override ValueTask CreateReaderForSolidExtractionAsync() + { + var stream = Volumes.Single().Stream; + stream.Position = 0; + return new(TarReader.Open(stream)); + } } diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 328aa7e0..b0da0d05 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -592,4 +592,11 @@ public class ZipArchive : AbstractWritableArchive ((IStreamStack)stream).StackSeek(0); return ZipReader.Open(stream, ReaderOptions, Entries); } + + protected override ValueTask CreateReaderForSolidExtractionAsync() + { + var stream = Volumes.Single().Stream; + stream.Position = 0; + return new(ZipReader.Open(stream)); + } } diff --git a/src/SharpCompress/Factories/AceFactory.cs b/src/SharpCompress/Factories/AceFactory.cs index 02a6e489..987176ec 100644 --- a/src/SharpCompress/Factories/AceFactory.cs +++ b/src/SharpCompress/Factories/AceFactory.cs @@ -27,18 +27,21 @@ namespace SharpCompress.Factories Stream stream, string? password = null, int bufferSize = ReaderOptions.DefaultBufferSize - ) - { - return AceHeader.IsArchive(stream); - } + ) => AceHeader.IsArchive(stream); public IReader OpenReader(Stream stream, ReaderOptions? options) => AceReader.Open(stream, options); - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default - ) => new(OpenReader(stream, options)); + ) => new(AceReader.Open(stream, options)); + + public override ValueTask IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) => new(IsArchive(stream, password, bufferSize)); } } diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index f497509a..5f7aa36e 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -44,14 +44,16 @@ namespace SharpCompress.Factories public IReader OpenReader(Stream stream, ReaderOptions? options) => ArcReader.Open(stream, options); - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return new(OpenReader(stream, options)); - } + ) => new(ArcReader.Open(stream, options)); + + public override ValueTask IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) => new(IsArchive(stream, password, bufferSize)); } } diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs index 0f69bcab..590aaad9 100644 --- a/src/SharpCompress/Factories/ArjFactory.cs +++ b/src/SharpCompress/Factories/ArjFactory.cs @@ -35,14 +35,16 @@ namespace SharpCompress.Factories public IReader OpenReader(Stream stream, ReaderOptions? options) => ArjReader.Open(stream, options); - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return new(OpenReader(stream, options)); - } + ) => new(ArjReader.Open(stream, options)); + + public override ValueTask IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) => new(IsArchive(stream, password, bufferSize)); } } diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 7662314c..652a7b96 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -59,6 +59,12 @@ public abstract class Factory : IFactory int bufferSize = ReaderOptions.DefaultBufferSize ); + public abstract ValueTask IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ); + /// public virtual ValueTask IsArchiveAsync( Stream stream, @@ -106,4 +112,34 @@ public abstract class Factory : IFactory return false; } + + internal virtual async ValueTask<(bool, IReaderAsync?)> TryOpenReaderAsync( + SharpCompressStream stream, + ReaderOptions options, + CancellationToken cancellationToken + ) + { + if (this is IReaderFactory readerFactory) + { + long pos = ((IStreamStack)stream).GetPosition(); + + if ( + await IsArchiveAsync( + stream, + options.Password, + options.BufferSize, + cancellationToken + ) + ) + { + ((IStreamStack)stream).StackSeek(pos); + return ( + true, + await readerFactory.OpenReaderAsync(stream, options, cancellationToken) + ); + } + } + + return (false, null); + } } diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index 7fc14d75..83ecfb58 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -71,6 +71,12 @@ public class GZipFactory CancellationToken cancellationToken = default ) => GZipArchive.OpenAsync(stream, readerOptions, cancellationToken); + public override ValueTask IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) => new(IsArchive(stream, password, bufferSize)); + /// public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) => GZipArchive.Open(fileInfo, readerOptions); @@ -147,14 +153,14 @@ public class GZipFactory GZipReader.Open(stream, options); /// - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return new(OpenReader(stream, options)); + return new(GZipReader.Open(stream, options)); } #endregion diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index 0180fefb..a856fe58 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -67,6 +67,12 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade CancellationToken cancellationToken = default ) => RarArchive.OpenAsync(fileInfo, readerOptions, cancellationToken); + public override ValueTask IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) => new(IsArchive(stream, password, bufferSize)); + #endregion #region IMultiArchiveFactory @@ -102,14 +108,14 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarReader.Open(stream, options); /// - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return new(OpenReader(stream, options)); + return new(RarReader.Open(stream, options)); } #endregion diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index 5a4be49e..567290a6 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -62,6 +62,12 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory CancellationToken cancellationToken = default ) => SevenZipArchive.OpenAsync(fileInfo, readerOptions, cancellationToken); + public override ValueTask IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) => new(IsArchive(stream, password, bufferSize)); + #endregion #region IMultiArchiveFactory diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 4adccf8b..7a74bdd7 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -61,6 +61,12 @@ public class TarFactory int bufferSize = ReaderOptions.DefaultBufferSize ) => TarArchive.IsTarFile(stream); + public override ValueTask IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) => new(IsArchive(stream, password, bufferSize)); + #endregion #region IArchiveFactory @@ -265,14 +271,14 @@ public class TarFactory TarReader.Open(stream, options); /// - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return new(OpenReader(stream, options)); + return new(TarReader.Open(stream, options)); } #endregion diff --git a/src/SharpCompress/Factories/ZStandardFactory.cs b/src/SharpCompress/Factories/ZStandardFactory.cs index a5c6d84f..d534a8bb 100644 --- a/src/SharpCompress/Factories/ZStandardFactory.cs +++ b/src/SharpCompress/Factories/ZStandardFactory.cs @@ -25,4 +25,10 @@ internal class ZStandardFactory : Factory string? password = null, int bufferSize = 65536 ) => ZStandardStream.IsZStandard(stream); + + public override ValueTask IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) => new(IsArchive(stream, password, bufferSize)); } diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index f8950b44..44a62696 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -81,6 +81,12 @@ public class ZipFactory return false; } + public override ValueTask IsArchiveAsync( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) => new(IsArchive(stream, password, bufferSize)); + /// public override async ValueTask IsArchiveAsync( Stream stream, @@ -189,14 +195,14 @@ public class ZipFactory ZipReader.Open(stream, options); /// - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return new(OpenReader(stream, options)); + return new(ZipReader.Open(stream, options)); } #endregion diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index 01340f24..5621085b 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -12,17 +12,13 @@ namespace SharpCompress.Readers; /// /// A generic push reader that reads unseekable comrpessed streams. /// -public abstract class AbstractReader : IReader +public abstract class AbstractReader : IReader, IReaderAsync where TEntry : Entry where TVolume : Volume { private bool _completed; private IEnumerator? _entriesForCurrentReadStream; - - /// - /// Holds the async entry enumerator when the reader is operating in an async-only mode. - /// - private IAsyncEnumerator? _asyncEntriesForCurrentReadStream; + private IAsyncEnumerator? _entriesForCurrentReadStreamAsync; private bool _wroteCurrentEntry; internal AbstractReader(ReaderOptions options, ArchiveType archiveType) @@ -43,19 +39,31 @@ public abstract class AbstractReader : IReader /// /// Current file entry (from either sync or async enumeration). /// - public TEntry Entry => - _entriesForCurrentReadStream?.Current - ?? _asyncEntriesForCurrentReadStream?.Current - ?? throw new InvalidOperationException("No current entry is available."); + public TEntry Entry + { + get + { + if (_entriesForCurrentReadStreamAsync is not null) + { + return _entriesForCurrentReadStreamAsync.Current; + } + return _entriesForCurrentReadStream.NotNull().Current; + } + } #region IDisposable Members public virtual void Dispose() { _entriesForCurrentReadStream?.Dispose(); - if (_asyncEntriesForCurrentReadStream is IDisposable disposable) + Volume?.Dispose(); + } + + public virtual async ValueTask DisposeAsync() + { + if (_entriesForCurrentReadStreamAsync is not null) { - disposable.Dispose(); + await _entriesForCurrentReadStreamAsync.DisposeAsync(); } Volume?.Dispose(); } @@ -79,7 +87,7 @@ public abstract class AbstractReader : IReader public bool MoveToNextEntry() { - if (_asyncEntriesForCurrentReadStream is not null) + if (_entriesForCurrentReadStreamAsync is not null) { throw new InvalidOperationException( $"{nameof(MoveToNextEntry)} cannot be used after {nameof(MoveToNextEntryAsync)} has been used." @@ -120,17 +128,16 @@ public abstract class AbstractReader : IReader { throw new ReaderCancelledException("Reader has been cancelled."); } - if (_entriesForCurrentReadStream is null && _asyncEntriesForCurrentReadStream is null) + if (_entriesForCurrentReadStreamAsync is null) { - return await LoadStreamForReadingAsync(RequestInitialStream(), cancellationToken) - .ConfigureAwait(false); + return await LoadStreamForReadingAsync(RequestInitialStream()); } if (!_wroteCurrentEntry) { await SkipEntryAsync(cancellationToken).ConfigureAwait(false); } _wroteCurrentEntry = false; - if (await NextEntryForCurrentStreamAsync(cancellationToken).ConfigureAwait(false)) + if (await NextEntryForCurrentStreamAsync(cancellationToken)) { return true; } @@ -140,7 +147,7 @@ public abstract class AbstractReader : IReader protected bool LoadStreamForReading(Stream stream) { - if (_asyncEntriesForCurrentReadStream is not null) + if (_entriesForCurrentReadStreamAsync is not null) { throw new InvalidOperationException( $"{nameof(LoadStreamForReading)} cannot be used after {nameof(LoadStreamForReadingAsync)} has been used." @@ -159,21 +166,12 @@ public abstract class AbstractReader : IReader return _entriesForCurrentReadStream.MoveNext(); } - /// - /// Loads the stream for reading entries asynchronously, using an async entry enumerator when available. - /// - protected async Task LoadStreamForReadingAsync( - Stream stream, - CancellationToken cancellationToken = default - ) + protected async ValueTask LoadStreamForReadingAsync(Stream stream) { - // Always reset the previous async enumerator so that a new stream can be loaded cleanly. - if (_asyncEntriesForCurrentReadStream is IDisposable disposable) + if (_entriesForCurrentReadStreamAsync is not null) { - disposable.Dispose(); + await _entriesForCurrentReadStreamAsync.DisposeAsync(); } - _asyncEntriesForCurrentReadStream = null; - if (stream is null || !stream.CanRead) { throw new MultipartStreamRequiredException( @@ -182,16 +180,8 @@ public abstract class AbstractReader : IReader + "'. A new readable stream is required. Use Cancel if it was intended." ); } - - var entriesAsync = GetEntriesAsync(stream); - if (entriesAsync is null) - { - _entriesForCurrentReadStream = GetEntries(stream).GetEnumerator(); - return _entriesForCurrentReadStream.MoveNext(); - } - - _asyncEntriesForCurrentReadStream = entriesAsync.GetAsyncEnumerator(cancellationToken); - return await _asyncEntriesForCurrentReadStream.MoveNextAsync().ConfigureAwait(false); + _entriesForCurrentReadStreamAsync = GetEntriesAsync(stream).GetAsyncEnumerator(); + return await _entriesForCurrentReadStreamAsync.MoveNextAsync(); } protected virtual Stream RequestInitialStream() => @@ -200,16 +190,19 @@ public abstract class AbstractReader : IReader internal virtual bool NextEntryForCurrentStream() => _entriesForCurrentReadStream.NotNull().MoveNext(); + internal virtual ValueTask NextEntryForCurrentStreamAsync() => + _entriesForCurrentReadStreamAsync.NotNull().MoveNextAsync(); + /// /// Moves the current async enumerator to the next entry. /// internal virtual ValueTask NextEntryForCurrentStreamAsync( - CancellationToken cancellationToken = default + CancellationToken cancellationToken ) { - if (_asyncEntriesForCurrentReadStream is not null) + if (_entriesForCurrentReadStreamAsync is not null) { - return _asyncEntriesForCurrentReadStream.MoveNextAsync(); + return _entriesForCurrentReadStreamAsync.MoveNextAsync(); } return new ValueTask(NextEntryForCurrentStream()); @@ -217,10 +210,14 @@ public abstract class AbstractReader : IReader protected abstract IEnumerable GetEntries(Stream stream); - /// - /// Optionally returns an async entry sequence for formats that support true async header parsing. - /// - protected virtual IAsyncEnumerable? GetEntriesAsync(Stream stream) => null; + protected virtual async IAsyncEnumerable GetEntriesAsync(Stream stream) + { + await Task.CompletedTask; + foreach (var entry in GetEntries(stream)) + { + yield return entry; + } + } #region Entry Skip/Write @@ -441,4 +438,5 @@ public abstract class AbstractReader : IReader #endregion IEntry IReader.Entry => Entry; + IEntry IReaderAsync.Entry => Entry; } diff --git a/src/SharpCompress/Readers/IReader.cs b/src/SharpCompress/Readers/IReader.cs index 57423708..6f163bd5 100644 --- a/src/SharpCompress/Readers/IReader.cs +++ b/src/SharpCompress/Readers/IReader.cs @@ -18,6 +18,28 @@ public interface IReader : IDisposable /// void WriteEntryTo(Stream writableStream); + bool Cancelled { get; } + void Cancel(); + + /// + /// Moves to the next entry by reading more data from the underlying stream. This skips if data has not been read. + /// + /// + bool MoveToNextEntry(); + + /// + /// 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(); +} + +public interface IReaderAsync : IAsyncDisposable +{ + ArchiveType ArchiveType { get; } + + IEntry Entry { get; } + /// /// Decompresses the current entry to the stream asynchronously. This cannot be called twice for the current entry. /// @@ -28,12 +50,6 @@ public interface IReader : IDisposable bool Cancelled { get; } void Cancel(); - /// - /// Moves to the next entry by reading more data from the underlying stream. This skips if data has not been read. - /// - /// - bool MoveToNextEntry(); - /// /// Moves to the next entry asynchronously by reading more data from the underlying stream. This skips if data has not been read. /// @@ -41,12 +57,6 @@ public interface IReader : IDisposable /// 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. diff --git a/src/SharpCompress/Readers/IReaderAsyncExtensions.cs b/src/SharpCompress/Readers/IReaderAsyncExtensions.cs new file mode 100644 index 00000000..26de3a13 --- /dev/null +++ b/src/SharpCompress/Readers/IReaderAsyncExtensions.cs @@ -0,0 +1,69 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; + +namespace SharpCompress.Readers; + +public static class IReaderAsyncExtensions +{ + extension(IReaderAsync reader) + { + /// + /// Extract to specific directory asynchronously, retaining filename + /// + public async Task WriteEntryToDirectoryAsync( + string destinationDirectory, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + await ExtractionMethods + .WriteEntryToDirectoryAsync( + reader.Entry, + destinationDirectory, + options, + reader.WriteEntryToFileAsync, + cancellationToken + ) + .ConfigureAwait(false); + + /// + /// Extract to specific file asynchronously + /// + public async Task WriteEntryToFileAsync( + string destinationFileName, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + await ExtractionMethods + .WriteEntryToFileAsync( + reader.Entry, + destinationFileName, + options, + async (x, fm, ct) => + { + using var fs = File.Open(destinationFileName, fm); + await reader.WriteEntryToAsync(fs, ct).ConfigureAwait(false); + }, + cancellationToken + ) + .ConfigureAwait(false); + + /// + /// Extract all remaining unread entries to specific directory asynchronously, retaining filename + /// + public async Task WriteAllToDirectoryAsync( + string destinationDirectory, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) + { + while (await reader.MoveToNextEntryAsync(cancellationToken)) + { + await reader + .WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken) + .ConfigureAwait(false); + } + } + } +} diff --git a/src/SharpCompress/Readers/IReaderExtensions.cs b/src/SharpCompress/Readers/IReaderExtensions.cs index 65c6b1fa..cfa7c13a 100644 --- a/src/SharpCompress/Readers/IReaderExtensions.cs +++ b/src/SharpCompress/Readers/IReaderExtensions.cs @@ -1,6 +1,4 @@ using System.IO; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common; namespace SharpCompress.Readers; @@ -66,62 +64,5 @@ public static class IReaderExtensions reader.WriteEntryTo(fs); } ); - - /// - /// Extract to specific directory asynchronously, retaining filename - /// - public async Task WriteEntryToDirectoryAsync( - string destinationDirectory, - ExtractionOptions? options = null, - CancellationToken cancellationToken = default - ) => - await ExtractionMethods - .WriteEntryToDirectoryAsync( - reader.Entry, - destinationDirectory, - options, - reader.WriteEntryToFileAsync, - cancellationToken - ) - .ConfigureAwait(false); - - /// - /// Extract to specific file asynchronously - /// - public async Task WriteEntryToFileAsync( - string destinationFileName, - ExtractionOptions? options = null, - CancellationToken cancellationToken = default - ) => - await ExtractionMethods - .WriteEntryToFileAsync( - reader.Entry, - destinationFileName, - options, - async (x, fm, ct) => - { - using var fs = File.Open(destinationFileName, fm); - await reader.WriteEntryToAsync(fs, ct).ConfigureAwait(false); - }, - cancellationToken - ) - .ConfigureAwait(false); - - /// - /// Extract all remaining unread entries to specific directory asynchronously, retaining filename - /// - public async Task WriteAllToDirectoryAsync( - string destinationDirectory, - ExtractionOptions? options = null, - CancellationToken cancellationToken = default - ) - { - while (await reader.MoveToNextEntryAsync(cancellationToken)) - { - await reader - .WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken) - .ConfigureAwait(false); - } - } } } diff --git a/src/SharpCompress/Readers/IReaderFactory.cs b/src/SharpCompress/Readers/IReaderFactory.cs index 4b311fde..9dec99d9 100644 --- a/src/SharpCompress/Readers/IReaderFactory.cs +++ b/src/SharpCompress/Readers/IReaderFactory.cs @@ -13,17 +13,9 @@ public interface IReaderFactory : Factories.IFactory /// /// IReader OpenReader(Stream stream, ReaderOptions? options); - - /// - /// Opens a Reader asynchronously for Non-seeking usage - /// - /// - /// - /// - /// - ValueTask OpenReaderAsync( + ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, - CancellationToken cancellationToken = default + CancellationToken cancellationToken ); } diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 6796c48f..8f937528 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -24,7 +24,7 @@ public static class ReaderFactory /// /// /// - public static Task OpenAsync( + public static ValueTask OpenAsync( string filePath, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -47,7 +47,7 @@ public static class ReaderFactory /// /// /// - public static Task OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -110,14 +110,7 @@ public static class ReaderFactory ); } - /// - /// Opens a Reader for Non-seeking usage asynchronously - /// - /// - /// - /// - /// - public static async Task OpenAsync( + public static async ValueTask OpenAsync( Stream stream, ReaderOptions? options = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Readers/Zip/ZipReader.cs b/src/SharpCompress/Readers/Zip/ZipReader.cs index 673d6ec7..d15fa7e0 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.cs @@ -98,7 +98,7 @@ public class ZipReader : AbstractReader /// /// Returns entries asynchronously for streams that only support async reads. /// - protected override IAsyncEnumerable? GetEntriesAsync(Stream stream) => + protected override IAsyncEnumerable GetEntriesAsync(Stream stream) => new ZipEntryAsyncEnumerable(_headerFactory, stream); /// diff --git a/tests/SharpCompress.Test/GZip/AsyncTests.cs b/tests/SharpCompress.Test/GZip/AsyncTests.cs index 562b82e8..3ab690f7 100644 --- a/tests/SharpCompress.Test/GZip/AsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/AsyncTests.cs @@ -9,6 +9,7 @@ using SharpCompress.Common; using SharpCompress.Compressors; using SharpCompress.Compressors.Deflate; using SharpCompress.Readers; +using SharpCompress.Test.Mocks; using SharpCompress.Writers; using Xunit; @@ -25,7 +26,7 @@ public class AsyncTests : TestBase #else await using var stream = File.OpenRead(testArchive); #endif - using var reader = ReaderFactory.Open(stream); + await using var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream)); await reader.WriteAllToDirectoryAsync( SCRATCH_FILES_PATH, @@ -50,9 +51,9 @@ public class AsyncTests : TestBase #else await using var stream = File.OpenRead(testArchive); #endif - using var reader = ReaderFactory.Open(stream); + await using var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream)); - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) { @@ -118,7 +119,10 @@ public class AsyncTests : TestBase var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"); using var stream = File.OpenRead(testArchive); - using var reader = ReaderFactory.Open(stream); + await using var reader = await ReaderFactory.OpenAsync( + new AsyncOnlyStream(stream), + cancellationToken: cts.Token + ); await reader.WriteAllToDirectoryAsync( SCRATCH_FILES_PATH, diff --git a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs index 20e9a34f..6eb46783 100644 --- a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs @@ -70,7 +70,7 @@ public class GZipReaderAsyncTests : ReaderTests bufferSize: options.BufferSize ); using var testStream = new TestStream(protectedStream); - using (var reader = ReaderFactory.Open(testStream, options)) + await using (var reader = await ReaderFactory.OpenAsync(testStream, options, default)) { await UseReaderAsync(reader, expectedCompression); protectedStream.ThrowOnDispose = false; @@ -82,9 +82,9 @@ public class GZipReaderAsyncTests : ReaderTests Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message); } - private async Task UseReaderAsync(IReader reader, CompressionType expectedCompression) + private async Task UseReaderAsync(IReaderAsync reader, CompressionType expectedCompression) { - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) { diff --git a/tests/SharpCompress.Test/ProgressReportTests.cs b/tests/SharpCompress.Test/ProgressReportTests.cs index 75fa2116..92e1507b 100644 --- a/tests/SharpCompress.Test/ProgressReportTests.cs +++ b/tests/SharpCompress.Test/ProgressReportTests.cs @@ -7,7 +7,9 @@ using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Archives.Zip; using SharpCompress.Common; +using SharpCompress.IO; using SharpCompress.Readers; +using SharpCompress.Test.Mocks; using SharpCompress.Writers; using SharpCompress.Writers.Tar; using SharpCompress.Writers.Zip; @@ -538,9 +540,14 @@ public class ProgressReportTests : TestBase archiveStream.Position = 0; var readerOptions = new ReaderOptions { Progress = progress }; - using (var reader = ReaderFactory.Open(archiveStream, readerOptions)) + await using ( + var reader = await ReaderFactory.OpenAsync( + new AsyncOnlyStream(archiveStream), + readerOptions + ) + ) { - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) { diff --git a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs index 1c6a33f0..2ff2547b 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs @@ -647,11 +647,11 @@ public class RarArchiveAsyncTests : ArchiveTests { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); using var stream = File.OpenRead(testArchive); - using var archive = ArchiveFactory.Open(stream); - Assert.True(archive.IsSolid); - using (var reader = archive.ExtractAllEntries()) + await using var archive = await ArchiveFactory.OpenAsync(stream); + Assert.True(await archive.IsSolidAsync()); + await using (var reader = await archive.ExtractAllEntriesAsync()) { - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) { @@ -665,7 +665,7 @@ public class RarArchiveAsyncTests : ArchiveTests } VerifyFiles(); - foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)) { await entry.WriteToDirectoryAsync( SCRATCH_FILES_PATH, diff --git a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs index ed497c7b..d1c81af5 100644 --- a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs @@ -7,6 +7,7 @@ using SharpCompress.Archives.Rar; using SharpCompress.Common; using SharpCompress.Readers; using SharpCompress.Readers.Rar; +using SharpCompress.Test.Mocks; using Xunit; namespace SharpCompress.Test.Rar; @@ -204,7 +205,7 @@ public class RarReaderAsyncTests : ReaderTests private async Task DoRar_Entry_Stream_Async(string filename) { using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename))) - using (var reader = ReaderFactory.Open(stream)) + await using (var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream))) { while (await reader.MoveToNextEntryAsync()) { @@ -248,9 +249,14 @@ public class RarReaderAsyncTests : ReaderTests using ( var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Rar.Audio_program.rar")) ) - using (var reader = ReaderFactory.Open(stream, new ReaderOptions { LookForHeader = true })) + await using ( + var reader = await ReaderFactory.OpenAsync( + new AsyncOnlyStream(stream), + new ReaderOptions { LookForHeader = true } + ) + ) { - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType); await reader.WriteEntryToDirectoryAsync( @@ -310,8 +316,11 @@ public class RarReaderAsyncTests : ReaderTests private async Task DoRar_Solid_Skip_Reader_Async(string filename) { using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); - using var reader = ReaderFactory.Open(stream, new ReaderOptions { LookForHeader = true }); - while (reader.MoveToNextEntry()) + await using var reader = await ReaderFactory.OpenAsync( + new AsyncOnlyStream(stream), + new ReaderOptions { LookForHeader = true } + ); + while (await reader.MoveToNextEntryAsync()) { if (reader.Entry.Key.NotNull().Contains("jpg")) { @@ -333,8 +342,11 @@ public class RarReaderAsyncTests : ReaderTests private async Task DoRar_Reader_Skip_Async(string filename) { using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); - using var reader = ReaderFactory.Open(stream, new ReaderOptions { LookForHeader = true }); - while (reader.MoveToNextEntry()) + await using var reader = await ReaderFactory.OpenAsync( + new AsyncOnlyStream(stream), + new ReaderOptions { LookForHeader = true } + ); + while (await reader.MoveToNextEntryAsync()) { if (reader.Entry.Key.NotNull().Contains("jpg")) { @@ -355,7 +367,10 @@ public class RarReaderAsyncTests : ReaderTests { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); using Stream stream = File.OpenRead(testArchive); - using var reader = ReaderFactory.Open(stream, readerOptions ?? new ReaderOptions()); + await using var reader = await ReaderFactory.OpenAsync( + new AsyncOnlyStream(stream), + readerOptions ?? new ReaderOptions() + ); while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 62b327e2..0f12feeb 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -145,7 +145,13 @@ public abstract class ReaderTests : TestBase bufferSize: options.BufferSize ); using var testStream = new TestStream(protectedStream); - using (var reader = ReaderFactory.Open(testStream, options)) + await using ( + var reader = await ReaderFactory.OpenAsync( + new AsyncOnlyStream(testStream), + options, + cancellationToken + ) + ) { await UseReaderAsync(reader, expectedCompression, cancellationToken); protectedStream.ThrowOnDispose = false; @@ -158,7 +164,7 @@ public abstract class ReaderTests : TestBase } public async Task UseReaderAsync( - IReader reader, + IReaderAsync reader, CompressionType? expectedCompression, CancellationToken cancellationToken = default ) diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index 0bc93d83..d7af1102 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -23,9 +23,9 @@ public class TarReaderAsyncTests : ReaderTests using Stream stream = new ForwardOnlyStream( File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")) ); - using var reader = ReaderFactory.Open(stream); + await using var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream)); var x = 0; - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) { @@ -182,14 +182,16 @@ public class TarReaderAsyncTests : ReaderTests { var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"); using Stream stream = File.OpenRead(archiveFullPath); - using var reader = ReaderFactory.Open(stream); + await using var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream)); var memoryStream = new MemoryStream(); - Assert.True(reader.MoveToNextEntry()); - Assert.True(reader.MoveToNextEntry()); + Assert.True(await reader.MoveToNextEntryAsync()); + Assert.True(await reader.MoveToNextEntryAsync()); await reader.WriteEntryToAsync(memoryStream); stream.Close(); - Assert.Throws(() => reader.MoveToNextEntry()); + await Assert.ThrowsAsync(async () => + await reader.MoveToNextEntryAsync() + ); } [Fact] @@ -197,14 +199,16 @@ public class TarReaderAsyncTests : ReaderTests { var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "TarCorrupted.tar"); using Stream stream = File.OpenRead(archiveFullPath); - using var reader = ReaderFactory.Open(stream); + await using var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream)); var memoryStream = new MemoryStream(); - Assert.True(reader.MoveToNextEntry()); - Assert.True(reader.MoveToNextEntry()); + Assert.True(await reader.MoveToNextEntryAsync()); + Assert.True(await reader.MoveToNextEntryAsync()); await reader.WriteEntryToAsync(memoryStream); stream.Close(); - Assert.Throws(() => reader.MoveToNextEntry()); + await Assert.ThrowsAsync(async () => + await reader.MoveToNextEntryAsync() + ); } #if LINUX diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index 410e69f7..984d3b91 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -92,9 +92,10 @@ public class WriterTests : TestBase readerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default; - using var reader = ReaderFactory.Open( - SharpCompressStream.Create(stream, leaveOpen: true), - readerOptions + await using var reader = await ReaderFactory.OpenAsync( + new AsyncOnlyStream(SharpCompressStream.Create(stream, leaveOpen: true)), + readerOptions, + cancellationToken ); await reader.WriteAllToDirectoryAsync( SCRATCH_FILES_PATH, diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index fbb5ee3a..34d612af 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -20,7 +20,7 @@ public class ZipReaderAsyncTests : ReaderTests { var path = Path.Combine(TEST_ARCHIVES_PATH, "PrePostHeaders.zip"); using Stream stream = new ForwardOnlyStream(File.OpenRead(path)); - using var reader = ReaderFactory.Open(stream); + await using var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream)); var count = 0; while (await reader.MoveToNextEntryAsync()) { @@ -65,7 +65,7 @@ public class ZipReaderAsyncTests : ReaderTests using Stream stream = new ForwardOnlyStream( File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) ); - using var reader = ReaderFactory.Open(stream); + await using var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream)); var x = 0; while (await reader.MoveToNextEntryAsync()) { @@ -144,7 +144,7 @@ public class ZipReaderAsyncTests : ReaderTests using var stream = new TestStream( File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) ); - using (var reader = ReaderFactory.Open(stream)) + await using (var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream))) { while (await reader.MoveToNextEntryAsync()) { @@ -168,7 +168,7 @@ public class ZipReaderAsyncTests : ReaderTests File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) ) ); - var reader = await ReaderFactory.OpenAsync(stream); + await using var reader = await ReaderFactory.OpenAsync(stream); while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) From b501bac54ae3f70fba9d86e437fb2e4ea79fd960 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 12:02:26 +0000 Subject: [PATCH 29/46] better names for new interfaces --- src/SharpCompress/Archives/AbstractArchive.cs | 8 +- src/SharpCompress/Archives/ArchiveFactory.cs | 10 +- .../Archives/AutoArchiveFactory.cs | 4 +- .../Archives/GZip/GZipArchive.cs | 10 +- src/SharpCompress/Archives/IArchive.cs | 37 ------- .../Archives/IArchiveAsyncExtensions.cs | 100 ------------------ src/SharpCompress/Archives/IArchiveFactory.cs | 4 +- src/SharpCompress/Archives/IAsyncArchive.cs | 43 ++++++++ .../Archives/IAsyncArchiveExtensions.cs | 93 ++++++++++++++++ .../Archives/IMultiArchiveFactory.cs | 4 +- src/SharpCompress/Archives/Rar/RarArchive.cs | 10 +- .../Archives/SevenZip/SevenZipArchive.cs | 10 +- src/SharpCompress/Archives/Tar/TarArchive.cs | 10 +- src/SharpCompress/Archives/Zip/ZipArchive.cs | 10 +- src/SharpCompress/Factories/AceFactory.cs | 2 +- src/SharpCompress/Factories/ArcFactory.cs | 2 +- src/SharpCompress/Factories/ArjFactory.cs | 2 +- src/SharpCompress/Factories/Factory.cs | 2 +- src/SharpCompress/Factories/GZipFactory.cs | 10 +- src/SharpCompress/Factories/RarFactory.cs | 10 +- .../Factories/SevenZipFactory.cs | 8 +- src/SharpCompress/Factories/TarFactory.cs | 10 +- src/SharpCompress/Factories/ZipFactory.cs | 10 +- src/SharpCompress/Readers/AbstractReader.cs | 4 +- src/SharpCompress/Readers/IAsyncReader.cs | 38 +++++++ ...xtensions.cs => IAsyncReaderExtensions.cs} | 4 +- src/SharpCompress/Readers/IReader.cs | 33 ------ src/SharpCompress/Readers/IReaderFactory.cs | 2 +- src/SharpCompress/Readers/ReaderFactory.cs | 6 +- .../GZip/GZipReaderAsyncTests.cs | 2 +- tests/SharpCompress.Test/ReaderTests.cs | 2 +- 31 files changed, 252 insertions(+), 248 deletions(-) delete mode 100644 src/SharpCompress/Archives/IArchiveAsyncExtensions.cs create mode 100644 src/SharpCompress/Archives/IAsyncArchive.cs create mode 100644 src/SharpCompress/Archives/IAsyncArchiveExtensions.cs create mode 100644 src/SharpCompress/Readers/IAsyncReader.cs rename src/SharpCompress/Readers/{IReaderAsyncExtensions.cs => IAsyncReaderExtensions.cs} (96%) diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index b66d7845..12ab15a6 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -7,7 +7,7 @@ using SharpCompress.Readers; namespace SharpCompress.Archives; -public abstract class AbstractArchive : IArchive, IArchiveAsync +public abstract class AbstractArchive : IArchive, IAsyncArchive where TEntry : IArchiveEntry where TVolume : IVolume { @@ -133,7 +133,7 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync } protected abstract IReader CreateReaderForSolidExtraction(); - protected abstract ValueTask CreateReaderForSolidExtractionAsync(); + protected abstract ValueTask CreateReaderForSolidExtractionAsync(); /// /// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files). @@ -187,12 +187,12 @@ public abstract class AbstractArchive : IArchive, IArchiveAsync } public virtual IAsyncEnumerable EntriesAsync => _lazyEntriesAsync; - IAsyncEnumerable IArchiveAsync.EntriesAsync => + IAsyncEnumerable IAsyncArchive.EntriesAsync => EntriesAsync.Cast(); public IAsyncEnumerable VolumesAsync => _lazyVolumesAsync.Cast(); - public async ValueTask ExtractAllEntriesAsync() + public async ValueTask ExtractAllEntriesAsync() { if (!IsSolid && Type != ArchiveType.SevenZip) { diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 245940f2..35e18db9 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -33,7 +33,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -79,7 +79,7 @@ public static class ArchiveFactory /// /// /// - public static Task OpenAsync( + public static Task OpenAsync( string filePath, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -107,7 +107,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( FileInfo fileInfo, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -152,7 +152,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IEnumerable fileInfos, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -212,7 +212,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async Task OpenAsync( IEnumerable streams, ReaderOptions? options = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/AutoArchiveFactory.cs b/src/SharpCompress/Archives/AutoArchiveFactory.cs index 0c36d1c0..7751c7e0 100644 --- a/src/SharpCompress/Archives/AutoArchiveFactory.cs +++ b/src/SharpCompress/Archives/AutoArchiveFactory.cs @@ -34,7 +34,7 @@ internal class AutoArchiveFactory : IArchiveFactory public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) => ArchiveFactory.Open(stream, readerOptions); - public async ValueTask OpenAsync( + public async ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -43,7 +43,7 @@ internal class AutoArchiveFactory : IArchiveFactory public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) => ArchiveFactory.Open(fileInfo, readerOptions); - public async ValueTask OpenAsync( + public async ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index 82d9c53d..f089d7d1 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -108,7 +108,7 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -124,7 +124,7 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -140,7 +140,7 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -156,7 +156,7 @@ public class GZipArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -337,7 +337,7 @@ public class GZipArchive : AbstractWritableArchive return GZipReader.Open(stream); } - protected override ValueTask CreateReaderForSolidExtractionAsync() + protected override ValueTask CreateReaderForSolidExtractionAsync() { var stream = Volumes.Single().Stream; stream.Position = 0; diff --git a/src/SharpCompress/Archives/IArchive.cs b/src/SharpCompress/Archives/IArchive.cs index 9f5b78a9..3ed7490d 100644 --- a/src/SharpCompress/Archives/IArchive.cs +++ b/src/SharpCompress/Archives/IArchive.cs @@ -1,47 +1,10 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Readers; namespace SharpCompress.Archives; -public interface IArchiveAsync : IAsyncDisposable -{ - IAsyncEnumerable EntriesAsync { get; } - IAsyncEnumerable VolumesAsync { get; } - - ArchiveType Type { get; } - - /// - /// Use this method to extract all entries in an archive in order. - /// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be - /// extracted sequentially for the best performance. - /// - ValueTask ExtractAllEntriesAsync(); - - /// - /// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files). - /// Rar Archives can be SOLID while all 7Zip archives are considered SOLID. - /// - ValueTask IsSolidAsync(); - - /// - /// This checks to see if all the known entries have IsComplete = true - /// - ValueTask IsCompleteAsync(); - - /// - /// The total size of the files compressed in the archive. - /// - ValueTask TotalSizeAsync(); - - /// - /// The total size of the files as uncompressed in the archive. - /// - ValueTask TotalUncompressSizeAsync(); -} - public interface IArchive : IDisposable { IEnumerable Entries { get; } diff --git a/src/SharpCompress/Archives/IArchiveAsyncExtensions.cs b/src/SharpCompress/Archives/IArchiveAsyncExtensions.cs deleted file mode 100644 index 95a1617f..00000000 --- a/src/SharpCompress/Archives/IArchiveAsyncExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using SharpCompress.Common; -using SharpCompress.Readers; - -namespace SharpCompress.Archives; - -public static class IArchiveAsyncExtensions -{ - /// The archive to extract. - extension(IArchiveAsync archive) - { - /// - /// Extract to specific directory asynchronously with progress reporting and cancellation support - /// - /// The folder to extract into. - /// Extraction options. - /// Optional progress reporter for tracking extraction progress. - /// Optional cancellation token. - public async Task WriteToDirectoryAsync( - string destinationDirectory, - ExtractionOptions? options = null, - IProgress? progress = null, - CancellationToken cancellationToken = default - ) - { - // For solid archives (Rar, 7Zip), use the optimized reader-based approach - if (await archive.IsSolidAsync() || archive.Type == ArchiveType.SevenZip) - { - await using var reader = await archive.ExtractAllEntriesAsync(); - await reader.WriteAllToDirectoryAsync( - destinationDirectory, - options, - cancellationToken - ); - } - else - { - // For non-solid archives, extract entries directly - await archive.WriteToDirectoryAsyncInternal( - destinationDirectory, - options, - progress, - cancellationToken - ); - } - } - - private async Task WriteToDirectoryAsyncInternal( - string destinationDirectory, - ExtractionOptions? options, - IProgress? progress, - CancellationToken cancellationToken - ) - { - // Prepare for progress reporting - var totalBytes = await archive.TotalUncompressSizeAsync(); - var bytesRead = 0L; - - // Tracking for created directories. - var seenDirectories = new HashSet(); - - // Extract - await foreach (var entry in archive.EntriesAsync.WithCancellation(cancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (entry.IsDirectory) - { - var dirPath = Path.Combine( - destinationDirectory, - entry.Key.NotNull("Entry Key is null") - ); - if ( - Path.GetDirectoryName(dirPath + "/") is { } parentDirectory - && seenDirectories.Add(dirPath) - ) - { - Directory.CreateDirectory(parentDirectory); - } - continue; - } - - // Use the entry's WriteToDirectoryAsync method which respects ExtractionOptions - await entry - .WriteToDirectoryAsync(destinationDirectory, options, cancellationToken) - .ConfigureAwait(false); - - // Update progress - bytesRead += entry.Size; - progress?.Report( - new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes) - ); - } - } - } -} diff --git a/src/SharpCompress/Archives/IArchiveFactory.cs b/src/SharpCompress/Archives/IArchiveFactory.cs index 40e25d00..1c1253f6 100644 --- a/src/SharpCompress/Archives/IArchiveFactory.cs +++ b/src/SharpCompress/Archives/IArchiveFactory.cs @@ -34,7 +34,7 @@ public interface IArchiveFactory : IFactory /// An open, readable and seekable stream. /// reading options. /// Cancellation token. - ValueTask OpenAsync( + ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -53,7 +53,7 @@ public interface IArchiveFactory : IFactory /// the file to open. /// reading options. /// Cancellation token. - ValueTask OpenAsync( + ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/IAsyncArchive.cs b/src/SharpCompress/Archives/IAsyncArchive.cs new file mode 100644 index 00000000..bd3f290e --- /dev/null +++ b/src/SharpCompress/Archives/IAsyncArchive.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Readers; + +namespace SharpCompress.Archives; + +public interface IAsyncArchive : IAsyncDisposable +{ + IAsyncEnumerable EntriesAsync { get; } + IAsyncEnumerable VolumesAsync { get; } + + ArchiveType Type { get; } + + /// + /// Use this method to extract all entries in an archive in order. + /// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be + /// extracted sequentially for the best performance. + /// + ValueTask ExtractAllEntriesAsync(); + + /// + /// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files). + /// Rar Archives can be SOLID while all 7Zip archives are considered SOLID. + /// + ValueTask IsSolidAsync(); + + /// + /// This checks to see if all the known entries have IsComplete = true + /// + ValueTask IsCompleteAsync(); + + /// + /// The total size of the files compressed in the archive. + /// + ValueTask TotalSizeAsync(); + + /// + /// The total size of the files as uncompressed in the archive. + /// + ValueTask TotalUncompressSizeAsync(); +} diff --git a/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs new file mode 100644 index 00000000..b6b0cad1 --- /dev/null +++ b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Readers; + +namespace SharpCompress.Archives; + +public static class IAsyncArchiveExtensions +{ + /// + /// Extract to specific directory asynchronously with progress reporting and cancellation support + /// + /// The archive to extract. + /// The folder to extract into. + /// Extraction options. + /// Optional progress reporter for tracking extraction progress. + /// Optional cancellation token. + public static async Task WriteToDirectoryAsync( + this IAsyncArchive archive, + string destinationDirectory, + ExtractionOptions? options = null, + IProgress? progress = null, + CancellationToken cancellationToken = default + ) + { + // For solid archives (Rar, 7Zip), use the optimized reader-based approach + if (await archive.IsSolidAsync() || archive.Type == ArchiveType.SevenZip) + { + await using var reader = await archive.ExtractAllEntriesAsync(); + await reader.WriteAllToDirectoryAsync(destinationDirectory, options, cancellationToken); + } + else + { + // For non-solid archives, extract entries directly + await archive.WriteToDirectoryAsyncInternal( + destinationDirectory, + options, + progress, + cancellationToken + ); + } + } + + private static async Task WriteToDirectoryAsyncInternal( + this IAsyncArchive archive, + string destinationDirectory, + ExtractionOptions? options, + IProgress? progress, + CancellationToken cancellationToken + ) + { + // Prepare for progress reporting + var totalBytes = await archive.TotalUncompressSizeAsync(); + var bytesRead = 0L; + + // Tracking for created directories. + var seenDirectories = new HashSet(); + + // Extract + await foreach (var entry in archive.EntriesAsync.WithCancellation(cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (entry.IsDirectory) + { + var dirPath = Path.Combine( + destinationDirectory, + entry.Key.NotNull("Entry Key is null") + ); + if ( + Path.GetDirectoryName(dirPath + "/") is { } parentDirectory + && seenDirectories.Add(dirPath) + ) + { + Directory.CreateDirectory(parentDirectory); + } + continue; + } + + // Use the entry's WriteToDirectoryAsync method which respects ExtractionOptions + await entry + .WriteToDirectoryAsync(destinationDirectory, options, cancellationToken) + .ConfigureAwait(false); + + // Update progress + bytesRead += entry.Size; + progress?.Report(new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)); + } + } +} diff --git a/src/SharpCompress/Archives/IMultiArchiveFactory.cs b/src/SharpCompress/Archives/IMultiArchiveFactory.cs index 2c96ef53..4fa94d7f 100644 --- a/src/SharpCompress/Archives/IMultiArchiveFactory.cs +++ b/src/SharpCompress/Archives/IMultiArchiveFactory.cs @@ -35,7 +35,7 @@ public interface IMultiArchiveFactory : IFactory /// /// reading options. /// Cancellation token. - ValueTask OpenAsync( + ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -54,7 +54,7 @@ public interface IMultiArchiveFactory : IFactory /// /// reading options. /// Cancellation token. - ValueTask OpenAsync( + ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/Rar/RarArchive.cs b/src/SharpCompress/Archives/Rar/RarArchive.cs index 400551b0..03b8d4d9 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.cs @@ -70,7 +70,7 @@ public class RarArchive : AbstractArchive protected override IReader CreateReaderForSolidExtraction() => CreateReaderForSolidExtractionInternal(); - protected override ValueTask CreateReaderForSolidExtractionAsync() => + protected override ValueTask CreateReaderForSolidExtractionAsync() => new(CreateReaderForSolidExtractionInternal()); private RarReader CreateReaderForSolidExtractionInternal() @@ -195,7 +195,7 @@ public class RarArchive : AbstractArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -211,7 +211,7 @@ public class RarArchive : AbstractArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -227,7 +227,7 @@ public class RarArchive : AbstractArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -243,7 +243,7 @@ public class RarArchive : AbstractArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index 54c59538..43f49abe 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -111,7 +111,7 @@ public class SevenZipArchive : AbstractArchive /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -127,7 +127,7 @@ public class SevenZipArchive : AbstractArchive /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -143,7 +143,7 @@ public class SevenZipArchive : AbstractArchive /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -159,7 +159,7 @@ public class SevenZipArchive : AbstractArchive /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -265,7 +265,7 @@ public class SevenZipArchive : AbstractArchive new SevenZipReader(ReaderOptions, this); - protected override ValueTask CreateReaderForSolidExtractionAsync() => + protected override ValueTask CreateReaderForSolidExtractionAsync() => new(new SevenZipReader(ReaderOptions, this)); public override bool IsSolid => diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index 195d7ee4..f75455ec 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -109,7 +109,7 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -125,7 +125,7 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -141,7 +141,7 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -157,7 +157,7 @@ public class TarArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -367,7 +367,7 @@ public class TarArchive : AbstractWritableArchive return TarReader.Open(stream); } - protected override ValueTask CreateReaderForSolidExtractionAsync() + protected override ValueTask CreateReaderForSolidExtractionAsync() { var stream = Volumes.Single().Stream; stream.Position = 0; diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index b0da0d05..4bd425bb 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -130,7 +130,7 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -146,7 +146,7 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -162,7 +162,7 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -178,7 +178,7 @@ public class ZipArchive : AbstractWritableArchive /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -593,7 +593,7 @@ public class ZipArchive : AbstractWritableArchive return ZipReader.Open(stream, ReaderOptions, Entries); } - protected override ValueTask CreateReaderForSolidExtractionAsync() + protected override ValueTask CreateReaderForSolidExtractionAsync() { var stream = Volumes.Single().Stream; stream.Position = 0; diff --git a/src/SharpCompress/Factories/AceFactory.cs b/src/SharpCompress/Factories/AceFactory.cs index 987176ec..95f647dd 100644 --- a/src/SharpCompress/Factories/AceFactory.cs +++ b/src/SharpCompress/Factories/AceFactory.cs @@ -32,7 +32,7 @@ namespace SharpCompress.Factories public IReader OpenReader(Stream stream, ReaderOptions? options) => AceReader.Open(stream, options); - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index 5f7aa36e..37984112 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -44,7 +44,7 @@ namespace SharpCompress.Factories public IReader OpenReader(Stream stream, ReaderOptions? options) => ArcReader.Open(stream, options); - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs index 590aaad9..6e5f7a30 100644 --- a/src/SharpCompress/Factories/ArjFactory.cs +++ b/src/SharpCompress/Factories/ArjFactory.cs @@ -35,7 +35,7 @@ namespace SharpCompress.Factories public IReader OpenReader(Stream stream, ReaderOptions? options) => ArjReader.Open(stream, options); - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 652a7b96..f28f3d24 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -113,7 +113,7 @@ public abstract class Factory : IFactory return false; } - internal virtual async ValueTask<(bool, IReaderAsync?)> TryOpenReaderAsync( + internal virtual async ValueTask<(bool, IAsyncReader?)> TryOpenReaderAsync( SharpCompressStream stream, ReaderOptions options, CancellationToken cancellationToken diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index 83ecfb58..48f5c63e 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -65,7 +65,7 @@ public class GZipFactory GZipArchive.Open(stream, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -82,7 +82,7 @@ public class GZipFactory GZipArchive.Open(fileInfo, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -97,7 +97,7 @@ public class GZipFactory GZipArchive.Open(streams, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -108,7 +108,7 @@ public class GZipFactory GZipArchive.Open(fileInfos, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -153,7 +153,7 @@ public class GZipFactory GZipReader.Open(stream, options); /// - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index a856fe58..fb9e03ab 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -50,7 +50,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(stream, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -61,7 +61,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(fileInfo, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -82,7 +82,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(streams, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -93,7 +93,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarArchive.Open(fileInfos, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -108,7 +108,7 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade RarReader.Open(stream, options); /// - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index 567290a6..c387e3b3 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -45,7 +45,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(stream, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -56,7 +56,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(fileInfo, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -77,7 +77,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(streams, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -88,7 +88,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory SevenZipArchive.Open(fileInfos, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 7a74bdd7..4e22e0bd 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -76,7 +76,7 @@ public class TarFactory TarArchive.Open(stream, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -87,7 +87,7 @@ public class TarFactory TarArchive.Open(fileInfo, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -102,7 +102,7 @@ public class TarFactory TarArchive.Open(streams, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -113,7 +113,7 @@ public class TarFactory TarArchive.Open(fileInfos, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -271,7 +271,7 @@ public class TarFactory TarReader.Open(stream, options); /// - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index 44a62696..a9e62f14 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -143,7 +143,7 @@ public class ZipFactory ZipArchive.Open(stream, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -154,7 +154,7 @@ public class ZipFactory ZipArchive.Open(fileInfo, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -169,7 +169,7 @@ public class ZipFactory ZipArchive.Open(streams, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -180,7 +180,7 @@ public class ZipFactory ZipArchive.Open(fileInfos, readerOptions); /// - public ValueTask OpenAsync( + public ValueTask OpenAsync( IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -195,7 +195,7 @@ public class ZipFactory ZipReader.Open(stream, options); /// - public ValueTask OpenReaderAsync( + public ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index 5621085b..63caf92b 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -12,7 +12,7 @@ namespace SharpCompress.Readers; /// /// A generic push reader that reads unseekable comrpessed streams. /// -public abstract class AbstractReader : IReader, IReaderAsync +public abstract class AbstractReader : IReader, IAsyncReader where TEntry : Entry where TVolume : Volume { @@ -438,5 +438,5 @@ public abstract class AbstractReader : IReader, IReaderAsync #endregion IEntry IReader.Entry => Entry; - IEntry IReaderAsync.Entry => Entry; + IEntry IAsyncReader.Entry => Entry; } diff --git a/src/SharpCompress/Readers/IAsyncReader.cs b/src/SharpCompress/Readers/IAsyncReader.cs new file mode 100644 index 00000000..d5695d72 --- /dev/null +++ b/src/SharpCompress/Readers/IAsyncReader.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; + +namespace SharpCompress.Readers; + +public interface IAsyncReader : IAsyncDisposable +{ + ArchiveType ArchiveType { get; } + + IEntry Entry { get; } + + /// + /// Decompresses the current entry to the stream asynchronously. This cannot be called twice for the current entry. + /// + /// + /// + Task WriteEntryToAsync(Stream writableStream, CancellationToken cancellationToken = default); + + bool Cancelled { get; } + void Cancel(); + + /// + /// 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 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/Readers/IReaderAsyncExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs similarity index 96% rename from src/SharpCompress/Readers/IReaderAsyncExtensions.cs rename to src/SharpCompress/Readers/IAsyncReaderExtensions.cs index 26de3a13..77e46964 100644 --- a/src/SharpCompress/Readers/IReaderAsyncExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -5,9 +5,9 @@ using SharpCompress.Common; namespace SharpCompress.Readers; -public static class IReaderAsyncExtensions +public static class IAsyncReaderExtensions { - extension(IReaderAsync reader) + extension(IAsyncReader reader) { /// /// Extract to specific directory asynchronously, retaining filename diff --git a/src/SharpCompress/Readers/IReader.cs b/src/SharpCompress/Readers/IReader.cs index 6f163bd5..a38d61d6 100644 --- a/src/SharpCompress/Readers/IReader.cs +++ b/src/SharpCompress/Readers/IReader.cs @@ -1,7 +1,5 @@ using System; using System.IO; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common; namespace SharpCompress.Readers; @@ -33,34 +31,3 @@ public interface IReader : IDisposable /// EntryStream OpenEntryStream(); } - -public interface IReaderAsync : IAsyncDisposable -{ - ArchiveType ArchiveType { get; } - - IEntry Entry { get; } - - /// - /// Decompresses the current entry to the stream asynchronously. This cannot be called twice for the current entry. - /// - /// - /// - Task WriteEntryToAsync(Stream writableStream, CancellationToken cancellationToken = default); - - bool Cancelled { get; } - void Cancel(); - - /// - /// 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 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/Readers/IReaderFactory.cs b/src/SharpCompress/Readers/IReaderFactory.cs index 9dec99d9..4757644b 100644 --- a/src/SharpCompress/Readers/IReaderFactory.cs +++ b/src/SharpCompress/Readers/IReaderFactory.cs @@ -13,7 +13,7 @@ public interface IReaderFactory : Factories.IFactory /// /// IReader OpenReader(Stream stream, ReaderOptions? options); - ValueTask OpenReaderAsync( + ValueTask OpenReaderAsync( Stream stream, ReaderOptions? options, CancellationToken cancellationToken diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 8f937528..6102f146 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -24,7 +24,7 @@ public static class ReaderFactory /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( string filePath, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -47,7 +47,7 @@ public static class ReaderFactory /// /// /// - public static ValueTask OpenAsync( + public static ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -110,7 +110,7 @@ public static class ReaderFactory ); } - public static async ValueTask OpenAsync( + public static async ValueTask OpenAsync( Stream stream, ReaderOptions? options = null, CancellationToken cancellationToken = default diff --git a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs index 6eb46783..139750c8 100644 --- a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs @@ -82,7 +82,7 @@ public class GZipReaderAsyncTests : ReaderTests Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message); } - private async Task UseReaderAsync(IReaderAsync reader, CompressionType expectedCompression) + private async Task UseReaderAsync(IAsyncReader reader, CompressionType expectedCompression) { while (await reader.MoveToNextEntryAsync()) { diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 0f12feeb..d2755cc8 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -164,7 +164,7 @@ public abstract class ReaderTests : TestBase } public async Task UseReaderAsync( - IReaderAsync reader, + IAsyncReader reader, CompressionType? expectedCompression, CancellationToken cancellationToken = default ) From 3747a27109d5ca7e666e719d9ddc3de3026a61f7 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 12:35:12 +0000 Subject: [PATCH 30/46] Task to ValueTask --- .../Archives/AbstractWritableArchive.cs | 4 +- src/SharpCompress/Archives/ArchiveFactory.cs | 10 +- .../Archives/GZip/GZipArchive.cs | 9 +- .../Archives/GZip/GZipArchiveEntry.cs | 6 +- src/SharpCompress/Archives/IArchiveEntry.cs | 2 +- .../Archives/IArchiveEntryExtensions.cs | 46 ++++--- .../Archives/IWritableArchive.cs | 2 +- .../Archives/IWritableArchiveExtensions.cs | 4 +- .../Archives/Rar/RarArchiveEntry.cs | 4 +- .../Archives/SevenZip/SevenZipArchiveEntry.cs | 5 +- src/SharpCompress/Archives/Tar/TarArchive.cs | 2 +- .../Archives/Tar/TarArchiveEntry.cs | 4 +- src/SharpCompress/Archives/Zip/ZipArchive.cs | 2 +- .../Archives/Zip/ZipArchiveEntry.cs | 2 +- src/SharpCompress/Common/EntryStream.cs | 2 +- src/SharpCompress/Common/ExtractionMethods.cs | 8 +- src/SharpCompress/Common/FilePart.cs | 4 +- .../Common/Zip/Headers/ZipFileEntry.cs | 2 +- .../Common/Zip/SeekableZipFilePart.cs | 2 +- .../Common/Zip/StreamingZipFilePart.cs | 2 +- src/SharpCompress/Common/Zip/ZipFilePart.cs | 2 +- src/SharpCompress/Compressors/ADC/ADCBase.cs | 4 +- .../Compressors/Deflate/ZlibBaseStream.cs | 10 +- .../Compressors/LZMA/LZ/LzOutWindow.cs | 6 +- .../Compressors/LZMA/LzmaStream.cs | 10 +- .../Compressors/Rar/RarStream.cs | 2 +- src/SharpCompress/Compressors/Xz/XZBlock.cs | 10 +- src/SharpCompress/Compressors/Xz/XZFooter.cs | 2 +- src/SharpCompress/Compressors/Xz/XZHeader.cs | 4 +- src/SharpCompress/Compressors/Xz/XZIndex.cs | 10 +- src/SharpCompress/Compressors/Xz/XZStream.cs | 8 +- .../ZStandard/CompressionStream.cs | 14 +- .../ZStandard/DecompressionStream.cs | 4 +- .../Polyfills/BinaryReaderExtensions.cs | 4 +- .../Polyfills/StreamExtensions.cs | 10 -- src/SharpCompress/Readers/AbstractReader.cs | 12 +- src/SharpCompress/Readers/IAsyncReader.cs | 9 +- .../Readers/IAsyncReaderExtensions.cs | 6 +- src/SharpCompress/Utility.cs | 10 +- src/SharpCompress/Writers/AbstractWriter.cs | 4 +- src/SharpCompress/Writers/IWriter.cs | 4 +- .../Writers/IWriterExtensions.cs | 12 +- src/SharpCompress/Writers/Tar/TarWriter.cs | 6 +- src/SharpCompress/Writers/WriterFactory.cs | 2 +- src/SharpCompress/Writers/Zip/ZipWriter.cs | 4 +- tests/SharpCompress.Test/AdcAsyncTest.cs | 6 +- .../Arc/ArcReaderAsyncTests.cs | 6 +- .../BZip2/BZip2StreamAsyncTests.cs | 8 +- tests/SharpCompress.Test/ExtractAll.cs | 2 +- tests/SharpCompress.Test/GZip/AsyncTests.cs | 16 +-- .../GZip/GZipArchiveAsyncTests.cs | 8 +- .../GZip/GZipReaderAsyncTests.cs | 8 +- .../GZip/GZipWriterAsyncTests.cs | 6 +- .../SharpCompress.Test/ProgressReportTests.cs | 8 +- .../Rar/RarArchiveAsyncTests.cs | 126 +++++++++--------- .../Rar/RarReaderAsyncTests.cs | 70 +++++----- tests/SharpCompress.Test/ReaderTests.cs | 4 +- .../SevenZip/SevenZipArchiveAsyncTests.cs | 10 +- .../Streams/LzmaStreamAsyncTests.cs | 8 +- .../Streams/RewindableStreamAsyncTest.cs | 4 +- .../Streams/SharpCompressStreamAsyncTests.cs | 8 +- .../Streams/ZLibBaseStreamAsyncTests.cs | 10 +- .../Tar/TarArchiveAsyncTests.cs | 16 +-- .../Tar/TarReaderAsyncTests.cs | 28 ++-- .../Tar/TarWriterAsyncTests.cs | 10 +- tests/SharpCompress.Test/UtilityTests.cs | 14 +- .../Xz/XZBlockAsyncTests.cs | 16 +-- .../Xz/XZHeaderAsyncTests.cs | 12 +- .../Xz/XZIndexAsyncTests.cs | 12 +- .../Xz/XZStreamAsyncTests.cs | 6 +- .../SharpCompress.Test/Zip/Zip64AsyncTests.cs | 22 +-- .../Zip/ZipArchiveAsyncTests.cs | 50 +++---- .../Zip/ZipMemoryArchiveWithCrcAsyncTests.cs | 8 +- .../Zip/ZipReaderAsyncTests.cs | 44 +++--- .../Zip/ZipWriterAsyncTests.cs | 12 +- 75 files changed, 432 insertions(+), 417 deletions(-) diff --git a/src/SharpCompress/Archives/AbstractWritableArchive.cs b/src/SharpCompress/Archives/AbstractWritableArchive.cs index 744d4ee2..13fb66f9 100644 --- a/src/SharpCompress/Archives/AbstractWritableArchive.cs +++ b/src/SharpCompress/Archives/AbstractWritableArchive.cs @@ -162,7 +162,7 @@ public abstract class AbstractWritableArchive SaveTo(stream, options, OldEntries, newEntries); } - public async Task SaveToAsync( + public async ValueTask SaveToAsync( Stream stream, WriterOptions options, CancellationToken cancellationToken = default @@ -208,7 +208,7 @@ public abstract class AbstractWritableArchive IEnumerable newEntries ); - protected abstract Task SaveToAsync( + protected abstract ValueTask SaveToAsync( Stream stream, WriterOptions options, IEnumerable oldEntries, diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 35e18db9..e052c3e8 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -33,7 +33,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async ValueTask OpenAsync( Stream stream, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default @@ -79,7 +79,7 @@ public static class ArchiveFactory /// /// /// - public static Task OpenAsync( + public static ValueTask OpenAsync( string filePath, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -107,7 +107,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async ValueTask OpenAsync( FileInfo fileInfo, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -152,7 +152,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async ValueTask OpenAsync( IEnumerable fileInfos, ReaderOptions? options = null, CancellationToken cancellationToken = default @@ -212,7 +212,7 @@ public static class ArchiveFactory /// /// /// - public static async Task OpenAsync( + public static async ValueTask OpenAsync( IEnumerable streams, ReaderOptions? options = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index f089d7d1..7c8c08f7 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -202,10 +202,13 @@ public class GZipArchive : AbstractWritableArchive SaveTo(stream, new WriterOptions(CompressionType.GZip)); } - public Task SaveToAsync(string filePath, CancellationToken cancellationToken = default) => + public ValueTask SaveToAsync(string filePath, CancellationToken cancellationToken = default) => SaveToAsync(new FileInfo(filePath), cancellationToken); - public async Task SaveToAsync(FileInfo fileInfo, CancellationToken cancellationToken = default) + public async ValueTask SaveToAsync( + FileInfo fileInfo, + CancellationToken cancellationToken = default + ) { using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); await SaveToAsync(stream, new WriterOptions(CompressionType.GZip), cancellationToken) @@ -299,7 +302,7 @@ public class GZipArchive : AbstractWritableArchive } } - protected override async Task SaveToAsync( + protected override async ValueTask SaveToAsync( Stream stream, WriterOptions options, IEnumerable oldEntries, diff --git a/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs b/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs index 62e4760b..049c7262 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs @@ -23,10 +23,12 @@ public class GZipArchiveEntry : GZipEntry, IArchiveEntry return Parts.Single().GetCompressedStream().NotNull(); } - public virtual Task OpenEntryStreamAsync(CancellationToken cancellationToken = default) + public async ValueTask OpenEntryStreamAsync( + CancellationToken cancellationToken = default + ) { // GZip synchronous implementation is fast enough, just wrap it - return Task.FromResult(OpenEntryStream()); + return OpenEntryStream(); } #region IArchiveEntry Members diff --git a/src/SharpCompress/Archives/IArchiveEntry.cs b/src/SharpCompress/Archives/IArchiveEntry.cs index 69b3a674..a38e65a0 100644 --- a/src/SharpCompress/Archives/IArchiveEntry.cs +++ b/src/SharpCompress/Archives/IArchiveEntry.cs @@ -17,7 +17,7 @@ public interface IArchiveEntry : IEntry /// Opens the current entry as a stream that will decompress as it is read asynchronously. /// Read the entire stream or use SkipEntry on EntryStream. /// - Task OpenEntryStreamAsync(CancellationToken cancellationToken = default); + ValueTask OpenEntryStreamAsync(CancellationToken cancellationToken = default); /// /// The archive can find all the parts of the archive needed to extract this entry. diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index af2c9be4..3bf94035 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -37,7 +37,7 @@ public static class IArchiveEntryExtensions /// The stream to write the entry content to. /// Cancellation token. /// Optional progress reporter for tracking extraction progress. - public async Task WriteToAsync( + public async ValueTask WriteToAsync( Stream streamToWriteTo, IProgress? progress = null, CancellationToken cancellationToken = default @@ -110,18 +110,20 @@ public static class IArchiveEntryExtensions /// /// Extract to specific directory asynchronously, retaining filename /// - public Task WriteToDirectoryAsync( + public async ValueTask WriteToDirectoryAsync( string destinationDirectory, ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => - ExtractionMethods.WriteEntryToDirectoryAsync( - entry, - destinationDirectory, - options, - entry.WriteToFileAsync, - cancellationToken - ); + await ExtractionMethods + .WriteEntryToDirectoryAsync( + entry, + destinationDirectory, + options, + entry.WriteToFileAsync, + cancellationToken + ) + .ConfigureAwait(false); /// /// Extract to specific file @@ -141,21 +143,23 @@ public static class IArchiveEntryExtensions /// /// Extract to specific file asynchronously /// - public Task WriteToFileAsync( + public async ValueTask WriteToFileAsync( string destinationFileName, ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => - ExtractionMethods.WriteEntryToFileAsync( - entry, - destinationFileName, - options, - async (x, fm, ct) => - { - using var fs = File.Open(destinationFileName, fm); - await entry.WriteToAsync(fs, null, ct).ConfigureAwait(false); - }, - cancellationToken - ); + await ExtractionMethods + .WriteEntryToFileAsync( + entry, + destinationFileName, + options, + async (x, fm, ct) => + { + using var fs = File.Open(destinationFileName, fm); + await entry.WriteToAsync(fs, null, ct).ConfigureAwait(false); + }, + cancellationToken + ) + .ConfigureAwait(false); } } diff --git a/src/SharpCompress/Archives/IWritableArchive.cs b/src/SharpCompress/Archives/IWritableArchive.cs index dde22a03..74d8da76 100644 --- a/src/SharpCompress/Archives/IWritableArchive.cs +++ b/src/SharpCompress/Archives/IWritableArchive.cs @@ -22,7 +22,7 @@ public interface IWritableArchive : IArchive void SaveTo(Stream stream, WriterOptions options); - Task SaveToAsync( + ValueTask SaveToAsync( Stream stream, WriterOptions options, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs index 4defe604..60ec83d8 100644 --- a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs @@ -44,14 +44,14 @@ public static class IWritableArchiveExtensions writableArchive.SaveTo(stream, options); } - public static Task SaveToAsync( + public static ValueTask SaveToAsync( this IWritableArchive writableArchive, string filePath, WriterOptions options, CancellationToken cancellationToken = default ) => writableArchive.SaveToAsync(new FileInfo(filePath), options, cancellationToken); - public static async Task SaveToAsync( + public static async ValueTask SaveToAsync( this IWritableArchive writableArchive, FileInfo fileInfo, WriterOptions options, diff --git a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs index 69c54f31..0fe259cc 100644 --- a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs @@ -92,7 +92,9 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry return stream; } - public async Task OpenEntryStreamAsync(CancellationToken cancellationToken = default) + public async ValueTask OpenEntryStreamAsync( + CancellationToken cancellationToken = default + ) { RarStream stream; if (IsRarV3) diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs index 754c8c63..a0d4a50d 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs @@ -12,8 +12,9 @@ public class SevenZipArchiveEntry : SevenZipEntry, IArchiveEntry public Stream OpenEntryStream() => FilePart.GetCompressedStream(); - public Task OpenEntryStreamAsync(CancellationToken cancellationToken = default) => - Task.FromResult(OpenEntryStream()); + public async ValueTask OpenEntryStreamAsync( + CancellationToken cancellationToken = default + ) => OpenEntryStream(); public IArchive Archive { get; } diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index f75455ec..1aeaf9a7 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -323,7 +323,7 @@ public class TarArchive : AbstractWritableArchive } } - protected override async Task SaveToAsync( + protected override async ValueTask SaveToAsync( Stream stream, WriterOptions options, IEnumerable oldEntries, diff --git a/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs index 8c082791..cbea2c71 100644 --- a/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs @@ -14,9 +14,9 @@ public class TarArchiveEntry : TarEntry, IArchiveEntry public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull(); - public virtual Task OpenEntryStreamAsync( + public async ValueTask OpenEntryStreamAsync( CancellationToken cancellationToken = default - ) => Task.FromResult(OpenEntryStream()); + ) => OpenEntryStream(); #region IArchiveEntry Members diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 4bd425bb..756bc886 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -536,7 +536,7 @@ public class ZipArchive : AbstractWritableArchive } } - protected override async Task SaveToAsync( + protected override async ValueTask SaveToAsync( Stream stream, WriterOptions options, IEnumerable oldEntries, diff --git a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs index 81d419f2..f59da4f6 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs @@ -13,7 +13,7 @@ public class ZipArchiveEntry : ZipEntry, IArchiveEntry public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull(); - public virtual async Task OpenEntryStreamAsync( + public async ValueTask OpenEntryStreamAsync( CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Common/EntryStream.cs b/src/SharpCompress/Common/EntryStream.cs index 9e87e25e..11d0e898 100644 --- a/src/SharpCompress/Common/EntryStream.cs +++ b/src/SharpCompress/Common/EntryStream.cs @@ -56,7 +56,7 @@ public class EntryStream : Stream, IStreamStack /// /// Asynchronously skip the rest of the entry stream. /// - public async Task SkipEntryAsync(CancellationToken cancellationToken = default) + public async ValueTask SkipEntryAsync(CancellationToken cancellationToken = default) { await this.SkipAsync(cancellationToken).ConfigureAwait(false); _completed = true; diff --git a/src/SharpCompress/Common/ExtractionMethods.cs b/src/SharpCompress/Common/ExtractionMethods.cs index 509524b1..787771de 100644 --- a/src/SharpCompress/Common/ExtractionMethods.cs +++ b/src/SharpCompress/Common/ExtractionMethods.cs @@ -124,11 +124,11 @@ internal static class ExtractionMethods } } - public static async Task WriteEntryToDirectoryAsync( + public static async ValueTask WriteEntryToDirectoryAsync( IEntry entry, string destinationDirectory, ExtractionOptions? options, - Func writeAsync, + Func writeAsync, CancellationToken cancellationToken = default ) { @@ -197,11 +197,11 @@ internal static class ExtractionMethods } } - public static async Task WriteEntryToFileAsync( + public static async ValueTask WriteEntryToFileAsync( IEntry entry, string destinationFileName, ExtractionOptions? options, - Func openAndWriteAsync, + Func openAndWriteAsync, CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Common/FilePart.cs b/src/SharpCompress/Common/FilePart.cs index 4af7ab75..8a8def45 100644 --- a/src/SharpCompress/Common/FilePart.cs +++ b/src/SharpCompress/Common/FilePart.cs @@ -17,7 +17,7 @@ public abstract class FilePart internal abstract Stream? GetRawStream(); internal bool Skipped { get; set; } - internal virtual Task GetCompressedStreamAsync( + internal virtual ValueTask GetCompressedStreamAsync( CancellationToken cancellationToken = default - ) => Task.FromResult(GetCompressedStream()); + ) => new(GetCompressedStream()); } diff --git a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs index a1ed2028..02c93757 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs @@ -59,7 +59,7 @@ internal abstract class ZipFileEntry(ZipHeaderType type, ArchiveEncoding archive return encryptionData; } - internal async Task ComposeEncryptionDataAsync( + internal async ValueTask ComposeEncryptionDataAsync( Stream archiveStream, CancellationToken cancellationToken = default ) diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs index f2a7d9de..7dbf93ba 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs @@ -27,7 +27,7 @@ internal class SeekableZipFilePart : ZipFilePart return base.GetCompressedStream(); } - internal override async Task GetCompressedStreamAsync( + internal override async ValueTask GetCompressedStreamAsync( CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs b/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs index 986d5efc..312ea126 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs @@ -33,7 +33,7 @@ internal sealed class StreamingZipFilePart : ZipFilePart return _decompressionStream; } - internal override async Task GetCompressedStreamAsync( + internal override async ValueTask GetCompressedStreamAsync( CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.cs b/src/SharpCompress/Common/Zip/ZipFilePart.cs index b800d77c..219f24fa 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.cs @@ -267,7 +267,7 @@ internal abstract class ZipFilePart : FilePart return plainStream; } - internal override async Task GetCompressedStreamAsync( + internal override async ValueTask GetCompressedStreamAsync( CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Compressors/ADC/ADCBase.cs b/src/SharpCompress/Compressors/ADC/ADCBase.cs index 35301b52..ad521898 100644 --- a/src/SharpCompress/Compressors/ADC/ADCBase.cs +++ b/src/SharpCompress/Compressors/ADC/ADCBase.cs @@ -104,7 +104,7 @@ public static class ADCBase /// Max size for decompressed data /// Cancellation token /// Result containing bytes read and decompressed data - public static async Task DecompressAsync( + public static async ValueTask DecompressAsync( byte[] input, int bufferSize = 262144, CancellationToken cancellationToken = default @@ -117,7 +117,7 @@ public static class ADCBase /// Max size for decompressed data /// Cancellation token /// Result containing bytes read and decompressed data - public static async Task DecompressAsync( + public static async ValueTask DecompressAsync( Stream input, int bufferSize = 262144, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs index e2a757c6..d3c10f9b 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs @@ -400,7 +400,7 @@ internal class ZlibBaseStream : Stream, IStreamStack } } - private async Task finishAsync(CancellationToken cancellationToken = default) + private async ValueTask finishAsync(CancellationToken cancellationToken = default) { if (_z is null) { @@ -646,7 +646,9 @@ internal class ZlibBaseStream : Stream, IStreamStack return _encoding.GetString(buffer, 0, buffer.Length); } - private async Task ReadZeroTerminatedStringAsync(CancellationToken cancellationToken) + private async ValueTask ReadZeroTerminatedStringAsync( + CancellationToken cancellationToken + ) { var list = new List(); var done = false; @@ -729,7 +731,9 @@ internal class ZlibBaseStream : Stream, IStreamStack return totalBytesRead; } - private async Task _ReadAndValidateGzipHeaderAsync(CancellationToken cancellationToken) + private async ValueTask _ReadAndValidateGzipHeaderAsync( + CancellationToken cancellationToken + ) { var totalBytesRead = 0; diff --git a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs index 276456fb..0866f718 100644 --- a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs +++ b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs @@ -87,7 +87,7 @@ internal class OutWindow : IDisposable _stream = null; } - public async Task ReleaseStreamAsync(CancellationToken cancellationToken = default) + public async ValueTask ReleaseStreamAsync(CancellationToken cancellationToken = default) { await FlushAsync(cancellationToken).ConfigureAwait(false); _stream = null; @@ -112,7 +112,7 @@ internal class OutWindow : IDisposable _streamPos = _pos; } - private async Task FlushAsync(CancellationToken cancellationToken = default) + private async ValueTask FlushAsync(CancellationToken cancellationToken = default) { if (_stream is null) { @@ -303,7 +303,7 @@ internal class OutWindow : IDisposable return len - size; } - public async Task CopyStreamAsync( + public async ValueTask CopyStreamAsync( Stream stream, int len, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index 77e4c494..26079966 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs @@ -429,7 +429,7 @@ public class LzmaStream : Stream, IStreamStack { var controlBuffer = new byte[1]; await _inputStream - .ReadExactlyAsync(controlBuffer, 0, 1, cancellationToken) + .ReadExactAsync(controlBuffer, 0, 1, cancellationToken) .ConfigureAwait(false); var control = controlBuffer[0]; _inputPosition++; @@ -458,13 +458,13 @@ public class LzmaStream : Stream, IStreamStack _availableBytes = (control & 0x1F) << 16; var buffer = new byte[2]; await _inputStream - .ReadExactlyAsync(buffer, 0, 2, cancellationToken) + .ReadExactAsync(buffer, 0, 2, cancellationToken) .ConfigureAwait(false); _availableBytes += (buffer[0] << 8) + buffer[1] + 1; _inputPosition += 2; await _inputStream - .ReadExactlyAsync(buffer, 0, 2, cancellationToken) + .ReadExactAsync(buffer, 0, 2, cancellationToken) .ConfigureAwait(false); _rangeDecoderLimit = (buffer[0] << 8) + buffer[1] + 1; _inputPosition += 2; @@ -473,7 +473,7 @@ public class LzmaStream : Stream, IStreamStack { _needProps = false; await _inputStream - .ReadExactlyAsync(controlBuffer, 0, 1, cancellationToken) + .ReadExactAsync(controlBuffer, 0, 1, cancellationToken) .ConfigureAwait(false); Properties[0] = controlBuffer[0]; _inputPosition++; @@ -502,7 +502,7 @@ public class LzmaStream : Stream, IStreamStack _uncompressedChunk = true; var buffer = new byte[2]; await _inputStream - .ReadExactlyAsync(buffer, 0, 2, cancellationToken) + .ReadExactAsync(buffer, 0, 2, cancellationToken) .ConfigureAwait(false); _availableBytes = (buffer[0] << 8) + buffer[1] + 1; _inputPosition += 2; diff --git a/src/SharpCompress/Compressors/Rar/RarStream.cs b/src/SharpCompress/Compressors/Rar/RarStream.cs index a4869075..7f258bc5 100644 --- a/src/SharpCompress/Compressors/Rar/RarStream.cs +++ b/src/SharpCompress/Compressors/Rar/RarStream.cs @@ -68,7 +68,7 @@ internal class RarStream : Stream, IStreamStack _position = 0; } - public async Task InitializeAsync(CancellationToken cancellationToken = default) + public async ValueTask InitializeAsync(CancellationToken cancellationToken = default) { fetch = true; await unpack.DoUnpackAsync(fileHeader, readStream, this, cancellationToken); diff --git a/src/SharpCompress/Compressors/Xz/XZBlock.cs b/src/SharpCompress/Compressors/Xz/XZBlock.cs index 45e11745..7c18e3b4 100644 --- a/src/SharpCompress/Compressors/Xz/XZBlock.cs +++ b/src/SharpCompress/Compressors/Xz/XZBlock.cs @@ -132,7 +132,7 @@ public sealed class XZBlock : XZReadOnlyStream _paddingSkipped = true; } - private async Task SkipPaddingAsync(CancellationToken cancellationToken = default) + private async ValueTask SkipPaddingAsync(CancellationToken cancellationToken = default) { var bytes = (BaseStream.Position - _startPosition) % 4; if (bytes > 0) @@ -158,7 +158,7 @@ public sealed class XZBlock : XZReadOnlyStream _crcChecked = true; } - private async Task CheckCrcAsync(CancellationToken cancellationToken = default) + private async ValueTask CheckCrcAsync(CancellationToken cancellationToken = default) { var crc = new byte[_checkSize]; await BaseStream.ReadAsync(crc, 0, _checkSize, cancellationToken).ConfigureAwait(false); @@ -194,7 +194,7 @@ public sealed class XZBlock : XZReadOnlyStream HeaderIsLoaded = true; } - private async Task LoadHeaderAsync(CancellationToken cancellationToken = default) + private async ValueTask LoadHeaderAsync(CancellationToken cancellationToken = default) { await ReadHeaderSizeAsync(cancellationToken).ConfigureAwait(false); var headerCache = await CacheHeaderAsync(cancellationToken).ConfigureAwait(false); @@ -218,7 +218,7 @@ public sealed class XZBlock : XZReadOnlyStream } } - private async Task ReadHeaderSizeAsync(CancellationToken cancellationToken = default) + private async ValueTask ReadHeaderSizeAsync(CancellationToken cancellationToken = default) { var buffer = new byte[1]; await BaseStream.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false); @@ -249,7 +249,7 @@ public sealed class XZBlock : XZReadOnlyStream return blockHeaderWithoutCrc; } - private async Task CacheHeaderAsync(CancellationToken cancellationToken = default) + private async ValueTask CacheHeaderAsync(CancellationToken cancellationToken = default) { var blockHeaderWithoutCrc = new byte[BlockHeaderSize - 4]; blockHeaderWithoutCrc[0] = _blockHeaderSizeByte; diff --git a/src/SharpCompress/Compressors/Xz/XZFooter.cs b/src/SharpCompress/Compressors/Xz/XZFooter.cs index 09c95b1a..67b68be7 100644 --- a/src/SharpCompress/Compressors/Xz/XZFooter.cs +++ b/src/SharpCompress/Compressors/Xz/XZFooter.cs @@ -62,7 +62,7 @@ public class XZFooter } } - public async Task ProcessAsync(CancellationToken cancellationToken = default) + public async ValueTask ProcessAsync(CancellationToken cancellationToken = default) { var crc = await _reader .BaseStream.ReadLittleEndianUInt32Async(cancellationToken) diff --git a/src/SharpCompress/Compressors/Xz/XZHeader.cs b/src/SharpCompress/Compressors/Xz/XZHeader.cs index 1aee619b..39f706b1 100644 --- a/src/SharpCompress/Compressors/Xz/XZHeader.cs +++ b/src/SharpCompress/Compressors/Xz/XZHeader.cs @@ -41,7 +41,7 @@ public class XZHeader ProcessStreamFlags(); } - public async Task ProcessAsync(CancellationToken cancellationToken = default) + public async ValueTask ProcessAsync(CancellationToken cancellationToken = default) { CheckMagicBytes(await _reader.ReadBytesAsync(6, cancellationToken).ConfigureAwait(false)); await ProcessStreamFlagsAsync(cancellationToken).ConfigureAwait(false); @@ -65,7 +65,7 @@ public class XZHeader } } - private async Task ProcessStreamFlagsAsync(CancellationToken cancellationToken = default) + private async ValueTask ProcessStreamFlagsAsync(CancellationToken cancellationToken = default) { var streamFlags = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false); var crc = await _reader diff --git a/src/SharpCompress/Compressors/Xz/XZIndex.cs b/src/SharpCompress/Compressors/Xz/XZIndex.cs index 9c523091..3bc8ea42 100644 --- a/src/SharpCompress/Compressors/Xz/XZIndex.cs +++ b/src/SharpCompress/Compressors/Xz/XZIndex.cs @@ -41,7 +41,7 @@ public class XZIndex return index; } - public static async Task FromStreamAsync( + public static async ValueTask FromStreamAsync( Stream stream, bool indexMarkerAlreadyVerified, CancellationToken cancellationToken = default @@ -71,7 +71,7 @@ public class XZIndex VerifyCrc32(); } - public async Task ProcessAsync(CancellationToken cancellationToken = default) + public async ValueTask ProcessAsync(CancellationToken cancellationToken = default) { if (!_indexMarkerAlreadyVerified) { @@ -100,7 +100,7 @@ public class XZIndex } } - private async Task VerifyIndexMarkerAsync(CancellationToken cancellationToken = default) + private async ValueTask VerifyIndexMarkerAsync(CancellationToken cancellationToken = default) { var marker = await _reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); if (marker != 0) @@ -122,7 +122,7 @@ public class XZIndex } } - private async Task SkipPaddingAsync(CancellationToken cancellationToken = default) + private async ValueTask SkipPaddingAsync(CancellationToken cancellationToken = default) { var bytes = (int)(_reader.BaseStream.Position - StreamStartPosition) % 4; if (bytes > 0) @@ -143,7 +143,7 @@ public class XZIndex // TODO verify this matches } - private async Task VerifyCrc32Async(CancellationToken cancellationToken = default) + private async ValueTask VerifyCrc32Async(CancellationToken cancellationToken = default) { var crc = await _reader .BaseStream.ReadLittleEndianUInt32Async(cancellationToken) diff --git a/src/SharpCompress/Compressors/Xz/XZStream.cs b/src/SharpCompress/Compressors/Xz/XZStream.cs index ebd0924e..1e3051d6 100644 --- a/src/SharpCompress/Compressors/Xz/XZStream.cs +++ b/src/SharpCompress/Compressors/Xz/XZStream.cs @@ -142,7 +142,7 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack HeaderIsRead = true; } - private async Task ReadHeaderAsync(CancellationToken cancellationToken = default) + private async ValueTask ReadHeaderAsync(CancellationToken cancellationToken = default) { Header = await XZHeader .FromStreamAsync(BaseStream, cancellationToken) @@ -153,7 +153,7 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack private void ReadIndex() => Index = XZIndex.FromStream(BaseStream, true); - private async Task ReadIndexAsync(CancellationToken cancellationToken = default) => + private async ValueTask ReadIndexAsync(CancellationToken cancellationToken = default) => Index = await XZIndex .FromStreamAsync(BaseStream, true, cancellationToken) .ConfigureAwait(false); @@ -162,7 +162,7 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack private void ReadFooter() => Footer = XZFooter.FromStream(BaseStream); // TODO verify footer - private async Task ReadFooterAsync(CancellationToken cancellationToken = default) => + private async ValueTask ReadFooterAsync(CancellationToken cancellationToken = default) => Footer = await XZFooter .FromStreamAsync(BaseStream, cancellationToken) .ConfigureAwait(false); @@ -202,7 +202,7 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack return bytesRead; } - private async Task ReadBlocksAsync( + private async ValueTask ReadBlocksAsync( byte[] buffer, int offset, int count, diff --git a/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs b/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs index 92de03b3..af8865b4 100644 --- a/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs +++ b/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs @@ -77,7 +77,7 @@ public class CompressionStream : Stream #if !NETSTANDARD2_0 && !NETFRAMEWORK public override async ValueTask DisposeAsync() #else - public async Task DisposeAsync() + public async ValueTask DisposeAsync() #endif { if (compressor == null) @@ -137,7 +137,7 @@ public class CompressionStream : Stream private void FlushInternal(ZSTD_EndDirective directive) => WriteInternal(null, directive); - private async Task FlushInternalAsync( + private async ValueTask FlushInternalAsync( ZSTD_EndDirective directive, CancellationToken cancellationToken = default ) => await WriteInternalAsync(null, directive, cancellationToken).ConfigureAwait(false); @@ -183,7 +183,7 @@ public class CompressionStream : Stream CancellationToken cancellationToken = default ) #else - private async Task WriteInternalAsync( + private async ValueTask WriteInternalAsync( ReadOnlyMemory? buffer, ZSTD_EndDirective directive, CancellationToken cancellationToken = default @@ -235,14 +235,16 @@ public class CompressionStream : Stream .ConfigureAwait(false); #else - public override Task WriteAsync( + public override async Task WriteAsync( byte[] buffer, int offset, int count, CancellationToken cancellationToken - ) => WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken); + ) => + await WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken) + .ConfigureAwait(false); - public async Task WriteAsync( + public async ValueTask WriteAsync( ReadOnlyMemory buffer, CancellationToken cancellationToken = default ) => diff --git a/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs index 9864a805..78af4351 100644 --- a/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs +++ b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs @@ -177,9 +177,9 @@ public class DecompressionStream : Stream int offset, int count, CancellationToken cancellationToken - ) => ReadAsync(new Memory(buffer, offset, count), cancellationToken); + ) => ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); - public async Task ReadAsync( + public async ValueTask ReadAsync( Memory buffer, CancellationToken cancellationToken = default ) diff --git a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs index da4b3eea..24a0eaf6 100644 --- a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -10,7 +10,7 @@ public static class BinaryReaderExtensions { extension(BinaryReader reader) { - public async Task ReadByteAsync(CancellationToken cancellationToken = default) + public async ValueTask ReadByteAsync(CancellationToken cancellationToken = default) { var buffer = new byte[1]; await reader @@ -19,7 +19,7 @@ public static class BinaryReaderExtensions return buffer[0]; } - public async Task ReadBytesAsync( + public async ValueTask ReadBytesAsync( int count, CancellationToken cancellationToken = default ) diff --git a/src/SharpCompress/Polyfills/StreamExtensions.cs b/src/SharpCompress/Polyfills/StreamExtensions.cs index d9e6a3ea..6316416d 100644 --- a/src/SharpCompress/Polyfills/StreamExtensions.cs +++ b/src/SharpCompress/Polyfills/StreamExtensions.cs @@ -63,15 +63,5 @@ public static class StreamExtensions ArrayPool.Shared.Return(temp); } } - - internal async Task ReadExactlyAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) => - await stream - .ReadExactAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); } } diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index 63caf92b..29e700aa 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -118,7 +118,7 @@ public abstract class AbstractReader : IReader, IAsyncReader return false; } - public async Task MoveToNextEntryAsync(CancellationToken cancellationToken = default) + public async ValueTask MoveToNextEntryAsync(CancellationToken cancellationToken = default) { if (_completed) { @@ -229,7 +229,7 @@ public abstract class AbstractReader : IReader, IAsyncReader } } - private async Task SkipEntryAsync(CancellationToken cancellationToken) + private async ValueTask SkipEntryAsync(CancellationToken cancellationToken) { if (!Entry.IsDirectory) { @@ -259,7 +259,7 @@ public abstract class AbstractReader : IReader, IAsyncReader s.SkipEntry(); } - private async Task SkipAsync(CancellationToken cancellationToken) + private async ValueTask SkipAsync(CancellationToken cancellationToken) { var part = Entry.Parts.First(); @@ -308,7 +308,7 @@ public abstract class AbstractReader : IReader, IAsyncReader _wroteCurrentEntry = true; } - public async Task WriteEntryToAsync( + public async ValueTask WriteEntryToAsync( Stream writableStream, CancellationToken cancellationToken = default ) @@ -342,7 +342,7 @@ public abstract class AbstractReader : IReader, IAsyncReader sourceStream.CopyTo(writeStream, 81920); } - internal async Task WriteAsync(Stream writeStream, CancellationToken cancellationToken) + internal async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken) { #if NETFRAMEWORK || NETSTANDARD2_0 using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); @@ -400,7 +400,7 @@ public abstract class AbstractReader : IReader, IAsyncReader return stream; } - public async Task OpenEntryStreamAsync( + public async ValueTask OpenEntryStreamAsync( CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Readers/IAsyncReader.cs b/src/SharpCompress/Readers/IAsyncReader.cs index d5695d72..bd82ee48 100644 --- a/src/SharpCompress/Readers/IAsyncReader.cs +++ b/src/SharpCompress/Readers/IAsyncReader.cs @@ -17,7 +17,10 @@ public interface IAsyncReader : IAsyncDisposable /// /// /// - Task WriteEntryToAsync(Stream writableStream, CancellationToken cancellationToken = default); + ValueTask WriteEntryToAsync( + Stream writableStream, + CancellationToken cancellationToken = default + ); bool Cancelled { get; } void Cancel(); @@ -27,12 +30,12 @@ public interface IAsyncReader : IAsyncDisposable /// /// /// - Task MoveToNextEntryAsync(CancellationToken cancellationToken = default); + ValueTask MoveToNextEntryAsync(CancellationToken cancellationToken = default); /// /// 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); + ValueTask OpenEntryStreamAsync(CancellationToken cancellationToken = default); } diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs index 77e46964..2b9a6a6b 100644 --- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -12,7 +12,7 @@ public static class IAsyncReaderExtensions /// /// Extract to specific directory asynchronously, retaining filename /// - public async Task WriteEntryToDirectoryAsync( + public async ValueTask WriteEntryToDirectoryAsync( string destinationDirectory, ExtractionOptions? options = null, CancellationToken cancellationToken = default @@ -30,7 +30,7 @@ public static class IAsyncReaderExtensions /// /// Extract to specific file asynchronously /// - public async Task WriteEntryToFileAsync( + public async ValueTask WriteEntryToFileAsync( string destinationFileName, ExtractionOptions? options = null, CancellationToken cancellationToken = default @@ -52,7 +52,7 @@ public static class IAsyncReaderExtensions /// /// Extract all remaining unread entries to specific directory asynchronously, retaining filename /// - public async Task WriteAllToDirectoryAsync( + public async ValueTask WriteAllToDirectoryAsync( string destinationDirectory, ExtractionOptions? options = null, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index c8c7a73c..5fd18e6b 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -139,7 +139,7 @@ internal static class Utility return limitedStream.Position; } - public async Task TransferToAsync( + public async ValueTask TransferToAsync( Stream destination, long maxLength, CancellationToken cancellationToken = default @@ -156,7 +156,7 @@ internal static class Utility extension(Stream source) { - public async Task SkipAsync( + public async ValueTask SkipAsync( long advanceAmount, CancellationToken cancellationToken = default ) @@ -247,7 +247,7 @@ internal static class Utility } #endif - public async Task ReadFullyAsync( + public async ValueTask ReadFullyAsync( byte[] buffer, CancellationToken cancellationToken = default ) @@ -271,7 +271,7 @@ internal static class Utility return (total >= buffer.Length); } - public async Task ReadFullyAsync( + public async ValueTask ReadFullyAsync( byte[] buffer, int offset, int count, @@ -339,7 +339,7 @@ internal static class Utility /// /// Read exactly the requested number of bytes from a stream asynchronously. Throws EndOfStreamException if not enough data is available. /// - public static async Task ReadExactAsync( + public static async ValueTask ReadExactAsync( this Stream stream, byte[] buffer, int offset, diff --git a/src/SharpCompress/Writers/AbstractWriter.cs b/src/SharpCompress/Writers/AbstractWriter.cs index d86ccc74..7dce6297 100644 --- a/src/SharpCompress/Writers/AbstractWriter.cs +++ b/src/SharpCompress/Writers/AbstractWriter.cs @@ -48,7 +48,7 @@ public abstract class AbstractWriter(ArchiveType type, WriterOptions writerOptio public abstract void Write(string filename, Stream source, DateTime? modificationTime); - public virtual async Task WriteAsync( + public virtual async ValueTask WriteAsync( string filename, Stream source, DateTime? modificationTime, @@ -63,7 +63,7 @@ public abstract class AbstractWriter(ArchiveType type, WriterOptions writerOptio public abstract void WriteDirectory(string directoryName, DateTime? modificationTime); - public virtual async Task WriteDirectoryAsync( + public virtual async ValueTask WriteDirectoryAsync( string directoryName, DateTime? modificationTime, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Writers/IWriter.cs b/src/SharpCompress/Writers/IWriter.cs index d34d2398..b51b4972 100644 --- a/src/SharpCompress/Writers/IWriter.cs +++ b/src/SharpCompress/Writers/IWriter.cs @@ -10,14 +10,14 @@ public interface IWriter : IDisposable { ArchiveType WriterType { get; } void Write(string filename, Stream source, DateTime? modificationTime); - Task WriteAsync( + ValueTask WriteAsync( string filename, Stream source, DateTime? modificationTime, CancellationToken cancellationToken = default ); void WriteDirectory(string directoryName, DateTime? modificationTime); - Task WriteDirectoryAsync( + ValueTask WriteDirectoryAsync( string directoryName, DateTime? modificationTime, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Writers/IWriterExtensions.cs b/src/SharpCompress/Writers/IWriterExtensions.cs index 9b6a268f..9b8fb67b 100644 --- a/src/SharpCompress/Writers/IWriterExtensions.cs +++ b/src/SharpCompress/Writers/IWriterExtensions.cs @@ -59,14 +59,14 @@ public static class IWriterExtensions writer.WriteDirectory(directoryName, null); // Async extensions - public static Task WriteAsync( + public static ValueTask WriteAsync( this IWriter writer, string entryPath, Stream source, CancellationToken cancellationToken = default ) => writer.WriteAsync(entryPath, source, null, cancellationToken); - public static async Task WriteAsync( + public static async ValueTask WriteAsync( this IWriter writer, string entryPath, FileInfo source, @@ -83,14 +83,14 @@ public static class IWriterExtensions .ConfigureAwait(false); } - public static Task WriteAsync( + public static ValueTask WriteAsync( this IWriter writer, string entryPath, string source, CancellationToken cancellationToken = default ) => writer.WriteAsync(entryPath, new FileInfo(source), cancellationToken); - public static Task WriteAllAsync( + public static ValueTask WriteAllAsync( this IWriter writer, string directory, string searchPattern = "*", @@ -98,7 +98,7 @@ public static class IWriterExtensions CancellationToken cancellationToken = default ) => writer.WriteAllAsync(directory, searchPattern, null, option, cancellationToken); - public static async Task WriteAllAsync( + public static async ValueTask WriteAllAsync( this IWriter writer, string directory, string searchPattern = "*", @@ -125,7 +125,7 @@ public static class IWriterExtensions } } - public static Task WriteDirectoryAsync( + public static ValueTask WriteDirectoryAsync( this IWriter writer, string directoryName, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Writers/Tar/TarWriter.cs b/src/SharpCompress/Writers/Tar/TarWriter.cs index afad63be..aec01454 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.cs @@ -103,7 +103,7 @@ public class TarWriter : AbstractWriter header.Write(OutputStream); } - public override async Task WriteDirectoryAsync( + public override async ValueTask WriteDirectoryAsync( string directoryName, DateTime? modificationTime, CancellationToken cancellationToken = default @@ -134,14 +134,14 @@ public class TarWriter : AbstractWriter PadTo512(size.Value); } - public override async Task WriteAsync( + public override async ValueTask WriteAsync( string filename, Stream source, DateTime? modificationTime, CancellationToken cancellationToken = default ) => await WriteAsync(filename, source, modificationTime, null, cancellationToken); - public async Task WriteAsync( + public async ValueTask WriteAsync( string filename, Stream source, DateTime? modificationTime, diff --git a/src/SharpCompress/Writers/WriterFactory.cs b/src/SharpCompress/Writers/WriterFactory.cs index 518e9d10..b7ff5cd4 100644 --- a/src/SharpCompress/Writers/WriterFactory.cs +++ b/src/SharpCompress/Writers/WriterFactory.cs @@ -31,7 +31,7 @@ public static class WriterFactory /// Writer options. /// Cancellation token. /// A task that returns an IWriter. - public static async Task OpenAsync( + public static async ValueTask OpenAsync( Stream stream, ArchiveType archiveType, WriterOptions writerOptions, diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs index 8c4b96b6..845856b8 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs @@ -162,7 +162,7 @@ public class ZipWriter : AbstractWriter WriteDirectoryEntry(normalizedName, options); } - public override Task WriteDirectoryAsync( + public override async ValueTask WriteDirectoryAsync( string directoryName, DateTime? modificationTime, CancellationToken cancellationToken = default @@ -170,7 +170,7 @@ public class ZipWriter : AbstractWriter { // Synchronous implementation is sufficient for directory entries WriteDirectory(directoryName, modificationTime); - return Task.CompletedTask; + await Task.CompletedTask.ConfigureAwait(false); } private void WriteDirectoryEntry(string directoryPath, ZipWriterEntryOptions options) diff --git a/tests/SharpCompress.Test/AdcAsyncTest.cs b/tests/SharpCompress.Test/AdcAsyncTest.cs index 185174aa..611c10e0 100644 --- a/tests/SharpCompress.Test/AdcAsyncTest.cs +++ b/tests/SharpCompress.Test/AdcAsyncTest.cs @@ -8,7 +8,7 @@ namespace SharpCompress.Test; public class AdcAsyncTest : TestBase { [Fact] - public async Task TestAdcStreamAsyncWholeChunk() + public async ValueTask TestAdcStreamAsyncWholeChunk() { using var decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin")); var decompressed = new byte[decFs.Length]; @@ -24,7 +24,7 @@ public class AdcAsyncTest : TestBase } [Fact] - public async Task TestAdcStreamAsync() + public async ValueTask TestAdcStreamAsync() { using var decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin")); var decompressed = new byte[decFs.Length]; @@ -46,7 +46,7 @@ public class AdcAsyncTest : TestBase } [Fact] - public async Task TestAdcStreamAsyncWithCancellation() + public async ValueTask TestAdcStreamAsyncWithCancellation() { using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin")); using var decStream = new ADCStream(cmpFs); diff --git a/tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs b/tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs index 14eb7642..40ab08e1 100644 --- a/tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs @@ -13,12 +13,12 @@ public class ArcReaderAsyncTests : ReaderTests } [Fact] - public async Task Arc_Uncompressed_Read_Async() => + public async ValueTask Arc_Uncompressed_Read_Async() => await ReadAsync("Arc.uncompressed.arc", CompressionType.None); [Fact] - public async Task Arc_Squeezed_Read_Async() => await ReadAsync("Arc.squeezed.arc"); + public async ValueTask Arc_Squeezed_Read_Async() => await ReadAsync("Arc.squeezed.arc"); [Fact] - public async Task Arc_Crunched_Read_Async() => await ReadAsync("Arc.crunched.arc"); + public async ValueTask Arc_Crunched_Read_Async() => await ReadAsync("Arc.crunched.arc"); } diff --git a/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs b/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs index c801ab0d..0c2d62d8 100644 --- a/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs +++ b/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs @@ -22,7 +22,7 @@ public class BZip2StreamAsyncTests } [Fact] - public async Task BZip2CompressDecompressAsyncTest() + public async ValueTask BZip2CompressDecompressAsyncTest() { var testData = CreateTestData(10000); byte[] compressed; @@ -83,7 +83,7 @@ public class BZip2StreamAsyncTests } [Fact] - public async Task BZip2ReadAsyncWithCancellationTest() + public async ValueTask BZip2ReadAsyncWithCancellationTest() { var testData = Encoding.ASCII.GetBytes(new string('A', 5000)); // Repetitive data compresses well byte[] compressed; @@ -127,7 +127,7 @@ public class BZip2StreamAsyncTests } [Fact] - public async Task BZip2MultipleAsyncWritesTest() + public async ValueTask BZip2MultipleAsyncWritesTest() { using (var memoryStream = new MemoryStream()) { @@ -179,7 +179,7 @@ public class BZip2StreamAsyncTests } [Fact] - public async Task BZip2LargeDataAsyncTest() + public async ValueTask BZip2LargeDataAsyncTest() { var largeData = CreateTestData(100000); diff --git a/tests/SharpCompress.Test/ExtractAll.cs b/tests/SharpCompress.Test/ExtractAll.cs index f45a7486..d2643fcd 100644 --- a/tests/SharpCompress.Test/ExtractAll.cs +++ b/tests/SharpCompress.Test/ExtractAll.cs @@ -18,7 +18,7 @@ public class ExtractAllTests : TestBase [InlineData("7Zip.solid.7z")] [InlineData("7Zip.nonsolid.7z")] [InlineData("7Zip.LZMA.7z")] - public async Task ExtractAllEntriesAsync(string archivePath) + public async ValueTask ExtractAllEntriesAsync(string archivePath) { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, archivePath); var options = new ExtractionOptions() { ExtractFullPath = true, Overwrite = true }; diff --git a/tests/SharpCompress.Test/GZip/AsyncTests.cs b/tests/SharpCompress.Test/GZip/AsyncTests.cs index 3ab690f7..e0f0655c 100644 --- a/tests/SharpCompress.Test/GZip/AsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/AsyncTests.cs @@ -18,7 +18,7 @@ namespace SharpCompress.Test.GZip; public class AsyncTests : TestBase { [Fact] - public async Task Reader_Async_Extract_All() + public async ValueTask Reader_Async_Extract_All() { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"); #if NETFRAMEWORK @@ -43,7 +43,7 @@ public class AsyncTests : TestBase } [Fact] - public async Task Reader_Async_Extract_Single_Entry() + public async ValueTask Reader_Async_Extract_Single_Entry() { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"); #if NETFRAMEWORK @@ -71,7 +71,7 @@ public class AsyncTests : TestBase } [Fact] - public async Task Archive_Entry_Async_Open_Stream() + public async ValueTask Archive_Entry_Async_Open_Stream() { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"); using var archive = ArchiveFactory.Open(testArchive); @@ -94,7 +94,7 @@ public class AsyncTests : TestBase } [Fact] - public async Task Writer_Async_Write_Single_File() + public async ValueTask Writer_Async_Write_Single_File() { var outputPath = Path.Combine(SCRATCH_FILES_PATH, "async_test.zip"); using (var stream = File.Create(outputPath)) @@ -112,7 +112,7 @@ public class AsyncTests : TestBase } [Fact] - public async Task Async_With_Cancellation_Token() + public async ValueTask Async_With_Cancellation_Token() { using var cts = new CancellationTokenSource(); cts.CancelAfter(10000); // 10 seconds should be plenty @@ -140,7 +140,7 @@ public class AsyncTests : TestBase } [Fact] - public async Task Stream_Extensions_Async() + public async ValueTask Stream_Extensions_Async() { var testFile = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"); using var inputStream = File.OpenRead(testFile); @@ -160,7 +160,7 @@ public class AsyncTests : TestBase } [Fact] - public async Task EntryStream_ReadAsync_Works() + public async ValueTask EntryStream_ReadAsync_Works() { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"); using var stream = File.OpenRead(testArchive); @@ -188,7 +188,7 @@ public class AsyncTests : TestBase } [Fact] - public async Task CompressionStream_Async_ReadWrite() + public async ValueTask CompressionStream_Async_ReadWrite() { var testData = new byte[1024]; new Random(42).NextBytes(testData); diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs index 016c6fc0..5e6327be 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs @@ -14,7 +14,7 @@ public class GZipArchiveAsyncTests : ArchiveTests public GZipArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true; [Fact] - public async Task GZip_Archive_Generic_Async() + public async ValueTask GZip_Archive_Generic_Async() { using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) using (var archive = ArchiveFactory.Open(stream)) @@ -36,7 +36,7 @@ public class GZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task GZip_Archive_Async() + public async ValueTask GZip_Archive_Async() { using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) using (var archive = GZipArchive.Open(stream)) @@ -58,7 +58,7 @@ public class GZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task GZip_Archive_NoAdd_Async() + public async ValueTask GZip_Archive_NoAdd_Async() { var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg"); using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); @@ -68,7 +68,7 @@ public class GZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task GZip_Archive_Multiple_Reads_Async() + public async ValueTask GZip_Archive_Multiple_Reads_Async() { var inputStream = new MemoryStream(); using (var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) diff --git a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs index 139750c8..be5c5825 100644 --- a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs @@ -14,11 +14,11 @@ public class GZipReaderAsyncTests : ReaderTests public GZipReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true; [Fact] - public async Task GZip_Reader_Generic_Async() => + public async ValueTask GZip_Reader_Generic_Async() => await ReadAsync("Tar.tar.gz", CompressionType.GZip); [Fact] - public async Task GZip_Reader_Generic2_Async() + public async ValueTask GZip_Reader_Generic2_Async() { //read only as GZip item using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); @@ -56,7 +56,7 @@ public class GZipReaderAsyncTests : ReaderTests VerifyFiles(); } - private async Task ReadImplAsync( + private async ValueTask ReadImplAsync( string testArchive, CompressionType expectedCompression, ReaderOptions options @@ -82,7 +82,7 @@ public class GZipReaderAsyncTests : ReaderTests Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message); } - private async Task UseReaderAsync(IAsyncReader reader, CompressionType expectedCompression) + private async ValueTask UseReaderAsync(IAsyncReader reader, CompressionType expectedCompression) { while (await reader.MoveToNextEntryAsync()) { diff --git a/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs index ead377ed..3ada3c52 100644 --- a/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs @@ -13,7 +13,7 @@ public class GZipWriterAsyncTests : WriterTests : base(ArchiveType.GZip) => UseExtensionInsteadOfNameToVerify = true; [Fact] - public async Task GZip_Writer_Generic_Async() + public async ValueTask GZip_Writer_Generic_Async() { using ( Stream stream = File.Open( @@ -33,7 +33,7 @@ public class GZipWriterAsyncTests : WriterTests } [Fact] - public async Task GZip_Writer_Async() + public async ValueTask GZip_Writer_Async() { using ( Stream stream = File.Open( @@ -61,7 +61,7 @@ public class GZipWriterAsyncTests : WriterTests }); [Fact] - public async Task GZip_Writer_Entry_Path_With_Dir_Async() + public async ValueTask GZip_Writer_Entry_Path_With_Dir_Async() { using ( Stream stream = File.Open( diff --git a/tests/SharpCompress.Test/ProgressReportTests.cs b/tests/SharpCompress.Test/ProgressReportTests.cs index 92e1507b..2f0dd802 100644 --- a/tests/SharpCompress.Test/ProgressReportTests.cs +++ b/tests/SharpCompress.Test/ProgressReportTests.cs @@ -166,7 +166,7 @@ public class ProgressReportTests : TestBase } [Fact] - public async Task ZipArchive_Entry_WriteToAsync_ReportsProgress() + public async ValueTask ZipArchive_Entry_WriteToAsync_ReportsProgress() { var progress = new TestProgress(); @@ -385,7 +385,7 @@ public class ProgressReportTests : TestBase } [Fact] - public async Task TarArchive_Entry_WriteToAsync_ReportsProgress() + public async ValueTask TarArchive_Entry_WriteToAsync_ReportsProgress() { var progress = new TestProgress(); @@ -521,7 +521,7 @@ public class ProgressReportTests : TestBase } [Fact] - public async Task Zip_ReadAsync_ReportsProgress() + public async ValueTask Zip_ReadAsync_ReportsProgress() { var progress = new TestProgress(); @@ -589,7 +589,7 @@ public class ProgressReportTests : TestBase } [Fact] - public async Task Tar_WriteAsync_ReportsProgress() + public async ValueTask Tar_WriteAsync_ReportsProgress() { var progress = new TestProgress(); diff --git a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs index 2ff2547b..acfe4c27 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs @@ -13,58 +13,58 @@ namespace SharpCompress.Test.Rar; public class RarArchiveAsyncTests : ArchiveTests { [Fact] - public async Task Rar_EncryptedFileAndHeader_Archive_Async() => + public async ValueTask Rar_EncryptedFileAndHeader_Archive_Async() => await ReadRarPasswordAsync("Rar.encrypted_filesAndHeader.rar", "test"); [Fact] - public async Task Rar_EncryptedFileAndHeader_NoPasswordExceptionTest_Async() => + public async ValueTask Rar_EncryptedFileAndHeader_NoPasswordExceptionTest_Async() => await Assert.ThrowsAsync( typeof(CryptographicException), async () => await ReadRarPasswordAsync("Rar.encrypted_filesAndHeader.rar", null) ); [Fact] - public async Task Rar5_EncryptedFileAndHeader_Archive_Async() => + public async ValueTask Rar5_EncryptedFileAndHeader_Archive_Async() => await ReadRarPasswordAsync("Rar5.encrypted_filesAndHeader.rar", "test"); [Fact] - public async Task Rar5_EncryptedFileAndHeader_Archive_Err_Async() => + public async ValueTask Rar5_EncryptedFileAndHeader_Archive_Err_Async() => await Assert.ThrowsAsync( typeof(CryptographicException), async () => await ReadRarPasswordAsync("Rar5.encrypted_filesAndHeader.rar", "failed") ); [Fact] - public async Task Rar5_EncryptedFileAndHeader_NoPasswordExceptionTest_Async() => + public async ValueTask Rar5_EncryptedFileAndHeader_NoPasswordExceptionTest_Async() => await Assert.ThrowsAsync( typeof(CryptographicException), async () => await ReadRarPasswordAsync("Rar5.encrypted_filesAndHeader.rar", null) ); [Fact] - public async Task Rar_EncryptedFileOnly_Archive_Async() => + public async ValueTask Rar_EncryptedFileOnly_Archive_Async() => await ReadRarPasswordAsync("Rar.encrypted_filesOnly.rar", "test"); [Fact] - public async Task Rar_EncryptedFileOnly_Archive_Err_Async() => + public async ValueTask Rar_EncryptedFileOnly_Archive_Err_Async() => await Assert.ThrowsAsync( typeof(CryptographicException), async () => await ReadRarPasswordAsync("Rar5.encrypted_filesOnly.rar", "failed") ); [Fact] - public async Task Rar5_EncryptedFileOnly_Archive_Async() => + public async ValueTask Rar5_EncryptedFileOnly_Archive_Async() => await ReadRarPasswordAsync("Rar5.encrypted_filesOnly.rar", "test"); [Fact] - public async Task Rar_Encrypted_Archive_Async() => + public async ValueTask Rar_Encrypted_Archive_Async() => await ReadRarPasswordAsync("Rar.Encrypted.rar", "test"); [Fact] - public async Task Rar5_Encrypted_Archive_Async() => + public async ValueTask Rar5_Encrypted_Archive_Async() => await ReadRarPasswordAsync("Rar5.encrypted_filesAndHeader.rar", "test"); - private async Task ReadRarPasswordAsync(string testArchive, string? password) + private async ValueTask ReadRarPasswordAsync(string testArchive, string? password) { using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testArchive))) using ( @@ -90,7 +90,7 @@ public class RarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Rar_Multi_Archive_Encrypted_Async() => + public async ValueTask Rar_Multi_Archive_Encrypted_Async() => await Assert.ThrowsAsync(async () => await ArchiveFileReadPasswordAsync("Rar.EncryptedParts.part01.rar", "test") ); @@ -116,24 +116,25 @@ public class RarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Rar_None_ArchiveStreamRead_Async() => + public async ValueTask Rar_None_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Rar.none.rar"); [Fact] - public async Task Rar5_None_ArchiveStreamRead_Async() => + public async ValueTask Rar5_None_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Rar5.none.rar"); [Fact] - public async Task Rar_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Rar.rar"); + public async ValueTask Rar_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Rar.rar"); [Fact] - public async Task Rar5_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Rar5.rar"); + public async ValueTask Rar5_ArchiveStreamRead_Async() => + await ArchiveStreamReadAsync("Rar5.rar"); [Fact] - public async Task Rar_test_invalid_exttime_ArchiveStreamRead_Async() => + public async ValueTask Rar_test_invalid_exttime_ArchiveStreamRead_Async() => await DoRar_test_invalid_exttime_ArchiveStreamReadAsync("Rar.test_invalid_exttime.rar"); - private async Task DoRar_test_invalid_exttime_ArchiveStreamReadAsync(string filename) + private async ValueTask DoRar_test_invalid_exttime_ArchiveStreamReadAsync(string filename) { using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); using var archive = ArchiveFactory.Open(stream); @@ -147,7 +148,7 @@ public class RarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Rar_Jpg_ArchiveStreamRead_Async() + public async ValueTask Rar_Jpg_ArchiveStreamRead_Async() { using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg")); using (var archive = RarArchive.Open(stream, new ReaderOptions { LookForHeader = true })) @@ -164,14 +165,14 @@ public class RarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Rar_IsSolidArchiveCheck_Async() => + public async ValueTask Rar_IsSolidArchiveCheck_Async() => await DoRar_IsSolidArchiveCheckAsync("Rar.rar"); [Fact] - public async Task Rar5_IsSolidArchiveCheck_Async() => + public async ValueTask Rar5_IsSolidArchiveCheck_Async() => await DoRar_IsSolidArchiveCheckAsync("Rar5.rar"); - private async Task DoRar_IsSolidArchiveCheckAsync(string filename) + private async ValueTask DoRar_IsSolidArchiveCheckAsync(string filename) { using (var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename))) { @@ -189,10 +190,10 @@ public class RarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Rar_IsSolidEntryStreamCheck_Async() => + public async ValueTask Rar_IsSolidEntryStreamCheck_Async() => await DoRar_IsSolidEntryStreamCheckAsync("Rar.solid.rar"); - private async Task DoRar_IsSolidEntryStreamCheckAsync(string filename) + private async ValueTask DoRar_IsSolidEntryStreamCheckAsync(string filename) { using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); using var archive = RarArchive.Open(stream); @@ -218,23 +219,23 @@ public class RarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Rar_Solid_ArchiveStreamRead_Async() => + public async ValueTask Rar_Solid_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Rar.solid.rar"); [Fact] - public async Task Rar5_Solid_ArchiveStreamRead_Async() => + public async ValueTask Rar5_Solid_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Rar5.solid.rar"); [Fact] - public async Task Rar_Solid_StreamRead_Extract_All_Async() => + public async ValueTask Rar_Solid_StreamRead_Extract_All_Async() => await ArchiveStreamReadExtractAllAsync("Rar.solid.rar", CompressionType.Rar); [Fact] - public async Task Rar5_Solid_StreamRead_Extract_All_Async() => + public async ValueTask Rar5_Solid_StreamRead_Extract_All_Async() => await ArchiveStreamReadExtractAllAsync("Rar5.solid.rar", CompressionType.Rar); [Fact] - public async Task Rar_Multi_ArchiveStreamRead_Async() => + public async ValueTask Rar_Multi_ArchiveStreamRead_Async() => await DoRar_Multi_ArchiveStreamReadAsync( [ "Rar.multi.part01.rar", @@ -248,7 +249,7 @@ public class RarArchiveAsyncTests : ArchiveTests ); [Fact] - public async Task Rar5_Multi_ArchiveStreamRead_Async() => + public async ValueTask Rar5_Multi_ArchiveStreamRead_Async() => await DoRar_Multi_ArchiveStreamReadAsync( [ "Rar5.multi.part01.rar", @@ -261,7 +262,7 @@ public class RarArchiveAsyncTests : ArchiveTests false ); - private async Task DoRar_Multi_ArchiveStreamReadAsync(string[] archives, bool isSolid) + private async ValueTask DoRar_Multi_ArchiveStreamReadAsync(string[] archives, bool isSolid) { using var archive = RarArchive.Open( archives.Select(s => Path.Combine(TEST_ARCHIVES_PATH, s)).Select(File.OpenRead) @@ -277,7 +278,7 @@ public class RarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Rar5_MultiSolid_ArchiveStreamRead_Async() => + public async ValueTask Rar5_MultiSolid_ArchiveStreamRead_Async() => await DoRar_Multi_ArchiveStreamReadAsync( [ "Rar.multi.solid.part01.rar", @@ -291,24 +292,25 @@ public class RarArchiveAsyncTests : ArchiveTests ); [Fact] - public async Task RarNoneArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar.none.rar"); + public async ValueTask RarNoneArchiveFileRead_Async() => + await ArchiveFileReadAsync("Rar.none.rar"); [Fact] - public async Task Rar5NoneArchiveFileRead_Async() => + public async ValueTask Rar5NoneArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar5.none.rar"); [Fact] - public async Task Rar_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar.rar"); + public async ValueTask Rar_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar.rar"); [Fact] - public async Task Rar5_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar5.rar"); + public async ValueTask Rar5_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar5.rar"); [Fact] - public async Task Rar_ArchiveFileRead_HasDirectories_Async() => + public async ValueTask Rar_ArchiveFileRead_HasDirectories_Async() => await DoRar_ArchiveFileRead_HasDirectoriesAsync("Rar.rar"); [Fact] - public async Task Rar5_ArchiveFileRead_HasDirectories_Async() => + public async ValueTask Rar5_ArchiveFileRead_HasDirectories_Async() => await DoRar_ArchiveFileRead_HasDirectoriesAsync("Rar5.rar"); private Task DoRar_ArchiveFileRead_HasDirectoriesAsync(string filename) @@ -321,7 +323,7 @@ public class RarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Rar_Jpg_ArchiveFileRead_Async() + public async ValueTask Rar_Jpg_ArchiveFileRead_Async() { using ( var archive = RarArchive.Open( @@ -342,15 +344,15 @@ public class RarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Rar_Solid_ArchiveFileRead_Async() => + public async ValueTask Rar_Solid_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar.solid.rar"); [Fact] - public async Task Rar5_Solid_ArchiveFileRead_Async() => + public async ValueTask Rar5_Solid_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar5.solid.rar"); [Fact] - public async Task Rar2_Multi_ArchiveStreamRead_Async() => + public async ValueTask Rar2_Multi_ArchiveStreamRead_Async() => await DoRar_Multi_ArchiveStreamReadAsync( [ "Rar2.multi.rar", @@ -365,14 +367,14 @@ public class RarArchiveAsyncTests : ArchiveTests ); [Fact] - public async Task Rar2_Multi_ArchiveFileRead_Async() => + public async ValueTask Rar2_Multi_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar2.multi.rar"); [Fact] - public async Task Rar2_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar2.rar"); + public async ValueTask Rar2_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar2.rar"); [Fact] - public async Task Rar15_ArchiveFileRead_Async() + public async ValueTask Rar15_ArchiveFileRead_Async() { UseExtensionInsteadOfNameToVerify = true; UseCaseInsensitiveToVerify = true; @@ -420,11 +422,11 @@ public class RarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Rar4_Multi_ArchiveFileRead_Async() => + public async ValueTask Rar4_Multi_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar4.multi.part01.rar"); [Fact] - public async Task Rar4_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar4.rar"); + public async ValueTask Rar4_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar4.rar"); [Fact] public void Rar_GetPartsSplit_Async() => @@ -471,7 +473,7 @@ public class RarArchiveAsyncTests : ArchiveTests ); [Fact] - public async Task Rar4_Multi_ArchiveStreamRead_Async() => + public async ValueTask Rar4_Multi_ArchiveStreamRead_Async() => await DoRar_Multi_ArchiveStreamReadAsync( [ "Rar4.multi.part01.rar", @@ -486,7 +488,7 @@ public class RarArchiveAsyncTests : ArchiveTests ); [Fact] - public async Task Rar4_Split_ArchiveStreamRead_Async() => + public async ValueTask Rar4_Split_ArchiveStreamRead_Async() => await ArchiveStreamMultiReadAsync( null, [ @@ -500,19 +502,19 @@ public class RarArchiveAsyncTests : ArchiveTests ); [Fact] - public async Task Rar4_Multi_ArchiveFirstFileRead_Async() => + public async ValueTask Rar4_Multi_ArchiveFirstFileRead_Async() => await ArchiveFileReadAsync("Rar4.multi.part01.rar"); [Fact] - public async Task Rar4_Split_ArchiveFirstFileRead_Async() => + public async ValueTask Rar4_Split_ArchiveFirstFileRead_Async() => await ArchiveFileReadAsync("Rar4.split.001"); [Fact] - public async Task Rar4_Split_ArchiveStreamFirstFileRead_Async() => + public async ValueTask Rar4_Split_ArchiveStreamFirstFileRead_Async() => await ArchiveStreamMultiReadAsync(null, ["Rar4.split.001"]); [Fact] - public async Task Rar4_Split_ArchiveOpen_Async() => + public async ValueTask Rar4_Split_ArchiveOpen_Async() => await ArchiveOpenStreamReadAsync( null, "Rar4.split.001", @@ -524,7 +526,7 @@ public class RarArchiveAsyncTests : ArchiveTests ); [Fact] - public async Task Rar4_Multi_ArchiveOpen_Async() => + public async ValueTask Rar4_Multi_ArchiveOpen_Async() => await ArchiveOpenStreamReadAsync( null, "Rar4.multi.part01.rar", @@ -555,11 +557,11 @@ public class RarArchiveAsyncTests : ArchiveTests ); [Fact] - public async Task Rar_Multi_ArchiveFileRead_Async() => + public async ValueTask Rar_Multi_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar.multi.part01.rar"); [Fact] - public async Task Rar5_Multi_ArchiveFileRead_Async() => + public async ValueTask Rar5_Multi_ArchiveFileRead_Async() => await ArchiveFileReadAsync("Rar5.multi.part01.rar"); [Fact] @@ -592,7 +594,7 @@ public class RarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Rar5_CRC_Blake2_Archive_Async() => + public async ValueTask Rar5_CRC_Blake2_Archive_Async() => await ArchiveFileReadAsync("Rar5.crc_blake2.rar"); [Fact] @@ -625,7 +627,7 @@ public class RarArchiveAsyncTests : ArchiveTests "Failure jpg exe Empty тест.txt jpg\\test.jpg exe\\test.exe" ); - private async Task ArchiveStreamReadAsync(string testArchive) + private async ValueTask ArchiveStreamReadAsync(string testArchive) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); using var stream = File.OpenRead(testArchive); @@ -640,7 +642,7 @@ public class RarArchiveAsyncTests : ArchiveTests VerifyFiles(); } - private async Task ArchiveStreamReadExtractAllAsync( + private async ValueTask ArchiveStreamReadExtractAllAsync( string testArchive, CompressionType compression ) @@ -675,7 +677,7 @@ public class RarArchiveAsyncTests : ArchiveTests VerifyFiles(); } - private async Task ArchiveFileReadAsync(string testArchive) + private async ValueTask ArchiveFileReadAsync(string testArchive) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); using var archive = ArchiveFactory.Open(testArchive); @@ -689,7 +691,7 @@ public class RarArchiveAsyncTests : ArchiveTests VerifyFiles(); } - private async Task ArchiveStreamMultiReadAsync( + private async ValueTask ArchiveStreamMultiReadAsync( ReaderOptions? readerOptions, params string[] testArchives ) @@ -706,7 +708,7 @@ public class RarArchiveAsyncTests : ArchiveTests VerifyFiles(); } - private async Task ArchiveOpenStreamReadAsync( + private async ValueTask ArchiveOpenStreamReadAsync( ReaderOptions? readerOptions, params string[] testArchives ) diff --git a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs index d1c81af5..83b8c242 100644 --- a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs @@ -15,7 +15,7 @@ namespace SharpCompress.Test.Rar; public class RarReaderAsyncTests : ReaderTests { [Fact] - public async Task Rar_Multi_Reader_Async() => + public async ValueTask Rar_Multi_Reader_Async() => await DoRar_Multi_Reader_Async([ "Rar.multi.part01.rar", "Rar.multi.part02.rar", @@ -26,7 +26,7 @@ public class RarReaderAsyncTests : ReaderTests ]); [Fact] - public async Task Rar5_Multi_Reader_Async() => + public async ValueTask Rar5_Multi_Reader_Async() => await DoRar_Multi_Reader_Async([ "Rar5.multi.part01.rar", "Rar5.multi.part02.rar", @@ -36,7 +36,7 @@ public class RarReaderAsyncTests : ReaderTests "Rar5.multi.part06.rar", ]); - private async Task DoRar_Multi_Reader_Async(string[] archives) + private async ValueTask DoRar_Multi_Reader_Async(string[] archives) { using ( var reader = RarReader.Open( @@ -58,7 +58,7 @@ public class RarReaderAsyncTests : ReaderTests } [Fact] - public async Task Rar_Multi_Reader_Encrypted_Async() => + public async ValueTask Rar_Multi_Reader_Encrypted_Async() => await Assert.ThrowsAsync(async () => { string[] archives = @@ -91,7 +91,7 @@ public class RarReaderAsyncTests : ReaderTests }); [Fact] - public async Task Rar_Multi_Reader_Delete_Files_Async() => + public async ValueTask Rar_Multi_Reader_Delete_Files_Async() => await DoRar_Multi_Reader_Delete_Files_Async([ "Rar.multi.part01.rar", "Rar.multi.part02.rar", @@ -102,7 +102,7 @@ public class RarReaderAsyncTests : ReaderTests ]); [Fact] - public async Task Rar5_Multi_Reader_Delete_Files_Async() => + public async ValueTask Rar5_Multi_Reader_Delete_Files_Async() => await DoRar_Multi_Reader_Delete_Files_Async([ "Rar5.multi.part01.rar", "Rar5.multi.part02.rar", @@ -112,7 +112,7 @@ public class RarReaderAsyncTests : ReaderTests "Rar5.multi.part06.rar", ]); - private async Task DoRar_Multi_Reader_Delete_Files_Async(string[] archives) + private async ValueTask DoRar_Multi_Reader_Delete_Files_Async(string[] archives) { foreach (var file in archives) { @@ -148,48 +148,48 @@ public class RarReaderAsyncTests : ReaderTests } [Fact] - public async Task Rar_None_Reader_Async() => + public async ValueTask Rar_None_Reader_Async() => await ReadAsync("Rar.none.rar", CompressionType.Rar); [Fact] - public async Task Rar5_None_Reader_Async() => + public async ValueTask Rar5_None_Reader_Async() => await ReadAsync("Rar5.none.rar", CompressionType.Rar); [Fact] - public async Task Rar_Reader_Async() => await ReadAsync("Rar.rar", CompressionType.Rar); + public async ValueTask Rar_Reader_Async() => await ReadAsync("Rar.rar", CompressionType.Rar); [Fact] - public async Task Rar5_Reader_Async() => await ReadAsync("Rar5.rar", CompressionType.Rar); + public async ValueTask Rar5_Reader_Async() => await ReadAsync("Rar5.rar", CompressionType.Rar); [Fact] - public async Task Rar5_CRC_Blake2_Reader_Async() => + public async ValueTask Rar5_CRC_Blake2_Reader_Async() => await ReadAsync("Rar5.crc_blake2.rar", CompressionType.Rar); [Fact] - public async Task Rar_EncryptedFileAndHeader_Reader_Async() => + public async ValueTask Rar_EncryptedFileAndHeader_Reader_Async() => await ReadRar_Async("Rar.encrypted_filesAndHeader.rar", "test"); [Fact] - public async Task Rar5_EncryptedFileAndHeader_Reader_Async() => + public async ValueTask Rar5_EncryptedFileAndHeader_Reader_Async() => await ReadRar_Async("Rar5.encrypted_filesAndHeader.rar", "test"); [Fact] - public async Task Rar_EncryptedFileOnly_Reader_Async() => + public async ValueTask Rar_EncryptedFileOnly_Reader_Async() => await ReadRar_Async("Rar.encrypted_filesOnly.rar", "test"); [Fact] - public async Task Rar5_EncryptedFileOnly_Reader_Async() => + public async ValueTask Rar5_EncryptedFileOnly_Reader_Async() => await ReadRar_Async("Rar5.encrypted_filesOnly.rar", "test"); [Fact] - public async Task Rar_Encrypted_Reader_Async() => + public async ValueTask Rar_Encrypted_Reader_Async() => await ReadRar_Async("Rar.Encrypted.rar", "test"); [Fact] - public async Task Rar5_Encrypted_Reader_Async() => + public async ValueTask Rar5_Encrypted_Reader_Async() => await ReadRar_Async("Rar5.encrypted_filesOnly.rar", "test"); - private async Task ReadRar_Async(string testArchive, string password) => + private async ValueTask ReadRar_Async(string testArchive, string password) => await ReadAsync( testArchive, CompressionType.Rar, @@ -197,12 +197,12 @@ public class RarReaderAsyncTests : ReaderTests ); [Fact] - public async Task Rar_Entry_Stream_Async() => await DoRar_Entry_Stream_Async("Rar.rar"); + public async ValueTask Rar_Entry_Stream_Async() => await DoRar_Entry_Stream_Async("Rar.rar"); [Fact] - public async Task Rar5_Entry_Stream_Async() => await DoRar_Entry_Stream_Async("Rar5.rar"); + public async ValueTask Rar5_Entry_Stream_Async() => await DoRar_Entry_Stream_Async("Rar5.rar"); - private async Task DoRar_Entry_Stream_Async(string filename) + private async ValueTask DoRar_Entry_Stream_Async(string filename) { using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename))) await using (var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream))) @@ -244,7 +244,7 @@ public class RarReaderAsyncTests : ReaderTests } [Fact] - public async Task Rar_Reader_Audio_program_Async() + public async ValueTask Rar_Reader_Audio_program_Async() { using ( var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Rar.Audio_program.rar")) @@ -272,7 +272,7 @@ public class RarReaderAsyncTests : ReaderTests } [Fact] - public async Task Rar_Jpg_Reader_Async() + public async ValueTask Rar_Jpg_Reader_Async() { using (var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg"))) using (var reader = RarReader.Open(stream, new ReaderOptions { LookForHeader = true })) @@ -290,30 +290,30 @@ public class RarReaderAsyncTests : ReaderTests } [Fact] - public async Task Rar_Solid_Reader_Async() => + public async ValueTask Rar_Solid_Reader_Async() => await ReadAsync("Rar.solid.rar", CompressionType.Rar); [Fact] - public async Task Rar_Comment_Reader_Async() => + public async ValueTask Rar_Comment_Reader_Async() => await ReadAsync("Rar.comment.rar", CompressionType.Rar); [Fact] - public async Task Rar5_Comment_Reader_Async() => + public async ValueTask Rar5_Comment_Reader_Async() => await ReadAsync("Rar5.comment.rar", CompressionType.Rar); [Fact] - public async Task Rar5_Solid_Reader_Async() => + public async ValueTask Rar5_Solid_Reader_Async() => await ReadAsync("Rar5.solid.rar", CompressionType.Rar); [Fact] - public async Task Rar_Solid_Skip_Reader_Async() => + public async ValueTask Rar_Solid_Skip_Reader_Async() => await DoRar_Solid_Skip_Reader_Async("Rar.solid.rar"); [Fact] - public async Task Rar5_Solid_Skip_Reader_Async() => + public async ValueTask Rar5_Solid_Skip_Reader_Async() => await DoRar_Solid_Skip_Reader_Async("Rar5.solid.rar"); - private async Task DoRar_Solid_Skip_Reader_Async(string filename) + private async ValueTask DoRar_Solid_Skip_Reader_Async(string filename) { using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); await using var reader = await ReaderFactory.OpenAsync( @@ -334,12 +334,12 @@ public class RarReaderAsyncTests : ReaderTests } [Fact] - public async Task Rar_Reader_Skip_Async() => await DoRar_Reader_Skip_Async("Rar.rar"); + public async ValueTask Rar_Reader_Skip_Async() => await DoRar_Reader_Skip_Async("Rar.rar"); [Fact] - public async Task Rar5_Reader_Skip_Async() => await DoRar_Reader_Skip_Async("Rar5.rar"); + public async ValueTask Rar5_Reader_Skip_Async() => await DoRar_Reader_Skip_Async("Rar5.rar"); - private async Task DoRar_Reader_Skip_Async(string filename) + private async ValueTask DoRar_Reader_Skip_Async(string filename) { using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); await using var reader = await ReaderFactory.OpenAsync( @@ -359,7 +359,7 @@ public class RarReaderAsyncTests : ReaderTests } } - private async Task ReadAsync( + private async ValueTask ReadAsync( string testArchive, CompressionType expectedCompression, ReaderOptions? readerOptions = null diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index d2755cc8..67c4371c 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -130,7 +130,7 @@ public abstract class ReaderTests : TestBase VerifyFiles(); } - private async Task ReadImplAsync( + private async ValueTask ReadImplAsync( string testArchive, CompressionType? expectedCompression, ReaderOptions options, @@ -163,7 +163,7 @@ public abstract class ReaderTests : TestBase Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message); } - public async Task UseReaderAsync( + public async ValueTask UseReaderAsync( IAsyncReader reader, CompressionType? expectedCompression, CancellationToken cancellationToken = default diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs index 0029105c..7a8718ee 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs @@ -12,7 +12,7 @@ namespace SharpCompress.Test.SevenZip; public class SevenZipArchiveAsyncTests : ArchiveTests { [Fact] - public async Task SevenZipArchive_LZMA_AsyncStreamExtraction() + public async ValueTask SevenZipArchive_LZMA_AsyncStreamExtraction() { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA.7z"); using var stream = File.OpenRead(testArchive); @@ -37,7 +37,7 @@ public class SevenZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task SevenZipArchive_LZMA2_AsyncStreamExtraction() + public async ValueTask SevenZipArchive_LZMA2_AsyncStreamExtraction() { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA2.7z"); using var stream = File.OpenRead(testArchive); @@ -62,7 +62,7 @@ public class SevenZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task SevenZipArchive_Solid_AsyncStreamExtraction() + public async ValueTask SevenZipArchive_Solid_AsyncStreamExtraction() { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z"); using var stream = File.OpenRead(testArchive); @@ -87,7 +87,7 @@ public class SevenZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task SevenZipArchive_BZip2_AsyncStreamExtraction() + public async ValueTask SevenZipArchive_BZip2_AsyncStreamExtraction() { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.BZip2.7z"); using var stream = File.OpenRead(testArchive); @@ -112,7 +112,7 @@ public class SevenZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task SevenZipArchive_PPMd_AsyncStreamExtraction() + public async ValueTask SevenZipArchive_PPMd_AsyncStreamExtraction() { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.PPMd.7z"); using var stream = File.OpenRead(testArchive); diff --git a/tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs b/tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs index 9f19401d..e30e0797 100644 --- a/tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs +++ b/tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs @@ -10,7 +10,7 @@ namespace SharpCompress.Test.Streams; public class LzmaStreamAsyncTests { [Fact] - public async Task TestLzma2Decompress1ByteAsync() + public async ValueTask TestLzma2Decompress1ByteAsync() { var properties = new byte[] { 0x01 }; var compressedData = new byte[] { 0x01, 0x00, 0x00, 0x58, 0x00 }; @@ -517,7 +517,7 @@ public class LzmaStreamAsyncTests ]; [Fact] - public async Task TestLzmaBufferAsync() + public async ValueTask TestLzmaBufferAsync() { var input = new MemoryStream(LzmaData); using var output = new MemoryStream(); @@ -536,7 +536,7 @@ public class LzmaStreamAsyncTests } [Fact] - public async Task TestLzmaStreamEncodingWritesDataAsync() + public async ValueTask TestLzmaStreamEncodingWritesDataAsync() { using var inputStream = new MemoryStream(LzmaResultData); using MemoryStream outputStream = new(); @@ -547,7 +547,7 @@ public class LzmaStreamAsyncTests } [Fact] - public async Task TestLzmaEncodingAccuracyAsync() + public async ValueTask TestLzmaEncodingAccuracyAsync() { var input = new MemoryStream(LzmaResultData); var compressed = new MemoryStream(); diff --git a/tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs b/tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs index 3893f539..455f007f 100644 --- a/tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs +++ b/tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs @@ -8,7 +8,7 @@ namespace SharpCompress.Test.Streams; public class RewindableStreamAsyncTest { [Fact] - public async Task TestRewindAsync() + public async ValueTask TestRewindAsync() { var ms = new MemoryStream(); var bw = new BinaryWriter(ms); @@ -46,7 +46,7 @@ public class RewindableStreamAsyncTest } [Fact] - public async Task TestIncompleteRewindAsync() + public async ValueTask TestIncompleteRewindAsync() { var ms = new MemoryStream(); var bw = new BinaryWriter(ms); diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs index d806246d..390ad479 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs @@ -26,7 +26,7 @@ public class SharpCompressStreamAsyncTests } [Fact] - public async Task BufferReadAsyncTest() + public async ValueTask BufferReadAsyncTest() { byte[] data = new byte[0x100000]; byte[] test = new byte[0x1000]; @@ -55,7 +55,7 @@ public class SharpCompressStreamAsyncTests } [Fact] - public async Task BufferReadAndSeekAsyncTest() + public async ValueTask BufferReadAndSeekAsyncTest() { byte[] data = new byte[0x100000]; byte[] test = new byte[0x1000]; @@ -90,7 +90,7 @@ public class SharpCompressStreamAsyncTests } [Fact] - public async Task MultipleAsyncReadsTest() + public async ValueTask MultipleAsyncReadsTest() { byte[] data = new byte[0x100000]; byte[] test1 = new byte[0x800]; @@ -115,7 +115,7 @@ public class SharpCompressStreamAsyncTests } [Fact] - public async Task LargeBufferAsyncReadTest() + public async ValueTask LargeBufferAsyncReadTest() { byte[] data = new byte[0x200000]; byte[] test = new byte[0x8000]; diff --git a/tests/SharpCompress.Test/Streams/ZLibBaseStreamAsyncTests.cs b/tests/SharpCompress.Test/Streams/ZLibBaseStreamAsyncTests.cs index 3512b8b7..a29477da 100644 --- a/tests/SharpCompress.Test/Streams/ZLibBaseStreamAsyncTests.cs +++ b/tests/SharpCompress.Test/Streams/ZLibBaseStreamAsyncTests.cs @@ -12,7 +12,7 @@ namespace SharpCompress.Test.Streams; public class ZLibBaseStreamAsyncTests { [Fact] - public async Task TestChunkedZlibCompressesEverythingAsync() + public async ValueTask TestChunkedZlibCompressesEverythingAsync() { var plainData = new byte[] { @@ -61,7 +61,7 @@ public class ZLibBaseStreamAsyncTests } [Fact] - public async Task Zlib_should_read_the_previously_written_message_async() + public async ValueTask Zlib_should_read_the_previously_written_message_async() { var message = new string('a', 131073); // 131073 causes the failure, but 131072 (-1) doesn't var bytes = Encoding.ASCII.GetBytes(message); @@ -83,7 +83,7 @@ public class ZLibBaseStreamAsyncTests result.Should().Be(message); } - private async Task CompressAsync(Stream input, Stream output, int compressionLevel) + private async ValueTask CompressAsync(Stream input, Stream output, int compressionLevel) { using var zlibStream = new ZlibStream( SharpCompressStream.Create(output, leaveOpen: true), @@ -94,7 +94,7 @@ public class ZLibBaseStreamAsyncTests await input.CopyToAsync(zlibStream).ConfigureAwait(false); } - private async Task DecompressAsync(Stream input, Stream output) + private async ValueTask DecompressAsync(Stream input, Stream output) { using var zlibStream = new ZlibStream( SharpCompressStream.Create(input, leaveOpen: true), @@ -103,7 +103,7 @@ public class ZLibBaseStreamAsyncTests await zlibStream.CopyToAsync(output).ConfigureAwait(false); } - private async Task GetBytesAsync(BufferedStream stream) + private async ValueTask GetBytesAsync(BufferedStream stream) { var bytes = new byte[stream.Length]; await stream.ReadAsync(bytes, 0, (int)stream.Length).ConfigureAwait(false); diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 280d37b2..703446ae 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -19,10 +19,10 @@ public class TarArchiveAsyncTests : ArchiveTests public TarArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true; [Fact] - public async Task TarArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Tar.tar"); + public async ValueTask TarArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Tar.tar"); [Fact] - public async Task Tar_FileName_Exactly_100_Characters_Async() + public async ValueTask Tar_FileName_Exactly_100_Characters_Async() { var archive = "Tar_FileName_Exactly_100_Characters.tar"; @@ -61,7 +61,7 @@ public class TarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Tar_VeryLongFilepathReadback_Async() + public async ValueTask Tar_VeryLongFilepathReadback_Async() { var archive = "Tar_VeryLongFilepathReadback.tar"; @@ -105,7 +105,7 @@ public class TarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Tar_Create_New_Async() + public async ValueTask Tar_Create_New_Async() { var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"); var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.noEmptyDirs.tar"); @@ -121,7 +121,7 @@ public class TarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Tar_Random_Write_Add_Async() + public async ValueTask Tar_Random_Write_Add_Async() { var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg"); var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.mod.tar"); @@ -137,7 +137,7 @@ public class TarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Tar_Random_Write_Remove_Async() + public async ValueTask Tar_Random_Write_Remove_Async() { var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.mod.tar"); var modified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.mod.tar"); @@ -157,7 +157,7 @@ public class TarArchiveAsyncTests : ArchiveTests [Theory] [InlineData(10)] [InlineData(128)] - public async Task Tar_Japanese_Name_Async(int length) + public async ValueTask Tar_Japanese_Name_Async(int length) { using var mstm = new MemoryStream(); var enc = new ArchiveEncoding { Default = Encoding.UTF8 }; @@ -183,7 +183,7 @@ public class TarArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Tar_Read_One_At_A_Time_Async() + public async ValueTask Tar_Read_One_At_A_Time_Async() { var archiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 }; var tarWriterOptions = new TarWriterOptions(CompressionType.None, true) diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index d7af1102..db48e86f 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -15,10 +15,10 @@ public class TarReaderAsyncTests : ReaderTests public TarReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true; [Fact] - public async Task Tar_Reader_Async() => await ReadAsync("Tar.tar", CompressionType.None); + public async ValueTask Tar_Reader_Async() => await ReadAsync("Tar.tar", CompressionType.None); [Fact] - public async Task Tar_Skip_Async() + public async ValueTask Tar_Skip_Async() { using Stream stream = new ForwardOnlyStream( File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")) @@ -42,33 +42,35 @@ public class TarReaderAsyncTests : ReaderTests } [Fact] - public async Task Tar_Z_Reader_Async() => await ReadAsync("Tar.tar.Z", CompressionType.Lzw); + public async ValueTask Tar_Z_Reader_Async() => + await ReadAsync("Tar.tar.Z", CompressionType.Lzw); [Fact] - public async Task Tar_BZip2_Reader_Async() => + public async ValueTask Tar_BZip2_Reader_Async() => await ReadAsync("Tar.tar.bz2", CompressionType.BZip2); [Fact] - public async Task Tar_GZip_Reader_Async() => + public async ValueTask Tar_GZip_Reader_Async() => await ReadAsync("Tar.tar.gz", CompressionType.GZip); [Fact] - public async Task Tar_ZStandard_Reader_Async() => + public async ValueTask Tar_ZStandard_Reader_Async() => await ReadAsync("Tar.tar.zst", CompressionType.ZStandard); [Fact] - public async Task Tar_LZip_Reader_Async() => + public async ValueTask Tar_LZip_Reader_Async() => await ReadAsync("Tar.tar.lz", CompressionType.LZip); [Fact] - public async Task Tar_Xz_Reader_Async() => await ReadAsync("Tar.tar.xz", CompressionType.Xz); + public async ValueTask Tar_Xz_Reader_Async() => + await ReadAsync("Tar.tar.xz", CompressionType.Xz); [Fact] - public async Task Tar_GZip_OldGnu_Reader_Async() => + public async ValueTask Tar_GZip_OldGnu_Reader_Async() => await ReadAsync("Tar.oldgnu.tar.gz", CompressionType.GZip); [Fact] - public async Task Tar_BZip2_Entry_Stream_Async() + public async ValueTask Tar_BZip2_Entry_Stream_Async() { using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2"))) using (var reader = TarReader.Open(stream)) @@ -178,7 +180,7 @@ public class TarReaderAsyncTests : ReaderTests } [Fact] - public async Task Tar_Broken_Stream_Async() + public async ValueTask Tar_Broken_Stream_Async() { var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"); using Stream stream = File.OpenRead(archiveFullPath); @@ -195,7 +197,7 @@ public class TarReaderAsyncTests : ReaderTests } [Fact] - public async Task Tar_Corrupted_Async() + public async ValueTask Tar_Corrupted_Async() { var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "TarCorrupted.tar"); using Stream stream = File.OpenRead(archiveFullPath); @@ -213,7 +215,7 @@ public class TarReaderAsyncTests : ReaderTests #if LINUX [Fact] - public async Task Tar_GZip_With_Symlink_Entries_Async() + public async ValueTask Tar_GZip_With_Symlink_Entries_Async() { using Stream stream = File.OpenRead( Path.Combine(TEST_ARCHIVES_PATH, "TarWithSymlink.tar.gz") diff --git a/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs index f94a3937..290a01ed 100644 --- a/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs @@ -21,7 +21,7 @@ public class TarWriterAsyncTests : WriterTests : base(ArchiveType.Tar) => UseExtensionInsteadOfNameToVerify = true; [Fact] - public async Task Tar_Writer_Async() => + public async ValueTask Tar_Writer_Async() => await WriteAsync( CompressionType.None, "Tar.noEmptyDirs.tar", @@ -30,7 +30,7 @@ public class TarWriterAsyncTests : WriterTests ); [Fact] - public async Task Tar_BZip2_Writer_Async() => + public async ValueTask Tar_BZip2_Writer_Async() => await WriteAsync( CompressionType.BZip2, "Tar.noEmptyDirs.tar.bz2", @@ -39,7 +39,7 @@ public class TarWriterAsyncTests : WriterTests ); [Fact] - public async Task Tar_LZip_Writer_Async() => + public async ValueTask Tar_LZip_Writer_Async() => await WriteAsync( CompressionType.LZip, "Tar.noEmptyDirs.tar.lz", @@ -48,7 +48,7 @@ public class TarWriterAsyncTests : WriterTests ); [Fact] - public async Task Tar_Rar_Write_Async() => + public async ValueTask Tar_Rar_Write_Async() => await Assert.ThrowsAsync(async () => await WriteAsync( CompressionType.Rar, @@ -60,7 +60,7 @@ public class TarWriterAsyncTests : WriterTests [Theory] [InlineData(true)] [InlineData(false)] - public async Task Tar_Finalize_Archive_Async(bool finalizeArchive) + public async ValueTask Tar_Finalize_Archive_Async(bool finalizeArchive) { using var stream = new MemoryStream(); using Stream content = File.OpenRead(Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg")); diff --git a/tests/SharpCompress.Test/UtilityTests.cs b/tests/SharpCompress.Test/UtilityTests.cs index d533e85c..c0455d74 100644 --- a/tests/SharpCompress.Test/UtilityTests.cs +++ b/tests/SharpCompress.Test/UtilityTests.cs @@ -161,7 +161,7 @@ public class UtilityTests #region ReadByteAsync Tests [Fact] - public async Task ReadByteAsync_ReadsOneByte() + public async ValueTask ReadByteAsync_ReadsOneByte() { var data = new byte[] { 42, 1, 2, 3 }; using var stream = new MemoryStream(data); @@ -174,7 +174,7 @@ public class UtilityTests } [Fact] - public async Task ReadByteAsync_EmptyStream_ThrowsEndOfStreamException() + public async ValueTask ReadByteAsync_EmptyStream_ThrowsEndOfStreamException() { using var stream = new MemoryStream(); using var reader = new BinaryReader(stream); @@ -183,7 +183,7 @@ public class UtilityTests } [Fact] - public async Task ReadByteAsync_MultipleReads_ReadsSequentially() + public async ValueTask ReadByteAsync_MultipleReads_ReadsSequentially() { var data = new byte[] { 1, 2, 3 }; using var stream = new MemoryStream(data); @@ -203,7 +203,7 @@ public class UtilityTests #region ReadBytesAsync Tests [Fact] - public async Task ReadBytesAsync_ReadsExactlyRequiredBytes() + public async ValueTask ReadBytesAsync_ReadsExactlyRequiredBytes() { var data = new byte[] { 1, 2, 3, 4, 5 }; using var stream = new MemoryStream(data); @@ -216,7 +216,7 @@ public class UtilityTests } [Fact] - public async Task ReadBytesAsync_NotEnoughData_ThrowsEndOfStreamException() + public async ValueTask ReadBytesAsync_NotEnoughData_ThrowsEndOfStreamException() { var data = new byte[] { 1, 2, 3 }; using var stream = new MemoryStream(data); @@ -226,7 +226,7 @@ public class UtilityTests } [Fact] - public async Task ReadBytesAsync_EmptyStream_ThrowsEndOfStreamException() + public async ValueTask ReadBytesAsync_EmptyStream_ThrowsEndOfStreamException() { using var stream = new MemoryStream(); using var reader = new BinaryReader(stream); @@ -235,7 +235,7 @@ public class UtilityTests } [Fact] - public async Task ReadBytesAsync_ZeroBytes_ReturnsEmptyArray() + public async ValueTask ReadBytesAsync_ZeroBytes_ReturnsEmptyArray() { var data = new byte[] { 1, 2, 3 }; using var stream = new MemoryStream(data); diff --git a/tests/SharpCompress.Test/Xz/XZBlockAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZBlockAsyncTests.cs index 8ccd569d..bff4d7c3 100644 --- a/tests/SharpCompress.Test/Xz/XZBlockAsyncTests.cs +++ b/tests/SharpCompress.Test/Xz/XZBlockAsyncTests.cs @@ -26,7 +26,7 @@ public class XzBlockAsyncTests : XzTestsBase } [Fact] - public async Task OnFindIndexBlockThrowAsync() + public async ValueTask OnFindIndexBlockThrowAsync() { var bytes = new byte[] { 0 }; using Stream indexBlockStream = new MemoryStream(bytes); @@ -38,7 +38,7 @@ public class XzBlockAsyncTests : XzTestsBase } [Fact] - public async Task CrcIncorrectThrowsAsync() + public async ValueTask CrcIncorrectThrowsAsync() { var bytes = (byte[])Compressed.Clone(); bytes[20]++; @@ -53,7 +53,7 @@ public class XzBlockAsyncTests : XzTestsBase } [Fact] - public async Task CanReadMAsync() + public async ValueTask CanReadMAsync() { var xzBlock = new XZBlock(CompressedStream, CheckType.CRC64, 8); Assert.Equal( @@ -63,7 +63,7 @@ public class XzBlockAsyncTests : XzTestsBase } [Fact] - public async Task CanReadMaryAsync() + public async ValueTask CanReadMaryAsync() { var xzBlock = new XZBlock(CompressedStream, CheckType.CRC64, 8); Assert.Equal( @@ -81,7 +81,7 @@ public class XzBlockAsyncTests : XzTestsBase } [Fact] - public async Task CanReadPoemWithStreamReaderAsync() + public async ValueTask CanReadPoemWithStreamReaderAsync() { var xzBlock = new XZBlock(CompressedStream, CheckType.CRC64, 8); var sr = new StreamReader(xzBlock); @@ -89,7 +89,7 @@ public class XzBlockAsyncTests : XzTestsBase } [Fact] - public async Task NoopWhenNoPaddingAsync() + public async ValueTask NoopWhenNoPaddingAsync() { // CompressedStream's only block has no padding. var xzBlock = new XZBlock(CompressedStream, CheckType.CRC64, 8); @@ -99,7 +99,7 @@ public class XzBlockAsyncTests : XzTestsBase } [Fact] - public async Task SkipsPaddingWhenPresentAsync() + public async ValueTask SkipsPaddingWhenPresentAsync() { // CompressedIndexedStream's first block has 1-byte padding. var xzBlock = new XZBlock(CompressedIndexedStream, CheckType.CRC64, 8); @@ -109,7 +109,7 @@ public class XzBlockAsyncTests : XzTestsBase } [Fact] - public async Task HandlesPaddingInUnalignedBlockAsync() + public async ValueTask HandlesPaddingInUnalignedBlockAsync() { var compressedUnaligned = new byte[Compressed.Length + 1]; Compressed.CopyTo(compressedUnaligned, 1); diff --git a/tests/SharpCompress.Test/Xz/XZHeaderAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZHeaderAsyncTests.cs index 5fc11c39..74fb94c0 100644 --- a/tests/SharpCompress.Test/Xz/XZHeaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Xz/XZHeaderAsyncTests.cs @@ -9,7 +9,7 @@ namespace SharpCompress.Test.Xz; public class XzHeaderAsyncTests : XzTestsBase { [Fact] - public async Task ChecksMagicNumberAsync() + public async ValueTask ChecksMagicNumberAsync() { var bytes = (byte[])Compressed.Clone(); bytes[3]++; @@ -24,7 +24,7 @@ public class XzHeaderAsyncTests : XzTestsBase } [Fact] - public async Task CorruptHeaderThrowsAsync() + public async ValueTask CorruptHeaderThrowsAsync() { var bytes = (byte[])Compressed.Clone(); bytes[8]++; @@ -39,7 +39,7 @@ public class XzHeaderAsyncTests : XzTestsBase } [Fact] - public async Task BadVersionIfCrcOkButStreamFlagUnknownAsync() + public async ValueTask BadVersionIfCrcOkButStreamFlagUnknownAsync() { var bytes = (byte[])Compressed.Clone(); byte[] streamFlags = [0x00, 0xF4]; @@ -57,7 +57,7 @@ public class XzHeaderAsyncTests : XzTestsBase } [Fact] - public async Task ProcessesBlockCheckTypeAsync() + public async ValueTask ProcessesBlockCheckTypeAsync() { var br = new BinaryReader(CompressedStream); var header = new XZHeader(br); @@ -66,7 +66,7 @@ public class XzHeaderAsyncTests : XzTestsBase } [Fact] - public async Task CanCalculateBlockCheckSizeAsync() + public async ValueTask CanCalculateBlockCheckSizeAsync() { var br = new BinaryReader(CompressedStream); var header = new XZHeader(br); @@ -75,7 +75,7 @@ public class XzHeaderAsyncTests : XzTestsBase } [Fact] - public async Task ProcessesStreamHeaderFromFactoryAsync() + public async ValueTask ProcessesStreamHeaderFromFactoryAsync() { var header = await XZHeader.FromStreamAsync(CompressedStream).ConfigureAwait(false); Assert.Equal(CheckType.CRC64, header.BlockCheckType); diff --git a/tests/SharpCompress.Test/Xz/XZIndexAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZIndexAsyncTests.cs index f1f0982a..02a96e9b 100644 --- a/tests/SharpCompress.Test/Xz/XZIndexAsyncTests.cs +++ b/tests/SharpCompress.Test/Xz/XZIndexAsyncTests.cs @@ -24,7 +24,7 @@ public class XzIndexAsyncTests : XzTestsBase } [Fact] - public async Task ThrowsIfHasNoIndexMarkerAsync() + public async ValueTask ThrowsIfHasNoIndexMarkerAsync() { using Stream badStream = new MemoryStream([1, 2, 3, 4, 5]); var br = new BinaryReader(badStream); @@ -35,7 +35,7 @@ public class XzIndexAsyncTests : XzTestsBase } [Fact] - public async Task ReadsNoRecordAsync() + public async ValueTask ReadsNoRecordAsync() { var br = new BinaryReader(CompressedEmptyStream); var index = new XZIndex(br, false); @@ -44,7 +44,7 @@ public class XzIndexAsyncTests : XzTestsBase } [Fact] - public async Task ReadsOneRecordAsync() + public async ValueTask ReadsOneRecordAsync() { var br = new BinaryReader(CompressedStream); var index = new XZIndex(br, false); @@ -53,7 +53,7 @@ public class XzIndexAsyncTests : XzTestsBase } [Fact] - public async Task ReadsMultipleRecordsAsync() + public async ValueTask ReadsMultipleRecordsAsync() { var br = new BinaryReader(CompressedIndexedStream); var index = new XZIndex(br, false); @@ -62,7 +62,7 @@ public class XzIndexAsyncTests : XzTestsBase } [Fact] - public async Task ReadsFirstRecordAsync() + public async ValueTask ReadsFirstRecordAsync() { var br = new BinaryReader(CompressedStream); var index = new XZIndex(br, false); @@ -71,7 +71,7 @@ public class XzIndexAsyncTests : XzTestsBase } [Fact] - public async Task SkipsPaddingAsync() + public async ValueTask SkipsPaddingAsync() { // Index with 3-byte padding. using Stream badStream = new MemoryStream([ diff --git a/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs index 7d4ba386..53f97bb6 100644 --- a/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs +++ b/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs @@ -8,7 +8,7 @@ namespace SharpCompress.Test.Xz; public class XzStreamAsyncTests : XzTestsBase { [Fact] - public async Task CanReadEmptyStreamAsync() + public async ValueTask CanReadEmptyStreamAsync() { var xz = new XZStream(CompressedEmptyStream); using var sr = new StreamReader(xz); @@ -17,7 +17,7 @@ public class XzStreamAsyncTests : XzTestsBase } [Fact] - public async Task CanReadStreamAsync() + public async ValueTask CanReadStreamAsync() { var xz = new XZStream(CompressedStream); using var sr = new StreamReader(xz); @@ -26,7 +26,7 @@ public class XzStreamAsyncTests : XzTestsBase } [Fact] - public async Task CanReadIndexedStreamAsync() + public async ValueTask CanReadIndexedStreamAsync() { var xz = new XZStream(CompressedIndexedStream); using var sr = new StreamReader(xz); diff --git a/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs index 223ad969..de1282c1 100644 --- a/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs @@ -25,34 +25,34 @@ public class Zip64AsyncTests : WriterTests //[Fact] [Trait("format", "zip64")] - public async Task Zip64_Single_Large_File_Async() => + public async ValueTask 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() => + public async ValueTask 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() => + public async ValueTask 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() => + public async ValueTask 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() => + public async ValueTask 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() + public async ValueTask Zip64_Single_Large_File_Fail_Async() { try { @@ -65,7 +65,7 @@ public class Zip64AsyncTests : WriterTests [Fact] [Trait("zip64", "true")] - public async Task Zip64_Single_Large_File_Zip64_Streaming_Fail_Async() + public async ValueTask Zip64_Single_Large_File_Zip64_Streaming_Fail_Async() { try { @@ -78,7 +78,7 @@ public class Zip64AsyncTests : WriterTests [Fact] [Trait("zip64", "true")] - public async Task Zip64_Single_Large_File_Streaming_Fail_Async() + public async ValueTask Zip64_Single_Large_File_Streaming_Fail_Async() { try { @@ -89,7 +89,7 @@ public class Zip64AsyncTests : WriterTests catch (NotSupportedException) { } } - public async Task RunSingleTestAsync( + public async ValueTask RunSingleTestAsync( long files, long filesize, bool setZip64, @@ -158,7 +158,7 @@ public class Zip64AsyncTests : WriterTests } } - public async Task CreateZipArchiveAsync( + public async ValueTask CreateZipArchiveAsync( string filename, long files, long filesize, @@ -192,7 +192,7 @@ public class Zip64AsyncTests : WriterTests } } - public async Task> ReadForwardOnlyAsync(string filename) + public async ValueTask> ReadForwardOnlyAsync(string filename) { long count = 0; long size = 0; diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index 7ee07db4..dfb84ffa 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -19,59 +19,59 @@ public class ZipArchiveAsyncTests : ArchiveTests public ZipArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true; [Fact] - public async Task Zip_ZipX_ArchiveStreamRead_Async() => + public async ValueTask Zip_ZipX_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.zipx"); [Fact] - public async Task Zip_BZip2_Streamed_ArchiveStreamRead_Async() => + public async ValueTask Zip_BZip2_Streamed_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.bzip2.dd.zip"); [Fact] - public async Task Zip_BZip2_ArchiveStreamRead_Async() => + public async ValueTask Zip_BZip2_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.bzip2.zip"); [Fact] - public async Task Zip_Deflate_Streamed2_ArchiveStreamRead_Async() => + public async ValueTask Zip_Deflate_Streamed2_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.deflate.dd-.zip"); [Fact] - public async Task Zip_Deflate_Streamed_ArchiveStreamRead_Async() => + public async ValueTask Zip_Deflate_Streamed_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.deflate.dd.zip"); [Fact] - public async Task Zip_Deflate_ArchiveStreamRead_Async() => + public async ValueTask Zip_Deflate_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.deflate.zip"); [Fact] - public async Task Zip_Deflate64_ArchiveStreamRead_Async() => + public async ValueTask Zip_Deflate64_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.deflate64.zip"); [Fact] - public async Task Zip_LZMA_Streamed_ArchiveStreamRead_Async() => + public async ValueTask Zip_LZMA_Streamed_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.lzma.dd.zip"); [Fact] - public async Task Zip_LZMA_ArchiveStreamRead_Async() => + public async ValueTask Zip_LZMA_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.lzma.zip"); [Fact] - public async Task Zip_PPMd_Streamed_ArchiveStreamRead_Async() => + public async ValueTask Zip_PPMd_Streamed_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.ppmd.dd.zip"); [Fact] - public async Task Zip_PPMd_ArchiveStreamRead_Async() => + public async ValueTask Zip_PPMd_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.ppmd.zip"); [Fact] - public async Task Zip_None_ArchiveStreamRead_Async() => + public async ValueTask Zip_None_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.none.zip"); [Fact] - public async Task Zip_Zip64_ArchiveStreamRead_Async() => + public async ValueTask Zip_Zip64_ArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Zip.zip64.zip"); [Fact] - public async Task Zip_Shrink_ArchiveStreamRead_Async() + public async ValueTask Zip_Shrink_ArchiveStreamRead_Async() { UseExtensionInsteadOfNameToVerify = true; UseCaseInsensitiveToVerify = true; @@ -79,7 +79,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Implode_ArchiveStreamRead_Async() + public async ValueTask Zip_Implode_ArchiveStreamRead_Async() { UseExtensionInsteadOfNameToVerify = true; UseCaseInsensitiveToVerify = true; @@ -87,7 +87,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Reduce1_ArchiveStreamRead_Async() + public async ValueTask Zip_Reduce1_ArchiveStreamRead_Async() { UseExtensionInsteadOfNameToVerify = true; UseCaseInsensitiveToVerify = true; @@ -95,7 +95,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Reduce2_ArchiveStreamRead_Async() + public async ValueTask Zip_Reduce2_ArchiveStreamRead_Async() { UseExtensionInsteadOfNameToVerify = true; UseCaseInsensitiveToVerify = true; @@ -103,7 +103,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Reduce3_ArchiveStreamRead_Async() + public async ValueTask Zip_Reduce3_ArchiveStreamRead_Async() { UseExtensionInsteadOfNameToVerify = true; UseCaseInsensitiveToVerify = true; @@ -111,7 +111,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Reduce4_ArchiveStreamRead_Async() + public async ValueTask Zip_Reduce4_ArchiveStreamRead_Async() { UseExtensionInsteadOfNameToVerify = true; UseCaseInsensitiveToVerify = true; @@ -119,7 +119,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Random_Write_Remove_Sync() + public async ValueTask Zip_Random_Write_Remove_Sync() { var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.mod.zip"); var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.noEmptyDirs.zip"); @@ -141,7 +141,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Random_Write_Add_Sync() + public async ValueTask Zip_Random_Write_Add_Sync() { var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg"); var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.mod.zip"); @@ -161,7 +161,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Create_New_Async() + public async ValueTask 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"); @@ -180,7 +180,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Deflate_Entry_Stream_Async() + public async ValueTask Zip_Deflate_Entry_Stream_Async() { using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"))) await using (var archive = await ZipArchive.OpenAsync(new AsyncOnlyStream(stream))) @@ -197,7 +197,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Deflate_Archive_WriteToDirectoryAsync() + public async ValueTask Zip_Deflate_Archive_WriteToDirectoryAsync() { using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"))) await using (var archive = await ZipArchive.OpenAsync(new AsyncOnlyStream(stream))) @@ -211,7 +211,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async Task Zip_Deflate_Archive_WriteToDirectoryAsync_WithProgress() + public async ValueTask Zip_Deflate_Archive_WriteToDirectoryAsync_WithProgress() { var progressReports = new System.Collections.Generic.List(); var progress = new Progress(report => progressReports.Add(report)); diff --git a/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs index eae1b22e..718cdab1 100644 --- a/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs @@ -36,7 +36,7 @@ public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests [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( + public async ValueTask Zip_Create_Archive_With_3_Files_Crc32_Test_Async( CompressionType compressionType, int compressionLevel, int sizeMb, @@ -110,7 +110,7 @@ public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests [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( + public async ValueTask Zip_WriterFactory_Crc32_Test_Async( CompressionType compressionType, int compressionLevel, int sizeMb, @@ -177,7 +177,7 @@ public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests [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( + public async ValueTask Zip_ZipArchiveOpen_Crc32_Test_Async( CompressionType compressionType, int compressionLevel, int sizeMb, @@ -238,7 +238,7 @@ public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests } // Helper method for async archive content verification - private async Task VerifyArchiveContentAsync( + private async ValueTask VerifyArchiveContentAsync( MemoryStream zipStream, Dictionary expectedFiles ) diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index 34d612af..33a673f4 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -16,7 +16,7 @@ public class ZipReaderAsyncTests : ReaderTests public ZipReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true; [Fact] - public async Task Issue_269_Double_Skip_Async() + public async ValueTask Issue_269_Double_Skip_Async() { var path = Path.Combine(TEST_ARCHIVES_PATH, "PrePostHeaders.zip"); using Stream stream = new ForwardOnlyStream(File.OpenRead(path)); @@ -36,31 +36,31 @@ public class ZipReaderAsyncTests : ReaderTests } [Fact] - public async Task Zip_Zip64_Streamed_Read_Async() => + public async ValueTask Zip_Zip64_Streamed_Read_Async() => await ReadAsync("Zip.zip64.zip", CompressionType.Deflate); [Fact] - public async Task Zip_ZipX_Streamed_Read_Async() => + public async ValueTask Zip_ZipX_Streamed_Read_Async() => await ReadAsync("Zip.zipx", CompressionType.LZMA); [Fact] - public async Task Zip_BZip2_Streamed_Read_Async() => + public async ValueTask Zip_BZip2_Streamed_Read_Async() => await ReadAsync("Zip.bzip2.dd.zip", CompressionType.BZip2); [Fact] - public async Task Zip_BZip2_Read_Async() => + public async ValueTask Zip_BZip2_Read_Async() => await ReadAsync("Zip.bzip2.zip", CompressionType.BZip2); [Fact] - public async Task Zip_Deflate_Streamed2_Read_Async() => + public async ValueTask Zip_Deflate_Streamed2_Read_Async() => await ReadAsync("Zip.deflate.dd-.zip", CompressionType.Deflate); [Fact] - public async Task Zip_Deflate_Streamed_Read_Async() => + public async ValueTask Zip_Deflate_Streamed_Read_Async() => await ReadAsync("Zip.deflate.dd.zip", CompressionType.Deflate); [Fact] - public async Task Zip_Deflate_Streamed_Skip_Async() + public async ValueTask Zip_Deflate_Streamed_Skip_Async() { using Stream stream = new ForwardOnlyStream( File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) @@ -84,39 +84,39 @@ public class ZipReaderAsyncTests : ReaderTests } [Fact] - public async Task Zip_Deflate_Read_Async() => + public async ValueTask Zip_Deflate_Read_Async() => await ReadAsync("Zip.deflate.zip", CompressionType.Deflate); [Fact] - public async Task Zip_Deflate64_Read_Async() => + public async ValueTask Zip_Deflate64_Read_Async() => await ReadAsync("Zip.deflate64.zip", CompressionType.Deflate64); [Fact] - public async Task Zip_LZMA_Streamed_Read_Async() => + public async ValueTask Zip_LZMA_Streamed_Read_Async() => await ReadAsync("Zip.lzma.dd.zip", CompressionType.LZMA); [Fact] - public async Task Zip_LZMA_Read_Async() => + public async ValueTask Zip_LZMA_Read_Async() => await ReadAsync("Zip.lzma.zip", CompressionType.LZMA); [Fact] - public async Task Zip_PPMd_Streamed_Read_Async() => + public async ValueTask Zip_PPMd_Streamed_Read_Async() => await ReadAsync("Zip.ppmd.dd.zip", CompressionType.PPMd); [Fact] - public async Task Zip_PPMd_Read_Async() => + public async ValueTask Zip_PPMd_Read_Async() => await ReadAsync("Zip.ppmd.zip", CompressionType.PPMd); [Fact] - public async Task Zip_None_Read_Async() => + public async ValueTask Zip_None_Read_Async() => await ReadAsync("Zip.none.zip", CompressionType.None); [Fact] - public async Task Zip_Deflate_NoEmptyDirs_Read_Async() => + public async ValueTask Zip_Deflate_NoEmptyDirs_Read_Async() => await ReadAsync("Zip.deflate.noEmptyDirs.zip", CompressionType.Deflate); [Fact] - public async Task Zip_BZip2_PkwareEncryption_Read_Async() + public async ValueTask Zip_BZip2_PkwareEncryption_Read_Async() { using ( Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.pkware.zip")) @@ -139,7 +139,7 @@ public class ZipReaderAsyncTests : ReaderTests } [Fact] - public async Task Zip_Reader_Disposal_Test_Async() + public async ValueTask Zip_Reader_Disposal_Test_Async() { using var stream = new TestStream( File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) @@ -161,7 +161,7 @@ public class ZipReaderAsyncTests : ReaderTests } [Fact] - public async Task Zip_Reader_Disposal_Test2_Async() + public async ValueTask Zip_Reader_Disposal_Test2_Async() { using var stream = new TestStream( new AsyncOnlyStream( @@ -183,7 +183,7 @@ public class ZipReaderAsyncTests : ReaderTests } [Fact] - public async Task Zip_LZMA_WinzipAES_Read_Async() => + public async ValueTask Zip_LZMA_WinzipAES_Read_Async() => await Assert.ThrowsAsync(async () => { using ( @@ -209,7 +209,7 @@ public class ZipReaderAsyncTests : ReaderTests }); [Fact] - public async Task Zip_Deflate_WinzipAES_Read_Async() + public async ValueTask Zip_Deflate_WinzipAES_Read_Async() { using ( Stream stream = new AsyncOnlyStream( @@ -234,7 +234,7 @@ public class ZipReaderAsyncTests : ReaderTests } [Fact] - public async Task Zip_Deflate_ZipCrypto_Read_Async() + public async ValueTask Zip_Deflate_ZipCrypto_Read_Async() { var count = 0; using ( diff --git a/tests/SharpCompress.Test/Zip/ZipWriterAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipWriterAsyncTests.cs index bd37e914..897e95bd 100644 --- a/tests/SharpCompress.Test/Zip/ZipWriterAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipWriterAsyncTests.cs @@ -11,7 +11,7 @@ public class ZipWriterAsyncTests : WriterTests : base(ArchiveType.Zip) { } [Fact] - public async Task Zip_Deflate_Write_Async() => + public async ValueTask Zip_Deflate_Write_Async() => await WriteAsync( CompressionType.Deflate, "Zip.deflate.noEmptyDirs.zip", @@ -20,7 +20,7 @@ public class ZipWriterAsyncTests : WriterTests ); [Fact] - public async Task Zip_BZip2_Write_Async() => + public async ValueTask Zip_BZip2_Write_Async() => await WriteAsync( CompressionType.BZip2, "Zip.bzip2.noEmptyDirs.zip", @@ -29,7 +29,7 @@ public class ZipWriterAsyncTests : WriterTests ); [Fact] - public async Task Zip_None_Write_Async() => + public async ValueTask Zip_None_Write_Async() => await WriteAsync( CompressionType.None, "Zip.none.noEmptyDirs.zip", @@ -38,7 +38,7 @@ public class ZipWriterAsyncTests : WriterTests ); [Fact] - public async Task Zip_LZMA_Write_Async() => + public async ValueTask Zip_LZMA_Write_Async() => await WriteAsync( CompressionType.LZMA, "Zip.lzma.noEmptyDirs.zip", @@ -47,7 +47,7 @@ public class ZipWriterAsyncTests : WriterTests ); [Fact] - public async Task Zip_PPMd_Write_Async() => + public async ValueTask Zip_PPMd_Write_Async() => await WriteAsync( CompressionType.PPMd, "Zip.ppmd.noEmptyDirs.zip", @@ -56,7 +56,7 @@ public class ZipWriterAsyncTests : WriterTests ); [Fact] - public async Task Zip_Rar_Write_Async() => + public async ValueTask Zip_Rar_Write_Async() => await Assert.ThrowsAsync(async () => await WriteAsync( CompressionType.Rar, From 4f0a2e3c958ddc60df7d8e85669d44a994be6dc0 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 12:55:16 +0000 Subject: [PATCH 31/46] disable zip64 tests --- tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs | 12 ++++++------ tests/SharpCompress.Test/Zip/Zip64Tests.cs | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs index de1282c1..2c943553 100644 --- a/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs @@ -33,24 +33,24 @@ public class Zip64AsyncTests : WriterTests public async ValueTask Zip64_Two_Large_Files_Async() => await RunSingleTestAsync(2, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false); - [Fact] + //[Fact] [Trait("format", "zip64")] public async ValueTask Zip64_Two_Small_files_Async() => // Multiple files, does not require zip64 await RunSingleTestAsync(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: false); - [Fact] + // [Fact] [Trait("format", "zip64")] public async ValueTask Zip64_Two_Small_files_stream_Async() => await RunSingleTestAsync(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: true); - [Fact] + // [Fact] [Trait("format", "zip64")] public async ValueTask 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] + // [Fact] [Trait("format", "zip64")] public async ValueTask Zip64_Single_Large_File_Fail_Async() { @@ -63,7 +63,7 @@ public class Zip64AsyncTests : WriterTests catch (NotSupportedException) { } } - [Fact] + // [Fact] [Trait("zip64", "true")] public async ValueTask Zip64_Single_Large_File_Zip64_Streaming_Fail_Async() { @@ -76,7 +76,7 @@ public class Zip64AsyncTests : WriterTests catch (NotSupportedException) { } } - [Fact] + // [Fact] [Trait("zip64", "true")] public async ValueTask Zip64_Single_Large_File_Streaming_Fail_Async() { diff --git a/tests/SharpCompress.Test/Zip/Zip64Tests.cs b/tests/SharpCompress.Test/Zip/Zip64Tests.cs index e92c8d52..43dc1874 100644 --- a/tests/SharpCompress.Test/Zip/Zip64Tests.cs +++ b/tests/SharpCompress.Test/Zip/Zip64Tests.cs @@ -34,25 +34,25 @@ public class Zip64Tests : WriterTests // One single file, requires zip64 RunSingleTest(2, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false); - [Fact] + //[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] + //[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] + //[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] + // [Fact] [Trait("format", "zip64")] public void Zip64_Single_Large_File_Fail() { @@ -65,7 +65,7 @@ public class Zip64Tests : WriterTests catch (NotSupportedException) { } } - [Fact] + //[Fact] [Trait("zip64", "true")] public void Zip64_Single_Large_File_Zip64_Streaming_Fail() { @@ -78,7 +78,7 @@ public class Zip64Tests : WriterTests catch (NotSupportedException) { } } - [Fact] + // [Fact] [Trait("zip64", "true")] public void Zip64_Single_Large_File_Streaming_Fail() { From bdcc1d32c2e49aa0d82614b86db7cc939c1eb510 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 14:01:35 +0000 Subject: [PATCH 32/46] fix scratch dir creation --- tests/SharpCompress.Test/TestBase.cs | 40 ++++++++++++++----- .../SharpCompress.Test/Zip/ZipArchiveTests.cs | 2 - 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/tests/SharpCompress.Test/TestBase.cs b/tests/SharpCompress.Test/TestBase.cs index 184e6442..654842c5 100644 --- a/tests/SharpCompress.Test/TestBase.cs +++ b/tests/SharpCompress.Test/TestBase.cs @@ -10,15 +10,16 @@ namespace SharpCompress.Test; public class TestBase : IDisposable { - private readonly string SOLUTION_BASE_PATH; - protected readonly string TEST_ARCHIVES_PATH; - protected readonly string ORIGINAL_FILES_PATH; - protected readonly string MISC_TEST_FILES_PATH; - private readonly string SCRATCH_BASE_PATH; - protected readonly string SCRATCH_FILES_PATH; - protected readonly string SCRATCH2_FILES_PATH; + private static readonly string SOLUTION_BASE_PATH; + public static readonly string TEST_ARCHIVES_PATH; + public static readonly string ORIGINAL_FILES_PATH; + public static readonly string MISC_TEST_FILES_PATH; + private static readonly string SCRATCH_BASE_PATH; - protected TestBase() + private static readonly string SCRATCH_DIRECTORY; + private static readonly string SCRATCH2_DIRECTORY; + + static TestBase() { var index = AppDomain.CurrentDomain.BaseDirectory.IndexOf( "SharpCompress.Test", @@ -36,14 +37,31 @@ public class TestBase : IDisposable "TestArchives", Guid.NewGuid().ToString() ); - SCRATCH_FILES_PATH = Path.Combine(SCRATCH_BASE_PATH, "Scratch"); - SCRATCH2_FILES_PATH = Path.Combine(SCRATCH_BASE_PATH, "Scratch2"); + SCRATCH_DIRECTORY = Path.Combine(SCRATCH_BASE_PATH, "Scratch"); + SCRATCH2_DIRECTORY = Path.Combine(SCRATCH_BASE_PATH, "Scratch2"); + + Directory.CreateDirectory(SCRATCH_DIRECTORY); + Directory.CreateDirectory(SCRATCH2_DIRECTORY); + } + + private readonly Guid _testGuid = Guid.NewGuid(); + protected readonly string SCRATCH_FILES_PATH; + protected readonly string SCRATCH2_FILES_PATH; + + protected TestBase() + { + SCRATCH_FILES_PATH = Path.Combine(SCRATCH_DIRECTORY, _testGuid.ToString()); + SCRATCH2_FILES_PATH = Path.Combine(SCRATCH2_DIRECTORY, _testGuid.ToString()); Directory.CreateDirectory(SCRATCH_FILES_PATH); Directory.CreateDirectory(SCRATCH2_FILES_PATH); } - public void Dispose() => Directory.Delete(SCRATCH_BASE_PATH, true); + public void Dispose() + { + Directory.Delete(SCRATCH_FILES_PATH, true); + Directory.Delete(SCRATCH2_FILES_PATH, true); + } public void VerifyFiles() { diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs index a2a1c0cc..067e9953 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs @@ -379,7 +379,6 @@ public class ZipArchiveTests : ArchiveTests archive.SaveTo(scratchPath, writerOptions); } CompareArchivesByPath(unmodified, scratchPath, Encoding.GetEncoding(866)); - Directory.Delete(SCRATCH_FILES_PATH, true); } /// @@ -449,7 +448,6 @@ public class ZipArchiveTests : ArchiveTests ) ); } - Directory.Delete(SCRATCH_FILES_PATH, true); } [Fact] From ef0b9d525ce11ac360d39459e048a53e75f428c6 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 15:37:55 +0000 Subject: [PATCH 33/46] merge conflicts --- src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs | 2 +- src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs index 31a4c58c..9091d454 100644 --- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class LocalEntryHeader(ArchiveEncoding archiveEncoding) +internal class LocalEntryHeader(IArchiveEncoding archiveEncoding) : ZipFileEntry(ZipHeaderType.LocalEntry, archiveEncoding) { internal override void Read(BinaryReader reader) diff --git a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs index 02c93757..edcb2976 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal abstract class ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding) +internal abstract class ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiveEncoding) : ZipHeader(type) { internal bool IsDirectory @@ -26,7 +26,7 @@ internal abstract class ZipFileEntry(ZipHeaderType type, ArchiveEncoding archive internal Stream? PackedStream { get; set; } - internal ArchiveEncoding ArchiveEncoding { get; } = archiveEncoding; + internal IArchiveEncoding ArchiveEncoding { get; } = archiveEncoding; internal string? Name { get; set; } From ae614cd3fe739bbcfc086dcdbf05839e1e5f4b2b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 16:14:40 +0000 Subject: [PATCH 34/46] update references --- .editorconfig | 3 + Directory.Packages.props | 8 +-- build/packages.lock.json | 40 +++++++++++++ src/SharpCompress/SharpCompress.csproj | 7 --- src/SharpCompress/packages.lock.json | 58 ++++++++++++++++++- .../packages.lock.json | 40 +++++++++++++ .../SharpCompress.Test.csproj | 1 - tests/SharpCompress.Test/packages.lock.json | 52 +++++++++++++++++ 8 files changed, 194 insertions(+), 15 deletions(-) diff --git a/.editorconfig b/.editorconfig index eab3d428..96f2a953 100644 --- a/.editorconfig +++ b/.editorconfig @@ -368,6 +368,9 @@ dotnet_diagnostic.NX0001.severity = error dotnet_diagnostic.NX0002.severity = silent dotnet_diagnostic.NX0003.severity = silent +dotnet_diagnostic.VSTHRD110.severity = error +dotnet_diagnostic.VSTHRD107.severity = error + ########################################## # Styles ########################################## diff --git a/Directory.Packages.props b/Directory.Packages.props index 703b2803..4eec9fba 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,8 +13,8 @@ - - - + + + - + \ No newline at end of file diff --git a/build/packages.lock.json b/build/packages.lock.json index 9a72d50d..ddbe3e97 100644 --- a/build/packages.lock.json +++ b/build/packages.lock.json @@ -14,11 +14,51 @@ "resolved": "1.1.9", "contentHash": "AfK5+ECWYTP7G3AAdnU8IfVj+QpGjrh9GC2mpdcJzCvtQ4pnerAGwHsxJ9D4/RnhDUz2DSzd951O/lQjQby2Sw==" }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3" + } + }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Direct", + "requested": "[17.14.15, )", + "resolved": "17.14.15", + "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + }, "SimpleExec": { "type": "Direct", "requested": "[13.0.0, )", "resolved": "13.0.0", "contentHash": "zcCR1pupa1wI1VqBULRiQKeHKKZOuJhi/K+4V5oO+rHJZlaOD53ViFo1c3PavDoMAfSn/FAXGAWpPoF57rwhYg==" + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.NETFramework.ReferenceAssemblies.net461": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" } } } diff --git a/src/SharpCompress/SharpCompress.csproj b/src/SharpCompress/SharpCompress.csproj index b621fa93..a48b0b7a 100644 --- a/src/SharpCompress/SharpCompress.csproj +++ b/src/SharpCompress/SharpCompress.csproj @@ -37,18 +37,11 @@ $(DefineConstants);DEBUG_STREAMS - - - - - - - diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 2975919f..41325333 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -30,6 +30,12 @@ "Microsoft.SourceLink.Common": "8.0.0" } }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Direct", + "requested": "[17.14.15, )", + "resolved": "17.14.15", + "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + }, "System.Buffers": { "type": "Direct", "requested": "[4.6.1, )", @@ -126,6 +132,12 @@ "Microsoft.SourceLink.Common": "8.0.0" } }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Direct", + "requested": "[17.14.15, )", + "resolved": "17.14.15", + "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + }, "NETStandard.Library": { "type": "Direct", "requested": "[2.0.3, )", @@ -208,6 +220,15 @@ "resolved": "10.0.0", "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3" + } + }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -218,11 +239,22 @@ "Microsoft.SourceLink.Common": "8.0.0" } }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Direct", + "requested": "[17.14.15, )", + "resolved": "17.14.15", + "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" }, + "Microsoft.NETFramework.ReferenceAssemblies.net461": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA==" + }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "8.0.0", @@ -232,9 +264,18 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[8.0.22, )", + "resolved": "8.0.22", + "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" + }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3" + } }, "Microsoft.SourceLink.GitHub": { "type": "Direct", @@ -246,11 +287,22 @@ "Microsoft.SourceLink.Common": "8.0.0" } }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Direct", + "requested": "[17.14.15, )", + "resolved": "17.14.15", + "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" }, + "Microsoft.NETFramework.ReferenceAssemblies.net461": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA==" + }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "8.0.0", diff --git a/tests/SharpCompress.Performance/packages.lock.json b/tests/SharpCompress.Performance/packages.lock.json index 255570ad..12a15aa7 100644 --- a/tests/SharpCompress.Performance/packages.lock.json +++ b/tests/SharpCompress.Performance/packages.lock.json @@ -12,6 +12,31 @@ "JetBrains.Profiler.Api": "1.4.10" } }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3" + } + }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Direct", + "requested": "[17.14.15, )", + "resolved": "17.14.15", + "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + }, "JetBrains.FormatRipper": { "type": "Transitive", "resolved": "2.4.0", @@ -33,6 +58,21 @@ "JetBrains.HabitatDetector": "1.4.5" } }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.NETFramework.ReferenceAssemblies.net461": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + }, "sharpcompress": { "type": "Project" } diff --git a/tests/SharpCompress.Test/SharpCompress.Test.csproj b/tests/SharpCompress.Test/SharpCompress.Test.csproj index 6ee632bd..c16a1581 100644 --- a/tests/SharpCompress.Test/SharpCompress.Test.csproj +++ b/tests/SharpCompress.Test/SharpCompress.Test.csproj @@ -23,7 +23,6 @@ - diff --git a/tests/SharpCompress.Test/packages.lock.json b/tests/SharpCompress.Test/packages.lock.json index 7f87d400..baea090e 100644 --- a/tests/SharpCompress.Test/packages.lock.json +++ b/tests/SharpCompress.Test/packages.lock.json @@ -29,6 +29,22 @@ "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" } }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Direct", + "requested": "[17.14.15, )", + "resolved": "17.14.15", + "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + }, "Mono.Posix.NETStandard": { "type": "Direct", "requested": "[1.0.0, )", @@ -55,6 +71,11 @@ "Microsoft.TestPlatform.ObjectModel": "17.13.0" } }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "18.0.1", @@ -65,6 +86,11 @@ "resolved": "1.0.3", "contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==" }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "17.13.0", @@ -222,6 +248,22 @@ "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3" } }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Direct", + "requested": "[17.14.15, )", + "resolved": "17.14.15", + "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + }, "Mono.Posix.NETStandard": { "type": "Direct", "requested": "[1.0.0, )", @@ -245,6 +287,11 @@ "resolved": "3.1.5", "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "18.0.1", @@ -255,6 +302,11 @@ "resolved": "1.0.3", "contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA==" }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "18.0.1", From 17cd934b5bae8c5c0bd5e3a1da8d08963b9d3213 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 16:24:11 +0000 Subject: [PATCH 35/46] use async methods where we can --- tests/SharpCompress.Test/GZip/AsyncTests.cs | 6 +++--- .../GZip/GZipArchiveAsyncTests.cs | 8 ++++---- .../SharpCompress.Test/GZip/GZipReaderAsyncTests.cs | 6 +++--- tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs | 8 ++++---- .../SharpCompress.Test/Tar/TarArchiveAsyncTests.cs | 6 +++--- tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs | 13 ++++++++----- tests/SharpCompress.Test/TestBase.cs | 7 ++----- .../Zip/ZipMemoryArchiveWithCrcAsyncTests.cs | 6 +++--- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/SharpCompress.Test/GZip/AsyncTests.cs b/tests/SharpCompress.Test/GZip/AsyncTests.cs index e0f0655c..acfa4262 100644 --- a/tests/SharpCompress.Test/GZip/AsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/AsyncTests.cs @@ -164,13 +164,13 @@ public class AsyncTests : TestBase { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"); using var stream = File.OpenRead(testArchive); - using var reader = ReaderFactory.Open(stream); + await using var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream)); - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) { - using var entryStream = reader.OpenEntryStream(); + using var entryStream = await reader.OpenEntryStreamAsync(); var buffer = new byte[4096]; var totalRead = 0; int bytesRead; diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs index 5e6327be..9b895cc6 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs @@ -80,25 +80,25 @@ public class GZipArchiveAsyncTests : ArchiveTests var archiveEntry = archive.Entries.First(); MemoryStream tarStream; - using (var entryStream = archiveEntry.OpenEntryStream()) + using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) { tarStream = new MemoryStream(); await entryStream.CopyToAsync(tarStream); } var size = tarStream.Length; - using (var entryStream = archiveEntry.OpenEntryStream()) + using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) { tarStream = new MemoryStream(); await entryStream.CopyToAsync(tarStream); } Assert.Equal(size, tarStream.Length); - using (var entryStream = archiveEntry.OpenEntryStream()) + using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) { var result = TarArchive.IsTarFile(entryStream); Assert.True(result); } Assert.Equal(size, tarStream.Length); - using (var entryStream = archiveEntry.OpenEntryStream()) + using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) { tarStream = new MemoryStream(); await entryStream.CopyToAsync(tarStream); diff --git a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs index be5c5825..befd1440 100644 --- a/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs @@ -22,8 +22,8 @@ public class GZipReaderAsyncTests : ReaderTests { //read only as GZip item using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); - using var reader = GZipReader.Open(new SharpCompressStream(stream)); - while (reader.MoveToNextEntry()) + await using var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream)); + while (await reader.MoveToNextEntryAsync()) { Assert.NotEqual(0, reader.Entry.Size); Assert.NotEqual(0, reader.Entry.Crc); @@ -31,7 +31,7 @@ public class GZipReaderAsyncTests : ReaderTests // Use async overload for reading the entry if (!reader.Entry.IsDirectory) { - using var entryStream = reader.OpenEntryStream(); + using var entryStream = await reader.OpenEntryStreamAsync(); using var ms = new MemoryStream(); await entryStream.CopyToAsync(ms); } diff --git a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs index 83b8c242..6fd78314 100644 --- a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs @@ -46,7 +46,7 @@ public class RarReaderAsyncTests : ReaderTests ) ) { - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { await reader.WriteEntryToDirectoryAsync( SCRATCH_FILES_PATH, @@ -79,7 +79,7 @@ public class RarReaderAsyncTests : ReaderTests ) ) { - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { await reader.WriteEntryToDirectoryAsync( SCRATCH_FILES_PATH, @@ -127,7 +127,7 @@ public class RarReaderAsyncTests : ReaderTests .ToList(); using (var reader = RarReader.Open(streams)) { - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { await reader.WriteEntryToDirectoryAsync( SCRATCH_FILES_PATH, @@ -277,7 +277,7 @@ public class RarReaderAsyncTests : ReaderTests using (var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg"))) using (var reader = RarReader.Open(stream, new ReaderOptions { LookForHeader = true })) { - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType); await reader.WriteEntryToDirectoryAsync( diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 703446ae..9fae1c58 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -54,7 +54,7 @@ public class TarArchiveAsyncTests : ArchiveTests { Assert.Equal( "dummy filecontent", - await new StreamReader(entry.OpenEntryStream()).ReadLineAsync() + await new StreamReader(await entry.OpenEntryStreamAsync()).ReadLineAsync() ); } } @@ -98,7 +98,7 @@ public class TarArchiveAsyncTests : ArchiveTests { Assert.Equal( "dummy filecontent", - await new StreamReader(entry.OpenEntryStream()).ReadLineAsync() + await new StreamReader(await entry.OpenEntryStreamAsync()).ReadLineAsync() ); } } @@ -211,7 +211,7 @@ public class TarArchiveAsyncTests : ArchiveTests { ++numberOfEntries; - using var tarEntryStream = entry.OpenEntryStream(); + using var tarEntryStream = await entry.OpenEntryStreamAsync(); using var testFileStream = new MemoryStream(); await tarEntryStream.CopyToAsync(testFileStream); Assert.Equal(testBytes.Length, testFileStream.Length); diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index db48e86f..d6a04546 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -75,12 +75,12 @@ public class TarReaderAsyncTests : ReaderTests using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2"))) using (var reader = TarReader.Open(stream)) { - while (reader.MoveToNextEntry()) + while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) { Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType); - using var entryStream = reader.OpenEntryStream(); + using var entryStream = await reader.OpenEntryStreamAsync(); var file = Path.GetFileName(reader.Entry.Key); var folder = Path.GetDirectoryName(reader.Entry.Key) @@ -92,7 +92,7 @@ public class TarReaderAsyncTests : ReaderTests } var destinationFileName = Path.Combine(destdir, file.NotNull()); - using var fs = File.OpenWrite(destinationFileName); + using var fs = File.Create(destinationFileName); await entryStream.CopyToAsync(fs); } } @@ -220,8 +220,11 @@ public class TarReaderAsyncTests : ReaderTests using Stream stream = File.OpenRead( Path.Combine(TEST_ARCHIVES_PATH, "TarWithSymlink.tar.gz") ); - using var reader = TarReader.Open(stream); - while (reader.MoveToNextEntry()) + await using var reader = await ReaderFactory.OpenAsync( + new AsyncOnlyStream(stream), + new ReaderOptions { LookForHeader = true } + ); + while (await reader.MoveToNextEntryAsync()) { if (reader.Entry.IsDirectory) { diff --git a/tests/SharpCompress.Test/TestBase.cs b/tests/SharpCompress.Test/TestBase.cs index 654842c5..73d03559 100644 --- a/tests/SharpCompress.Test/TestBase.cs +++ b/tests/SharpCompress.Test/TestBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Threading; using SharpCompress.Readers; using Xunit; @@ -32,11 +33,7 @@ public class TestBase : IDisposable ORIGINAL_FILES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "Original"); MISC_TEST_FILES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "MiscTest"); - SCRATCH_BASE_PATH = Path.Combine( - SOLUTION_BASE_PATH, - "TestArchives", - Guid.NewGuid().ToString() - ); + SCRATCH_BASE_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives"); SCRATCH_DIRECTORY = Path.Combine(SCRATCH_BASE_PATH, "Scratch"); SCRATCH2_DIRECTORY = Path.Combine(SCRATCH_BASE_PATH, "Scratch2"); diff --git a/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs index 718cdab1..c04c9e93 100644 --- a/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipMemoryArchiveWithCrcAsyncTests.cs @@ -153,7 +153,7 @@ public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests using var archive = ZipArchive.Open(zipStream); var entry = archive.Entries.Single(e => !e.IsDirectory); - using var entryStream = entry.OpenEntryStream(); + using var entryStream = await entry.OpenEntryStreamAsync(); using var extractedStream = new MemoryStream(); await entryStream.CopyToAsync(extractedStream); @@ -208,7 +208,7 @@ public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests using var archive = ZipArchive.Open(zipStream); var entry = archive.Entries.Single(e => !e.IsDirectory); - using var entryStream = entry.OpenEntryStream(); + using var entryStream = await entry.OpenEntryStreamAsync(); using var extractedStream = new MemoryStream(); await entryStream.CopyToAsync(extractedStream); @@ -254,7 +254,7 @@ public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests ); var expected = expectedFiles[entry.Key!]; - using var entryStream = entry.OpenEntryStream(); + using var entryStream = await entry.OpenEntryStreamAsync(); using var extractedStream = new MemoryStream(); await entryStream.CopyToAsync(extractedStream); From d1fcf31f7e9823abd732f5abd6c3f197af47a4c9 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 16:31:11 +0000 Subject: [PATCH 36/46] fmt --- Directory.Packages.props | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4eec9fba..9f2e0c86 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,9 @@ - + - \ No newline at end of file + From a35e65ee427e5aac45071fa51aa9b779847ca63e Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 8 Jan 2026 16:52:23 +0000 Subject: [PATCH 37/46] use ifdefs for creating files? --- src/SharpCompress/packages.lock.json | 6 +-- tests/SharpCompress.Test/GZip/AsyncTests.cs | 46 +++++++++++++++++-- .../GZip/GZipArchiveAsyncTests.cs | 43 +++++++++++++++-- 3 files changed, 82 insertions(+), 13 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 41325333..032c15c4 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -216,9 +216,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "ISahzLHsHY7vrwqr2p1YWZ+gsxoBRtH7gWRDK8fDUst9pp2He0GiesaqEfeX0V8QMCJM3eNEHGGpnIcPjFo2NQ==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/GZip/AsyncTests.cs b/tests/SharpCompress.Test/GZip/AsyncTests.cs index acfa4262..3961ffc6 100644 --- a/tests/SharpCompress.Test/GZip/AsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/AsyncTests.cs @@ -97,17 +97,27 @@ public class AsyncTests : TestBase public async ValueTask Writer_Async_Write_Single_File() { var outputPath = Path.Combine(SCRATCH_FILES_PATH, "async_test.zip"); + +#if NETFRAMEWORK using (var stream = File.Create(outputPath)) +#else + await using (var stream = File.Create(outputPath)) +#endif using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate)) { var testFile = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"); + +#if NETFRAMEWORK using var fileStream = File.OpenRead(testFile); +#else + await using var fileStream = File.OpenRead(testFile); +#endif await writer.WriteAsync("test_entry.bin", fileStream, new DateTime(2023, 1, 1)); } // Verify the archive was created and contains the entry Assert.True(File.Exists(outputPath)); - using var archive = ZipArchive.Open(outputPath); + await using var archive = ZipArchive.Open(outputPath); Assert.Single(archive.Entries.Where(e => !e.IsDirectory)); } @@ -118,7 +128,11 @@ public class AsyncTests : TestBase cts.CancelAfter(10000); // 10 seconds should be plenty var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"); +#if NETFRAMEWORK using var stream = File.OpenRead(testArchive); +#else + await using var stream = File.OpenRead(testArchive); +#endif await using var reader = await ReaderFactory.OpenAsync( new AsyncOnlyStream(stream), cancellationToken: cts.Token @@ -143,9 +157,14 @@ public class AsyncTests : TestBase public async ValueTask Stream_Extensions_Async() { var testFile = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"); - using var inputStream = File.OpenRead(testFile); var outputPath = Path.Combine(SCRATCH_FILES_PATH, "async_copy.bin"); +#if NETFRAMEWORK + using var inputStream = File.OpenRead(testFile); using var outputStream = File.Create(outputPath); +#else + await using var inputStream = File.OpenRead(testFile); + await using var outputStream = File.Create(outputPath); +#endif // Test the async extension method var buffer = new byte[8192]; @@ -163,14 +182,22 @@ public class AsyncTests : TestBase public async ValueTask EntryStream_ReadAsync_Works() { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"); +#if NETFRAMEWORK using var stream = File.OpenRead(testArchive); +#else + await using var stream = File.OpenRead(testArchive); +#endif await using var reader = await ReaderFactory.OpenAsync(new AsyncOnlyStream(stream)); while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) { +#if NETFRAMEWORK using var entryStream = await reader.OpenEntryStreamAsync(); +#else + await using var entryStream = await reader.OpenEntryStreamAsync(); +#endif var buffer = new byte[4096]; var totalRead = 0; int bytesRead; @@ -196,8 +223,13 @@ public class AsyncTests : TestBase var compressedPath = Path.Combine(SCRATCH_FILES_PATH, "async_compressed.gz"); // Test async write with GZipStream +#if NETFRAMEWORK using (var fileStream = File.Create(compressedPath)) using (var gzipStream = new GZipStream(fileStream, CompressionMode.Compress)) +#else + await using (var fileStream = File.Create(compressedPath)) + await using (var gzipStream = new GZipStream(fileStream, CompressionMode.Compress)) +#endif { await gzipStream.WriteAsync(testData, 0, testData.Length); await gzipStream.FlushAsync(); @@ -205,10 +237,14 @@ public class AsyncTests : TestBase Assert.True(File.Exists(compressedPath)); Assert.True(new FileInfo(compressedPath).Length > 0); - +#if NETFRAMEWORK + using (var fileStream = File.Create(compressedPath)) + using (var gzipStream = new GZipStream(fileStream, CompressionMode.Compress)) +#else // Test async read with GZipStream - using (var fileStream = File.OpenRead(compressedPath)) - using (var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress)) + await using (var fileStream = File.Create(compressedPath)) + await using (var gzipStream = new GZipStream(fileStream, CompressionMode.Compress)) +#endif { var decompressed = new byte[testData.Length]; var totalRead = 0; diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs index 9b895cc6..caa30ffa 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs @@ -16,7 +16,11 @@ public class GZipArchiveAsyncTests : ArchiveTests [Fact] public async ValueTask GZip_Archive_Generic_Async() { +#if NETFRAMEWORK using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) +#else + await using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) +#endif using (var archive = ArchiveFactory.Open(stream)) { var entry = archive.Entries.First(); @@ -38,8 +42,12 @@ public class GZipArchiveAsyncTests : ArchiveTests [Fact] public async ValueTask GZip_Archive_Async() { +#if NETFRAMEWORK using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) - using (var archive = GZipArchive.Open(stream)) +#else + await using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) +#endif + await using (var archive = GZipArchive.Open(stream)) { var entry = archive.Entries.First(); await entry.WriteToFileAsync(Path.Combine(SCRATCH_FILES_PATH, entry.Key.NotNull())); @@ -61,8 +69,12 @@ public class GZipArchiveAsyncTests : ArchiveTests public async ValueTask GZip_Archive_NoAdd_Async() { var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg"); +#if NETFRAMEWORK using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); - using var archive = GZipArchive.Open(stream); +#else + await using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); +#endif + await using var archive = GZipArchive.Open(stream); Assert.Throws(() => archive.AddEntry("jpg\\test.jpg", jpg)); await archive.SaveToAsync(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz")); } @@ -71,34 +83,55 @@ public class GZipArchiveAsyncTests : ArchiveTests public async ValueTask GZip_Archive_Multiple_Reads_Async() { var inputStream = new MemoryStream(); - using (var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) +#if NETFRAMEWORK + using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) +#else + await using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) +#endif { - await fileStream.CopyToAsync(inputStream); + await stream.CopyToAsync(inputStream); inputStream.Position = 0; } - using var archive = GZipArchive.Open(inputStream); + + await using var archive = GZipArchive.Open(inputStream); var archiveEntry = archive.Entries.First(); MemoryStream tarStream; +#if NETFRAMEWORK using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) +#else + await using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) +#endif { tarStream = new MemoryStream(); await entryStream.CopyToAsync(tarStream); } var size = tarStream.Length; +#if NETFRAMEWORK using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) +#else + await using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) +#endif { tarStream = new MemoryStream(); await entryStream.CopyToAsync(tarStream); } Assert.Equal(size, tarStream.Length); +#if NETFRAMEWORK using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) +#else + await using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) +#endif { var result = TarArchive.IsTarFile(entryStream); Assert.True(result); } Assert.Equal(size, tarStream.Length); +#if NETFRAMEWORK using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) +#else + await using (var entryStream = await archiveEntry.OpenEntryStreamAsync()) +#endif { tarStream = new MemoryStream(); await entryStream.CopyToAsync(tarStream); From 3fb07d129fec04ab49f9122cd3037b261d768355 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 12 Jan 2026 10:19:01 +0000 Subject: [PATCH 38/46] Use async dispose always --- tests/SharpCompress.Test/TestBase.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/SharpCompress.Test/TestBase.cs b/tests/SharpCompress.Test/TestBase.cs index 73d03559..e65305e1 100644 --- a/tests/SharpCompress.Test/TestBase.cs +++ b/tests/SharpCompress.Test/TestBase.cs @@ -4,12 +4,13 @@ using System.IO; using System.Linq; using System.Text; using System.Threading; +using System.Threading.Tasks; using SharpCompress.Readers; using Xunit; namespace SharpCompress.Test; -public class TestBase : IDisposable +public class TestBase : IAsyncDisposable { private static readonly string SOLUTION_BASE_PATH; public static readonly string TEST_ARCHIVES_PATH; @@ -54,8 +55,10 @@ public class TestBase : IDisposable Directory.CreateDirectory(SCRATCH2_FILES_PATH); } - public void Dispose() + //akways use async dispose since we have I/O and sync Dispose doesn't wait when using xunit + public async ValueTask DisposeAsync() { + await Task.CompletedTask; Directory.Delete(SCRATCH_FILES_PATH, true); Directory.Delete(SCRATCH2_FILES_PATH, true); } From 0f37049aad33c302e42965c84bcc4bdf0cfa457c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:05:04 +0000 Subject: [PATCH 39/46] Initial plan From 90c8ff8650697b64b638a05db4b5d6c1eb3a961b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:05:11 +0000 Subject: [PATCH 40/46] Initial plan From 292da90184f6e835a955a9320567a48fb36b09ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:05:22 +0000 Subject: [PATCH 41/46] Initial plan From 3a636531e887495fd51c90edcb1dca307e345665 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:05:34 +0000 Subject: [PATCH 42/46] Initial plan From 64a09eb0f8a99fcbd8311f2a4efbec6352ba3c45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:06:47 +0000 Subject: [PATCH 43/46] Fix typo in TestBase.cs comment: 'akways' -> 'always' Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- tests/SharpCompress.Test/TestBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SharpCompress.Test/TestBase.cs b/tests/SharpCompress.Test/TestBase.cs index e65305e1..702d6bca 100644 --- a/tests/SharpCompress.Test/TestBase.cs +++ b/tests/SharpCompress.Test/TestBase.cs @@ -55,7 +55,7 @@ public class TestBase : IAsyncDisposable Directory.CreateDirectory(SCRATCH2_FILES_PATH); } - //akways use async dispose since we have I/O and sync Dispose doesn't wait when using xunit + //always use async dispose since we have I/O and sync Dispose doesn't wait when using xunit public async ValueTask DisposeAsync() { await Task.CompletedTask; From 921cff00a5bc3e68e4a676d7c1b521f951b14d42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:08:09 +0000 Subject: [PATCH 44/46] Fix async test method naming: rename Sync to Async Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index dfb84ffa..41420f98 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -119,7 +119,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async ValueTask Zip_Random_Write_Remove_Sync() + public async ValueTask 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"); @@ -141,7 +141,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async ValueTask Zip_Random_Write_Add_Sync() + public async ValueTask 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"); From f4b1780d8ad0d749a1ff59b6e4aa45b90228b583 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:08:23 +0000 Subject: [PATCH 45/46] Rename async test methods to use _Async suffix instead of _Sync Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index dfb84ffa..41420f98 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -119,7 +119,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async ValueTask Zip_Random_Write_Remove_Sync() + public async ValueTask 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"); @@ -141,7 +141,7 @@ public class ZipArchiveAsyncTests : ArchiveTests } [Fact] - public async ValueTask Zip_Random_Write_Add_Sync() + public async ValueTask 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"); From 95c409d97944444952d67f00a764a19b0f57c6a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:09:12 +0000 Subject: [PATCH 46/46] Change File.Create to File.OpenWrite in TarReaderAsyncTests for consistency Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index d6a04546..4be59584 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -92,7 +92,7 @@ public class TarReaderAsyncTests : ReaderTests } var destinationFileName = Path.Combine(destdir, file.NotNull()); - using var fs = File.Create(destinationFileName); + using var fs = File.OpenWrite(destinationFileName); await entryStream.CopyToAsync(fs); } }