From a92f82ef2819849d7d8ea4c4755eeb8b3239d1f2 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 12 Feb 2026 15:09:21 +0000 Subject: [PATCH] complete the usage of providers for detection and async --- README.md | 2 +- docs/API.md | 4 +- docs/USAGE.md | 2 + .../Archives/GZip/GZipArchive.Async.cs | 2 +- .../Archives/GZip/GZipArchive.cs | 2 +- .../Archives/Tar/TarArchive.Async.cs | 22 +- .../Archives/Tar/TarArchive.Factory.cs | 34 ++- src/SharpCompress/Archives/Tar/TarArchive.cs | 49 ++-- src/SharpCompress/Archives/Zip/ZipArchive.cs | 2 +- .../Common/Lzw/LzwEntry.Async.cs | 4 +- src/SharpCompress/Common/Lzw/LzwEntry.cs | 5 +- .../Common/Lzw/LzwFilePart.Async.cs | 4 +- src/SharpCompress/Common/Lzw/LzwFilePart.cs | 21 +- .../Common/Zip/ZipFilePart.Async.cs | 229 +++++----------- src/SharpCompress/Factories/Factory.cs | 24 ++ src/SharpCompress/Factories/GZipFactory.cs | 6 +- src/SharpCompress/Factories/LzwFactory.cs | 7 +- src/SharpCompress/Factories/TarFactory.cs | 70 ++++- .../Providers/CompressionProviderRegistry.cs | 4 +- .../SystemDeflateCompressionProvider.cs | 4 +- .../System/SystemGZipCompressionProvider.cs | 4 +- .../Readers/ReaderFactory.Async.cs | 35 +-- .../Readers/Tar/TarReader.Factory.cs | 140 +++------- src/SharpCompress/Writers/Zip/ZipWriter.cs | 32 +++ .../Benchmarks/TarBenchmarks.cs | 1 + .../CompressionProviderTests.cs | 258 +++++++++++++++++- 26 files changed, 617 insertions(+), 350 deletions(-) diff --git a/README.md b/README.md index 1d4147d0..2bdb58d3 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Check the [Supported Formats](docs/FORMATS.md) and [Basic Usage.](docs/USAGE.md) ## Custom Compression Providers -If you need to swap out SharpCompress’s built-in codecs, the `Providers` property (and `WithProviders(...)` extensions) on `ReaderOptions` and `WriterOptions` lets you supply a `CompressionProviderRegistry`. The default registry is already wired up, so customization is only necessary when you want to plug in alternatives such as `SystemGZipCompressionProvider` or a third-party `CompressionProvider`. See [docs/USAGE.md#custom-compression-providers](docs/USAGE.md#custom-compression-providers) for guided examples. +If you need to swap out SharpCompress’s built-in codecs, the `Providers` property (and `WithProviders(...)` extensions) on `ReaderOptions` and `WriterOptions` lets you supply a `CompressionProviderRegistry`. The selected registry is used by Reader/Writer APIs, Archive APIs, and async extraction paths, so the same provider choice is applied consistently across open/read/write flows. The default registry is already wired up, so customization is only necessary when you want to plug in alternatives such as `SystemGZipCompressionProvider` or a third-party `CompressionProvider`. See [docs/USAGE.md#custom-compression-providers](docs/USAGE.md#custom-compression-providers) for guided examples. ## Recommended Formats diff --git a/docs/API.md b/docs/API.md index aa54ca13..15511e5d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -320,7 +320,7 @@ ZipWriterEntryOptions: per-entry ZIP overrides (compression, level, timestamps, ### Compression Providers -`ReaderOptions` and `WriterOptions` expose a `Providers` registry that controls which `ICompressionProvider` implementations are used for each `CompressionType`. The registry defaults to `CompressionProviderRegistry.Default`, so you only need to set it if you want to swap in a custom provider (for example the `SystemGZipCompressionProvider`). +`ReaderOptions` and `WriterOptions` expose a `Providers` registry that controls which `ICompressionProvider` implementations are used for each `CompressionType`. The registry defaults to `CompressionProviderRegistry.Default`, so you only need to set it if you want to swap in a custom provider (for example the `SystemGZipCompressionProvider`). The selected registry is honored by Reader/Writer APIs, Archive APIs, and async entry-stream extraction paths. ```csharp var registry = CompressionProviderRegistry.Default.With(new SystemGZipCompressionProvider()); @@ -334,7 +334,7 @@ using var reader = ReaderFactory.OpenReader(input, readerOptions); using var writer = WriterFactory.OpenWriter(output, ArchiveType.GZip, writerOptions); ``` -When a format needs additional initialization/finalization data (LZMA, PPMd, etc.) the registry exposes `GetCompressingProvider` which returns the `ICompressionProviderHooks` contract; the rest of the API continues to flow through `Providers`. +When a format needs additional initialization/finalization data (LZMA, PPMd, etc.) the registry exposes `GetCompressingProvider` which returns the `ICompressionProviderHooks` contract; the rest of the API continues to flow through `Providers`, including pre/properties/post compression hook data. --- diff --git a/docs/USAGE.md b/docs/USAGE.md index afa3493a..772bf90d 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -210,6 +210,8 @@ foreach(var entry in archive.Entries) By default `ReaderOptions` and `WriterOptions` already include `CompressionProviderRegistry.Default` via their `Providers` property, so you can read and write without touching the registry yet still get SharpCompress’s built-in implementations. +The configured registry is used consistently across Reader APIs, Writer APIs, Archive APIs, and async entry-stream extraction, including compressed TAR wrappers and ZIP async decompression. + To replace a specific algorithm (for example to use `System.IO.Compression` for GZip or Deflate), create a modified registry and pass it through the same options: ```C# diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs index da03f315..67443ab9 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs @@ -77,7 +77,7 @@ public partial class GZipArchive { var stream = Volumes.Single().Stream; stream.Position = 0; - return new((IAsyncReader)GZipReader.OpenReader(stream)); + return new((IAsyncReader)GZipReader.OpenReader(stream, ReaderOptions)); } protected override async IAsyncEnumerable LoadEntriesAsync( diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index 6b1baaaf..06000d0a 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -96,6 +96,6 @@ public partial class GZipArchive { var stream = Volumes.Single().Stream; stream.Position = 0; - return GZipReader.OpenReader(stream); + return GZipReader.OpenReader(stream, ReaderOptions); } } diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs index 1cd7dd91..64cbf5dc 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -89,26 +89,32 @@ public partial class TarArchive { var stream = Volumes.Single().Stream; stream.Position = 0; - return new((IAsyncReader)TarReader.OpenReader(stream)); + return new((IAsyncReader)new TarReader(stream, ReaderOptions, _compressionType)); } protected override async IAsyncEnumerable LoadEntriesAsync( IAsyncEnumerable volumes ) { - var stream = (await volumes.SingleAsync().ConfigureAwait(false)).Stream; + var sourceStream = (await volumes.SingleAsync().ConfigureAwait(false)).Stream; + var stream = GetStream(sourceStream); if (stream.CanSeek) { stream.Position = 0; } + var streamingMode = + _compressionType == CompressionType.None + ? StreamingMode.Seekable + : StreamingMode.Streaming; + // Always use async header reading in LoadEntriesAsync for consistency { // Use async header reading for async-only streams TarHeader? previousHeader = null; await foreach ( var header in TarHeaderFactory.ReadHeaderAsync( - StreamingMode.Seekable, + streamingMode, stream, ReaderOptions.ArchiveEncoding ) @@ -126,7 +132,10 @@ public partial class TarArchive { var entry = new TarArchiveEntry( this, - new TarFilePart(previousHeader, stream), + new TarFilePart( + previousHeader, + _compressionType == CompressionType.None ? stream : null + ), CompressionType.None, ReaderOptions ); @@ -151,7 +160,10 @@ public partial class TarArchive } yield return new TarArchiveEntry( this, - new TarFilePart(header, stream), + new TarFilePart( + header, + _compressionType == CompressionType.None ? stream : null + ), CompressionType.None, ReaderOptions ); diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index 99425528..e1775875 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -56,7 +56,10 @@ public partial class TarArchive i => i < files.Length ? files[i] : null, readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false } ); - var compressionType = TarFactory.GetCompressionType(sourceStream); + var compressionType = TarFactory.GetCompressionType( + sourceStream, + sourceStream.ReaderOptions.Providers + ); sourceStream.Seek(0, SeekOrigin.Begin); return new TarArchive(sourceStream, compressionType); } @@ -73,7 +76,10 @@ public partial class TarArchive i => i < strms.Length ? strms[i] : null, readerOptions ?? new ReaderOptions() ); - var compressionType = TarFactory.GetCompressionType(sourceStream); + var compressionType = TarFactory.GetCompressionType( + sourceStream, + sourceStream.ReaderOptions.Providers + ); sourceStream.Seek(0, SeekOrigin.Begin); return new TarArchive(sourceStream, compressionType); } @@ -106,7 +112,11 @@ public partial class TarArchive readerOptions ?? new ReaderOptions() ); var compressionType = await TarFactory - .GetCompressionTypeAsync(sourceStream, cancellationToken) + .GetCompressionTypeAsync( + sourceStream, + sourceStream.ReaderOptions.Providers, + cancellationToken + ) .ConfigureAwait(false); sourceStream.Seek(0, SeekOrigin.Begin); return new TarArchive(sourceStream, compressionType); @@ -134,7 +144,11 @@ public partial class TarArchive readerOptions ??= new ReaderOptions() { LeaveStreamOpen = false }; var sourceStream = new SourceStream(fileInfo, i => null, readerOptions); var compressionType = await TarFactory - .GetCompressionTypeAsync(sourceStream, cancellationToken) + .GetCompressionTypeAsync( + sourceStream, + sourceStream.ReaderOptions.Providers, + cancellationToken + ) .ConfigureAwait(false); sourceStream.Seek(0, SeekOrigin.Begin); return new TarArchive(sourceStream, compressionType); @@ -155,7 +169,11 @@ public partial class TarArchive readerOptions ?? new ReaderOptions() ); var compressionType = await TarFactory - .GetCompressionTypeAsync(sourceStream, cancellationToken) + .GetCompressionTypeAsync( + sourceStream, + sourceStream.ReaderOptions.Providers, + cancellationToken + ) .ConfigureAwait(false); sourceStream.Seek(0, SeekOrigin.Begin); return new TarArchive(sourceStream, compressionType); @@ -176,7 +194,11 @@ public partial class TarArchive readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false } ); var compressionType = await TarFactory - .GetCompressionTypeAsync(sourceStream, cancellationToken) + .GetCompressionTypeAsync( + sourceStream, + sourceStream.ReaderOptions.Providers, + cancellationToken + ) .ConfigureAwait(false); sourceStream.Seek(0, SeekOrigin.Begin); return new TarArchive(sourceStream, compressionType); diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index 130bb47f..59d6c64d 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -5,17 +5,10 @@ using System.Linq; using SharpCompress.Common; using SharpCompress.Common.Tar; using SharpCompress.Common.Tar.Headers; -using SharpCompress.Compressors.BZip2; -using SharpCompress.Compressors.Deflate; -using SharpCompress.Compressors.LZMA; -using SharpCompress.Compressors.Lzw; -using SharpCompress.Compressors.Xz; -using SharpCompress.Compressors.ZStandard; using SharpCompress.IO; using SharpCompress.Readers; using SharpCompress.Readers.Tar; using SharpCompress.Writers.Tar; -using CompressionMode = SharpCompress.Compressors.CompressionMode; using Constants = SharpCompress.Common.Constants; namespace SharpCompress.Archives.Tar; @@ -43,12 +36,30 @@ public partial class TarArchive private Stream GetStream(Stream stream) => _compressionType switch { - CompressionType.BZip2 => BZip2Stream.Create(stream, CompressionMode.Decompress, false), - CompressionType.GZip => new GZipStream(stream, CompressionMode.Decompress), - CompressionType.ZStandard => new ZStandardStream(stream), - CompressionType.LZip => new LZipStream(stream, CompressionMode.Decompress), - CompressionType.Xz => new XZStream(stream), - CompressionType.Lzw => new LzwStream(stream), + CompressionType.BZip2 => ReaderOptions.Providers.CreateDecompressStream( + CompressionType.BZip2, + stream + ), + CompressionType.GZip => ReaderOptions.Providers.CreateDecompressStream( + CompressionType.GZip, + stream + ), + CompressionType.ZStandard => ReaderOptions.Providers.CreateDecompressStream( + CompressionType.ZStandard, + stream + ), + CompressionType.LZip => ReaderOptions.Providers.CreateDecompressStream( + CompressionType.LZip, + stream + ), + CompressionType.Xz => ReaderOptions.Providers.CreateDecompressStream( + CompressionType.Xz, + stream + ), + CompressionType.Lzw => ReaderOptions.Providers.CreateDecompressStream( + CompressionType.Lzw, + stream + ), CompressionType.None => stream, _ => throw new NotSupportedException("Invalid compression type: " + _compressionType), }; @@ -83,7 +94,10 @@ public partial class TarArchive { var entry = new TarArchiveEntry( this, - new TarFilePart(previousHeader, stream), + new TarFilePart( + previousHeader, + _compressionType == CompressionType.None ? stream : null + ), CompressionType.None, ReaderOptions ); @@ -106,7 +120,10 @@ public partial class TarArchive } yield return new TarArchiveEntry( this, - new TarFilePart(header, stream), + new TarFilePart( + header, + _compressionType == CompressionType.None ? stream : null + ), CompressionType.None, ReaderOptions ); @@ -178,6 +195,6 @@ public partial class TarArchive { var stream = Volumes.Single().Stream; stream.Position = 0; - return TarReader.OpenReader(GetStream(stream)); + return new TarReader(stream, ReaderOptions, _compressionType); } } diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 9e54732f..1d009f03 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -176,6 +176,6 @@ public partial class ZipArchive { var stream = Volumes.Single().Stream; stream.Position = 0; - return new((IAsyncReader)ZipReader.OpenReader(stream)); + return new((IAsyncReader)ZipReader.OpenReader(stream, ReaderOptions, Entries)); } } diff --git a/src/SharpCompress/Common/Lzw/LzwEntry.Async.cs b/src/SharpCompress/Common/Lzw/LzwEntry.Async.cs index a1982d1b..d42ceb41 100644 --- a/src/SharpCompress/Common/Lzw/LzwEntry.Async.cs +++ b/src/SharpCompress/Common/Lzw/LzwEntry.Async.cs @@ -15,7 +15,9 @@ public partial class LzwEntry ) { yield return new LzwEntry( - await LzwFilePart.CreateAsync(stream, options.ArchiveEncoding, cancellationToken), + await LzwFilePart + .CreateAsync(stream, options.ArchiveEncoding, options.Providers, cancellationToken) + .ConfigureAwait(false), options ); } diff --git a/src/SharpCompress/Common/Lzw/LzwEntry.cs b/src/SharpCompress/Common/Lzw/LzwEntry.cs index 92490428..ba795368 100644 --- a/src/SharpCompress/Common/Lzw/LzwEntry.cs +++ b/src/SharpCompress/Common/Lzw/LzwEntry.cs @@ -46,7 +46,10 @@ public partial class LzwEntry : Entry internal static IEnumerable GetEntries(Stream stream, ReaderOptions options) { - yield return new LzwEntry(LzwFilePart.Create(stream, options.ArchiveEncoding), options); + yield return new LzwEntry( + LzwFilePart.Create(stream, options.ArchiveEncoding, options.Providers), + options + ); } // Async methods moved to LzwEntry.Async.cs diff --git a/src/SharpCompress/Common/Lzw/LzwFilePart.Async.cs b/src/SharpCompress/Common/Lzw/LzwFilePart.Async.cs index 8b6a5a32..ebc8df26 100644 --- a/src/SharpCompress/Common/Lzw/LzwFilePart.Async.cs +++ b/src/SharpCompress/Common/Lzw/LzwFilePart.Async.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Providers; namespace SharpCompress.Common.Lzw; @@ -9,11 +10,12 @@ internal sealed partial class LzwFilePart internal static async ValueTask CreateAsync( Stream stream, IArchiveEncoding archiveEncoding, + CompressionProviderRegistry compressionProviders, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - var part = new LzwFilePart(stream, archiveEncoding); + var part = new LzwFilePart(stream, archiveEncoding, compressionProviders); // For non-seekable streams, we can't track position, so use 0 since the stream will be // read sequentially from its current position. diff --git a/src/SharpCompress/Common/Lzw/LzwFilePart.cs b/src/SharpCompress/Common/Lzw/LzwFilePart.cs index 74d682ea..08802d2f 100644 --- a/src/SharpCompress/Common/Lzw/LzwFilePart.cs +++ b/src/SharpCompress/Common/Lzw/LzwFilePart.cs @@ -1,5 +1,6 @@ using System.IO; -using SharpCompress.Compressors.Lzw; +using SharpCompress.Common; +using SharpCompress.Providers; namespace SharpCompress.Common.Lzw; @@ -7,10 +8,15 @@ internal sealed partial class LzwFilePart : FilePart { private readonly Stream _stream; private readonly string? _name; + private readonly CompressionProviderRegistry _compressionProviders; - internal static LzwFilePart Create(Stream stream, IArchiveEncoding archiveEncoding) + internal static LzwFilePart Create( + Stream stream, + IArchiveEncoding archiveEncoding, + CompressionProviderRegistry compressionProviders + ) { - var part = new LzwFilePart(stream, archiveEncoding); + var part = new LzwFilePart(stream, archiveEncoding, compressionProviders); // For non-seekable streams, we can't track position, so use 0 since the stream will be // read sequentially from its current position. @@ -18,11 +24,16 @@ internal sealed partial class LzwFilePart : FilePart return part; } - private LzwFilePart(Stream stream, IArchiveEncoding archiveEncoding) + private LzwFilePart( + Stream stream, + IArchiveEncoding archiveEncoding, + CompressionProviderRegistry compressionProviders + ) : base(archiveEncoding) { _stream = stream; _name = DeriveFileName(stream); + _compressionProviders = compressionProviders; } internal long EntryStartPosition { get; private set; } @@ -30,7 +41,7 @@ internal sealed partial class LzwFilePart : FilePart internal override string? FilePartName => _name; internal override Stream GetCompressedStream() => - new LzwStream(_stream) { IsStreamOwner = false }; + _compressionProviders.CreateDecompressStream(CompressionType.Lzw, _stream); internal override Stream GetRawStream() => _stream; diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs index 786f2226..7c47e0cd 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs @@ -6,17 +6,8 @@ using System.Threading; using System.Threading.Tasks; using SharpCompress.Common.Zip.Headers; using SharpCompress.Compressors; -using SharpCompress.Compressors.BZip2; -using SharpCompress.Compressors.Deflate; -using SharpCompress.Compressors.Deflate64; -using SharpCompress.Compressors.Explode; -using SharpCompress.Compressors.LZMA; -using SharpCompress.Compressors.PPMd; -using SharpCompress.Compressors.Reduce; -using SharpCompress.Compressors.Shrink; -using SharpCompress.Compressors.Xz; -using SharpCompress.Compressors.ZStandard; using SharpCompress.IO; +using SharpCompress.Providers; namespace SharpCompress.Common.Zip; @@ -123,6 +114,7 @@ internal abstract partial class ZipFilePart CancellationToken cancellationToken = default ) { + // Handle special cases first switch (method) { case ZipCompressionMethod.None: @@ -134,98 +126,24 @@ internal abstract partial class ZipFilePart return stream; } - case ZipCompressionMethod.Shrink: + case ZipCompressionMethod.WinzipAes: { - return await ShrinkStream - .CreateAsync( - stream, - CompressionMode.Decompress, - Header.CompressedSize, - Header.UncompressedSize, - cancellationToken - ) - .ConfigureAwait(false); - } - case ZipCompressionMethod.Reduce1: - { - return await ReduceStream - .CreateAsync( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 1, - cancellationToken - ) - .ConfigureAwait(false); - } - case ZipCompressionMethod.Reduce2: - { - return await ReduceStream - .CreateAsync( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 2, - cancellationToken - ) - .ConfigureAwait(false); - } - case ZipCompressionMethod.Reduce3: - { - return await ReduceStream - .CreateAsync( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 3, - cancellationToken - ) - .ConfigureAwait(false); - } - case ZipCompressionMethod.Reduce4: - { - return await ReduceStream - .CreateAsync( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 4, - cancellationToken - ) - .ConfigureAwait(false); - } - case ZipCompressionMethod.Explode: - { - return await ExplodeStream - .CreateAsync( - stream, - Header.CompressedSize, - Header.UncompressedSize, - Header.Flags, - cancellationToken - ) + return await CreateWinzipAesDecompressionStreamAsync(stream, cancellationToken) .ConfigureAwait(false); } + } - case ZipCompressionMethod.Deflate: - { - return new DeflateStream(stream, CompressionMode.Decompress); - } - case ZipCompressionMethod.Deflate64: - { - return new Deflate64Stream(stream, CompressionMode.Decompress); - } - case ZipCompressionMethod.BZip2: - { - return await BZip2Stream - .CreateAsync( - stream, - CompressionMode.Decompress, - false, - cancellationToken: cancellationToken - ) - .ConfigureAwait(false); - } + var compressionType = ToCompressionType(method); + var providers = GetProviders(); + var context = new CompressionContext + { + InputSize = Header.CompressedSize, + OutputSize = Header.UncompressedSize, + CanSeek = stream.CanSeek, + }; + + switch (method) + { case ZipCompressionMethod.LZMA: { if (FlagUtility.HasFlag(Header.Flags, HeaderFlags.Encrypted)) @@ -234,81 +152,80 @@ internal abstract partial class ZipFilePart } 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 await LzmaStream - .CreateAsync( - props, - stream, + + context = context with + { + Properties = props, + InputSize = Header.CompressedSize > 0 ? Header.CompressedSize - 4 - props.Length : -1, - FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) - ? -1 - : Header.UncompressedSize - ) - .ConfigureAwait(false); - } - case ZipCompressionMethod.Xz: - { - return new XZStream(stream); - } - case ZipCompressionMethod.ZStandard: - { - return new DecompressionStream(stream); + OutputSize = FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) + ? -1 + : Header.UncompressedSize, + }; + + return providers.CreateDecompressStream(compressionType, stream, context); } case ZipCompressionMethod.PPMd: { var props = new byte[2]; await stream.ReadFullyAsync(props, 0, 2, cancellationToken).ConfigureAwait(false); - return await PpmdStream - .CreateAsync(new PpmdProperties(props), stream, false, cancellationToken) - .ConfigureAwait(false); + context = context with { Properties = props }; + return providers.CreateDecompressStream(compressionType, stream, context); } - case ZipCompressionMethod.WinzipAes: + case ZipCompressionMethod.Explode: { - 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."); - } - - 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 - ) - .ConfigureAwait(false); + context = context with { FormatOptions = Header.Flags }; + return providers.CreateDecompressStream(compressionType, stream, context); } default: { - throw new NotSupportedException("CompressionMethod: " + Header.CompressionMethod); + return providers.CreateDecompressStream(compressionType, stream, context); } } } + + private async ValueTask CreateWinzipAesDecompressionStreamAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + 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."); + } + + 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 + ) + .ConfigureAwait(false); + } } diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 69254cad..8c8cbde2 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -96,4 +96,28 @@ public abstract class Factory : IFactory stream.Rewind(); return false; } + + internal virtual async ValueTask TryOpenReaderAsync( + SharpCompressStream stream, + ReaderOptions options, + CancellationToken cancellationToken = default + ) + { + if (this is IReaderFactory readerFactory) + { + stream.Rewind(); + if ( + await IsArchiveAsync(stream, options.Password, cancellationToken) + .ConfigureAwait(false) + ) + { + stream.Rewind(true); + return await readerFactory + .OpenAsyncReader(stream, options, cancellationToken) + .ConfigureAwait(false); + } + } + stream.Rewind(); + return null; + } } diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index ab40438c..4317bb05 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.IO.Compression; using System.Threading; using System.Threading.Tasks; using SharpCompress.Archives; @@ -128,7 +127,10 @@ public class GZipFactory if (GZipArchive.IsGZipFile(sharpCompressStream)) { sharpCompressStream.Rewind(); - var testStream = new GZipStream(sharpCompressStream, CompressionMode.Decompress); + using var testStream = options.Providers.CreateDecompressStream( + CompressionType.GZip, + SharpCompressStream.CreateNonDisposing(sharpCompressStream) + ); if (TarArchive.IsTarFile(testStream)) { sharpCompressStream.StopRecording(); diff --git a/src/SharpCompress/Factories/LzwFactory.cs b/src/SharpCompress/Factories/LzwFactory.cs index 7a0eeec0..c4797be5 100644 --- a/src/SharpCompress/Factories/LzwFactory.cs +++ b/src/SharpCompress/Factories/LzwFactory.cs @@ -58,7 +58,12 @@ public class LzwFactory : Factory, IReaderFactory if (LzwStream.IsLzwStream(sharpCompressStream)) { sharpCompressStream.Rewind(); - using (var testStream = new LzwStream(sharpCompressStream) { IsStreamOwner = false }) + using ( + var testStream = options.Providers.CreateDecompressStream( + CompressionType.Lzw, + SharpCompressStream.CreateNonDisposing(sharpCompressStream) + ) + ) { if (TarArchive.IsTarFile(testStream)) { diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index fd2f528f..e3fa7487 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -9,6 +8,7 @@ using SharpCompress.Archives.Tar; using SharpCompress.Common; using SharpCompress.Common.Options; using SharpCompress.IO; +using SharpCompress.Providers; using SharpCompress.Readers; using SharpCompress.Readers.Tar; using SharpCompress.Writers; @@ -50,6 +50,7 @@ public class TarFactory /// public override bool IsArchive(Stream stream, string? password = null) { + var providers = CompressionProviderRegistry.Default; var sharpCompressStream = new SharpCompressStream(stream); sharpCompressStream.StartRecording(); foreach (var wrapper in TarWrapper.Wrappers) @@ -58,7 +59,11 @@ public class TarFactory if (wrapper.IsMatch(sharpCompressStream)) { sharpCompressStream.Rewind(); - var decompressedStream = wrapper.CreateStream(sharpCompressStream); + var decompressedStream = CreateProbeDecompressionStream( + sharpCompressStream, + wrapper.CompressionType, + providers + ); if (TarArchive.IsTarFile(decompressedStream)) { sharpCompressStream.Rewind(); @@ -77,6 +82,7 @@ public class TarFactory CancellationToken cancellationToken = default ) { + var providers = CompressionProviderRegistry.Default; var sharpCompressStream = new SharpCompressStream(stream); sharpCompressStream.StartRecording(); foreach (var wrapper in TarWrapper.Wrappers) @@ -89,9 +95,11 @@ public class TarFactory ) { sharpCompressStream.Rewind(); - var decompressedStream = await wrapper - .CreateStreamAsync(sharpCompressStream, cancellationToken) - .ConfigureAwait(false); + var decompressedStream = CreateProbeDecompressionStream( + sharpCompressStream, + wrapper.CompressionType, + providers + ); if ( await TarArchive .IsTarFileAsync(decompressedStream, cancellationToken) @@ -109,8 +117,24 @@ public class TarFactory #endregion - public static CompressionType GetCompressionType(Stream stream) + private static Stream CreateProbeDecompressionStream( + Stream stream, + CompressionType compressionType, + CompressionProviderRegistry providers + ) { + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(stream); + return compressionType == CompressionType.None + ? nonDisposingStream + : providers.CreateDecompressStream(compressionType, nonDisposingStream); + } + + public static CompressionType GetCompressionType( + Stream stream, + CompressionProviderRegistry? providers = null + ) + { + providers ??= CompressionProviderRegistry.Default; stream.Seek(0, SeekOrigin.Begin); foreach (var wrapper in TarWrapper.Wrappers) { @@ -118,7 +142,11 @@ public class TarFactory if (wrapper.IsMatch(stream)) { stream.Seek(0, SeekOrigin.Begin); - var decompressedStream = wrapper.CreateStream(stream); + var decompressedStream = CreateProbeDecompressionStream( + stream, + wrapper.CompressionType, + providers + ); if (TarArchive.IsTarFile(decompressedStream)) { return wrapper.CompressionType; @@ -130,17 +158,23 @@ public class TarFactory public static async ValueTask GetCompressionTypeAsync( Stream stream, + CompressionProviderRegistry? providers = null, CancellationToken cancellationToken = default ) { + providers ??= CompressionProviderRegistry.Default; stream.Seek(0, SeekOrigin.Begin); foreach (var wrapper in TarWrapper.Wrappers) { stream.Seek(0, SeekOrigin.Begin); - if (wrapper.IsMatch(stream)) + if (await wrapper.IsMatchAsync(stream, cancellationToken).ConfigureAwait(false)) { stream.Seek(0, SeekOrigin.Begin); - var decompressedStream = wrapper.CreateStream(stream); + var decompressedStream = CreateProbeDecompressionStream( + stream, + wrapper.CompressionType, + providers + ); if ( await TarArchive .IsTarFileAsync(decompressedStream, cancellationToken) @@ -228,7 +262,11 @@ public class TarFactory if (wrapper.IsMatch(sharpCompressStream)) { sharpCompressStream.Rewind(); - var decompressedStream = wrapper.CreateStream(sharpCompressStream); + var decompressedStream = CreateProbeDecompressionStream( + sharpCompressStream, + wrapper.CompressionType, + options.Providers + ); if (TarArchive.IsTarFile(decompressedStream)) { sharpCompressStream.StopRecording(); @@ -260,9 +298,11 @@ public class TarFactory ) { sharpCompressStream.Rewind(); - var decompressedStream = await wrapper - .CreateStreamAsync(sharpCompressStream, cancellationToken) - .ConfigureAwait(false); + var decompressedStream = CreateProbeDecompressionStream( + sharpCompressStream, + wrapper.CompressionType, + options.Providers + ); if ( await TarArchive .IsTarFileAsync(decompressedStream, cancellationToken) @@ -275,7 +315,9 @@ public class TarFactory } } } - return (IAsyncReader)TarReader.OpenReader(stream, options); + + sharpCompressStream.Rewind(); + return (IAsyncReader)TarReader.OpenReader(sharpCompressStream, options); } #endregion diff --git a/src/SharpCompress/Providers/CompressionProviderRegistry.cs b/src/SharpCompress/Providers/CompressionProviderRegistry.cs index 34805949..df21e1c6 100644 --- a/src/SharpCompress/Providers/CompressionProviderRegistry.cs +++ b/src/SharpCompress/Providers/CompressionProviderRegistry.cs @@ -38,7 +38,9 @@ public sealed class CompressionProviderRegistry private readonly Dictionary _providers; - private CompressionProviderRegistry(Dictionary providers) => _providers = providers; + private CompressionProviderRegistry( + Dictionary providers + ) => _providers = providers; /// /// Gets the provider for a given compression type, or null if none is registered. diff --git a/src/SharpCompress/Providers/System/SystemDeflateCompressionProvider.cs b/src/SharpCompress/Providers/System/SystemDeflateCompressionProvider.cs index fdd6dc94..81ed2f12 100644 --- a/src/SharpCompress/Providers/System/SystemDeflateCompressionProvider.cs +++ b/src/SharpCompress/Providers/System/SystemDeflateCompressionProvider.cs @@ -20,7 +20,7 @@ public sealed class SystemDeflateCompressionProvider : ICompressionProvider public Stream CreateCompressStream(Stream destination, int compressionLevel) { var bclLevel = MapCompressionLevel(compressionLevel); - return new DeflateStream(destination, bclLevel, leaveOpen: true); + return new DeflateStream(destination, bclLevel, leaveOpen: false); } public Stream CreateCompressStream( @@ -38,7 +38,7 @@ public sealed class SystemDeflateCompressionProvider : ICompressionProvider return new DeflateStream( source, global::System.IO.Compression.CompressionMode.Decompress, - leaveOpen: true + leaveOpen: false ); } diff --git a/src/SharpCompress/Providers/System/SystemGZipCompressionProvider.cs b/src/SharpCompress/Providers/System/SystemGZipCompressionProvider.cs index 33c24d40..dcaa7f49 100644 --- a/src/SharpCompress/Providers/System/SystemGZipCompressionProvider.cs +++ b/src/SharpCompress/Providers/System/SystemGZipCompressionProvider.cs @@ -20,7 +20,7 @@ public sealed class SystemGZipCompressionProvider : ICompressionProvider public Stream CreateCompressStream(Stream destination, int compressionLevel) { var bclLevel = MapCompressionLevel(compressionLevel); - return new GZipStream(destination, bclLevel, leaveOpen: true); + return new GZipStream(destination, bclLevel, leaveOpen: false); } public Stream CreateCompressStream( @@ -38,7 +38,7 @@ public sealed class SystemGZipCompressionProvider : ICompressionProvider return new GZipStream( source, global::System.IO.Compression.CompressionMode.Decompress, - leaveOpen: true + leaveOpen: false ); } diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index c74a7133..1bed74d5 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -69,20 +69,14 @@ public static partial class ReaderFactory a.GetSupportedExtensions() .Contains(options.ExtensionHint, StringComparer.CurrentCultureIgnoreCase) ); - if (testedFactory is IReaderFactory readerFactory) + if (testedFactory is not null) { - sharpCompressStream.Rewind(); - if ( - await testedFactory - .IsArchiveAsync(sharpCompressStream, cancellationToken: cancellationToken) - .ConfigureAwait(false) - ) + var reader = await testedFactory + .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) + .ConfigureAwait(false); + if (reader is not null) { - sharpCompressStream.Rewind(); - sharpCompressStream.StopRecording(); - return await readerFactory - .OpenAsyncReader(sharpCompressStream, options, cancellationToken) - .ConfigureAwait(false); + return reader; } } sharpCompressStream.Rewind(); @@ -94,19 +88,12 @@ public static partial class ReaderFactory { continue; // Already tested above } - sharpCompressStream.Rewind(); - if ( - factory is IReaderFactory readerFactory - && await factory - .IsArchiveAsync(sharpCompressStream, cancellationToken: cancellationToken) - .ConfigureAwait(false) - ) + var reader = await factory + .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) + .ConfigureAwait(false); + if (reader is not null) { - sharpCompressStream.Rewind(); - sharpCompressStream.StopRecording(); - return await readerFactory - .OpenAsyncReader(sharpCompressStream, options, cancellationToken) - .ConfigureAwait(false); + return reader; } } diff --git a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs index fc775d71..95bb53c2 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs @@ -1,15 +1,11 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using SharpCompress.Archives.GZip; using SharpCompress.Archives.Tar; using SharpCompress.Common; -using SharpCompress.Compressors; -using SharpCompress.Compressors.BZip2; -using SharpCompress.Compressors.Deflate; -using SharpCompress.Compressors.LZMA; -using SharpCompress.Compressors.ZStandard; +using SharpCompress.Factories; using SharpCompress.IO; +using SharpCompress.Providers; namespace SharpCompress.Readers.Tar; @@ -18,6 +14,18 @@ public partial class TarReader : IReaderOpenable #endif { + private static Stream CreateProbeDecompressionStream( + Stream stream, + CompressionType compressionType, + CompressionProviderRegistry providers + ) + { + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(stream); + return compressionType == CompressionType.None + ? nonDisposingStream + : providers.CreateDecompressStream(compressionType, nonDisposingStream); + } + public static ValueTask OpenAsyncReader( string path, ReaderOptions? readerOptions = null, @@ -42,81 +50,38 @@ public partial class TarReader bufferSize: options.RewindableBufferSize ); long pos = sharpCompressStream.Position; - if ( - await GZipArchive - .IsGZipFileAsync(sharpCompressStream, cancellationToken) - .ConfigureAwait(false) - ) + foreach (var wrapper in TarWrapper.Wrappers) { sharpCompressStream.Position = pos; - var testStream = new GZipStream(sharpCompressStream, CompressionMode.Decompress); if ( - await TarArchive.IsTarFileAsync(testStream, cancellationToken).ConfigureAwait(false) + !await wrapper + .IsMatchAsync(sharpCompressStream, cancellationToken) + .ConfigureAwait(false) ) { - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.GZip); + continue; } - throw new InvalidFormatException("Not a tar file."); - } - sharpCompressStream.Position = pos; - if ( - await BZip2Stream - .IsBZip2Async(sharpCompressStream, cancellationToken) - .ConfigureAwait(false) - ) - { + sharpCompressStream.Position = pos; - var testStream = BZip2Stream.Create( + var testStream = CreateProbeDecompressionStream( sharpCompressStream, - CompressionMode.Decompress, - false + wrapper.CompressionType, + options.Providers ); if ( await TarArchive.IsTarFileAsync(testStream, cancellationToken).ConfigureAwait(false) ) { sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.BZip2); + return new TarReader(sharpCompressStream, options, wrapper.CompressionType); } - throw new InvalidFormatException("Not a tar file."); - } - sharpCompressStream.Position = pos; - if ( - await ZStandardStream - .IsZStandardAsync(sharpCompressStream, cancellationToken) - .ConfigureAwait(false) - ) - { - sharpCompressStream.Position = pos; - var testStream = new ZStandardStream(sharpCompressStream); - if ( - await TarArchive.IsTarFileAsync(testStream, cancellationToken).ConfigureAwait(false) - ) + + if (wrapper.CompressionType != CompressionType.None) { - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.ZStandard); + throw new InvalidFormatException("Not a tar file."); } - throw new InvalidFormatException("Not a tar file."); - } - sharpCompressStream.Position = pos; - if ( - await LZipStream - .IsLZipFileAsync(sharpCompressStream, cancellationToken) - .ConfigureAwait(false) - ) - { - sharpCompressStream.Position = pos; - var testStream = new LZipStream(sharpCompressStream, CompressionMode.Decompress); - if ( - await TarArchive.IsTarFileAsync(testStream, cancellationToken).ConfigureAwait(false) - ) - { - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.LZip); - } - throw new InvalidFormatException("Not a tar file."); } + sharpCompressStream.Position = pos; return new TarReader(sharpCompressStream, options, CompressionType.None); } @@ -159,57 +124,32 @@ public partial class TarReader bufferSize: options.RewindableBufferSize ); long pos = sharpCompressStream.Position; - if (GZipArchive.IsGZipFile(sharpCompressStream)) + foreach (var wrapper in TarWrapper.Wrappers) { sharpCompressStream.Position = pos; - var testStream = new GZipStream(sharpCompressStream, CompressionMode.Decompress); - if (TarArchive.IsTarFile(testStream)) + if (!wrapper.IsMatch(sharpCompressStream)) { - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.GZip); + continue; } - throw new InvalidFormatException("Not a tar file."); - } - sharpCompressStream.Position = pos; - if (BZip2Stream.IsBZip2(sharpCompressStream)) - { + sharpCompressStream.Position = pos; - var testStream = BZip2Stream.Create( + var testStream = CreateProbeDecompressionStream( sharpCompressStream, - CompressionMode.Decompress, - false + wrapper.CompressionType, + options.Providers ); if (TarArchive.IsTarFile(testStream)) { sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.BZip2); + return new TarReader(sharpCompressStream, options, wrapper.CompressionType); } - throw new InvalidFormatException("Not a tar file."); - } - sharpCompressStream.Position = pos; - if (ZStandardStream.IsZStandard(sharpCompressStream)) - { - sharpCompressStream.Position = pos; - var testStream = new ZStandardStream(sharpCompressStream); - if (TarArchive.IsTarFile(testStream)) + + if (wrapper.CompressionType != CompressionType.None) { - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.ZStandard); + throw new InvalidFormatException("Not a tar file."); } - throw new InvalidFormatException("Not a tar file."); - } - sharpCompressStream.Position = pos; - if (LZipStream.IsLZipFile(sharpCompressStream)) - { - sharpCompressStream.Position = pos; - var testStream = new LZipStream(sharpCompressStream, CompressionMode.Decompress); - if (TarArchive.IsTarFile(testStream)) - { - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.LZip); - } - throw new InvalidFormatException("Not a tar file."); } + sharpCompressStream.Position = pos; return new TarReader(sharpCompressStream, options, CompressionType.None); } diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs index c4c34348..6ab572c2 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs @@ -379,6 +379,8 @@ public partial class ZipWriter : AbstractWriter private readonly ZipWriter writer; private readonly ZipCompressionMethod zipCompressionMethod; private readonly int compressionLevel; + private ICompressionProviderHooks? compressionProviderHooks; + private CompressionContext? compressionContext; private CountingStream? counting; private ulong decompressed; @@ -458,6 +460,8 @@ public partial class ZipWriter : AbstractWriter } var context = new CompressionContext { CanSeek = originalStream.CanSeek }; + compressionProviderHooks = compressingProvider; + compressionContext = context; // Write pre-compression data (magic bytes) var preData = compressingProvider.GetPreCompressionData(context); @@ -498,6 +502,8 @@ public partial class ZipWriter : AbstractWriter CanSeek = originalStream.CanSeek, FormatOptions = writer.PpmdProperties, }; + compressionProviderHooks = compressingProvider; + compressionContext = context; // Write pre-compression data (properties) var preData = compressingProvider.GetPreCompressionData(context); @@ -551,6 +557,8 @@ public partial class ZipWriter : AbstractWriter return; } + WritePostCompressionData(); + var countingCount = counting?.BytesWritten ?? 0; entry.Crc = (uint)crc.Crc32Result; entry.Compressed = (ulong)countingCount; @@ -688,6 +696,30 @@ public partial class ZipWriter : AbstractWriter } } } + + private void WritePostCompressionData() + { + if ( + compressionProviderHooks is null + || compressionContext is null + || counting is null + || zipCompressionMethod == ZipCompressionMethod.None + ) + { + return; + } + + var postData = compressionProviderHooks.GetPostCompressionData( + writeStream, + compressionContext + ); + if (postData is null || postData.Length == 0) + { + return; + } + + counting.Write(postData, 0, postData.Length); + } } #endregion Nested type: ZipWritingStream diff --git a/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs index 62fc165c..558a989d 100644 --- a/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs +++ b/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs @@ -7,6 +7,7 @@ using SharpCompress.Archives.Tar; using SharpCompress.Common; using SharpCompress.Compressors; using SharpCompress.Providers; +using SharpCompress.Providers.System; using SharpCompress.Readers; using SharpCompress.Writers; diff --git a/tests/SharpCompress.Test/CompressionProviderTests.cs b/tests/SharpCompress.Test/CompressionProviderTests.cs index b0059b0a..29cdb629 100644 --- a/tests/SharpCompress.Test/CompressionProviderTests.cs +++ b/tests/SharpCompress.Test/CompressionProviderTests.cs @@ -1,7 +1,10 @@ using System; using System.IO; +using System.Linq; using System.Text; +using System.Threading.Tasks; using AwesomeAssertions; +using SharpCompress.Archives.Tar; using SharpCompress.Common; using SharpCompress.Compressors; using SharpCompress.IO; @@ -18,6 +21,102 @@ namespace SharpCompress.Test; public class CompressionProviderTests { + private sealed class TrackingCompressionProvider : ICompressionProvider + { + private readonly ICompressionProvider _inner; + + public TrackingCompressionProvider(ICompressionProvider inner) + { + _inner = inner; + } + + public int CompressionCalls { get; private set; } + + public int DecompressionCalls { get; private set; } + + public CompressionType CompressionType => _inner.CompressionType; + + public bool SupportsCompression => _inner.SupportsCompression; + + public bool SupportsDecompression => _inner.SupportsDecompression; + + public Stream CreateCompressStream(Stream destination, int compressionLevel) + { + CompressionCalls++; + return _inner.CreateCompressStream(destination, compressionLevel); + } + + public Stream CreateCompressStream( + Stream destination, + int compressionLevel, + CompressionContext context + ) + { + CompressionCalls++; + return _inner.CreateCompressStream(destination, compressionLevel, context); + } + + public Stream CreateDecompressStream(Stream source) + { + DecompressionCalls++; + return _inner.CreateDecompressStream(source); + } + + public Stream CreateDecompressStream(Stream source, CompressionContext context) + { + DecompressionCalls++; + return _inner.CreateDecompressStream(source, context); + } + } + + private sealed class TrackingLzmaHooksProvider : ICompressionProviderHooks + { + public int PreCalls { get; private set; } + public int PropertiesCalls { get; private set; } + public int PostCalls { get; private set; } + + public CompressionType CompressionType => CompressionType.LZMA; + + public bool SupportsCompression => true; + + public bool SupportsDecompression => false; + + public Stream CreateCompressStream(Stream destination, int compressionLevel) + { + CompressionContext context = new() { CanSeek = destination.CanSeek }; + return CreateCompressStream(destination, compressionLevel, context); + } + + public Stream CreateCompressStream( + Stream destination, + int compressionLevel, + CompressionContext context + ) => SharpCompressStream.CreateNonDisposing(destination); + + public Stream CreateDecompressStream(Stream source) => throw new NotSupportedException(); + + public Stream CreateDecompressStream(Stream source, CompressionContext context) => + throw new NotSupportedException(); + + public byte[]? GetPreCompressionData(CompressionContext context) + { + PreCalls++; + return []; + } + + public byte[]? GetCompressionProperties(Stream stream, CompressionContext context) + { + PropertiesCalls++; + return []; + } + + public byte[]? GetPostCompressionData(Stream stream, CompressionContext context) + { + PostCalls++; + return [1, 2, 3]; + } + } + [Fact] public void CompressionProviderRegistry_Default_ReturnsInternalProviders() { @@ -52,7 +151,8 @@ public class CompressionProviderTests // Original should still have the default provider var originalProvider = original.GetProvider(CompressionType.Deflate); - originalProvider.Should().NotBeSameAs(modified); + var modifiedProvider = modified.GetProvider(CompressionType.Deflate); + originalProvider.Should().NotBeSameAs(modifiedProvider); originalProvider.Should().NotBeSameAs(customProvider); } @@ -219,6 +319,146 @@ public class CompressionProviderTests clone.LeaveStreamOpen.Should().BeTrue(); } + [Fact] + public void TarArchive_OpenArchive_UsesCustomGZipProvider() + { + using var archiveStream = new MemoryStream(); + using ( + var writer = new TarWriter( + archiveStream, + new TarWriterOptions(CompressionType.GZip, true) + ) + ) + { + var data = Encoding.UTF8.GetBytes("tar archive provider usage"); + writer.Write("test.txt", new MemoryStream(data), DateTime.Now); + } + + var trackingProvider = new TrackingCompressionProvider(new GZipCompressionProvider()); + var registry = CompressionProviderRegistry.Default.With(trackingProvider); + var readOptions = new ReaderOptions { Providers = registry }; + + archiveStream.Position = 0; + using var archive = TarArchive.OpenArchive(archiveStream, readOptions); + var entry = archive.Entries.First(x => !x.IsDirectory); + using var entryStream = entry.OpenEntryStream(); + using var resultStream = new MemoryStream(); + entryStream.CopyTo(resultStream); + + trackingProvider.DecompressionCalls.Should().BeGreaterThan(0); + } + + [Fact] + public async Task TarArchive_OpenAsyncArchive_UsesCustomGZipProvider() + { + using var archiveStream = new MemoryStream(); + using ( + var writer = new TarWriter( + archiveStream, + new TarWriterOptions(CompressionType.GZip, true) + ) + ) + { + var data = Encoding.UTF8.GetBytes("tar async archive provider usage"); + writer.Write("test.txt", new MemoryStream(data), DateTime.Now); + } + + var trackingProvider = new TrackingCompressionProvider(new GZipCompressionProvider()); + var registry = CompressionProviderRegistry.Default.With(trackingProvider); + var readOptions = new ReaderOptions { Providers = registry }; + + archiveStream.Position = 0; + await using var archive = await TarArchive.OpenAsyncArchive(archiveStream, readOptions); + await foreach (var entry in archive.EntriesAsync) + { + if (entry.IsDirectory) + { + continue; + } + + using var entryStream = await entry.OpenEntryStreamAsync(); + using var resultStream = new MemoryStream(); + await entryStream.CopyToAsync(resultStream); + break; + } + + trackingProvider.DecompressionCalls.Should().BeGreaterThan(0); + } + + [Fact] + public async Task ZipReader_OpenEntryStreamAsync_UsesCustomDeflateProvider() + { + using var zipStream = new MemoryStream(); + using ( + var writer = WriterFactory.OpenWriter( + zipStream, + ArchiveType.Zip, + new WriterOptions(CompressionType.Deflate) { LeaveStreamOpen = true } + ) + ) + { + var data = Encoding.UTF8.GetBytes("zip async provider usage"); + writer.Write("test.txt", new MemoryStream(data)); + } + + var trackingProvider = new TrackingCompressionProvider(new DeflateCompressionProvider()); + var registry = CompressionProviderRegistry.Default.With(trackingProvider); + var options = new ReaderOptions { Providers = registry }; + + zipStream.Position = 0; + await using var reader = await ReaderFactory.OpenAsyncReader(zipStream, options); + (await reader.MoveToNextEntryAsync()).Should().BeTrue(); + using var entryStream = await reader.OpenEntryStreamAsync(); + using var resultStream = new MemoryStream(); + await entryStream.CopyToAsync(resultStream); + + trackingProvider.DecompressionCalls.Should().BeGreaterThan(0); + } + + [Fact] + public void LzwReader_OpenReader_UsesCustomLzwProvider() + { + var archivePath = Path.Combine(TestBase.TEST_ARCHIVES_PATH, "Tar.tar.Z"); + var trackingProvider = new TrackingCompressionProvider(new LzwCompressionProvider()); + var registry = CompressionProviderRegistry.Default.With(trackingProvider); + var options = new ReaderOptions { Providers = registry }; + + using var stream = File.OpenRead(archivePath); + using var reader = ReaderFactory.OpenReader(stream, options); + reader.MoveToNextEntry().Should().BeTrue(); + reader.WriteEntryTo(Stream.Null); + + trackingProvider.DecompressionCalls.Should().BeGreaterThan(0); + } + + [Fact] + public void ZipWriter_LzmaProviderHook_WritesPostCompressionData() + { + var trackingProvider = new TrackingLzmaHooksProvider(); + var registry = CompressionProviderRegistry.Default.With(trackingProvider); + using var zipStream = new MemoryStream(); + + using ( + var writer = WriterFactory.OpenWriter( + zipStream, + ArchiveType.Zip, + new WriterOptions(CompressionType.LZMA) + { + LeaveStreamOpen = true, + Providers = registry, + } + ) + ) + { + var data = Encoding.UTF8.GetBytes("hook provider"); + writer.Write("test.txt", new MemoryStream(data)); + } + + trackingProvider.PreCalls.Should().BeGreaterThan(0); + trackingProvider.PropertiesCalls.Should().BeGreaterThan(0); + trackingProvider.PostCalls.Should().BeGreaterThan(0); + } + #region System.IO.Compression Tests [Fact] @@ -230,8 +470,8 @@ public class CompressionProviderTests ); using var compressedStream = new MemoryStream(); - // System.IO.Compression streams leave the underlying stream open by default with leaveOpen: true - using (var compressStream = provider.CreateCompressStream(compressedStream, 6)) + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = provider.CreateCompressStream(nonDisposingStream, 6)) { compressStream.Write(original, 0, original.Length); } @@ -258,7 +498,8 @@ public class CompressionProviderTests ); using var compressedStream = new MemoryStream(); - using (var compressStream = provider.CreateCompressStream(compressedStream, level)) + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = provider.CreateCompressStream(nonDisposingStream, level)) { compressStream.Write(original, 0, original.Length); } @@ -282,7 +523,8 @@ public class CompressionProviderTests ); using var compressedStream = new MemoryStream(); - using (var compressStream = systemProvider.CreateCompressStream(compressedStream, 6)) + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = systemProvider.CreateCompressStream(nonDisposingStream, 6)) { compressStream.Write(original, 0, original.Length); } @@ -377,7 +619,8 @@ public class CompressionProviderTests ); using var compressedStream = new MemoryStream(); - using (var compressStream = provider.CreateCompressStream(compressedStream, 6)) + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = provider.CreateCompressStream(nonDisposingStream, 6)) { compressStream.Write(original, 0, original.Length); } @@ -401,7 +644,8 @@ public class CompressionProviderTests ); using var compressedStream = new MemoryStream(); - using (var compressStream = systemProvider.CreateCompressStream(compressedStream, 6)) + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = systemProvider.CreateCompressStream(nonDisposingStream, 6)) { compressStream.Write(original, 0, original.Length); }