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)