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