mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-24 07:54:39 +00:00
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>
This commit is contained in:
@@ -22,6 +22,13 @@ class AutoArchiveFactory : IArchiveFactory
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> 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) =>
|
||||
|
||||
@@ -231,6 +231,28 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<bool> 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) { }
|
||||
|
||||
|
||||
@@ -283,6 +283,92 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> 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<bool> 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<ZipVolume> LoadVolumes(SourceStream stream)
|
||||
{
|
||||
stream.LoadAllParts(); //request all streams
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual Task<bool> IsArchiveAsync(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(IsArchive(stream, password, bufferSize));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual FileInfo? GetFilePart(int index, FileInfo part1) => null;
|
||||
|
||||
|
||||
@@ -48,6 +48,14 @@ public class GZipFactory
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => GZipArchive.IsGZipFile(stream);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task<bool> IsArchiveAsync(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize,
|
||||
CancellationToken cancellationToken = default
|
||||
) => GZipArchive.IsGZipFileAsync(stream, cancellationToken);
|
||||
|
||||
#endregion
|
||||
|
||||
#region IArchiveFactory
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the stream represents an archive of the format defined by this type asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="stream">A stream, pointing to the beginning of the archive.</param>
|
||||
/// <param name="password">optional password</param>
|
||||
/// <param name="bufferSize">buffer size for reading</param>
|
||||
/// <param name="cancellationToken">cancellation token</param>
|
||||
Task<bool> IsArchiveAsync(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// From a passed in archive (zip, rar, 7z, 001), return all parts.
|
||||
/// </summary>
|
||||
|
||||
@@ -81,6 +81,49 @@ public class ZipFactory
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<bool> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override FileInfo? GetFilePart(int index, FileInfo part1) =>
|
||||
ZipArchiveVolumeFactory.GetFilePart(index, part1);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
) => stream.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override ValueTask<int> ReadAsync(
|
||||
Memory<byte> 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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user