diff --git a/README.md b/README.md index d28ed2dfd..2bdb58d34 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ Post Issues on Github! 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 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 In general, I recommend GZip (Deflate)/BZip2 (BZip)/LZip (LZMA) as the simplicity of the formats lend to better long term archival as well as the streamability. Tar is often used in conjunction for multiple files in a single archive (e.g. `.tar.gz`) diff --git a/docs/API.md b/docs/API.md index bc8e05412..15511e5dd 100644 --- a/docs/API.md +++ b/docs/API.md @@ -318,6 +318,24 @@ WriterOptions: write-time behavior (compression type/level, encoding, stream own ZipWriterEntryOptions: per-entry ZIP overrides (compression, level, timestamps, comments, zip64) ``` +### 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`). 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()); +var readerOptions = ReaderOptions.ForOwnedFile().WithProviders(registry); +var writerOptions = new WriterOptions(CompressionType.GZip) +{ + CompressionLevel = 6, +}.WithProviders(registry); + +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`, including pre/properties/post compression hook data. + --- ## Compression Types diff --git a/docs/USAGE.md b/docs/USAGE.md index df863d329..772bf90d5 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -206,6 +206,29 @@ foreach(var entry in archive.Entries) } ``` +## Custom Compression Providers + +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# +var systemGZip = new SystemGZipCompressionProvider(); +var customRegistry = CompressionProviderRegistry.Default.With(systemGZip); + +var readerOptions = ReaderOptions.ForOwnedFile() + .WithProviders(customRegistry); +using var reader = ReaderFactory.OpenReader(stream, readerOptions); + +var writerOptions = new WriterOptions(CompressionType.GZip) + .WithProviders(customRegistry); +using var writer = WriterFactory.OpenWriter(outputStream, ArchiveType.GZip, writerOptions); +``` + +The registry also exposes `GetCompressingProvider` (now returning `ICompressionProviderHooks`) when a compression format needs pre- or post-stream data (e.g., LZMA/PPMd). Implementations that need extra headers can supply those bytes through the `ICompressionProviderHooks` members while the rest of the API still works through the `Providers` property. + ## Async Examples ### Async Reader Examples diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs index afc64bfae..67443ab92 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs @@ -77,7 +77,7 @@ protected override ValueTask CreateReaderForSolidExtractionAsync() { 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( @@ -88,7 +88,7 @@ IAsyncEnumerable volumes yield return new GZipArchiveEntry( this, await GZipFilePart - .CreateAsync(stream, ReaderOptions.ArchiveEncoding) + .CreateAsync(stream, ReaderOptions.ArchiveEncoding, ReaderOptions.Providers) .ConfigureAwait(false), ReaderOptions ); diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index 56f61fa59..06000d0a2 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -87,7 +87,7 @@ protected override IEnumerable LoadEntries(IEnumerable OpenEntryStreamAsync(CancellationToken cancellationToken = default) + public async ValueTask OpenEntryStreamAsync( + CancellationToken cancellationToken = default + ) { - // GZip synchronous implementation is fast enough, just wrap it - return new(OpenEntryStream()); + // Reset the stream position if seekable + var part = (GZipFilePart)Parts.Single(); + var rawStream = part.GetRawStream(); + if (rawStream.CanSeek && rawStream.Position != part.EntryStartPosition) + { + rawStream.Position = part.EntryStartPosition; + } + return ( + await Parts.Single().GetCompressedStreamAsync(cancellationToken).ConfigureAwait(false) + ).NotNull(); } #region IArchiveEntry Members diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs index 1cd7dd91e..c27ff0f9e 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -89,26 +89,32 @@ protected override ValueTask CreateReaderForSolidExtractionAsync() { 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 = await GetStreamAsync(sourceStream).ConfigureAwait(false); 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 @@ var header in TarHeaderFactory.ReadHeaderAsync( { var entry = new TarArchiveEntry( this, - new TarFilePart(previousHeader, stream), + new TarFilePart( + previousHeader, + _compressionType == CompressionType.None ? stream : null + ), CompressionType.None, ReaderOptions ); @@ -151,7 +160,10 @@ var header in TarHeaderFactory.ReadHeaderAsync( } 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 994255281..e1775875f 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -56,7 +56,10 @@ public static IWritableArchive OpenArchive( 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 static IWritableArchive OpenArchive( 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 static async ValueTask> OpenAsync 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 static async ValueTask> OpenAsync 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 static async ValueTask> OpenAsync 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 static async ValueTask> OpenAsync 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 130bb47f9..860fb4829 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -2,20 +2,16 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; 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.Providers; 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,16 +39,76 @@ private 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, + CompressionContext.FromStream(stream).WithReaderOptions(ReaderOptions) + ), + 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), }; + private ValueTask GetStreamAsync( + Stream stream, + CancellationToken cancellationToken = default + ) => + _compressionType switch + { + CompressionType.BZip2 => ReaderOptions.Providers.CreateDecompressStreamAsync( + CompressionType.BZip2, + stream, + cancellationToken + ), + CompressionType.GZip => ReaderOptions.Providers.CreateDecompressStreamAsync( + CompressionType.GZip, + stream, + CompressionContext.FromStream(stream).WithReaderOptions(ReaderOptions), + cancellationToken + ), + CompressionType.ZStandard => ReaderOptions.Providers.CreateDecompressStreamAsync( + CompressionType.ZStandard, + stream, + cancellationToken + ), + CompressionType.LZip => ReaderOptions.Providers.CreateDecompressStreamAsync( + CompressionType.LZip, + stream, + cancellationToken + ), + CompressionType.Xz => ReaderOptions.Providers.CreateDecompressStreamAsync( + CompressionType.Xz, + stream, + cancellationToken + ), + CompressionType.Lzw => ReaderOptions.Providers.CreateDecompressStreamAsync( + CompressionType.Lzw, + stream, + cancellationToken + ), + CompressionType.None => new ValueTask(stream), + _ => throw new NotSupportedException("Invalid compression type: " + _compressionType), + }; + protected override IEnumerable LoadEntries(IEnumerable volumes) { var stream = GetStream(volumes.Single().Stream); @@ -83,7 +139,10 @@ var header in TarHeaderFactory.ReadHeader( { var entry = new TarArchiveEntry( this, - new TarFilePart(previousHeader, stream), + new TarFilePart( + previousHeader, + _compressionType == CompressionType.None ? stream : null + ), CompressionType.None, ReaderOptions ); @@ -106,7 +165,10 @@ var header in TarHeaderFactory.ReadHeader( } yield return new TarArchiveEntry( this, - new TarFilePart(header, stream), + new TarFilePart( + header, + _compressionType == CompressionType.None ? stream : null + ), CompressionType.None, ReaderOptions ); @@ -178,6 +240,6 @@ protected override IReader CreateReaderForSolidExtraction() { 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.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs index c8139b067..4f17405ca 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs @@ -55,7 +55,12 @@ var h in headerFactory.NotNull().ReadSeekableHeaderAsync(volsArray.Last().Stream yield return new ZipArchiveEntry( this, - new SeekableZipFilePart(headerFactory.NotNull(), deh, s), + new SeekableZipFilePart( + headerFactory.NotNull(), + deh, + s, + ReaderOptions.Providers + ), ReaderOptions ); } diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 8fc423bd1..1d009f03e 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -96,7 +96,12 @@ protected override IEnumerable LoadEntries(IEnumerable CreateReaderForSolidExtractionAsync() { 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/GZip/GZipEntry.Async.cs b/src/SharpCompress/Common/GZip/GZipEntry.Async.cs index bdd56dd26..7bb5f9837 100644 --- a/src/SharpCompress/Common/GZip/GZipEntry.Async.cs +++ b/src/SharpCompress/Common/GZip/GZipEntry.Async.cs @@ -12,7 +12,9 @@ ReaderOptions options ) { yield return new GZipEntry( - await GZipFilePart.CreateAsync(stream, options.ArchiveEncoding).ConfigureAwait(false), + await GZipFilePart + .CreateAsync(stream, options.ArchiveEncoding, options.Providers) + .ConfigureAwait(false), options ); } diff --git a/src/SharpCompress/Common/GZip/GZipEntry.cs b/src/SharpCompress/Common/GZip/GZipEntry.cs index f3d41c455..635fc4cd3 100644 --- a/src/SharpCompress/Common/GZip/GZipEntry.cs +++ b/src/SharpCompress/Common/GZip/GZipEntry.cs @@ -46,7 +46,10 @@ internal GZipEntry(GZipFilePart? filePart, IReaderOptions readerOptions) internal static IEnumerable GetEntries(Stream stream, ReaderOptions options) { - yield return new GZipEntry(GZipFilePart.Create(stream, options.ArchiveEncoding), options); + yield return new GZipEntry( + GZipFilePart.Create(stream, options.ArchiveEncoding, options.Providers), + options + ); } // Async methods moved to GZipEntry.Async.cs diff --git a/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs b/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs index 1d56c7bdb..ae3e118af 100644 --- a/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs +++ b/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs @@ -5,7 +5,9 @@ using System.Threading; using System.Threading.Tasks; using SharpCompress.Common.Tar.Headers; +using SharpCompress.Compressors; using SharpCompress.Compressors.Deflate; +using SharpCompress.Providers; namespace SharpCompress.Common.GZip; @@ -14,10 +16,11 @@ internal sealed partial class GZipFilePart internal static async ValueTask CreateAsync( Stream stream, IArchiveEncoding archiveEncoding, + CompressionProviderRegistry compressionProviders, CancellationToken cancellationToken = default ) { - var part = new GZipFilePart(stream, archiveEncoding); + var part = new GZipFilePart(stream, archiveEncoding, compressionProviders); await part.ReadAndValidateGzipHeaderAsync(cancellationToken).ConfigureAwait(false); if (stream.CanSeek) @@ -131,4 +134,14 @@ private async ValueTask ReadZeroTerminatedStringAsync( var buffer = list.ToArray(); return ArchiveEncoding.Decode(buffer); } + + internal override async ValueTask GetCompressedStreamAsync( + CancellationToken cancellationToken = default + ) + { + // GZip uses Deflate compression + return await _compressionProviders + .CreateDecompressStreamAsync(CompressionType.Deflate, _stream, cancellationToken) + .ConfigureAwait(false); + } } diff --git a/src/SharpCompress/Common/GZip/GZipFilePart.cs b/src/SharpCompress/Common/GZip/GZipFilePart.cs index 1134fa89e..e8d89a5d5 100644 --- a/src/SharpCompress/Common/GZip/GZipFilePart.cs +++ b/src/SharpCompress/Common/GZip/GZipFilePart.cs @@ -5,6 +5,7 @@ using SharpCompress.Common.Tar.Headers; using SharpCompress.Compressors; using SharpCompress.Compressors.Deflate; +using SharpCompress.Providers; namespace SharpCompress.Common.GZip; @@ -12,10 +13,15 @@ internal sealed partial class GZipFilePart : FilePart { private string? _name; private readonly Stream _stream; + private readonly CompressionProviderRegistry _compressionProviders; - internal static GZipFilePart Create(Stream stream, IArchiveEncoding archiveEncoding) + internal static GZipFilePart Create( + Stream stream, + IArchiveEncoding archiveEncoding, + CompressionProviderRegistry compressionProviders + ) { - var part = new GZipFilePart(stream, archiveEncoding); + var part = new GZipFilePart(stream, archiveEncoding, compressionProviders); part.ReadAndValidateGzipHeader(); if (stream.CanSeek) @@ -35,8 +41,16 @@ internal static GZipFilePart Create(Stream stream, IArchiveEncoding archiveEncod return part; } - private GZipFilePart(Stream stream, IArchiveEncoding archiveEncoding) - : base(archiveEncoding) => _stream = stream; + private GZipFilePart( + Stream stream, + IArchiveEncoding archiveEncoding, + CompressionProviderRegistry compressionProviders + ) + : base(archiveEncoding) + { + _stream = stream; + _compressionProviders = compressionProviders; + } internal long EntryStartPosition { get; private set; } @@ -46,13 +60,11 @@ private GZipFilePart(Stream stream, IArchiveEncoding archiveEncoding) internal override string? FilePartName => _name; - internal override Stream GetCompressedStream() => - new DeflateStream( - _stream, - CompressionMode.Decompress, - CompressionLevel.Default, - leaveOpen: true - ); + internal override Stream GetCompressedStream() + { + //GZip uses Deflate compression, at this point we need a deflate stream + return _compressionProviders.CreateDecompressStream(CompressionType.Deflate, _stream); + } internal override Stream GetRawStream() => _stream; diff --git a/src/SharpCompress/Common/Lzw/LzwEntry.Async.cs b/src/SharpCompress/Common/Lzw/LzwEntry.Async.cs index a1982d1b8..d42ceb41b 100644 --- a/src/SharpCompress/Common/Lzw/LzwEntry.Async.cs +++ b/src/SharpCompress/Common/Lzw/LzwEntry.Async.cs @@ -15,7 +15,9 @@ internal static async IAsyncEnumerable GetEntriesAsync( ) { 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 924904283..ba7953685 100644 --- a/src/SharpCompress/Common/Lzw/LzwEntry.cs +++ b/src/SharpCompress/Common/Lzw/LzwEntry.cs @@ -46,7 +46,10 @@ internal LzwEntry(LzwFilePart? filePart, IReaderOptions readerOptions) 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 8b6a5a32d..f0dd25b67 100644 --- a/src/SharpCompress/Common/Lzw/LzwFilePart.Async.cs +++ b/src/SharpCompress/Common/Lzw/LzwFilePart.Async.cs @@ -1,6 +1,8 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Providers; namespace SharpCompress.Common.Lzw; @@ -9,15 +11,25 @@ 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. part.EntryStartPosition = stream.CanSeek ? stream.Position : 0; return part; } + + internal override async ValueTask GetCompressedStreamAsync( + CancellationToken cancellationToken = default + ) + { + return await _compressionProviders + .CreateDecompressStreamAsync(CompressionType.Lzw, _stream, cancellationToken) + .ConfigureAwait(false); + } } diff --git a/src/SharpCompress/Common/Lzw/LzwFilePart.cs b/src/SharpCompress/Common/Lzw/LzwFilePart.cs index 74d682ead..08802d2fe 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 static LzwFilePart Create(Stream stream, IArchiveEncoding archiveEncodi 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 @@ private LzwFilePart(Stream stream, IArchiveEncoding archiveEncoding) 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/Options/IReaderOptions.cs b/src/SharpCompress/Common/Options/IReaderOptions.cs index d9f51880e..f054e31f4 100644 --- a/src/SharpCompress/Common/Options/IReaderOptions.cs +++ b/src/SharpCompress/Common/Options/IReaderOptions.cs @@ -1,3 +1,6 @@ +using SharpCompress.Compressors; +using SharpCompress.Providers; + namespace SharpCompress.Common.Options; public interface IReaderOptions @@ -6,10 +9,40 @@ public interface IReaderOptions IProgressOptions, IExtractionOptions { + /// + /// Look for RarArchive (Check for self-extracting archives or cases where RarArchive isn't at the start of the file) + /// bool LookForHeader { get; init; } + + /// + /// Password for encrypted archives. + /// string? Password { get; init; } + + /// + /// Disable checking for incomplete archives. + /// bool DisableCheckIncomplete { get; init; } + + /// + /// Buffer size for stream operations. + /// int BufferSize { get; init; } + + /// + /// Provide a hint for the extension of the archive being read, can speed up finding the correct decoder. + /// string? ExtensionHint { get; init; } + + /// + /// Size of the rewindable buffer for non-seekable streams. + /// int? RewindableBufferSize { get; init; } + + /// + /// Registry of compression providers. + /// Defaults to but can be replaced with custom providers. + /// Use this to provide alternative decompression implementations. + /// + CompressionProviderRegistry Providers { get; init; } } diff --git a/src/SharpCompress/Common/Options/IWriterOptions.cs b/src/SharpCompress/Common/Options/IWriterOptions.cs index 2363ea8b8..73dd1ec38 100644 --- a/src/SharpCompress/Common/Options/IWriterOptions.cs +++ b/src/SharpCompress/Common/Options/IWriterOptions.cs @@ -1,9 +1,28 @@ using SharpCompress.Common; +using SharpCompress.Compressors; +using SharpCompress.Providers; namespace SharpCompress.Common.Options; +/// +/// Options for configuring writer behavior when creating archives. +/// public interface IWriterOptions : IStreamOptions, IEncodingOptions, IProgressOptions { + /// + /// The compression type to use for the archive. + /// CompressionType CompressionType { get; init; } + + /// + /// The compression level to be used when the compression type supports variable levels. + /// int CompressionLevel { get; init; } + + /// + /// Registry of compression providers. + /// Defaults to but can be replaced with custom providers, such as + /// System.IO.Compression for Deflate/GZip on modern .NET. + /// + CompressionProviderRegistry Providers { get; init; } } diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs index 9e2e06b07..f9c59dfdc 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs @@ -1,5 +1,7 @@ using System.IO; using SharpCompress.Common.Zip.Headers; +using SharpCompress.Compressors; +using SharpCompress.Providers; namespace SharpCompress.Common.Zip; @@ -11,9 +13,10 @@ internal partial class SeekableZipFilePart : ZipFilePart internal SeekableZipFilePart( SeekableZipHeaderFactory headerFactory, DirectoryEntryHeader header, - Stream stream + Stream stream, + CompressionProviderRegistry compressionProviders ) - : base(header, stream) => _headerFactory = headerFactory; + : base(header, stream, compressionProviders) => _headerFactory = headerFactory; internal override Stream GetCompressedStream() { diff --git a/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs b/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs index 5368469f9..e8ef7c554 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs @@ -1,7 +1,8 @@ using System.IO; using SharpCompress.Common.Zip.Headers; -using SharpCompress.Compressors.Deflate; +using SharpCompress.Compressors; using SharpCompress.IO; +using SharpCompress.Providers; namespace SharpCompress.Common.Zip; @@ -9,8 +10,12 @@ internal sealed partial class StreamingZipFilePart : ZipFilePart { private Stream? _decompressionStream; - internal StreamingZipFilePart(ZipFileEntry header, Stream stream) - : base(header, stream) { } + internal StreamingZipFilePart( + ZipFileEntry header, + Stream stream, + CompressionProviderRegistry compressionProviders + ) + : base(header, stream, compressionProviders) { } protected override Stream CreateBaseStream() => Header.PackedStream.NotNull(); @@ -47,11 +52,6 @@ internal BinaryReader FixStreamedFileLocation(ref Stream stream) // If we had TotalIn / TotalOut we could have used them Header.CompressedSize = _decompressionStream.Position; - if (_decompressionStream is DeflateStream deflateStream) - { - stream.Position = 0; - } - Skipped = true; } var reader = new BinaryReader(stream, System.Text.Encoding.Default, leaveOpen: true); diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs index 786f22264..8b06727d3 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs @@ -6,17 +6,8 @@ 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 @@ protected async ValueTask CreateDecompressionStreamAsync( CancellationToken cancellationToken = default ) { + // Handle special cases first switch (method) { case ZipCompressionMethod.None: @@ -134,98 +126,24 @@ protected async ValueTask CreateDecompressionStreamAsync( return stream; } - case ZipCompressionMethod.Shrink: - { - 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: + case ZipCompressionMethod.WinzipAes: { - 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,108 @@ protected async ValueTask CreateDecompressionStreamAsync( } 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 + OutputSize = FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) + ? -1 + : Header.UncompressedSize, + }; + + return await providers + .CreateDecompressStreamAsync( + compressionType, + stream, + context, + cancellationToken ) .ConfigureAwait(false); } - case ZipCompressionMethod.Xz: - { - return new XZStream(stream); - } - case ZipCompressionMethod.ZStandard: - { - return new DecompressionStream(stream); - } case ZipCompressionMethod.PPMd: { var props = new byte[2]; await stream.ReadFullyAsync(props, 0, 2, cancellationToken).ConfigureAwait(false); - return await PpmdStream - .CreateAsync(new PpmdProperties(props), stream, false, cancellationToken) + context = context with { Properties = props }; + return await providers + .CreateDecompressStreamAsync( + compressionType, + stream, + context, + cancellationToken + ) .ConfigureAwait(false); } - 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( + context = context with { FormatOptions = Header.Flags }; + return await providers + .CreateDecompressStreamAsync( + compressionType, stream, - (ZipCompressionMethod) - BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(5)), + context, cancellationToken ) .ConfigureAwait(false); } default: { - throw new NotSupportedException("CompressionMethod: " + Header.CompressionMethod); + return await providers + .CreateDecompressStreamAsync( + compressionType, + stream, + context, + cancellationToken + ) + .ConfigureAwait(false); } } } + + 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/Common/Zip/ZipFilePart.cs b/src/SharpCompress/Common/Zip/ZipFilePart.cs index 7dd860e76..61e838bfe 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.cs @@ -15,17 +15,25 @@ using SharpCompress.Compressors.Xz; using SharpCompress.Compressors.ZStandard; using SharpCompress.IO; +using SharpCompress.Providers; namespace SharpCompress.Common.Zip; internal abstract partial class ZipFilePart : FilePart { - internal ZipFilePart(ZipFileEntry header, Stream stream) + private readonly CompressionProviderRegistry _compressionProviders; + + internal ZipFilePart( + ZipFileEntry header, + Stream stream, + CompressionProviderRegistry compressionProviders + ) : base(header.ArchiveEncoding) { Header = header; header.Part = this; BaseStream = stream; + _compressionProviders = compressionProviders; } internal Stream BaseStream { get; } @@ -64,8 +72,37 @@ internal override Stream GetRawStream() protected bool LeaveStreamOpen => FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor) || Header.IsZip64; + /// + /// Gets the compression provider registry, falling back to default if not set. + /// + protected CompressionProviderRegistry GetProviders() => _compressionProviders; + + /// + /// Converts ZipCompressionMethod to CompressionType. + /// + protected static CompressionType ToCompressionType(ZipCompressionMethod method) => + method switch + { + ZipCompressionMethod.None => CompressionType.None, + ZipCompressionMethod.Deflate => CompressionType.Deflate, + ZipCompressionMethod.Deflate64 => CompressionType.Deflate64, + ZipCompressionMethod.BZip2 => CompressionType.BZip2, + ZipCompressionMethod.LZMA => CompressionType.LZMA, + ZipCompressionMethod.PPMd => CompressionType.PPMd, + ZipCompressionMethod.ZStandard => CompressionType.ZStandard, + ZipCompressionMethod.Xz => CompressionType.Xz, + ZipCompressionMethod.Shrink => CompressionType.Shrink, + ZipCompressionMethod.Reduce1 => CompressionType.Reduce1, + ZipCompressionMethod.Reduce2 => CompressionType.Reduce2, + ZipCompressionMethod.Reduce3 => CompressionType.Reduce3, + ZipCompressionMethod.Reduce4 => CompressionType.Reduce4, + ZipCompressionMethod.Explode => CompressionType.Explode, + _ => throw new NotSupportedException($"Unsupported compression method: {method}"), + }; + protected Stream CreateDecompressionStream(Stream stream, ZipCompressionMethod method) { + // Handle special cases first switch (method) { case ZipCompressionMethod.None: @@ -74,76 +111,29 @@ protected Stream CreateDecompressionStream(Stream stream, ZipCompressionMethod m { return new DataDescriptorStream(stream); } - return stream; } - case ZipCompressionMethod.Shrink: - { - return new ShrinkStream( - stream, - CompressionMode.Decompress, - Header.CompressedSize, - Header.UncompressedSize - ); - } - case ZipCompressionMethod.Reduce1: - { - return ReduceStream.Create( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 1 - ); - } - case ZipCompressionMethod.Reduce2: - { - return ReduceStream.Create( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 2 - ); - } - case ZipCompressionMethod.Reduce3: - { - return ReduceStream.Create( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 3 - ); - } - case ZipCompressionMethod.Reduce4: - { - return ReduceStream.Create( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 4 - ); - } - case ZipCompressionMethod.Explode: + case ZipCompressionMethod.WinzipAes: { - return ExplodeStream.Create( - stream, - Header.CompressedSize, - Header.UncompressedSize, - Header.Flags - ); + return CreateWinzipAesDecompressionStream(stream); } + } - case ZipCompressionMethod.Deflate: - { - return new DeflateStream(stream, CompressionMode.Decompress); - } - case ZipCompressionMethod.Deflate64: - { - return new Deflate64Stream(stream, CompressionMode.Decompress); - } - case ZipCompressionMethod.BZip2: - { - return BZip2Stream.Create(stream, CompressionMode.Decompress, false); - } + // Get the compression type and providers + var compressionType = ToCompressionType(method); + var providers = GetProviders(); + + // Build context with header information + var context = new CompressionContext + { + InputSize = Header.CompressedSize, + OutputSize = Header.UncompressedSize, + CanSeek = stream.CanSeek, + }; + + // Handle methods that need special context + switch (method) + { case ZipCompressionMethod.LZMA: { if (FlagUtility.HasFlag(Header.Flags, HeaderFlags.Encrypted)) @@ -158,71 +148,71 @@ protected Stream CreateDecompressionStream(Stream stream, ZipCompressionMethod m ) ) { - reader.ReadUInt16(); //LZMA version - var props = new byte[reader.ReadUInt16()]; + reader.ReadUInt16(); // LZMA version + var propsLength = reader.ReadUInt16(); + var props = new byte[propsLength]; reader.Read(props, 0, props.Length); - return LzmaStream.Create( - props, - stream, - Header.CompressedSize > 0 ? Header.CompressedSize - 4 - props.Length : -1, - FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) + context = context with + { + Properties = props, + InputSize = + Header.CompressedSize > 0 + ? Header.CompressedSize - 4 - props.Length + : -1, + OutputSize = FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) ? -1 - : Header.UncompressedSize - ); + : Header.UncompressedSize, + }; + return providers.CreateDecompressStream(compressionType, stream, context); } } - case ZipCompressionMethod.Xz: - { - return new XZStream(stream); - } - case ZipCompressionMethod.ZStandard: - { - return new DecompressionStream(stream); - } case ZipCompressionMethod.PPMd: { Span props = stackalloc byte[2]; stream.ReadFully(props); - return PpmdStream.Create(new PpmdProperties(props), stream, false); + context = context with { Properties = props.ToArray() }; + 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); + context = context with { FormatOptions = Header.Flags }; + return providers.CreateDecompressStream(compressionType, stream, context); + } + } - if (compressedMethod != 0x01 && compressedMethod != 0x02) - { - throw new InvalidFormatException( - "Unexpected vendor version number for WinZip AES metadata" - ); - } + // For simple methods, use the basic decompress + return providers.CreateDecompressStream(compressionType, stream, context); + } - var vendorId = BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(2)); - if (vendorId != 0x4541) - { - throw new InvalidFormatException( - "Unexpected vendor ID for WinZip AES metadata" - ); - } - return CreateDecompressionStream( - stream, - (ZipCompressionMethod) - BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(5)) - ); - } - default: - { - throw new NotSupportedException("CompressionMethod: " + Header.CompressionMethod); - } + private Stream CreateWinzipAesDecompressionStream(Stream stream) + { + 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 CreateDecompressionStream( + stream, + (ZipCompressionMethod)BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(5)) + ); } protected Stream GetCryptoStream(Stream plainStream) diff --git a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs index 0aa690f67..57b729ba3 100644 --- a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs +++ b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs @@ -3,10 +3,11 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Providers; namespace SharpCompress.Compressors.BZip2; -public sealed partial class BZip2Stream : Stream +public sealed partial class BZip2Stream : Stream, IFinishable { private Stream stream = default!; private bool isDisposed; diff --git a/src/SharpCompress/Compressors/Deflate/GZipStream.cs b/src/SharpCompress/Compressors/Deflate/GZipStream.cs index 95b7c03df..b0b132161 100644 --- a/src/SharpCompress/Compressors/Deflate/GZipStream.cs +++ b/src/SharpCompress/Compressors/Deflate/GZipStream.cs @@ -32,6 +32,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Options; namespace SharpCompress.Compressors.Deflate; @@ -53,8 +55,23 @@ public partial class GZipStream : Stream public GZipStream(Stream stream, CompressionMode mode) : this(stream, mode, CompressionLevel.Default, Encoding.UTF8) { } - public GZipStream(Stream stream, CompressionMode mode, CompressionLevel level) - : this(stream, mode, level, Encoding.UTF8) { } + public GZipStream(Stream stream, CompressionMode mode, IReaderOptions readerOptions) + : this(stream, mode, CompressionLevel.Default, readerOptions) { } + + public GZipStream( + Stream stream, + CompressionMode mode, + CompressionLevel level, + IReaderOptions readerOptions + ) + : this( + stream, + mode, + level, + ( + readerOptions ?? throw new ArgumentNullException(nameof(readerOptions)) + ).ArchiveEncoding.GetEncoding() + ) { } public GZipStream( Stream stream, diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.cs index 297447c20..262aa0e64 100644 --- a/src/SharpCompress/Compressors/LZMA/LZipStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LZipStream.cs @@ -6,6 +6,7 @@ using SharpCompress.Common; using SharpCompress.Crypto; using SharpCompress.IO; +using SharpCompress.Providers; namespace SharpCompress.Compressors.LZMA; @@ -17,7 +18,7 @@ namespace SharpCompress.Compressors.LZMA; /// /// Stream supporting the LZIP format, as documented at http://www.nongnu.org/lzip/manual/lzip_manual.html /// -public sealed partial class LZipStream : Stream +public sealed partial class LZipStream : Stream, IFinishable { private readonly Stream _stream; private readonly CountingStream? _countingWritableSubStream; diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 69254cad0..8c8cbde23 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -96,4 +96,28 @@ out IReader? reader 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 ab40438c6..e4062e62a 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; @@ -10,6 +9,7 @@ using SharpCompress.Common; using SharpCompress.Common.Options; using SharpCompress.IO; +using SharpCompress.Providers; using SharpCompress.Readers; using SharpCompress.Readers.GZip; using SharpCompress.Readers.Tar; @@ -128,7 +128,11 @@ out IReader? reader if (GZipArchive.IsGZipFile(sharpCompressStream)) { sharpCompressStream.Rewind(); - var testStream = new GZipStream(sharpCompressStream, CompressionMode.Decompress); + using var testStream = options.Providers.CreateDecompressStream( + CompressionType.GZip, + SharpCompressStream.CreateNonDisposing(sharpCompressStream), + CompressionContext.FromStream(sharpCompressStream).WithReaderOptions(options) + ); if (TarArchive.IsTarFile(testStream)) { sharpCompressStream.StopRecording(); diff --git a/src/SharpCompress/Factories/LzwFactory.cs b/src/SharpCompress/Factories/LzwFactory.cs index 7a0eeec05..c4797be54 100644 --- a/src/SharpCompress/Factories/LzwFactory.cs +++ b/src/SharpCompress/Factories/LzwFactory.cs @@ -58,7 +58,12 @@ out IReader? reader 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 fd2f528ff..32014744a 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.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 override IEnumerable GetSupportedExtensions() /// 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 override bool IsArchive(Stream stream, string? password = null) 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 override async ValueTask IsArchiveAsync( CancellationToken cancellationToken = default ) { + var providers = CompressionProviderRegistry.Default; var sharpCompressStream = new SharpCompressStream(stream); sharpCompressStream.StartRecording(); foreach (var wrapper in TarWrapper.Wrappers) @@ -89,8 +95,12 @@ await wrapper ) { sharpCompressStream.Rewind(); - var decompressedStream = await wrapper - .CreateStreamAsync(sharpCompressStream, cancellationToken) + var decompressedStream = await CreateProbeDecompressionStreamAsync( + sharpCompressStream, + wrapper.CompressionType, + providers, + cancellationToken: cancellationToken + ) .ConfigureAwait(false); if ( await TarArchive @@ -109,8 +119,70 @@ await TarArchive #endregion - public static CompressionType GetCompressionType(Stream stream) + private static Stream CreateProbeDecompressionStream( + Stream stream, + CompressionType compressionType, + CompressionProviderRegistry providers, + IReaderOptions? readerOptions = null + ) + { + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(stream); + if (compressionType == CompressionType.None) + { + return nonDisposingStream; + } + + if (compressionType == CompressionType.GZip && readerOptions is not null) + { + return providers.CreateDecompressStream( + compressionType, + nonDisposingStream, + CompressionContext.FromStream(nonDisposingStream).WithReaderOptions(readerOptions) + ); + } + + return providers.CreateDecompressStream(compressionType, nonDisposingStream); + } + + private static async ValueTask CreateProbeDecompressionStreamAsync( + Stream stream, + CompressionType compressionType, + CompressionProviderRegistry providers, + IReaderOptions? readerOptions = null, + CancellationToken cancellationToken = default + ) + { + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(stream); + if (compressionType == CompressionType.None) + { + return nonDisposingStream; + } + + if (compressionType == CompressionType.GZip && readerOptions is not null) + { + return await providers + .CreateDecompressStreamAsync( + compressionType, + nonDisposingStream, + CompressionContext + .FromStream(nonDisposingStream) + .WithReaderOptions(readerOptions), + cancellationToken + ) + .ConfigureAwait(false); + } + + return await providers + .CreateDecompressStreamAsync(compressionType, nonDisposingStream, cancellationToken) + .ConfigureAwait(false); + } + + 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 +190,11 @@ public static CompressionType GetCompressionType(Stream stream) 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 +206,25 @@ public static CompressionType GetCompressionType(Stream stream) 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 = await CreateProbeDecompressionStreamAsync( + stream, + wrapper.CompressionType, + providers, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); if ( await TarArchive .IsTarFileAsync(decompressedStream, cancellationToken) @@ -228,7 +312,12 @@ public IReader OpenReader(Stream stream, ReaderOptions? options) if (wrapper.IsMatch(sharpCompressStream)) { sharpCompressStream.Rewind(); - var decompressedStream = wrapper.CreateStream(sharpCompressStream); + var decompressedStream = CreateProbeDecompressionStream( + sharpCompressStream, + wrapper.CompressionType, + options.Providers, + options + ); if (TarArchive.IsTarFile(decompressedStream)) { sharpCompressStream.StopRecording(); @@ -260,8 +349,13 @@ await wrapper ) { sharpCompressStream.Rewind(); - var decompressedStream = await wrapper - .CreateStreamAsync(sharpCompressStream, cancellationToken) + var decompressedStream = await CreateProbeDecompressionStreamAsync( + sharpCompressStream, + wrapper.CompressionType, + options.Providers, + options, + cancellationToken + ) .ConfigureAwait(false); if ( await TarArchive @@ -275,7 +369,9 @@ await TarArchive } } } - return (IAsyncReader)TarReader.OpenReader(stream, options); + + sharpCompressStream.Rewind(); + return (IAsyncReader)TarReader.OpenReader(sharpCompressStream, options); } #endregion diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index b47b70313..a7c09072a 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -206,8 +206,22 @@ public override void Flush() throw new NotSupportedException(); } - public override long Length => - _isPassthrough ? stream.Length : throw new NotSupportedException(); + public override long Length + { + get + { + if (_isPassthrough) + { + return stream.Length; + } + + if (_ringBuffer is not null) + { + return _ringBuffer.Length; + } + throw new NotSupportedException(); + } + } public override long Position { diff --git a/src/SharpCompress/Providers/CompressionContext.cs b/src/SharpCompress/Providers/CompressionContext.cs new file mode 100644 index 000000000..e0680646a --- /dev/null +++ b/src/SharpCompress/Providers/CompressionContext.cs @@ -0,0 +1,66 @@ +using System.IO; +using SharpCompress.Common.Options; + +namespace SharpCompress.Providers; + +/// +/// Provides context information for compression operations. +/// Carries format-specific parameters that some compression types require. +/// +public sealed record CompressionContext +{ + /// + /// The size of the input data, or -1 if unknown. + /// + public long InputSize { get; init; } = -1; + + /// + /// The expected output size, or -1 if unknown. + /// + public long OutputSize { get; init; } = -1; + + /// + /// Properties bytes for the compression format (e.g., LZMA properties). + /// + public byte[]? Properties { get; init; } + + /// + /// Whether the underlying stream supports seeking. + /// + public bool CanSeek { get; init; } + + /// + /// Additional format-specific options. + /// + /// + /// This value is consumed by provider implementations that need caller-supplied metadata + /// that is not tied to ReaderOptions. For archive header encoding, use instead. + /// Examples of valid FormatOptions values include compression properties (e.g., LZMA properties), + /// format flags, or algorithm-specific configuration. + /// + public object? FormatOptions { get; init; } + + /// + /// Creates a CompressionContext from a stream. + /// + /// The stream to extract context from. + /// A CompressionContext populated from the stream. + public static CompressionContext FromStream(Stream stream) => + new() { CanSeek = stream.CanSeek, InputSize = stream.CanSeek ? stream.Length : -1 }; + + /// + /// Reader options for accessing archive metadata such as header encoding. + /// + public IReaderOptions? ReaderOptions { get; init; } + + /// + /// Returns a new with the specified reader options. + /// + /// The reader options to set. + /// A new instance. + public CompressionContext WithReaderOptions(IReaderOptions? readerOptions) => + this with + { + ReaderOptions = readerOptions, + }; +} diff --git a/src/SharpCompress/Providers/CompressionContextExtensions.cs b/src/SharpCompress/Providers/CompressionContextExtensions.cs new file mode 100644 index 000000000..fdfa5dc4a --- /dev/null +++ b/src/SharpCompress/Providers/CompressionContextExtensions.cs @@ -0,0 +1,18 @@ +using System.Text; +using SharpCompress.Common; +using SharpCompress.Common.Options; + +namespace SharpCompress.Providers; + +public static class CompressionContextExtensions +{ + /// + /// Resolves the archive header encoding from . + /// + /// + /// Returns when ReaderOptions is set, + /// otherwise falls back to UTF-8. + /// + public static Encoding ResolveArchiveEncoding(this CompressionContext context) => + context.ReaderOptions?.ArchiveEncoding.GetEncoding() ?? Encoding.UTF8; +} diff --git a/src/SharpCompress/Providers/CompressionProviderBase.cs b/src/SharpCompress/Providers/CompressionProviderBase.cs new file mode 100644 index 000000000..77ec72a50 --- /dev/null +++ b/src/SharpCompress/Providers/CompressionProviderBase.cs @@ -0,0 +1,129 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; + +namespace SharpCompress.Providers; + +/// +/// Base class for compression providers that provides default async implementations +/// delegating to synchronous methods. Providers can inherit from this class for +/// simpler implementations or implement ICompressionProvider directly for full control. +/// +/// +/// +/// This base class implements the async methods by calling the synchronous versions. +/// Providers that need true async implementations should override these methods. +/// +/// +public abstract class CompressionProviderBase : ICompressionProvider +{ + /// + public abstract CompressionType CompressionType { get; } + + /// + public abstract bool SupportsCompression { get; } + + /// + public abstract bool SupportsDecompression { get; } + + /// + public abstract Stream CreateCompressStream(Stream destination, int compressionLevel); + + /// + public virtual Stream CreateCompressStream( + Stream destination, + int compressionLevel, + CompressionContext context + ) => CreateCompressStream(destination, compressionLevel); + + /// + public abstract Stream CreateDecompressStream(Stream source); + + /// + public virtual Stream CreateDecompressStream(Stream source, CompressionContext context) => + CreateDecompressStream(source); + + /// + /// Asynchronously creates a compression stream. + /// Default implementation delegates to the synchronous CreateCompressStream. + /// + public virtual ValueTask CreateCompressStreamAsync( + Stream destination, + int compressionLevel, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return new ValueTask(CreateCompressStream(destination, compressionLevel)); + } + + /// + /// Asynchronously creates a compression stream with context. + /// Default implementation delegates to the synchronous CreateCompressStream with context. + /// + public virtual ValueTask CreateCompressStreamAsync( + Stream destination, + int compressionLevel, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + return CreateCompressStreamAsync(destination, compressionLevel, cancellationToken); + } + + /// + /// Asynchronously creates a decompression stream. + /// Default implementation delegates to the synchronous CreateDecompressStream. + /// + public virtual ValueTask CreateDecompressStreamAsync( + Stream source, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return new ValueTask(CreateDecompressStream(source)); + } + + /// + /// Asynchronously creates a decompression stream with context. + /// Default implementation delegates to the synchronous CreateDecompressStream with context. + /// + public virtual ValueTask CreateDecompressStreamAsync( + Stream source, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + return CreateDecompressStreamAsync(source, cancellationToken); + } + + protected static void ValidateRequiredSizes(CompressionContext context, string algorithmName) + { + if (context.InputSize < 0 || context.OutputSize < 0) + { + throw new ArgumentException( + $"{algorithmName} decompression requires InputSize and OutputSize in CompressionContext.", + nameof(context) + ); + } + } + + protected static T RequireFormatOption( + CompressionContext context, + string algorithmName, + string optionName + ) + { + if (context.FormatOptions is not T options) + { + throw new ArgumentException( + $"{algorithmName} decompression requires {optionName} in CompressionContext.FormatOptions.", + nameof(context) + ); + } + + return options; + } +} diff --git a/src/SharpCompress/Providers/CompressionProviderRegistry.cs b/src/SharpCompress/Providers/CompressionProviderRegistry.cs new file mode 100644 index 000000000..27de3181b --- /dev/null +++ b/src/SharpCompress/Providers/CompressionProviderRegistry.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Providers.Default; + +namespace SharpCompress.Providers; + +/// +/// A registry of compression providers, keyed by CompressionType. +/// Used to resolve which implementation to use for a given compression type. +/// +/// +/// +/// This class is immutable. Use the With method to create modified copies +/// that add or replace providers: +/// +/// +/// var customRegistry = CompressionProviderRegistry.Default +/// .With(new MyCustomGZipProvider()); +/// var options = new WriterOptions(CompressionType.GZip) +/// { +/// Providers = customRegistry +/// }; +/// +/// +public sealed class CompressionProviderRegistry +{ + /// + /// The default registry using SharpCompress internal implementations. + /// + public static CompressionProviderRegistry Default { get; } = CreateDefault(); + + /// + /// The empty registry for tests + /// + public static CompressionProviderRegistry Empty { get; } = CreateEmpty(); + + private readonly Dictionary _providers; + + private CompressionProviderRegistry( + Dictionary providers + ) => _providers = providers; + + /// + /// Gets the provider for a given compression type, or null if none is registered. + /// + /// The compression type to look up. + /// The provider for the type, or null if not found. + public ICompressionProvider? GetProvider(CompressionType type) + { + _providers.TryGetValue(type, out var provider); + return provider; + } + + /// + /// Creates a compression stream for the specified type. + /// + /// The compression type. + /// The destination stream. + /// The compression level. + /// A compression stream. + /// If no provider is registered for the type. + /// If the provider does not support compression. + public Stream CreateCompressStream(CompressionType type, Stream destination, int level) + { + var provider = GetProvider(type); + if (provider is null) + { + throw new InvalidOperationException( + $"No compression provider registered for type: {type}" + ); + } + return provider.CreateCompressStream(destination, level); + } + + /// + /// Creates a decompression stream for the specified type. + /// + /// The compression type. + /// The source stream. + /// A decompression stream. + /// If no provider is registered for the type. + /// If the provider does not support decompression. + public Stream CreateDecompressStream(CompressionType type, Stream source) + { + var provider = GetProvider(type); + if (provider is null) + { + throw new InvalidOperationException( + $"No compression provider registered for type: {type}" + ); + } + return provider.CreateDecompressStream(source); + } + + /// + /// Creates a compression stream for the specified type with context. + /// + /// The compression type. + /// The destination stream. + /// The compression level. + /// Context information for the compression. + /// A compression stream. + /// If no provider is registered for the type. + /// If the provider does not support compression. + public Stream CreateCompressStream( + CompressionType type, + Stream destination, + int level, + CompressionContext context + ) + { + var provider = GetProvider(type); + if (provider is null) + { + throw new InvalidOperationException( + $"No compression provider registered for type: {type}" + ); + } + return provider.CreateCompressStream(destination, level, context); + } + + /// + /// Creates a decompression stream for the specified type with context. + /// + /// The compression type. + /// The source stream. + /// Context information for the decompression. + /// A decompression stream. + /// If no provider is registered for the type. + /// If the provider does not support decompression. + public Stream CreateDecompressStream( + CompressionType type, + Stream source, + CompressionContext context + ) + { + var provider = GetProvider(type); + if (provider is null) + { + throw new InvalidOperationException( + $"No compression provider registered for type: {type}" + ); + } + return provider.CreateDecompressStream(source, context); + } + + /// + /// Asynchronously creates a compression stream for the specified type. + /// + /// The compression type. + /// The destination stream. + /// The compression level. + /// Cancellation token. + /// A task containing the compression stream. + /// If no provider is registered for the type. + /// If the provider does not support compression. + public ValueTask CreateCompressStreamAsync( + CompressionType type, + Stream destination, + int level, + CancellationToken cancellationToken = default + ) + { + var provider = GetProvider(type); + if (provider is null) + { + throw new InvalidOperationException( + $"No compression provider registered for type: {type}" + ); + } + return provider.CreateCompressStreamAsync(destination, level, cancellationToken); + } + + /// + /// Asynchronously creates a decompression stream for the specified type. + /// + /// The compression type. + /// The source stream. + /// Cancellation token. + /// A task containing the decompression stream. + /// If no provider is registered for the type. + /// If the provider does not support decompression. + public ValueTask CreateDecompressStreamAsync( + CompressionType type, + Stream source, + CancellationToken cancellationToken = default + ) + { + var provider = GetProvider(type); + if (provider is null) + { + throw new InvalidOperationException( + $"No compression provider registered for type: {type}" + ); + } + return provider.CreateDecompressStreamAsync(source, cancellationToken); + } + + /// + /// Asynchronously creates a compression stream for the specified type with context. + /// + /// The compression type. + /// The destination stream. + /// The compression level. + /// Context information for the compression. + /// Cancellation token. + /// A task containing the compression stream. + /// If no provider is registered for the type. + /// If the provider does not support compression. + public ValueTask CreateCompressStreamAsync( + CompressionType type, + Stream destination, + int level, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + var provider = GetProvider(type); + if (provider is null) + { + throw new InvalidOperationException( + $"No compression provider registered for type: {type}" + ); + } + return provider.CreateCompressStreamAsync(destination, level, context, cancellationToken); + } + + /// + /// Asynchronously creates a decompression stream for the specified type with context. + /// + /// The compression type. + /// The source stream. + /// Context information for the decompression. + /// Cancellation token. + /// A task containing the decompression stream. + /// If no provider is registered for the type. + /// If the provider does not support decompression. + public ValueTask CreateDecompressStreamAsync( + CompressionType type, + Stream source, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + var provider = GetProvider(type); + if (provider is null) + { + throw new InvalidOperationException( + $"No compression provider registered for type: {type}" + ); + } + return provider.CreateDecompressStreamAsync(source, context, cancellationToken); + } + + /// + /// Gets the provider as an ICompressionProviderHooks if it supports complex initialization. + /// + /// The compression type. + /// The compressing provider, or null if the provider doesn't support complex initialization. + public ICompressionProviderHooks? GetCompressingProvider(CompressionType type) + { + var provider = GetProvider(type); + return provider as ICompressionProviderHooks; + } + + /// + /// Creates a new registry with the specified provider added or replaced. + /// + /// The provider to add or replace. + /// A new registry instance with the provider included. + /// If provider is null. + public CompressionProviderRegistry With(ICompressionProvider provider) + { + if (provider is null) + { + throw new ArgumentNullException(nameof(provider)); + } + + var newProviders = new Dictionary(_providers) + { + [provider.CompressionType] = provider, + }; + + return new CompressionProviderRegistry(newProviders); + } + + private static CompressionProviderRegistry CreateDefault() + { + var providers = new Dictionary + { + [CompressionType.Deflate] = new DeflateCompressionProvider(), + [CompressionType.GZip] = new GZipCompressionProvider(), + [CompressionType.BZip2] = new BZip2CompressionProvider(), + [CompressionType.ZStandard] = new ZStandardCompressionProvider(), + [CompressionType.LZip] = new LZipCompressionProvider(), + [CompressionType.Xz] = new XzCompressionProvider(), + [CompressionType.Lzw] = new LzwCompressionProvider(), + [CompressionType.Deflate64] = new Deflate64CompressionProvider(), + [CompressionType.Shrink] = new ShrinkCompressionProvider(), + [CompressionType.Reduce1] = new Reduce1CompressionProvider(), + [CompressionType.Reduce2] = new Reduce2CompressionProvider(), + [CompressionType.Reduce3] = new Reduce3CompressionProvider(), + [CompressionType.Reduce4] = new Reduce4CompressionProvider(), + [CompressionType.Explode] = new ExplodeCompressionProvider(), + [CompressionType.LZMA] = new LzmaCompressingProvider(), + [CompressionType.PPMd] = new PpmdCompressingProvider(), + }; + + return new CompressionProviderRegistry(providers); + } + + private static CompressionProviderRegistry CreateEmpty() + { + var providers = new Dictionary(); + return new CompressionProviderRegistry(providers); + } +} diff --git a/src/SharpCompress/Providers/ContextRequiredDecompressionProviderBase.cs b/src/SharpCompress/Providers/ContextRequiredDecompressionProviderBase.cs new file mode 100644 index 000000000..fac51678e --- /dev/null +++ b/src/SharpCompress/Providers/ContextRequiredDecompressionProviderBase.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Providers; + +public abstract class ContextRequiredDecompressionProviderBase : DecompressionOnlyProviderBase +{ + protected abstract string DecompressionContextRequirementDescription { get; } + + protected virtual string DecompressionContextRequirementSuffix => string.Empty; + + public sealed override Stream CreateDecompressStream(Stream source) => + throw new InvalidOperationException( + $"{DecompressionContextRequirementDescription}. " + + $"Use CreateDecompressStream(Stream, CompressionContext) overload{DecompressionContextRequirementSuffix}." + ); + + public sealed override ValueTask CreateDecompressStreamAsync( + Stream source, + CancellationToken cancellationToken = default + ) => + throw new InvalidOperationException( + $"{DecompressionContextRequirementDescription}. " + + "Use CreateDecompressStreamAsync(Stream, CompressionContext, CancellationToken) " + + $"overload{DecompressionContextRequirementSuffix}." + ); +} diff --git a/src/SharpCompress/Providers/DecompressionOnlyProviderBase.cs b/src/SharpCompress/Providers/DecompressionOnlyProviderBase.cs new file mode 100644 index 000000000..bdd8e5613 --- /dev/null +++ b/src/SharpCompress/Providers/DecompressionOnlyProviderBase.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; +using SharpCompress.Common; + +namespace SharpCompress.Providers; + +public abstract class DecompressionOnlyProviderBase : CompressionProviderBase +{ + public override bool SupportsCompression => false; + public override bool SupportsDecompression => true; + + protected abstract string CompressionNotSupportedMessage { get; } + + public sealed override Stream CreateCompressStream(Stream destination, int compressionLevel) => + throw new NotSupportedException(CompressionNotSupportedMessage); + + public sealed override Stream CreateCompressStream( + Stream destination, + int compressionLevel, + CompressionContext context + ) => throw new NotSupportedException(CompressionNotSupportedMessage); +} diff --git a/src/SharpCompress/Providers/Default/BZip2CompressionProvider.cs b/src/SharpCompress/Providers/Default/BZip2CompressionProvider.cs new file mode 100644 index 000000000..aa8bf5577 --- /dev/null +++ b/src/SharpCompress/Providers/Default/BZip2CompressionProvider.cs @@ -0,0 +1,39 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Compressors; +using SharpCompress.Compressors.BZip2; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides BZip2 compression using SharpCompress's internal implementation. +/// +public sealed class BZip2CompressionProvider : CompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.BZip2; + public override bool SupportsCompression => true; + public override bool SupportsDecompression => true; + + public override Stream CreateCompressStream(Stream destination, int compressionLevel) + { + // BZip2 doesn't use compressionLevel parameter in this implementation + return BZip2Stream.Create(destination, CompressionMode.Compress, false); + } + + public override Stream CreateDecompressStream(Stream source) + { + return BZip2Stream.Create(source, CompressionMode.Decompress, false); + } + + public override async ValueTask CreateDecompressStreamAsync( + Stream source, + CancellationToken cancellationToken = default + ) + { + return await BZip2Stream + .CreateAsync(source, CompressionMode.Decompress, false, false, cancellationToken) + .ConfigureAwait(false); + } +} diff --git a/src/SharpCompress/Providers/Default/Deflate64CompressionProvider.cs b/src/SharpCompress/Providers/Default/Deflate64CompressionProvider.cs new file mode 100644 index 000000000..482cdfe00 --- /dev/null +++ b/src/SharpCompress/Providers/Default/Deflate64CompressionProvider.cs @@ -0,0 +1,22 @@ +using System.IO; +using SharpCompress.Common; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Deflate64; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides Deflate64 decompression using SharpCompress's internal implementation. +/// Note: Deflate64 compression is not supported; this provider is decompression-only. +/// +public sealed class Deflate64CompressionProvider : DecompressionOnlyProviderBase +{ + public override CompressionType CompressionType => CompressionType.Deflate64; + protected override string CompressionNotSupportedMessage => + "Deflate64 compression is not supported by SharpCompress's internal implementation."; + + public override Stream CreateDecompressStream(Stream source) + { + return new Deflate64Stream(source, CompressionMode.Decompress); + } +} diff --git a/src/SharpCompress/Providers/Default/DeflateCompressionProvider.cs b/src/SharpCompress/Providers/Default/DeflateCompressionProvider.cs new file mode 100644 index 000000000..3a49899ad --- /dev/null +++ b/src/SharpCompress/Providers/Default/DeflateCompressionProvider.cs @@ -0,0 +1,27 @@ +using System.IO; +using SharpCompress.Common; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Deflate; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides Deflate compression using SharpCompress's internal implementation. +/// +public sealed class DeflateCompressionProvider : CompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.Deflate; + public override bool SupportsCompression => true; + public override bool SupportsDecompression => true; + + public override Stream CreateCompressStream(Stream destination, int compressionLevel) + { + var level = (CompressionLevel)compressionLevel; + return new DeflateStream(destination, CompressionMode.Compress, level); + } + + public override Stream CreateDecompressStream(Stream source) + { + return new DeflateStream(source, CompressionMode.Decompress); + } +} diff --git a/src/SharpCompress/Providers/Default/ExplodeCompressionProvider.cs b/src/SharpCompress/Providers/Default/ExplodeCompressionProvider.cs new file mode 100644 index 000000000..fb00ba89d --- /dev/null +++ b/src/SharpCompress/Providers/Default/ExplodeCompressionProvider.cs @@ -0,0 +1,49 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Zip.Headers; +using SharpCompress.Compressors.Explode; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides Explode decompression using SharpCompress's internal implementation. +/// Note: Explode compression is not supported; this provider is decompression-only. +/// +/// +/// Explode requires compressed size, uncompressed size, and flags which must be provided via CompressionContext. +/// +public sealed class ExplodeCompressionProvider : ContextRequiredDecompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.Explode; + protected override string CompressionNotSupportedMessage => + "Explode compression is not supported by SharpCompress's internal implementation."; + + protected override string DecompressionContextRequirementDescription => + "Explode decompression requires compressed size, uncompressed size, and flags"; + + protected override string DecompressionContextRequirementSuffix => " with FormatOptions"; + + public override Stream CreateDecompressStream(Stream source, CompressionContext context) + { + ValidateRequiredSizes(context, "Explode"); + var flags = RequireFormatOption(context, "Explode", "HeaderFlags"); + + return ExplodeStream.Create(source, context.InputSize, context.OutputSize, flags); + } + + public override async ValueTask CreateDecompressStreamAsync( + Stream source, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + ValidateRequiredSizes(context, "Explode"); + var flags = RequireFormatOption(context, "Explode", "HeaderFlags"); + + return await ExplodeStream + .CreateAsync(source, context.InputSize, context.OutputSize, flags, cancellationToken) + .ConfigureAwait(false); + } +} diff --git a/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs b/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs new file mode 100644 index 000000000..1365e0a77 --- /dev/null +++ b/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs @@ -0,0 +1,50 @@ +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Deflate; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides GZip compression using SharpCompress's internal implementation. +/// +public sealed class GZipCompressionProvider : CompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.GZip; + public override bool SupportsCompression => true; + public override bool SupportsDecompression => true; + + public override Stream CreateCompressStream(Stream destination, int compressionLevel) + { + var level = (CompressionLevel)compressionLevel; + return new GZipStream(destination, CompressionMode.Compress, level, Encoding.UTF8); + } + + public override Stream CreateDecompressStream(Stream source) + { + return new GZipStream(source, CompressionMode.Decompress); + } + + public override Stream CreateDecompressStream(Stream source, CompressionContext context) + { + return new GZipStream( + source, + CompressionMode.Decompress, + CompressionLevel.Default, + context.ResolveArchiveEncoding() + ); + } + + public override ValueTask CreateDecompressStreamAsync( + Stream source, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return new ValueTask(CreateDecompressStream(source, context)); + } +} diff --git a/src/SharpCompress/Providers/Default/LZipCompressionProvider.cs b/src/SharpCompress/Providers/Default/LZipCompressionProvider.cs new file mode 100644 index 000000000..c4fe48b60 --- /dev/null +++ b/src/SharpCompress/Providers/Default/LZipCompressionProvider.cs @@ -0,0 +1,26 @@ +using System.IO; +using SharpCompress.Common; +using SharpCompress.Compressors; +using SharpCompress.Compressors.LZMA; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides LZip compression using SharpCompress's internal implementation. +/// +public sealed class LZipCompressionProvider : CompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.LZip; + public override bool SupportsCompression => true; + public override bool SupportsDecompression => true; + + public override Stream CreateCompressStream(Stream destination, int compressionLevel) + { + return new LZipStream(destination, CompressionMode.Compress); + } + + public override Stream CreateDecompressStream(Stream source) + { + return new LZipStream(source, CompressionMode.Decompress); + } +} diff --git a/src/SharpCompress/Providers/Default/LzmaCompressingProvider.cs b/src/SharpCompress/Providers/Default/LzmaCompressingProvider.cs new file mode 100644 index 000000000..c2ac5e362 --- /dev/null +++ b/src/SharpCompress/Providers/Default/LzmaCompressingProvider.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Compressors.LZMA; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides LZMA compression and decompression using SharpCompress's internal implementation. +/// This is a complex provider that requires initialization data for compression. +/// +public sealed class LzmaCompressingProvider : CompressionProviderBase, ICompressionProviderHooks +{ + public override CompressionType CompressionType => CompressionType.LZMA; + public override bool SupportsCompression => true; + public override bool SupportsDecompression => true; + + public override Stream CreateCompressStream(Stream destination, int compressionLevel) + { + throw new InvalidOperationException( + "LZMA compression requires context with CanSeek information. " + + "Use CreateCompressStream(Stream, int, CompressionContext) overload." + ); + } + + public override Stream CreateCompressStream( + Stream destination, + int compressionLevel, + CompressionContext context + ) + { + // LZMA stream creation returns the encoder stream + // Note: Pre-compression data and properties are handled via ICompressionProviderHooks methods + var props = new LzmaEncoderProperties(!context.CanSeek); + return LzmaStream.Create(props, false, destination); + } + + public override Stream CreateDecompressStream(Stream source) + { + throw new InvalidOperationException( + "LZMA decompression requires properties. " + + "Use CreateDecompressStream(Stream, CompressionContext) overload with Properties." + ); + } + + public override Stream CreateDecompressStream(Stream source, CompressionContext context) + { + if (context.Properties is null || context.Properties.Length < 5) + { + throw new ArgumentException( + "LZMA decompression requires Properties (at least 5 bytes) in CompressionContext.", + nameof(context) + ); + } + + return LzmaStream.Create(context.Properties, source, context.InputSize, context.OutputSize); + } + + public override ValueTask CreateDecompressStreamAsync( + Stream source, + CancellationToken cancellationToken = default + ) => + throw new InvalidOperationException( + "LZMA decompression requires properties. " + + "Use CreateDecompressStreamAsync(Stream, CompressionContext, CancellationToken) overload with Properties." + ); + + public override async ValueTask CreateDecompressStreamAsync( + Stream source, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + if (context.Properties is null || context.Properties.Length < 5) + { + throw new ArgumentException( + "LZMA decompression requires Properties (at least 5 bytes) in CompressionContext.", + nameof(context) + ); + } + + return await LzmaStream + .CreateAsync( + context.Properties, + source, + context.InputSize, + context.OutputSize, + leaveOpen: false + ) + .ConfigureAwait(false); + } + + public byte[]? GetPreCompressionData(CompressionContext context) + { + // Zip format writes these magic bytes before the LZMA stream + return new byte[] { 9, 20, 5, 0 }; + } + + public byte[]? GetCompressionProperties(Stream stream, CompressionContext context) + { + // The LZMA stream exposes its properties after creation + if (stream is LzmaStream lzmaStream) + { + return lzmaStream.Properties; + } + return null; + } + + public byte[]? GetPostCompressionData(Stream stream, CompressionContext context) + { + // No post-compression data needed for LZMA in Zip + return null; + } +} diff --git a/src/SharpCompress/Providers/Default/LzwCompressionProvider.cs b/src/SharpCompress/Providers/Default/LzwCompressionProvider.cs new file mode 100644 index 000000000..73821cbea --- /dev/null +++ b/src/SharpCompress/Providers/Default/LzwCompressionProvider.cs @@ -0,0 +1,21 @@ +using System.IO; +using SharpCompress.Common; +using SharpCompress.Compressors.Lzw; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides LZW compression decompression using SharpCompress's internal implementation. +/// Note: Compression is not supported by this provider. +/// +public sealed class LzwCompressionProvider : DecompressionOnlyProviderBase +{ + public override CompressionType CompressionType => CompressionType.Lzw; + protected override string CompressionNotSupportedMessage => + "LZW compression is not supported by SharpCompress's internal implementation."; + + public override Stream CreateDecompressStream(Stream source) + { + return new LzwStream(source); + } +} diff --git a/src/SharpCompress/Providers/Default/PpmdCompressingProvider.cs b/src/SharpCompress/Providers/Default/PpmdCompressingProvider.cs new file mode 100644 index 000000000..8253ceeb1 --- /dev/null +++ b/src/SharpCompress/Providers/Default/PpmdCompressingProvider.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Compressors.PPMd; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides PPMd compression and decompression using SharpCompress's internal implementation. +/// This is a complex provider that requires initialization data for compression. +/// +public sealed class PpmdCompressingProvider : CompressionProviderBase, ICompressionProviderHooks +{ + public override CompressionType CompressionType => CompressionType.PPMd; + public override bool SupportsCompression => true; + public override bool SupportsDecompression => true; + + public override Stream CreateCompressStream(Stream destination, int compressionLevel) + { + // Ppmd doesn't use compressionLevel, uses PpmdProperties instead + var props = new PpmdProperties(); + return PpmdStream.Create(props, destination, true); + } + + public override Stream CreateCompressStream( + Stream destination, + int compressionLevel, + CompressionContext context + ) + { + // Context not used for Ppmd compression, but we could use FormatOptions for custom properties + if (context.FormatOptions is PpmdProperties customProps) + { + return PpmdStream.Create(customProps, destination, true); + } + + return CreateCompressStream(destination, compressionLevel); + } + + public override Stream CreateDecompressStream(Stream source) + { + throw new InvalidOperationException( + "PPMd decompression requires properties. " + + "Use CreateDecompressStream(Stream, CompressionContext) overload with Properties." + ); + } + + public override Stream CreateDecompressStream(Stream source, CompressionContext context) + { + if (context.Properties is null || context.Properties.Length < 2) + { + throw new ArgumentException( + "PPMd decompression requires Properties (at least 2 bytes) in CompressionContext.", + nameof(context) + ); + } + + var props = new PpmdProperties(context.Properties); + return PpmdStream.Create(props, source, false); + } + + public override ValueTask CreateDecompressStreamAsync( + Stream source, + CancellationToken cancellationToken = default + ) => + throw new InvalidOperationException( + "PPMd decompression requires properties. " + + "Use CreateDecompressStreamAsync(Stream, CompressionContext, CancellationToken) overload with Properties." + ); + + public override async ValueTask CreateDecompressStreamAsync( + Stream source, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + if (context.Properties is null || context.Properties.Length < 2) + { + throw new ArgumentException( + "PPMd decompression requires Properties (at least 2 bytes) in CompressionContext.", + nameof(context) + ); + } + + var props = new PpmdProperties(context.Properties); + return await PpmdStream + .CreateAsync(props, source, false, cancellationToken) + .ConfigureAwait(false); + } + + public byte[]? GetPreCompressionData(CompressionContext context) + { + // Ppmd writes its properties before the compressed data + if (context.FormatOptions is PpmdProperties customProps) + { + return customProps.Properties; + } + + var defaultProps = new PpmdProperties(); + return defaultProps.Properties; + } + + public byte[]? GetCompressionProperties(Stream stream, CompressionContext context) + { + // Properties are already written in GetPreCompressionData + return null; + } + + public byte[]? GetPostCompressionData(Stream stream, CompressionContext context) + { + // No post-compression data needed for Ppmd + return null; + } +} diff --git a/src/SharpCompress/Providers/Default/Reduce1CompressionProvider.cs b/src/SharpCompress/Providers/Default/Reduce1CompressionProvider.cs new file mode 100644 index 000000000..c984b4ad8 --- /dev/null +++ b/src/SharpCompress/Providers/Default/Reduce1CompressionProvider.cs @@ -0,0 +1,13 @@ +using SharpCompress.Common; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides Reduce1 decompression using SharpCompress's internal implementation. +/// Note: Reduce compression is not supported; this provider is decompression-only. +/// +public sealed class Reduce1CompressionProvider : ReduceCompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.Reduce1; + protected override int Factor => 1; +} diff --git a/src/SharpCompress/Providers/Default/Reduce2CompressionProvider.cs b/src/SharpCompress/Providers/Default/Reduce2CompressionProvider.cs new file mode 100644 index 000000000..9ca67bf22 --- /dev/null +++ b/src/SharpCompress/Providers/Default/Reduce2CompressionProvider.cs @@ -0,0 +1,13 @@ +using SharpCompress.Common; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides Reduce2 decompression using SharpCompress's internal implementation. +/// Note: Reduce compression is not supported; this provider is decompression-only. +/// +public sealed class Reduce2CompressionProvider : ReduceCompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.Reduce2; + protected override int Factor => 2; +} diff --git a/src/SharpCompress/Providers/Default/Reduce3CompressionProvider.cs b/src/SharpCompress/Providers/Default/Reduce3CompressionProvider.cs new file mode 100644 index 000000000..01fa708ca --- /dev/null +++ b/src/SharpCompress/Providers/Default/Reduce3CompressionProvider.cs @@ -0,0 +1,13 @@ +using SharpCompress.Common; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides Reduce3 decompression using SharpCompress's internal implementation. +/// Note: Reduce compression is not supported; this provider is decompression-only. +/// +public sealed class Reduce3CompressionProvider : ReduceCompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.Reduce3; + protected override int Factor => 3; +} diff --git a/src/SharpCompress/Providers/Default/Reduce4CompressionProvider.cs b/src/SharpCompress/Providers/Default/Reduce4CompressionProvider.cs new file mode 100644 index 000000000..14b8e5439 --- /dev/null +++ b/src/SharpCompress/Providers/Default/Reduce4CompressionProvider.cs @@ -0,0 +1,13 @@ +using SharpCompress.Common; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides Reduce4 decompression using SharpCompress's internal implementation. +/// Note: Reduce compression is not supported; this provider is decompression-only. +/// +public sealed class Reduce4CompressionProvider : ReduceCompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.Reduce4; + protected override int Factor => 4; +} diff --git a/src/SharpCompress/Providers/Default/ReduceCompressionProviderBase.cs b/src/SharpCompress/Providers/Default/ReduceCompressionProviderBase.cs new file mode 100644 index 000000000..17b01a1a8 --- /dev/null +++ b/src/SharpCompress/Providers/Default/ReduceCompressionProviderBase.cs @@ -0,0 +1,36 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Compressors.Reduce; + +namespace SharpCompress.Providers.Default; + +public abstract class ReduceCompressionProviderBase : ContextRequiredDecompressionProviderBase +{ + protected abstract int Factor { get; } + + protected override string DecompressionContextRequirementDescription => + "Reduce decompression requires compressed and uncompressed sizes"; + + protected override string CompressionNotSupportedMessage => + "Reduce compression is not supported by SharpCompress's internal implementation."; + + public sealed override Stream CreateDecompressStream(Stream source, CompressionContext context) + { + ValidateRequiredSizes(context, "Reduce"); + return ReduceStream.Create(source, context.InputSize, context.OutputSize, Factor); + } + + public sealed override async ValueTask CreateDecompressStreamAsync( + Stream source, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + ValidateRequiredSizes(context, "Reduce"); + return await ReduceStream + .CreateAsync(source, context.InputSize, context.OutputSize, Factor, cancellationToken) + .ConfigureAwait(false); + } +} diff --git a/src/SharpCompress/Providers/Default/ShrinkCompressionProvider.cs b/src/SharpCompress/Providers/Default/ShrinkCompressionProvider.cs new file mode 100644 index 000000000..2e26c7a7e --- /dev/null +++ b/src/SharpCompress/Providers/Default/ShrinkCompressionProvider.cs @@ -0,0 +1,56 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Shrink; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides Shrink decompression using SharpCompress's internal implementation. +/// Note: Shrink compression is not supported; this provider is decompression-only. +/// +/// +/// Shrink requires compressed and uncompressed sizes which must be provided via CompressionContext. +/// +public sealed class ShrinkCompressionProvider : ContextRequiredDecompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.Shrink; + protected override string CompressionNotSupportedMessage => + "Shrink compression is not supported by SharpCompress's internal implementation."; + + protected override string DecompressionContextRequirementDescription => + "Shrink decompression requires compressed and uncompressed sizes"; + + public override Stream CreateDecompressStream(Stream source, CompressionContext context) + { + ValidateRequiredSizes(context, "Shrink"); + + return new ShrinkStream( + source, + CompressionMode.Decompress, + context.InputSize, + context.OutputSize + ); + } + + public override async ValueTask CreateDecompressStreamAsync( + Stream source, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + ValidateRequiredSizes(context, "Shrink"); + + return await ShrinkStream + .CreateAsync( + source, + CompressionMode.Decompress, + context.InputSize, + context.OutputSize, + cancellationToken + ) + .ConfigureAwait(false); + } +} diff --git a/src/SharpCompress/Providers/Default/XzCompressionProvider.cs b/src/SharpCompress/Providers/Default/XzCompressionProvider.cs new file mode 100644 index 000000000..db005ab0d --- /dev/null +++ b/src/SharpCompress/Providers/Default/XzCompressionProvider.cs @@ -0,0 +1,21 @@ +using System.IO; +using SharpCompress.Common; +using SharpCompress.Compressors.Xz; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides XZ compression decompression using SharpCompress's internal implementation. +/// Note: Compression is not supported by this provider. +/// +public sealed class XzCompressionProvider : DecompressionOnlyProviderBase +{ + public override CompressionType CompressionType => CompressionType.Xz; + protected override string CompressionNotSupportedMessage => + "XZ compression is not supported by SharpCompress's internal implementation."; + + public override Stream CreateDecompressStream(Stream source) + { + return new XZStream(source); + } +} diff --git a/src/SharpCompress/Providers/Default/ZStandardCompressionProvider.cs b/src/SharpCompress/Providers/Default/ZStandardCompressionProvider.cs new file mode 100644 index 000000000..973e0be85 --- /dev/null +++ b/src/SharpCompress/Providers/Default/ZStandardCompressionProvider.cs @@ -0,0 +1,25 @@ +using System.IO; +using SharpCompress.Common; +using ZStd = SharpCompress.Compressors.ZStandard; + +namespace SharpCompress.Providers.Default; + +/// +/// Provides ZStandard compression using SharpCompress's internal implementation. +/// +public sealed class ZStandardCompressionProvider : CompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.ZStandard; + public override bool SupportsCompression => true; + public override bool SupportsDecompression => true; + + public override Stream CreateCompressStream(Stream destination, int compressionLevel) + { + return new ZStd.CompressionStream(destination, compressionLevel); + } + + public override Stream CreateDecompressStream(Stream source) + { + return new ZStd.DecompressionStream(source); + } +} diff --git a/src/SharpCompress/Providers/ICompressionProvider.cs b/src/SharpCompress/Providers/ICompressionProvider.cs new file mode 100644 index 000000000..57d4ba3a3 --- /dev/null +++ b/src/SharpCompress/Providers/ICompressionProvider.cs @@ -0,0 +1,151 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; + +namespace SharpCompress.Providers; + +/// +/// Provides compression and decompression stream creation for a specific compression type. +/// Implement this interface to supply alternative compression implementations. +/// +/// +/// +/// This interface abstracts the creation of compression and decompression streams, +/// allowing SharpCompress to use different implementations of the same compression type. +/// For example, you can provide an implementation that uses System.IO.Compression +/// for Deflate/GZip instead of the internal DotNetZip-derived implementation. +/// +/// +/// Implementations should be thread-safe for concurrent decompression operations, +/// but CreateCompressStream/CreateDecompressStream themselves return new stream instances +/// that are not shared. +/// +/// +/// For simpler implementations, derive from which provides +/// default async implementations that delegate to the synchronous methods. +/// +/// +public interface ICompressionProvider +{ + /// + /// The compression type this provider handles. + /// + CompressionType CompressionType { get; } + + /// + /// Whether this provider supports compression (writing). + /// + bool SupportsCompression { get; } + + /// + /// Whether this provider supports decompression (reading). + /// + bool SupportsDecompression { get; } + + /// + /// Creates a compression stream that compresses data written to it. + /// + /// The destination stream to write compressed data to. + /// The compression level (0-9, algorithm-specific). + /// A stream that compresses data written to it. + /// Thrown if SupportsCompression is false. + Stream CreateCompressStream(Stream destination, int compressionLevel); + + /// + /// Creates a compression stream with context information. + /// + /// The destination stream. + /// The compression level. + /// Context information about the compression. + /// A compression stream. + /// Thrown if SupportsCompression is false. + Stream CreateCompressStream( + Stream destination, + int compressionLevel, + CompressionContext context + ); + + /// + /// Creates a decompression stream that decompresses data read from it. + /// + /// The source stream to read compressed data from. + /// A stream that decompresses data read from it. + /// Thrown if SupportsDecompression is false. + Stream CreateDecompressStream(Stream source); + + /// + /// Creates a decompression stream with context information. + /// + /// The source stream. + /// + /// Context information about the decompression. Providers may use + /// for archive header encoding + /// (via ) and + /// for format-specific metadata + /// such as compression properties or algorithm-specific configuration. + /// + /// A decompression stream. + /// Thrown if SupportsDecompression is false. + Stream CreateDecompressStream(Stream source, CompressionContext context); + + /// + /// Asynchronously creates a compression stream that compresses data written to it. + /// + /// The destination stream to write compressed data to. + /// The compression level (0-9, algorithm-specific). + /// Cancellation token. + /// A task containing the compression stream. + /// Thrown if SupportsCompression is false. + ValueTask CreateCompressStreamAsync( + Stream destination, + int compressionLevel, + CancellationToken cancellationToken = default + ); + + /// + /// Asynchronously creates a compression stream with context information. + /// + /// The destination stream. + /// The compression level. + /// Context information about the compression. + /// Cancellation token. + /// A task containing the compression stream. + /// Thrown if SupportsCompression is false. + ValueTask CreateCompressStreamAsync( + Stream destination, + int compressionLevel, + CompressionContext context, + CancellationToken cancellationToken = default + ); + + /// + /// Asynchronously creates a decompression stream that decompresses data read from it. + /// + /// The source stream to read compressed data from. + /// Cancellation token. + /// A task containing the decompression stream. + /// Thrown if SupportsDecompression is false. + ValueTask CreateDecompressStreamAsync( + Stream source, + CancellationToken cancellationToken = default + ); + + /// + /// Asynchronously creates a decompression stream with context information. + /// + /// The source stream. + /// + /// Context information about the decompression. Providers may use + /// for format-specific metadata + /// (for example, archive header encoding). + /// + /// Cancellation token. + /// A task containing the decompression stream. + /// Thrown if SupportsDecompression is false. + ValueTask CreateDecompressStreamAsync( + Stream source, + CompressionContext context, + CancellationToken cancellationToken = default + ); +} diff --git a/src/SharpCompress/Providers/ICompressionProviderHooks.cs b/src/SharpCompress/Providers/ICompressionProviderHooks.cs new file mode 100644 index 000000000..796f013d7 --- /dev/null +++ b/src/SharpCompress/Providers/ICompressionProviderHooks.cs @@ -0,0 +1,42 @@ +using System.IO; + +namespace SharpCompress.Providers; + +/// +/// Extended compression provider interface for formats that require initialization/finalization data. +/// +/// +/// Some compression formats (like LZMA and PPMd in Zip) require special handling: +/// - Data written before compression starts (magic bytes, properties headers) +/// - Data written after compression completes (properties, footers) +/// This interface extends ICompressionProvider to support these complex initialization patterns +/// while keeping the simple ICompressionProvider interface for formats that don't need it. +/// +public interface ICompressionProviderHooks : ICompressionProvider +{ + /// + /// Gets initialization data to write before compression starts. + /// Returns null if no pre-compression data is needed. + /// + /// Context information. + /// Bytes to write before compression, or null. + byte[]? GetPreCompressionData(CompressionContext context); + + /// + /// Gets properties/data to write after creating the compression stream but before writing data. + /// Returns null if no properties are needed. + /// + /// The compression stream that was created. + /// Context information. + /// Bytes to write after stream creation, or null. + byte[]? GetCompressionProperties(Stream stream, CompressionContext context); + + /// + /// Gets data to write after compression is complete. + /// Returns null if no post-compression data is needed. + /// + /// The compression stream. + /// Context information. + /// Bytes to write after compression, or null. + byte[]? GetPostCompressionData(Stream stream, CompressionContext context); +} diff --git a/src/SharpCompress/Providers/IFinishable.cs b/src/SharpCompress/Providers/IFinishable.cs new file mode 100644 index 000000000..53173d534 --- /dev/null +++ b/src/SharpCompress/Providers/IFinishable.cs @@ -0,0 +1,19 @@ +namespace SharpCompress.Providers; + +/// +/// Interface for compression streams that require explicit finalization +/// before disposal to ensure all compressed data is flushed properly. +/// +/// +/// Some compression formats (notably BZip2 and LZip) require explicit +/// finalization to write trailer/footer data. Implementing this interface +/// allows generic code to handle finalization without knowing the specific stream type. +/// +public interface IFinishable +{ + /// + /// Finalizes the compression, flushing any remaining buffered data + /// and writing format-specific trailer/footer bytes. + /// + void Finish(); +} diff --git a/src/SharpCompress/Providers/System/SystemDeflateCompressionProvider.cs b/src/SharpCompress/Providers/System/SystemDeflateCompressionProvider.cs new file mode 100644 index 000000000..8e5faa013 --- /dev/null +++ b/src/SharpCompress/Providers/System/SystemDeflateCompressionProvider.cs @@ -0,0 +1,51 @@ +using System.IO; +using System.IO.Compression; +using SharpCompress.Common; + +namespace SharpCompress.Providers.System; + +/// +/// Provides Deflate compression using System.IO.Compression.DeflateStream. +/// +/// +/// On modern .NET (5+), System.IO.Compression uses hardware-accelerated zlib +/// and is significantly faster than SharpCompress's pure C# implementation. +/// +public sealed class SystemDeflateCompressionProvider : CompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.Deflate; + public override bool SupportsCompression => true; + public override bool SupportsDecompression => true; + + public override Stream CreateCompressStream(Stream destination, int compressionLevel) + { + var bclLevel = MapCompressionLevel(compressionLevel); + return new DeflateStream(destination, bclLevel, leaveOpen: false); + } + + public override Stream CreateDecompressStream(Stream source) + { + return new DeflateStream( + source, + global::System.IO.Compression.CompressionMode.Decompress, + leaveOpen: false + ); + } + + /// + /// Maps SharpCompress compression level (0-9) to BCL CompressionLevel. + /// + private static global::System.IO.Compression.CompressionLevel MapCompressionLevel(int level) + { + // Map 0-9 to appropriate BCL levels + return level switch + { + 0 => global::System.IO.Compression.CompressionLevel.NoCompression, + <= 2 => global::System.IO.Compression.CompressionLevel.Fastest, +#if NET7_0_OR_GREATER + >= 8 => global::System.IO.Compression.CompressionLevel.SmallestSize, +#endif + _ => global::System.IO.Compression.CompressionLevel.Optimal, + }; + } +} diff --git a/src/SharpCompress/Providers/System/SystemGZipCompressionProvider.cs b/src/SharpCompress/Providers/System/SystemGZipCompressionProvider.cs new file mode 100644 index 000000000..d0335ef68 --- /dev/null +++ b/src/SharpCompress/Providers/System/SystemGZipCompressionProvider.cs @@ -0,0 +1,51 @@ +using System.IO; +using System.IO.Compression; +using SharpCompress.Common; + +namespace SharpCompress.Providers.System; + +/// +/// Provides GZip compression using System.IO.Compression.GZipStream. +/// +/// +/// On modern .NET (5+), System.IO.Compression uses hardware-accelerated zlib +/// and is significantly faster than SharpCompress's pure C# implementation. +/// +public sealed class SystemGZipCompressionProvider : CompressionProviderBase +{ + public override CompressionType CompressionType => CompressionType.GZip; + public override bool SupportsCompression => true; + public override bool SupportsDecompression => true; + + public override Stream CreateCompressStream(Stream destination, int compressionLevel) + { + var bclLevel = MapCompressionLevel(compressionLevel); + return new GZipStream(destination, bclLevel, leaveOpen: false); + } + + public override Stream CreateDecompressStream(Stream source) + { + return new GZipStream( + source, + global::System.IO.Compression.CompressionMode.Decompress, + leaveOpen: false + ); + } + + /// + /// Maps SharpCompress compression level (0-9) to BCL CompressionLevel. + /// + private static global::System.IO.Compression.CompressionLevel MapCompressionLevel(int level) + { + // Map 0-9 to appropriate BCL levels + return level switch + { + 0 => global::System.IO.Compression.CompressionLevel.NoCompression, + <= 2 => global::System.IO.Compression.CompressionLevel.Fastest, +#if NET7_0_OR_GREATER + >= 8 => global::System.IO.Compression.CompressionLevel.SmallestSize, +#endif + _ => global::System.IO.Compression.CompressionLevel.Optimal, + }; + } +} diff --git a/src/SharpCompress/Readers/AbstractReader.Async.cs b/src/SharpCompress/Readers/AbstractReader.Async.cs index dc73e6fcc..0a507a34d 100644 --- a/src/SharpCompress/Readers/AbstractReader.Async.cs +++ b/src/SharpCompress/Readers/AbstractReader.Async.cs @@ -43,7 +43,10 @@ public async ValueTask MoveToNextEntryAsync(CancellationToken cancellation } if (_entriesForCurrentReadStreamAsync is null) { - return await LoadStreamForReadingAsync(RequestInitialStream()).ConfigureAwait(false); + return await LoadStreamForReadingAsync( + await RequestInitialStreamAsync(cancellationToken).ConfigureAwait(false) + ) + .ConfigureAwait(false); } if (!_wroteCurrentEntry) { diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index c2a12a2ae..5dd143819 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -140,6 +142,10 @@ protected bool LoadStreamForReading(Stream stream) protected virtual Stream RequestInitialStream() => Volume.NotNull("Volume isn't loaded.").Stream; + protected virtual ValueTask RequestInitialStreamAsync( + CancellationToken cancellationToken = default + ) => new(RequestInitialStream()); + internal virtual bool NextEntryForCurrentStream() => _entriesForCurrentReadStream.NotNull().MoveNext(); diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index c74a7133e..1bed74d5f 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -69,20 +69,14 @@ public static async ValueTask OpenAsyncReader( 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 @@ await testedFactory { 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/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index 0caac68df..41423c532 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -1,6 +1,8 @@ using System; using SharpCompress.Common; using SharpCompress.Common.Options; +using SharpCompress.Compressors; +using SharpCompress.Providers; namespace SharpCompress.Readers; @@ -21,16 +23,6 @@ namespace SharpCompress.Readers; /// public sealed record ReaderOptions : IReaderOptions { - /// - /// The default buffer size for stream operations. - /// This value (65536 bytes) is preserved for backward compatibility. - /// New code should use Constants.BufferSize instead (81920 bytes), which matches .NET's Stream.CopyTo default. - /// - [Obsolete( - "Use Constants.BufferSize instead. This constant will be removed in a future version." - )] - public const int DefaultBufferSize = 0x10000; - /// /// SharpCompress will keep the supplied streams open. Default is true. /// @@ -149,6 +141,14 @@ public sealed record ReaderOptions : IReaderOptions /// public Action? SymbolicLinkHandler { get; init; } + /// + /// Registry of compression providers. + /// Defaults to but can be replaced with custom implementations, such as + /// System.IO.Compression for Deflate/GZip on modern .NET. + /// + public CompressionProviderRegistry Providers { get; init; } = + CompressionProviderRegistry.Default; + /// /// Creates a new ReaderOptions instance with default values. /// diff --git a/src/SharpCompress/Readers/ReaderOptionsExtensions.cs b/src/SharpCompress/Readers/ReaderOptionsExtensions.cs index 26a8dfbe0..33f248079 100644 --- a/src/SharpCompress/Readers/ReaderOptionsExtensions.cs +++ b/src/SharpCompress/Readers/ReaderOptionsExtensions.cs @@ -1,6 +1,8 @@ using System; using SharpCompress.Common; using SharpCompress.Common.Options; +using SharpCompress.Compressors; +using SharpCompress.Providers; namespace SharpCompress.Readers; @@ -124,4 +126,17 @@ public static ReaderOptions WithSymbolicLinkHandler( this ReaderOptions options, Action? handler ) => options with { SymbolicLinkHandler = handler }; + + /// + /// Creates a copy with the specified compression provider registry. + /// + /// Thrown if is null. + public static ReaderOptions WithProviders( + this ReaderOptions options, + CompressionProviderRegistry providers + ) + { + _ = providers ?? throw new ArgumentNullException(nameof(providers)); + return options with { Providers = providers }; + } } diff --git a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs index fc775d711..f537ecab9 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,62 @@ public partial class TarReader : IReaderOpenable #endif { + private static Stream CreateProbeDecompressionStream( + Stream stream, + CompressionType compressionType, + CompressionProviderRegistry providers, + ReaderOptions options + ) + { + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(stream); + if (compressionType == CompressionType.None) + { + return nonDisposingStream; + } + + if (compressionType == CompressionType.GZip) + { + return providers.CreateDecompressStream( + compressionType, + nonDisposingStream, + CompressionContext.FromStream(nonDisposingStream).WithReaderOptions(options) + ); + } + + return providers.CreateDecompressStream(compressionType, nonDisposingStream); + } + + private static async ValueTask CreateProbeDecompressionStreamAsync( + Stream stream, + CompressionType compressionType, + CompressionProviderRegistry providers, + ReaderOptions options, + CancellationToken cancellationToken = default + ) + { + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(stream); + if (compressionType == CompressionType.None) + { + return nonDisposingStream; + } + + if (compressionType == CompressionType.GZip) + { + return await providers + .CreateDecompressStreamAsync( + compressionType, + nonDisposingStream, + CompressionContext.FromStream(nonDisposingStream).WithReaderOptions(options), + cancellationToken + ) + .ConfigureAwait(false); + } + + return await providers + .CreateDecompressStreamAsync(compressionType, nonDisposingStream, cancellationToken) + .ConfigureAwait(false); + } + public static ValueTask OpenAsyncReader( string path, ReaderOptions? readerOptions = null, @@ -42,81 +94,41 @@ public static async ValueTask OpenAsyncReader( bufferSize: options.RewindableBufferSize ); long pos = sharpCompressStream.Position; - if ( - await GZipArchive - .IsGZipFileAsync(sharpCompressStream, cancellationToken) - .ConfigureAwait(false) - ) - { - sharpCompressStream.Position = pos; - var testStream = new GZipStream(sharpCompressStream, CompressionMode.Decompress); - if ( - await TarArchive.IsTarFileAsync(testStream, cancellationToken).ConfigureAwait(false) - ) - { - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.GZip); - } - throw new InvalidFormatException("Not a tar file."); - } - sharpCompressStream.Position = pos; - if ( - await BZip2Stream - .IsBZip2Async(sharpCompressStream, cancellationToken) - .ConfigureAwait(false) - ) + foreach (var wrapper in TarWrapper.Wrappers) { sharpCompressStream.Position = pos; - var testStream = BZip2Stream.Create( - sharpCompressStream, - CompressionMode.Decompress, - false - ); if ( - await TarArchive.IsTarFileAsync(testStream, cancellationToken).ConfigureAwait(false) + !await wrapper + .IsMatchAsync(sharpCompressStream, cancellationToken) + .ConfigureAwait(false) ) { - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.BZip2); + continue; } - 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); + var testStream = await CreateProbeDecompressionStreamAsync( + sharpCompressStream, + wrapper.CompressionType, + options.Providers, + options, + cancellationToken + ) + .ConfigureAwait(false); if ( await TarArchive.IsTarFileAsync(testStream, cancellationToken).ConfigureAwait(false) ) { sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.ZStandard); + return new TarReader(sharpCompressStream, options, wrapper.CompressionType); } - 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) - ) + + if (wrapper.CompressionType != CompressionType.None) { - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.LZip); + throw new InvalidFormatException("Not a tar file."); } - throw new InvalidFormatException("Not a tar file."); } + sharpCompressStream.Position = pos; return new TarReader(sharpCompressStream, options, CompressionType.None); } @@ -159,57 +171,33 @@ public static IReader OpenReader(Stream stream, ReaderOptions? options = null) 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, + options ); if (TarArchive.IsTarFile(testStream)) { sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.BZip2); - } - 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)) - { - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.ZStandard); + return new TarReader(sharpCompressStream, options, wrapper.CompressionType); } - 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)) + + if (wrapper.CompressionType != CompressionType.None) { - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, options, CompressionType.LZip); + throw new InvalidFormatException("Not a tar file."); } - throw new InvalidFormatException("Not a tar file."); } + sharpCompressStream.Position = pos; return new TarReader(sharpCompressStream, options, CompressionType.None); } diff --git a/src/SharpCompress/Readers/Tar/TarReader.cs b/src/SharpCompress/Readers/Tar/TarReader.cs index ce7c822b9..f7c5abbac 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Tar; using SharpCompress.Compressors; @@ -11,6 +13,7 @@ using SharpCompress.Compressors.Xz; using SharpCompress.Compressors.ZStandard; using SharpCompress.IO; +using SharpCompress.Providers; namespace SharpCompress.Readers.Tar; @@ -30,19 +33,77 @@ internal TarReader(Stream stream, ReaderOptions options, CompressionType compres protected override Stream RequestInitialStream() { var stream = base.RequestInitialStream(); + + var providers = Options.Providers; + return 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 => providers.CreateDecompressStream( + CompressionType.BZip2, + stream + ), + CompressionType.GZip => providers.CreateDecompressStream( + CompressionType.GZip, + stream, + CompressionContext.FromStream(stream).WithReaderOptions(Options) + ), + CompressionType.ZStandard => providers.CreateDecompressStream( + CompressionType.ZStandard, + stream + ), + CompressionType.LZip => providers.CreateDecompressStream(CompressionType.LZip, stream), + CompressionType.Xz => providers.CreateDecompressStream(CompressionType.Xz, stream), + CompressionType.Lzw => providers.CreateDecompressStream(CompressionType.Lzw, stream), CompressionType.None => stream, _ => throw new NotSupportedException("Invalid compression type: " + compressionType), }; } + protected override ValueTask RequestInitialStreamAsync( + CancellationToken cancellationToken = default + ) + { + var stream = base.RequestInitialStream(); + var providers = Options.Providers; + + return compressionType switch + { + CompressionType.BZip2 => providers.CreateDecompressStreamAsync( + CompressionType.BZip2, + stream, + cancellationToken + ), + CompressionType.GZip => providers.CreateDecompressStreamAsync( + CompressionType.GZip, + stream, + CompressionContext.FromStream(stream).WithReaderOptions(Options), + cancellationToken + ), + CompressionType.ZStandard => providers.CreateDecompressStreamAsync( + CompressionType.ZStandard, + stream, + cancellationToken + ), + CompressionType.LZip => providers.CreateDecompressStreamAsync( + CompressionType.LZip, + stream, + cancellationToken + ), + CompressionType.Xz => providers.CreateDecompressStreamAsync( + CompressionType.Xz, + stream, + cancellationToken + ), + CompressionType.Lzw => providers.CreateDecompressStreamAsync( + CompressionType.Lzw, + stream, + cancellationToken + ), + CompressionType.None => new ValueTask(stream), + _ => throw new NotSupportedException("Invalid compression type: " + compressionType), + }; + } + protected override IEnumerable GetEntries(Stream stream) => TarEntry.GetEntries( StreamingMode.Streaming, diff --git a/src/SharpCompress/Readers/Zip/ZipReader.Async.cs b/src/SharpCompress/Readers/Zip/ZipReader.Async.cs index bb2496de1..889955ba4 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.Async.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.Async.cs @@ -7,6 +7,7 @@ using SharpCompress.Common.Options; using SharpCompress.Common.Zip; using SharpCompress.Common.Zip.Headers; +using SharpCompress.Compressors; namespace SharpCompress.Readers.Zip; @@ -77,7 +78,11 @@ public async ValueTask MoveNextAsync() { case ZipHeaderType.LocalEntry: _current = new ZipEntry( - new StreamingZipFilePart((LocalEntryHeader)header, _stream), + new StreamingZipFilePart( + (LocalEntryHeader)header, + _stream, + _options.Providers + ), _options ); return true; diff --git a/src/SharpCompress/Readers/Zip/ZipReader.cs b/src/SharpCompress/Readers/Zip/ZipReader.cs index 803dad0ca..ac1b4419b 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.cs @@ -74,7 +74,11 @@ protected override IEnumerable GetEntries(Stream stream) case ZipHeaderType.LocalEntry: { yield return new ZipEntry( - new StreamingZipFilePart((LocalEntryHeader)h, stream), + new StreamingZipFilePart( + (LocalEntryHeader)h, + stream, + Options.Providers + ), Options ); } diff --git a/src/SharpCompress/Writers/GZip/GZipWriter.cs b/src/SharpCompress/Writers/GZip/GZipWriter.cs index 672692727..41e2ef1e2 100644 --- a/src/SharpCompress/Writers/GZip/GZipWriter.cs +++ b/src/SharpCompress/Writers/GZip/GZipWriter.cs @@ -18,14 +18,24 @@ public GZipWriter(Stream destination, GZipWriterOptions? options = null) { destination = SharpCompressStream.CreateNonDisposing(destination); } - InitializeStream( - new GZipStream( - destination, - CompressionMode.Compress, - (CompressionLevel)(options?.CompressionLevel ?? (int)CompressionLevel.Default), - WriterOptions.ArchiveEncoding.GetEncoding() - ) + + // Use the configured compression providers + var providers = WriterOptions.Providers; + + // Create the GZip stream using the provider + var compressionStream = providers.CreateCompressStream( + CompressionType.GZip, + destination, + WriterOptions.CompressionLevel ); + + // If using internal GZipStream, set the encoding for header filename + if (compressionStream is GZipStream gzipStream) + { + // Note: FileName and LastModified will be set in Write() + } + + InitializeStream(compressionStream); } protected override void Dispose(bool isDisposing) @@ -44,11 +54,16 @@ public override void Write(string filename, Stream source, DateTime? modificatio { throw new ArgumentException("Can only write a single stream to a GZip file."); } - var stream = (GZipStream)OutputStream; - stream.FileName = filename; - stream.LastModified = modificationTime; + + // Set metadata on the stream if it's the internal GZipStream + if (OutputStream is GZipStream gzipStream) + { + gzipStream.FileName = filename; + gzipStream.LastModified = modificationTime; + } + var progressStream = WrapWithProgress(source, filename); - progressStream.CopyTo(stream, Constants.BufferSize); + progressStream.CopyTo(OutputStream, Constants.BufferSize); _wroteToStream = true; } diff --git a/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs b/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs index 3477cb3e2..b70b647a1 100644 --- a/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs +++ b/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs @@ -1,6 +1,8 @@ using System; using SharpCompress.Common; using SharpCompress.Common.Options; +using SharpCompress.Compressors; +using SharpCompress.Providers; using SharpCompress.Writers; using D = SharpCompress.Compressors.Deflate; @@ -66,6 +68,14 @@ public int CompressionLevel /// public IProgress? Progress { get; init; } + /// + /// Registry of compression providers. + /// Defaults to but can be replaced with custom implementations, such as + /// System.IO.Compression for GZip on modern .NET. + /// + public CompressionProviderRegistry Providers { get; init; } = + CompressionProviderRegistry.Default; + /// /// Creates a new GZipWriterOptions instance with default values. /// @@ -105,6 +115,7 @@ public GZipWriterOptions(WriterOptions options) LeaveStreamOpen = options.LeaveStreamOpen; ArchiveEncoding = options.ArchiveEncoding; Progress = options.Progress; + Providers = options.Providers; } /// @@ -117,5 +128,6 @@ public GZipWriterOptions(IWriterOptions options) LeaveStreamOpen = options.LeaveStreamOpen; ArchiveEncoding = options.ArchiveEncoding; Progress = options.Progress; + Providers = options.Providers; } } diff --git a/src/SharpCompress/Writers/Tar/TarWriter.cs b/src/SharpCompress/Writers/Tar/TarWriter.cs index eb7070fb1..01a3685f1 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.cs @@ -5,10 +5,8 @@ using SharpCompress.Common; using SharpCompress.Common.Tar.Headers; using SharpCompress.Compressors; -using SharpCompress.Compressors.BZip2; -using SharpCompress.Compressors.Deflate; -using SharpCompress.Compressors.LZMA; using SharpCompress.IO; +using SharpCompress.Providers; namespace SharpCompress.Writers.Tar; @@ -31,32 +29,32 @@ public TarWriter(Stream destination, TarWriterOptions options) { destination = SharpCompressStream.CreateNonDisposing(destination); } - switch (options.CompressionType) + + var providers = options.Providers; + + destination = options.CompressionType switch { - case CompressionType.None: - break; - case CompressionType.BZip2: - { - destination = BZip2Stream.Create(destination, CompressionMode.Compress, false); - } - break; - case CompressionType.GZip: - { - destination = new GZipStream(destination, CompressionMode.Compress); - } - break; - case CompressionType.LZip: - { - destination = new LZipStream(destination, CompressionMode.Compress); - } - break; - default: - { - throw new InvalidFormatException( - "Tar does not support compression: " + options.CompressionType - ); - } - } + CompressionType.None => destination, + CompressionType.BZip2 => providers.CreateCompressStream( + CompressionType.BZip2, + destination, + options.CompressionLevel + ), + CompressionType.GZip => providers.CreateCompressStream( + CompressionType.GZip, + destination, + options.CompressionLevel + ), + CompressionType.LZip => providers.CreateCompressStream( + CompressionType.LZip, + destination, + options.CompressionLevel + ), + _ => throw new InvalidFormatException( + "Tar does not support compression: " + options.CompressionType + ), + }; + InitializeStream(destination); } @@ -138,18 +136,10 @@ protected override void Dispose(bool isDisposing) { OutputStream.Write(stackalloc byte[1024]); } - switch (OutputStream) + // Use IFinishable interface for generic finalization + if (OutputStream is IFinishable finishable) { - case BZip2Stream b: - { - b.Finish(); - break; - } - case LZipStream l: - { - l.Finish(); - break; - } + finishable.Finish(); } } base.Dispose(isDisposing); diff --git a/src/SharpCompress/Writers/Tar/TarWriterOptions.cs b/src/SharpCompress/Writers/Tar/TarWriterOptions.cs index 80bf6e635..16e5df9f9 100755 --- a/src/SharpCompress/Writers/Tar/TarWriterOptions.cs +++ b/src/SharpCompress/Writers/Tar/TarWriterOptions.cs @@ -2,6 +2,8 @@ using SharpCompress.Common; using SharpCompress.Common.Options; using SharpCompress.Common.Tar.Headers; +using SharpCompress.Compressors; +using SharpCompress.Providers; namespace SharpCompress.Writers.Tar; @@ -42,6 +44,13 @@ public sealed record TarWriterOptions : IWriterOptions /// public IProgress? Progress { get; init; } + /// + /// Registry of compression providers. + /// Defaults to but can be replaced with custom implementations. + /// + public CompressionProviderRegistry Providers { get; init; } = + CompressionProviderRegistry.Default; + /// /// Indicates if archive should be finalized (by 2 empty blocks) on close. /// @@ -96,6 +105,7 @@ public TarWriterOptions(WriterOptions options) LeaveStreamOpen = options.LeaveStreamOpen; ArchiveEncoding = options.ArchiveEncoding; Progress = options.Progress; + Providers = options.Providers; } /// @@ -109,6 +119,7 @@ public TarWriterOptions(IWriterOptions options) LeaveStreamOpen = options.LeaveStreamOpen; ArchiveEncoding = options.ArchiveEncoding; Progress = options.Progress; + Providers = options.Providers; } /// diff --git a/src/SharpCompress/Writers/WriterOptions.cs b/src/SharpCompress/Writers/WriterOptions.cs index 370379769..cc0e0370e 100644 --- a/src/SharpCompress/Writers/WriterOptions.cs +++ b/src/SharpCompress/Writers/WriterOptions.cs @@ -1,6 +1,8 @@ using System; using SharpCompress.Common; using SharpCompress.Common.Options; +using SharpCompress.Compressors; +using SharpCompress.Providers; using D = SharpCompress.Compressors.Deflate; namespace SharpCompress.Writers; @@ -62,6 +64,14 @@ public int CompressionLevel /// public IProgress? Progress { get; init; } + /// + /// Registry of compression providers. + /// Defaults to but can be replaced with custom implementations, such as + /// System.IO.Compression for Deflate/GZip on modern .NET. + /// + public CompressionProviderRegistry Providers { get; init; } = + CompressionProviderRegistry.Default; + /// /// Creates a new WriterOptions instance with the specified compression type. /// Compression level is automatically set based on the compression type. diff --git a/src/SharpCompress/Writers/WriterOptionsExtensions.cs b/src/SharpCompress/Writers/WriterOptionsExtensions.cs index d86b54060..9e0ce7e75 100644 --- a/src/SharpCompress/Writers/WriterOptionsExtensions.cs +++ b/src/SharpCompress/Writers/WriterOptionsExtensions.cs @@ -1,6 +1,8 @@ using System; using SharpCompress.Common; using SharpCompress.Common.Options; +using SharpCompress.Compressors; +using SharpCompress.Providers; namespace SharpCompress.Writers; @@ -52,4 +54,17 @@ public static WriterOptions WithProgress( this WriterOptions options, IProgress progress ) => options with { Progress = progress }; + + /// + /// Creates a copy with the specified compression provider registry. + /// + /// Thrown if is null. + public static WriterOptions WithProviders( + this WriterOptions options, + CompressionProviderRegistry providers + ) + { + _ = providers ?? throw new ArgumentNullException(nameof(providers)); + return options with { Providers = providers }; + } } diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs index 3edd2bb53..6ab572c2c 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs @@ -15,6 +15,7 @@ using SharpCompress.Compressors.PPMd; using SharpCompress.Compressors.ZStandard; using SharpCompress.IO; +using SharpCompress.Providers; using Constants = SharpCompress.Common.Constants; namespace SharpCompress.Writers.Zip; @@ -378,6 +379,8 @@ internal class ZipWritingStream : Stream private readonly ZipWriter writer; private readonly ZipCompressionMethod zipCompressionMethod; private readonly int compressionLevel; + private ICompressionProviderHooks? compressionProviderHooks; + private CompressionContext? compressionContext; private CountingStream? counting; private ulong decompressed; @@ -420,6 +423,9 @@ private Stream GetWriteStream(Stream writeStream) { counting = new CountingStream(SharpCompressStream.CreateNonDisposing(writeStream)); Stream output = counting; + + var providers = writer.WriterOptions.Providers; + switch (zipCompressionMethod) { case ZipCompressionMethod.None: @@ -428,39 +434,98 @@ private Stream GetWriteStream(Stream writeStream) } case ZipCompressionMethod.Deflate: { - return new DeflateStream( + return providers.CreateCompressStream( + CompressionType.Deflate, counting, - CompressionMode.Compress, - (CompressionLevel)compressionLevel + compressionLevel ); } case ZipCompressionMethod.BZip2: { - return BZip2Stream.Create(counting, CompressionMode.Compress, false); + return providers.CreateCompressStream( + CompressionType.BZip2, + counting, + compressionLevel + ); } case ZipCompressionMethod.LZMA: { - counting.WriteByte(9); - counting.WriteByte(20); - counting.WriteByte(5); - counting.WriteByte(0); - - var lzmaStream = LzmaStream.Create( - new LzmaEncoderProperties(!originalStream.CanSeek), - false, - counting + // Use ICompressionProviderHooks for complex initialization + var compressingProvider = providers.GetCompressingProvider( + CompressionType.LZMA ); - counting.Write(lzmaStream.Properties, 0, lzmaStream.Properties.Length); + if (compressingProvider is null) + { + throw new InvalidOperationException("LZMA compression provider not found."); + } + + var context = new CompressionContext { CanSeek = originalStream.CanSeek }; + compressionProviderHooks = compressingProvider; + compressionContext = context; + + // Write pre-compression data (magic bytes) + var preData = compressingProvider.GetPreCompressionData(context); + if (preData != null) + { + counting.Write(preData, 0, preData.Length); + } + + // Create compression stream + var lzmaStream = compressingProvider.CreateCompressStream( + counting, + compressionLevel, + context + ); + + // Write compression properties + var props = compressingProvider.GetCompressionProperties(lzmaStream, context); + if (props != null) + { + counting.Write(props, 0, props.Length); + } + return lzmaStream; } case ZipCompressionMethod.PPMd: { - counting.Write(writer.PpmdProperties.Properties, 0, 2); - return PpmdStream.Create(writer.PpmdProperties, counting, true); + // Use ICompressionProviderHooks for complex initialization + var compressingProvider = providers.GetCompressingProvider( + CompressionType.PPMd + ); + if (compressingProvider is null) + { + throw new InvalidOperationException("PPMd compression provider not found."); + } + + var context = new CompressionContext + { + CanSeek = originalStream.CanSeek, + FormatOptions = writer.PpmdProperties, + }; + compressionProviderHooks = compressingProvider; + compressionContext = context; + + // Write pre-compression data (properties) + var preData = compressingProvider.GetPreCompressionData(context); + if (preData != null) + { + counting.Write(preData, 0, preData.Length); + } + + // Create compression stream + return compressingProvider.CreateCompressStream( + counting, + compressionLevel, + context + ); } case ZipCompressionMethod.ZStandard: { - return new CompressionStream(counting, compressionLevel); + return providers.CreateCompressStream( + CompressionType.ZStandard, + counting, + compressionLevel + ); } default: { @@ -492,6 +557,8 @@ protected override void Dispose(bool disposing) return; } + WritePostCompressionData(); + var countingCount = counting?.BytesWritten ?? 0; entry.Crc = (uint)crc.Crc32Result; entry.Compressed = (ulong)countingCount; @@ -629,6 +696,30 @@ public override void Write(byte[] buffer, int offset, int count) } } } + + 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/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs b/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs index 9ba8ea48c..0089d7c82 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs @@ -1,7 +1,9 @@ using System; using SharpCompress.Common; using SharpCompress.Common.Options; +using SharpCompress.Compressors; using SharpCompress.Compressors.Deflate; +using SharpCompress.Providers; using SharpCompress.Writers; using D = SharpCompress.Compressors.Deflate; @@ -59,6 +61,13 @@ public int CompressionLevel /// public IProgress? Progress { get; init; } + /// + /// Registry of compression providers. + /// Defaults to but can be replaced with custom implementations. + /// + public CompressionProviderRegistry Providers { get; init; } = + CompressionProviderRegistry.Default; + /// /// Optional comment for the archive. /// @@ -119,6 +128,7 @@ public ZipWriterOptions(WriterOptions options) LeaveStreamOpen = options.LeaveStreamOpen; ArchiveEncoding = options.ArchiveEncoding; Progress = options.Progress; + Providers = options.Providers; } /// @@ -131,6 +141,7 @@ public ZipWriterOptions(IWriterOptions options) LeaveStreamOpen = options.LeaveStreamOpen; ArchiveEncoding = options.ArchiveEncoding; Progress = options.Progress; + Providers = options.Providers; } /// diff --git a/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs index 521e74386..558a989dd 100644 --- a/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs +++ b/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs @@ -5,6 +5,9 @@ using BenchmarkDotNet.Attributes; using SharpCompress.Archives.Tar; using SharpCompress.Common; +using SharpCompress.Compressors; +using SharpCompress.Providers; +using SharpCompress.Providers.System; using SharpCompress.Readers; using SharpCompress.Writers; @@ -61,16 +64,38 @@ public void TarExtractReaderApi() } } - [Benchmark(Description = "Tar: Extract all entries (Reader API, Async)")] - public async Task TarExtractReaderApiAsync() + [Benchmark(Description = "Tar: Extract all entries (Archive API) - SystemGzip")] + public void SystemTarExtractArchiveApi() { using var stream = new MemoryStream(_tarBytes); - await using var reader = await ReaderFactory.OpenAsyncReader(stream).ConfigureAwait(false); - while (await reader.MoveToNextEntryAsync().ConfigureAwait(false)) + using var archive = TarArchive.OpenArchive( + stream, + new ReaderOptions().WithProviders( + CompressionProviderRegistry.Empty.With(new SystemGZipCompressionProvider()) + ) + ); + foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + entryStream.CopyTo(Stream.Null); + } + } + + [Benchmark(Description = "Tar: Extract all entries (Reader API) - SystemGzip")] + public void SystemTarExtractReaderApi() + { + using var stream = new MemoryStream(_tarBytes); + using var reader = ReaderFactory.OpenReader( + stream, + new ReaderOptions().WithProviders( + CompressionProviderRegistry.Empty.With(new SystemGZipCompressionProvider()) + ) + ); + while (reader.MoveToNextEntry()) { if (!reader.Entry.IsDirectory) { - await reader.WriteEntryToAsync(Stream.Null).ConfigureAwait(false); + reader.WriteEntryTo(Stream.Null); } } } diff --git a/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs index 375c56905..f9c53c27b 100644 --- a/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs +++ b/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs @@ -5,6 +5,9 @@ using BenchmarkDotNet.Attributes; using SharpCompress.Archives.Zip; using SharpCompress.Common; +using SharpCompress.Compressors; +using SharpCompress.Providers; +using SharpCompress.Providers.System; using SharpCompress.Readers; using SharpCompress.Writers; @@ -23,6 +26,23 @@ public void Setup() _archiveBytes = File.ReadAllBytes(_archivePath); } + [Benchmark(Description = "Zip: Extract all entries (Archive API) - SystemDeflate")] + public void SystemZipExtractArchiveApi() + { + using var stream = new MemoryStream(_archiveBytes); + using var archive = ZipArchive.OpenArchive( + stream, + new ReaderOptions().WithProviders( + CompressionProviderRegistry.Empty.With(new SystemDeflateCompressionProvider()) + ) + ); + foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + entryStream.CopyTo(Stream.Null); + } + } + [Benchmark(Description = "Zip: Extract all entries (Archive API)")] public void ZipExtractArchiveApi() { @@ -47,6 +67,25 @@ public async Task ZipExtractArchiveApiAsync() } } + [Benchmark(Description = "Zip: Extract all entries (Reader API) - SystemDeflate")] + public void SystemZipExtractReaderApi() + { + using var stream = new MemoryStream(_archiveBytes); + using var reader = ReaderFactory.OpenReader( + stream, + new ReaderOptions().WithProviders( + CompressionProviderRegistry.Empty.With(new SystemDeflateCompressionProvider()) + ) + ); + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryTo(Stream.Null); + } + } + } + [Benchmark(Description = "Zip: Extract all entries (Reader API)")] public void ZipExtractReaderApi() { diff --git a/tests/SharpCompress.Performance/baseline-results.md b/tests/SharpCompress.Performance/baseline-results.md index 6d9874a1b..fa34fd66e 100644 --- a/tests/SharpCompress.Performance/baseline-results.md +++ b/tests/SharpCompress.Performance/baseline-results.md @@ -46,4 +46,4 @@ | 'Zip: Extract all entries (Reader API)' | 542.2 μs | 1.10 μs | 1.46 μs | - | - | 121.04 KB | | 'Zip: Extract all entries (Reader API, Async)' | 562.8 μs | 2.42 μs | 3.55 μs | - | - | 123.34 KB | | 'Zip: Create archive with small files' | 271.1 μs | 12.93 μs | 18.95 μs | 166.6667 | 33.3333 | 2806.28 KB | -| 'Zip: Create archive with small files (Async)' | 394.3 μs | 25.59 μs | 36.71 μs | 166.6667 | 33.3333 | 2811.42 KB | \ No newline at end of file +| 'Zip: Create archive with small files (Async)' | 394.3 μs | 25.59 μs | 36.71 μs | 166.6667 | 33.3333 | 2811.42 KB | diff --git a/tests/SharpCompress.Test/CompressionProviderTests.cs b/tests/SharpCompress.Test/CompressionProviderTests.cs new file mode 100644 index 000000000..8578de9df --- /dev/null +++ b/tests/SharpCompress.Test/CompressionProviderTests.cs @@ -0,0 +1,886 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AwesomeAssertions; +using SharpCompress.Archives.Tar; +using SharpCompress.Common; +using SharpCompress.Common.Options; +using SharpCompress.Compressors; +using SharpCompress.IO; +using SharpCompress.Providers; +using SharpCompress.Providers.Default; +using SharpCompress.Providers.System; +using SharpCompress.Readers; +using SharpCompress.Readers.Tar; +using SharpCompress.Writers; +using SharpCompress.Writers.Tar; +using Xunit; + +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 int AsyncCompressionCalls { get; private set; } + + public int AsyncDecompressionCalls { 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); + } + + public ValueTask CreateCompressStreamAsync( + Stream destination, + int compressionLevel, + CancellationToken cancellationToken = default + ) + { + AsyncCompressionCalls++; + return _inner.CreateCompressStreamAsync( + destination, + compressionLevel, + cancellationToken + ); + } + + public ValueTask CreateCompressStreamAsync( + Stream destination, + int compressionLevel, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + AsyncCompressionCalls++; + return _inner.CreateCompressStreamAsync( + destination, + compressionLevel, + context, + cancellationToken + ); + } + + public ValueTask CreateDecompressStreamAsync( + Stream source, + CancellationToken cancellationToken = default + ) + { + AsyncDecompressionCalls++; + return _inner.CreateDecompressStreamAsync(source, cancellationToken); + } + + public ValueTask CreateDecompressStreamAsync( + Stream source, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + AsyncDecompressionCalls++; + return _inner.CreateDecompressStreamAsync(source, context, cancellationToken); + } + } + + 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 ValueTask CreateCompressStreamAsync( + Stream destination, + int compressionLevel, + CancellationToken cancellationToken = default + ) + { + CompressionContext context = new() { CanSeek = destination.CanSeek }; + return CreateCompressStreamAsync( + destination, + compressionLevel, + context, + cancellationToken + ); + } + + public ValueTask CreateCompressStreamAsync( + Stream destination, + int compressionLevel, + CompressionContext context, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return new ValueTask(SharpCompressStream.CreateNonDisposing(destination)); + } + + public ValueTask CreateDecompressStreamAsync( + Stream source, + CancellationToken cancellationToken = default + ) => throw new NotSupportedException(); + + public ValueTask CreateDecompressStreamAsync( + Stream source, + CompressionContext context, + CancellationToken cancellationToken = default + ) => 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]; + } + } + + private sealed class ContextRequiredGZipProvider : CompressionProviderBase + { + private readonly GZipCompressionProvider _inner = new(); + + public override CompressionType CompressionType => CompressionType.GZip; + + public override bool SupportsCompression => true; + + public override bool SupportsDecompression => true; + + public override Stream CreateCompressStream(Stream destination, int compressionLevel) => + _inner.CreateCompressStream(destination, compressionLevel); + + public override Stream CreateDecompressStream(Stream source) => + throw new InvalidOperationException("Context is required for GZip decompression."); + + public override Stream CreateDecompressStream(Stream source, CompressionContext context) + { + context.ReaderOptions.Should().NotBeNull(); + return _inner.CreateDecompressStream(source, context); + } + } + + [Fact] + public void CompressionProviderRegistry_Default_ReturnsInternalProviders() + { + var registry = CompressionProviderRegistry.Default; + + registry.GetProvider(CompressionType.Deflate).Should().NotBeNull(); + registry.GetProvider(CompressionType.GZip).Should().NotBeNull(); + registry.GetProvider(CompressionType.BZip2).Should().NotBeNull(); + registry.GetProvider(CompressionType.ZStandard).Should().NotBeNull(); + registry.GetProvider(CompressionType.LZip).Should().NotBeNull(); + registry.GetProvider(CompressionType.Xz).Should().NotBeNull(); + registry.GetProvider(CompressionType.Lzw).Should().NotBeNull(); + } + + [Fact] + public void CompressionProviderRegistry_With_ReplacesProvider() + { + var customProvider = new DeflateCompressionProvider(); + var registry = CompressionProviderRegistry.Default.With(customProvider); + + // Should return the new provider + var retrieved = registry.GetProvider(CompressionType.Deflate); + retrieved.Should().BeSameAs(customProvider); + } + + [Fact] + public void CompressionProviderRegistry_With_DoesNotModifyOriginal() + { + var original = CompressionProviderRegistry.Default; + var customProvider = new DeflateCompressionProvider(); + var modified = original.With(customProvider); + + // Original should still have the default provider + var originalProvider = original.GetProvider(CompressionType.Deflate); + var modifiedProvider = modified.GetProvider(CompressionType.Deflate); + originalProvider.Should().NotBeSameAs(modifiedProvider); + originalProvider.Should().NotBeSameAs(customProvider); + } + + [Fact] + public void DeflateProvider_RoundTrip_Works() + { + var provider = new DeflateCompressionProvider(); + var original = Encoding.UTF8.GetBytes("Hello, World! This is a test of compression."); + + using var compressedStream = new MemoryStream(); + // Wrap in NonDisposingStream so the compression stream doesn't close it + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = provider.CreateCompressStream(nonDisposingStream, 6)) + { + compressStream.Write(original, 0, original.Length); + } + + compressedStream.Position = 0; + using var decompressStream = provider.CreateDecompressStream(compressedStream); + using var resultStream = new MemoryStream(); + decompressStream.CopyTo(resultStream); + + var result = resultStream.ToArray(); + result.Should().Equal(original); + } + + [Fact] + public void GZipProvider_RoundTrip_Works() + { + var provider = new GZipCompressionProvider(); + var original = Encoding.UTF8.GetBytes("Hello, World! This is a test of compression."); + + using var compressedStream = new MemoryStream(); + // Wrap in NonDisposingStream so the compression stream doesn't close it + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = provider.CreateCompressStream(nonDisposingStream, 6)) + { + compressStream.Write(original, 0, original.Length); + } + + compressedStream.Position = 0; + using var decompressStream = provider.CreateDecompressStream(compressedStream); + using var resultStream = new MemoryStream(); + decompressStream.CopyTo(resultStream); + + var result = resultStream.ToArray(); + result.Should().Equal(original); + } + + [Fact] + public void GZipProvider_Decompress_WithReaderOptionsContext_UsesArchiveEncoding() + { + var provider = new GZipCompressionProvider(); + var data = Encoding.UTF8.GetBytes("gzip filename encoding"); + var expectedFileName = "café.txt"; + var archiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding("iso-8859-1") }; + + using var compressedStream = CreateGZipWithFileName( + data, + expectedFileName, + archiveEncoding.Default + ); + + compressedStream.Position = 0; + var readerOptions = new ReaderOptions { ArchiveEncoding = archiveEncoding }; + var context = CompressionContext.FromStream(compressedStream) with + { + ReaderOptions = readerOptions, + }; + + using var decompressStream = provider.CreateDecompressStream(compressedStream, context); + using var resultStream = new MemoryStream(); + decompressStream.CopyTo(resultStream); + + resultStream.ToArray().Should().Equal(data); + decompressStream.Should().BeOfType(); + var gzipStream = (SharpCompress.Compressors.Deflate.GZipStream)decompressStream; + gzipStream.FileName.Should().Be(expectedFileName); + } + + [Fact] + public void GZipProvider_Decompress_WithNullReaderOptions_FallsBackToUtf8() + { + var provider = new GZipCompressionProvider(); + var data = Encoding.UTF8.GetBytes("gzip filename encoding"); + var expectedFileName = "café.txt"; + + using var compressedStream = CreateGZipWithFileName(data, expectedFileName, Encoding.UTF8); + + compressedStream.Position = 0; + var context = CompressionContext.FromStream(compressedStream); + // ReaderOptions is null by default + + using var decompressStream = provider.CreateDecompressStream(compressedStream, context); + using var resultStream = new MemoryStream(); + decompressStream.CopyTo(resultStream); + + resultStream.ToArray().Should().Equal(data); + var gzipStream = (SharpCompress.Compressors.Deflate.GZipStream)decompressStream; + gzipStream.FileName.Should().Be(expectedFileName); + } + + [Fact] + public void BZip2Provider_SupportsCompressionAndDecompression() + { + var provider = new BZip2CompressionProvider(); + + // Verify the provider reports correct capabilities + provider.CompressionType.Should().Be(CompressionType.BZip2); + provider.SupportsCompression.Should().BeTrue(); + provider.SupportsDecompression.Should().BeTrue(); + } + + [Fact] + public void TarWriter_WithCustomProvider_UsesProvider() + { + var customProvider = new GZipCompressionProvider(); + var registry = CompressionProviderRegistry.Default.With(customProvider); + + using var stream = new MemoryStream(); + var options = new TarWriterOptions(CompressionType.GZip, true) { Providers = registry }; + + using (var writer = new TarWriter(stream, options)) + { + var data = Encoding.UTF8.GetBytes("Test content"); + writer.Write("test.txt", new MemoryStream(data), DateTime.Now); + } + + // Should have written compressed data + stream.Position = 0; + stream.Length.Should().BeGreaterThan(0); + } + + [Fact] + public void TarWriter_WithoutCustomProvider_UsesDefault() + { + using var stream = new MemoryStream(); + var options = new TarWriterOptions(CompressionType.GZip, true); + + using (var writer = new TarWriter(stream, options)) + { + var data = Encoding.UTF8.GetBytes("Test content"); + writer.Write("test.txt", new MemoryStream(data), DateTime.Now); + } + + stream.Position = 0; + stream.Length.Should().BeGreaterThan(0); + } + + [Fact] + public void TarReader_WithCustomProvider_UsesProvider() + { + // First, create a tar.gz file + using var archiveStream = new MemoryStream(); + var writeOptions = new TarWriterOptions(CompressionType.GZip, true); + using (var writer = new TarWriter(archiveStream, writeOptions)) + { + var data = Encoding.UTF8.GetBytes("Test content for reading"); + writer.Write("test.txt", new MemoryStream(data), DateTime.Now); + } + + // Now read it back with a custom provider + archiveStream.Position = 0; + var customProvider = new GZipCompressionProvider(); + var registry = CompressionProviderRegistry.Default.With(customProvider); + var readOptions = new ReaderOptions { Providers = registry }; + + using var reader = TarReader.OpenReader(archiveStream, readOptions); + reader.MoveToNextEntry().Should().BeTrue(); + using var entryStream = reader.OpenEntryStream(); + using var resultStream = new MemoryStream(); + entryStream.CopyTo(resultStream); + + var result = Encoding.UTF8.GetString(resultStream.ToArray()); + result.Should().Be("Test content for reading"); + } + + [Fact] + public void TarReader_OpenReader_WithContextRequiredGZipProvider_Succeeds() + { + using var archiveStream = new MemoryStream(); + using ( + var writer = new TarWriter( + archiveStream, + new TarWriterOptions(CompressionType.GZip, true) + ) + ) + { + var data = Encoding.UTF8.GetBytes("Test content for context-required provider"); + writer.Write("test.txt", new MemoryStream(data), DateTime.Now); + } + + archiveStream.Position = 0; + var registry = CompressionProviderRegistry.Default.With(new ContextRequiredGZipProvider()); + var readOptions = new ReaderOptions { Providers = registry }; + + using var reader = TarReader.OpenReader(archiveStream, readOptions); + reader.MoveToNextEntry().Should().BeTrue(); + using var entryStream = reader.OpenEntryStream(); + using var resultStream = new MemoryStream(); + entryStream.CopyTo(resultStream); + + var result = Encoding.UTF8.GetString(resultStream.ToArray()); + result.Should().Be("Test content for context-required provider"); + } + + [Fact] + public void WriterOptions_WithProviders_CanBeCloned() + { + var customProvider = new DeflateCompressionProvider(); + var registry = CompressionProviderRegistry.Default.With(customProvider); + + var original = new WriterOptions(CompressionType.GZip) + { + Providers = registry, + LeaveStreamOpen = false, + }; + + // Clone using 'with' expression + var clone = original with + { + LeaveStreamOpen = true, + }; + + clone.CompressionType.Should().Be(original.CompressionType); + clone.CompressionLevel.Should().Be(original.CompressionLevel); + clone.Providers.Should().BeSameAs(original.Providers); + clone.LeaveStreamOpen.Should().BeTrue(); + } + + [Fact] + public void ReaderOptions_WithProviders_CanBeCloned() + { + var customProvider = new DeflateCompressionProvider(); + var registry = CompressionProviderRegistry.Default.With(customProvider); + + var original = new ReaderOptions { Providers = registry, LeaveStreamOpen = false }; + + // Clone using 'with' expression + var clone = original with + { + LeaveStreamOpen = true, + }; + + clone.Providers.Should().BeSameAs(original.Providers); + 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.AsyncDecompressionCalls.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.AsyncDecompressionCalls.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 + + private static MemoryStream CreateGZipWithFileName( + byte[] data, + string fileName, + Encoding encoding + ) + { + var compressedStream = new MemoryStream(); + using ( + var compressStream = new SharpCompress.Compressors.Deflate.GZipStream( + SharpCompressStream.CreateNonDisposing(compressedStream), + CompressionMode.Compress, + SharpCompress.Compressors.Deflate.CompressionLevel.Default, + encoding + ) + ) + { + compressStream.FileName = fileName; + compressStream.Write(data, 0, data.Length); + } + + compressedStream.Position = 0; + return compressedStream; + } + + [Fact] + public void SystemGZipProvider_RoundTrip_Works() + { + var provider = new SystemGZipCompressionProvider(); + var original = Encoding.UTF8.GetBytes( + "Hello, World! This is a test of System.IO.Compression.GZipStream." + ); + + using var compressedStream = new MemoryStream(); + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = provider.CreateCompressStream(nonDisposingStream, 6)) + { + compressStream.Write(original, 0, original.Length); + } + + compressedStream.Position = 0; + using var decompressStream = provider.CreateDecompressStream(compressedStream); + using var resultStream = new MemoryStream(); + decompressStream.CopyTo(resultStream); + + var result = resultStream.ToArray(); + result.Should().Equal(original); + } + + [Theory] + [InlineData(0)] // No compression + [InlineData(3)] // Fast + [InlineData(6)] // Default + [InlineData(9)] // Best compression + public void SystemGZipProvider_DifferentCompressionLevels_Work(int level) + { + var provider = new SystemGZipCompressionProvider(); + var original = Encoding.UTF8.GetBytes( + "Test data for compression level testing with System.IO.Compression." + ); + + using var compressedStream = new MemoryStream(); + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = provider.CreateCompressStream(nonDisposingStream, level)) + { + compressStream.Write(original, 0, original.Length); + } + + compressedStream.Position = 0; + using var decompressStream = provider.CreateDecompressStream(compressedStream); + using var resultStream = new MemoryStream(); + decompressStream.CopyTo(resultStream); + + var result = resultStream.ToArray(); + result.Should().Equal(original); + } + + [Fact] + public void SystemGZipProvider_Compress_InternalProvider_Decompress_CrossCompatibility() + { + // Compress with System.IO.Compression + var systemProvider = new SystemGZipCompressionProvider(); + var original = Encoding.UTF8.GetBytes( + "Cross-compatibility test between System.IO.Compression and internal GZip." + ); + + using var compressedStream = new MemoryStream(); + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = systemProvider.CreateCompressStream(nonDisposingStream, 6)) + { + compressStream.Write(original, 0, original.Length); + } + + // Decompress with internal provider + compressedStream.Position = 0; + var internalProvider = new GZipCompressionProvider(); + using var decompressStream = internalProvider.CreateDecompressStream(compressedStream); + using var resultStream = new MemoryStream(); + decompressStream.CopyTo(resultStream); + + var result = resultStream.ToArray(); + result.Should().Equal(original); + } + + [Fact] + public void InternalProvider_Compress_SystemGZipProvider_Decompress_CrossCompatibility() + { + // Compress with internal provider + var internalProvider = new GZipCompressionProvider(); + var original = Encoding.UTF8.GetBytes( + "Cross-compatibility test between internal GZip and System.IO.Compression." + ); + + using var compressedStream = new MemoryStream(); + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = internalProvider.CreateCompressStream(nonDisposingStream, 6)) + { + compressStream.Write(original, 0, original.Length); + } + + // Decompress with System.IO.Compression + compressedStream.Position = 0; + var systemProvider = new SystemGZipCompressionProvider(); + using var decompressStream = systemProvider.CreateDecompressStream(compressedStream); + using var resultStream = new MemoryStream(); + decompressStream.CopyTo(resultStream); + + var result = resultStream.ToArray(); + result.Should().Equal(original); + } + + [Fact] + public void TarWriter_WithSystemGZipProvider_CreatesReadableArchive() + { + // Create tar.gz using System.IO.Compression provider + var systemProvider = new SystemGZipCompressionProvider(); + var registry = CompressionProviderRegistry.Default.With(systemProvider); + + using var archiveStream = new MemoryStream(); + var writeOptions = new TarWriterOptions(CompressionType.GZip, true) + { + Providers = registry, + }; + + using (var writer = new TarWriter(archiveStream, writeOptions)) + { + var data = Encoding.UTF8.GetBytes("Content written with System.IO.Compression"); + writer.Write("test.txt", new MemoryStream(data), DateTime.Now); + } + + // Read back using internal provider (should be compatible) + archiveStream.Position = 0; + var readOptions = new ReaderOptions(); + using var reader = TarReader.OpenReader(archiveStream, readOptions); + reader.MoveToNextEntry().Should().BeTrue(); + using var entryStream = reader.OpenEntryStream(); + using var resultStream = new MemoryStream(); + entryStream.CopyTo(resultStream); + + var result = Encoding.UTF8.GetString(resultStream.ToArray()); + result.Should().Be("Content written with System.IO.Compression"); + } + + [Fact] + public void SystemGZipProvider_SupportsCompressionAndDecompression() + { + var provider = new SystemGZipCompressionProvider(); + + // Verify the provider reports correct capabilities + provider.CompressionType.Should().Be(CompressionType.GZip); + provider.SupportsCompression.Should().BeTrue(); + provider.SupportsDecompression.Should().BeTrue(); + } + + [Fact] + public void SystemDeflateProvider_RoundTrip_Works() + { + var provider = new SystemDeflateCompressionProvider(); + var original = Encoding.UTF8.GetBytes( + "Hello, World! This is a test of System.IO.Compression.DeflateStream." + ); + + using var compressedStream = new MemoryStream(); + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = provider.CreateCompressStream(nonDisposingStream, 6)) + { + compressStream.Write(original, 0, original.Length); + } + + compressedStream.Position = 0; + using var decompressStream = provider.CreateDecompressStream(compressedStream); + using var resultStream = new MemoryStream(); + decompressStream.CopyTo(resultStream); + + var result = resultStream.ToArray(); + result.Should().Equal(original); + } + + [Fact] + public void SystemDeflateProvider_Compress_InternalProvider_Decompress_CrossCompatibility() + { + // Compress with System.IO.Compression Deflate + var systemProvider = new SystemDeflateCompressionProvider(); + var original = Encoding.UTF8.GetBytes( + "Cross-compatibility test between System.IO.Compression and internal Deflate." + ); + + using var compressedStream = new MemoryStream(); + var nonDisposingStream = SharpCompressStream.CreateNonDisposing(compressedStream); + using (var compressStream = systemProvider.CreateCompressStream(nonDisposingStream, 6)) + { + compressStream.Write(original, 0, original.Length); + } + + // Decompress with internal provider + compressedStream.Position = 0; + var internalProvider = new DeflateCompressionProvider(); + using var decompressStream = internalProvider.CreateDecompressStream(compressedStream); + using var resultStream = new MemoryStream(); + decompressStream.CopyTo(resultStream); + + var result = resultStream.ToArray(); + result.Should().Equal(original); + } + + #endregion +} diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 6e5b4b9ec..56ae3c31d 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -5,12 +5,10 @@ using System.Threading; using System.Threading.Tasks; using AwesomeAssertions; -using SharpCompress.Archives; using SharpCompress.Common; using SharpCompress.Factories; using SharpCompress.IO; using SharpCompress.Readers; -using SharpCompress.Readers.GZip; using SharpCompress.Test.Mocks; using Xunit; @@ -18,19 +16,14 @@ namespace SharpCompress.Test; public abstract class ReaderTests : TestBase { - protected void Read(string testArchive, ReaderOptions? options = null) - { + protected void Read(string testArchive, ReaderOptions? options = null) => ReadCore(testArchive, options, ReadImpl); - } protected void Read( string testArchive, CompressionType expectedCompression, ReaderOptions? options = null - ) - { - ReadCore(testArchive, options, (path, opts) => ReadImpl(path, expectedCompression, opts)); - } + ) => ReadCore(testArchive, options, (path, opts) => ReadImpl(path, expectedCompression, opts)); private void ReadCore( string testArchive, @@ -50,19 +43,14 @@ Action readImpl VerifyFiles(); } - private void ReadImpl(string testArchive, ReaderOptions options) - { + private void ReadImpl(string testArchive, ReaderOptions options) => ReadImplCore(testArchive, options, UseReader); - } private void ReadImpl( string testArchive, CompressionType expectedCompression, ReaderOptions options - ) - { - ReadImplCore(testArchive, options, r => UseReader(r, expectedCompression)); - } + ) => ReadImplCore(testArchive, options, r => UseReader(r, expectedCompression)); private void ReadImplCore(string testArchive, ReaderOptions options, Action useReader) { @@ -105,15 +93,22 @@ private void UseReader(IReader reader) } } - protected async Task AssertArchiveAsync( + protected async Task AssertArchiveAsync( string testArchive, CancellationToken cancellationToken = default ) - where T : IFactory { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); var factory = new TarFactory(); - factory.IsArchive(new FileInfo(testArchive).OpenRead()).Should().BeTrue(); + ( + await factory.IsArchiveAsync( + new FileInfo(testArchive).OpenRead(), + null, + cancellationToken + ) + ) + .Should() + .BeTrue(); ( await factory.IsArchiveAsync( new FileInfo(testArchive).OpenRead(), diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTest.cs deleted file mode 100644 index 6b417f7c0..000000000 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTest.cs +++ /dev/null @@ -1,905 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using SharpCompress.IO; -using SharpCompress.Test.Mocks; -using Xunit; - -namespace SharpCompress.Test.Streams; - -public class SharpCompressStreamAsyncTest -{ - [Fact] - public async ValueTask TestRewindAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - stream.Rewind(true); - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(5, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(6, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(7, await ReadInt32Async(stream).ConfigureAwait(false)); - } - - [Fact] - public async ValueTask TestIncompleteRewindAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - stream.Rewind(true); - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(5, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(6, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(7, await ReadInt32Async(stream).ConfigureAwait(false)); - } - - [Fact] - public async ValueTask TestRecordingAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - stream.Rewind(false); - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - } - - [Fact] - public async ValueTask TestAsyncProducesSameResultAsSync() - { - var testData = new byte[100 * 4]; - for (int i = 0; i < 100; i++) - { - var bytes = BitConverter.GetBytes(i); - Array.Copy(bytes, 0, testData, i * 4, 4); - } - - byte[] syncResult; - byte[] asyncResult; - - var ms1 = new MemoryStream(testData); - using (var stream = new SharpCompressStream(ms1)) - { - syncResult = ReadAllSync(stream); - } - - var ms2 = new MemoryStream(testData); - using (var stream = new SharpCompressStream(ms2)) - { - asyncResult = await ReadAllAsync(stream).ConfigureAwait(false); - } - - Assert.Equal(syncResult, asyncResult); - } - - [Fact] - public async ValueTask TestAsyncWithRewind() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - for (int i = 0; i < 50; i++) - { - bw.Write(i); - } - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - - var buffer = new byte[8]; - await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - Assert.Equal(0, BitConverter.ToInt32(buffer, 0)); - Assert.Equal(1, BitConverter.ToInt32(buffer, 4)); - - stream.Rewind(); - await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - Assert.Equal(0, BitConverter.ToInt32(buffer, 0)); - Assert.Equal(1, BitConverter.ToInt32(buffer, 4)); - - await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - Assert.Equal(2, BitConverter.ToInt32(buffer, 0)); - Assert.Equal(3, BitConverter.ToInt32(buffer, 4)); - } - - [Fact] - public async ValueTask TestAsyncCancellationSupport() - { - var ms = new MemoryStream(new byte[10000]); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - - var cts = new CancellationTokenSource(); - var buffer = new byte[4096]; - - // Just verify that cancellation token can be passed without throwing - int bytesRead = await stream - .ReadAsync(buffer, 0, buffer.Length, cts.Token) - .ConfigureAwait(false); - Assert.Equal(buffer.Length, bytesRead); - } - - [Fact] - public async ValueTask TestAsyncEmptyBuffer() - { - var ms = new MemoryStream(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - - var buffer = new byte[0]; - int bytesRead = await stream.ReadAsync(buffer, 0, 0).ConfigureAwait(false); - Assert.Equal(0, bytesRead); - } - - [Fact] - public async ValueTask TestAsyncMultipleReads() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - for (int i = 0; i < 50; i++) - { - bw.Write(i); - } - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - - var totalData = new byte[50 * 4]; - var buffer = new byte[8]; - int offset = 0; - int bytesRead; - - while ( - (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0 - ) - { - Array.Copy(buffer, 0, totalData, offset, bytesRead); - offset += bytesRead; - } - - Assert.Equal(50 * 4, offset); - Assert.Equal(0, BitConverter.ToInt32(totalData, 0)); - Assert.Equal(49, BitConverter.ToInt32(totalData, 49 * 4)); - } - - [Fact] - public async ValueTask TestAsyncReturnsZeroAtEndOfStream() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - - var buffer = new byte[4096]; - - int bytesRead; - while ( - (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0 - ) { } - - Assert.Equal(0, bytesRead); - - bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - Assert.Equal(0, bytesRead); - } - - [Fact] - public async ValueTask TestAsyncPosition() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - for (int i = 0; i < 10; i++) - { - bw.Write(i); - } - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - Assert.Equal(0, stream.Position); - - var buffer = new byte[4]; - await stream.ReadAsync(buffer, 0, 4).ConfigureAwait(false); - Assert.Equal(4, stream.Position); - - stream.StartRecording(); - await stream.ReadAsync(buffer, 0, 4).ConfigureAwait(false); - Assert.Equal(8, stream.Position); - - stream.Rewind(); - Assert.Equal(4, stream.Position); - } - -#if !LEGACY_DOTNET - [Fact] - public async ValueTask TestAsyncMemoryCancellationSupport() - { - var ms = new MemoryStream(new byte[10000]); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - - var cts = new CancellationTokenSource(); - var buffer = new byte[4096]; - - // Just verify that cancellation token can be passed without throwing - int bytesRead = await stream.ReadAsync(buffer.AsMemory(), cts.Token).ConfigureAwait(false); - Assert.Equal(buffer.Length, bytesRead); - } - - [Fact] - public async ValueTask TestAsyncMemoryEmptyBuffer() - { - var ms = new MemoryStream(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - - var buffer = Memory.Empty; - int bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false); - Assert.Equal(0, bytesRead); - } - - [Fact] - public async ValueTask TestAsyncMemoryMultipleReads() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - for (int i = 0; i < 50; i++) - { - bw.Write(i); - } - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - - var totalData = new byte[50 * 4]; - var buffer = new byte[8]; - int offset = 0; - int bytesRead; - - while ((bytesRead = await stream.ReadAsync(buffer.AsMemory()).ConfigureAwait(false)) > 0) - { - Array.Copy(buffer, 0, totalData, offset, bytesRead); - offset += bytesRead; - } - - Assert.Equal(50 * 4, offset); - Assert.Equal(0, BitConverter.ToInt32(totalData, 0)); - Assert.Equal(49, BitConverter.ToInt32(totalData, 49 * 4)); - } -#endif - - private static async Task ReadInt32Async(Stream stream) - { - var buffer = new byte[4]; - var bytesRead = await stream.ReadAsync(buffer, 0, 4).ConfigureAwait(false); - if (bytesRead != 4) - { - throw new EndOfStreamException(); - } - return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24); - } - -#if !LEGACY_DOTNET - private static async ValueTask ReadInt32AsyncMemory(Stream stream) - { - var buffer = new byte[4]; - var bytesRead = await stream.ReadAsync(buffer.AsMemory()).ConfigureAwait(false); - if (bytesRead != 4) - { - throw new EndOfStreamException(); - } - return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24); - } -#endif - - private static byte[] ReadAllSync(SharpCompressStream stream) - { - var result = new List(); - var buffer = new byte[4096]; - int bytesRead; - - while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) - { - for (int i = 0; i < bytesRead; i++) - { - result.Add(buffer[i]); - } - } - - return result.ToArray(); - } - - private static async Task ReadAllAsync(SharpCompressStream stream) - { - var result = new List(); - var buffer = new byte[4096]; - int bytesRead; - - while ( - (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0 - ) - { - for (int i = 0; i < bytesRead; i++) - { - result.Add(buffer[i]); - } - } - - return result.ToArray(); - } - -#if !LEGACY_DOTNET - private static async ValueTask ReadAllAsyncMemory(SharpCompressStream stream) - { - var result = new List(); - var buffer = new byte[4096]; - int bytesRead; - - while ((bytesRead = await stream.ReadAsync(buffer.AsMemory()).ConfigureAwait(false)) > 0) - { - for (int i = 0; i < bytesRead; i++) - { - result.Add(buffer[i]); - } - } - - return result.ToArray(); - } -#endif - - [Fact] - public async ValueTask TestStopRecordingAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - - stream.StopRecording(); - - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(5, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(6, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(7, await ReadInt32Async(stream).ConfigureAwait(false)); - - Assert.False(stream.IsRecording); - } - - [Fact] - public async ValueTask TestStopRecordingNoFurtherBufferingAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - - var buffer = new byte[8]; - await stream.ReadAsync(buffer, 0, 8).ConfigureAwait(false); - - stream.StopRecording(); - - await stream.ReadAsync(buffer, 0, 8).ConfigureAwait(false); - Assert.Equal(BitConverter.GetBytes(1), buffer.Take(4).ToArray()); - Assert.Equal(BitConverter.GetBytes(2), buffer.Skip(4).Take(4).ToArray()); - - int bytesRead = await stream.ReadAsync(buffer, 0, 8).ConfigureAwait(false); - Assert.Equal(8, bytesRead); - - Assert.False(stream.IsRecording); - } - - [Fact] - public async ValueTask TestStopRecordingThenRewindAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Write(8); - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - - // Read first 4 values (gets buffered) - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - - // Stop recording - stream.StopRecording(); - Assert.False(stream.IsRecording); - - // Rewind to start of buffer - stream.Rewind(true); - - // Should be able to read from buffer again - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - - // Continue reading remaining data from underlying stream - Assert.Equal(5, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(6, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(7, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(8, await ReadInt32Async(stream).ConfigureAwait(false)); - } - - [Fact] - public async ValueTask TestMultipleRewindsAfterStopRecordingAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Write(8); - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - - // Read first 4 values (gets buffered) - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - - // Stop recording - stream.StopRecording(); - Assert.False(stream.IsRecording); - - // First rewind - read all buffered data, then continue with underlying stream - stream.Rewind(); - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(5, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(6, await ReadInt32Async(stream).ConfigureAwait(false)); - - // Second rewind - should still be able to read from buffer - stream.Rewind(); - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - - // Third rewind - still works - stream.Rewind(); - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(5, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(6, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(7, await ReadInt32Async(stream).ConfigureAwait(false)); - Assert.Equal(8, await ReadInt32Async(stream).ConfigureAwait(false)); - } - - [Fact] - public async ValueTask TestStopRecordingTwiceThrowsAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - - Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); - - // First StopRecording should succeed - stream.StopRecording(); - Assert.False(stream.IsRecording); - - // Second StopRecording should throw - Assert.Throws(() => stream.StopRecording()); - } - - [Fact] - public async ValueTask TestReadMoreThanBufferSizeAfterRewindAsync() - { - // This test verifies the fix for the bug where reading more bytes than - // are in the buffer after a rewind would only return the buffered bytes - // instead of continuing to read from the underlying stream. - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 29 bytes (simulating Arc header) - for (int i = 0; i < 29; i++) - { - bw.Write((byte)i); - } - - // Write 5252 bytes (simulating Arc compressed data) - for (int i = 0; i < 5252; i++) - { - bw.Write((byte)(i % 256)); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Simulate factory detection: record first 512 bytes - stream.StartRecording(); - var probeBuffer = new byte[512]; - int probeRead = await stream.ReadAsync(probeBuffer, 0, 512).ConfigureAwait(false); - Assert.Equal(512, probeRead); - - // Stop recording and rewind (simulates what ReaderFactory does) - stream.Rewind(true); - - // Read header (29 bytes) - should come from buffer - var headerBuffer = new byte[29]; - int headerRead = await stream.ReadAsync(headerBuffer, 0, 29).ConfigureAwait(false); - Assert.Equal(29, headerRead); - - // Read compressed data (5252 bytes) - buffer has 483 bytes left, - // but we need 5252 bytes. This should read all 5252 bytes, not just 483. - var dataBuffer = new byte[5252]; - int dataRead = await stream.ReadAsync(dataBuffer, 0, 5252).ConfigureAwait(false); - Assert.Equal(5252, dataRead); - - // Verify we read the correct data - for (int i = 0; i < 5252; i++) - { - Assert.Equal((byte)(i % 256), dataBuffer[i]); - } - - // Verify stream position is correct (29 + 5252 = 5281) - Assert.Equal(5281, stream.Position); - } - - [Fact] - public async ValueTask TestReadExactlyBufferSizeAfterRewindAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 1024 bytes - for (int i = 0; i < 1024; i++) - { - bw.Write((byte)(i % 256)); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Record first 512 bytes - stream.StartRecording(); - var probeBuffer = new byte[512]; - await stream.ReadAsync(probeBuffer, 0, 512).ConfigureAwait(false); - stream.Rewind(true); - - // Read exactly the buffer size (512 bytes) - var buffer = new byte[512]; - int bytesRead = await stream.ReadAsync(buffer, 0, 512).ConfigureAwait(false); - Assert.Equal(512, bytesRead); - - // Verify we read the correct data - for (int i = 0; i < 512; i++) - { - Assert.Equal((byte)(i % 256), buffer[i]); - } - } - - [Fact] - public async ValueTask TestReadLessThanBufferSizeAfterRewindAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 1024 bytes - for (int i = 0; i < 1024; i++) - { - bw.Write((byte)(i % 256)); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Record first 512 bytes - stream.StartRecording(); - var probeBuffer = new byte[512]; - await stream.ReadAsync(probeBuffer, 0, 512).ConfigureAwait(false); - stream.Rewind(true); - - // Read less than buffer size (256 bytes) - var buffer = new byte[256]; - int bytesRead = await stream.ReadAsync(buffer, 0, 256).ConfigureAwait(false); - Assert.Equal(256, bytesRead); - - // Verify we read the correct data - for (int i = 0; i < 256; i++) - { - Assert.Equal((byte)(i % 256), buffer[i]); - } - } - - [Fact] - public async ValueTask TestMultipleReadsExceedingBufferAfterRewindAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 2048 bytes - for (int i = 0; i < 2048; i++) - { - bw.Write((byte)(i % 256)); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Record first 512 bytes - stream.StartRecording(); - var probeBuffer = new byte[512]; - await stream.ReadAsync(probeBuffer, 0, 512).ConfigureAwait(false); - stream.Rewind(true); - - // Read in chunks that will exceed the buffer - var buffer = new byte[800]; - - // First read: 800 bytes (512 from buffer + 288 from underlying stream) - int bytesRead1 = await stream.ReadAsync(buffer, 0, 800).ConfigureAwait(false); - Assert.Equal(800, bytesRead1); - - // Second read: 800 bytes (all from underlying stream) - int bytesRead2 = await stream.ReadAsync(buffer, 0, 800).ConfigureAwait(false); - Assert.Equal(800, bytesRead2); - - // Third read: remaining 448 bytes - int bytesRead3 = await stream.ReadAsync(buffer, 0, 800).ConfigureAwait(false); - Assert.Equal(448, bytesRead3); - - // Verify stream position - Assert.Equal(2048, stream.Position); - } - - [Fact] - public async ValueTask TestReadPartiallyFromBufferThenUnderlyingStreamAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 1000 bytes with specific pattern - for (int i = 0; i < 1000; i++) - { - bw.Write((byte)i); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Record first 100 bytes - stream.StartRecording(); - var probeBuffer = new byte[100]; - await stream.ReadAsync(probeBuffer, 0, 100).ConfigureAwait(false); - stream.Rewind(true); - - // Read 50 bytes (from buffer) - var buffer1 = new byte[50]; - int read1 = await stream.ReadAsync(buffer1, 0, 50).ConfigureAwait(false); - Assert.Equal(50, read1); - for (int i = 0; i < 50; i++) - { - Assert.Equal((byte)i, buffer1[i]); - } - - // Read 150 bytes (50 from buffer + 100 from underlying stream) - var buffer2 = new byte[150]; - int read2 = await stream.ReadAsync(buffer2, 0, 150).ConfigureAwait(false); - Assert.Equal(150, read2); - for (int i = 0; i < 150; i++) - { - Assert.Equal((byte)(i + 50), buffer2[i]); - } - - // Verify position - Assert.Equal(200, stream.Position); - } - -#if !LEGACY_DOTNET - [Fact] - public async ValueTask TestReadMoreThanBufferSizeAfterRewindMemoryAsync() - { - // Same as TestReadMoreThanBufferSizeAfterRewindAsync but using Memory - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 29 bytes (simulating Arc header) - for (int i = 0; i < 29; i++) - { - bw.Write((byte)i); - } - - // Write 5252 bytes (simulating Arc compressed data) - for (int i = 0; i < 5252; i++) - { - bw.Write((byte)(i % 256)); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Simulate factory detection: record first 512 bytes - stream.StartRecording(); - var probeBuffer = new byte[512]; - int probeRead = await stream.ReadAsync(probeBuffer.AsMemory()).ConfigureAwait(false); - Assert.Equal(512, probeRead); - - // Stop recording and rewind (simulates what ReaderFactory does) - stream.Rewind(true); - - // Read header (29 bytes) - should come from buffer - var headerBuffer = new byte[29]; - int headerRead = await stream.ReadAsync(headerBuffer.AsMemory()).ConfigureAwait(false); - Assert.Equal(29, headerRead); - - // Read compressed data (5252 bytes) - buffer has 483 bytes left, - // but we need 5252 bytes. This should read all 5252 bytes, not just 483. - var dataBuffer = new byte[5252]; - int dataRead = await stream.ReadAsync(dataBuffer.AsMemory()).ConfigureAwait(false); - Assert.Equal(5252, dataRead); - - // Verify we read the correct data - for (int i = 0; i < 5252; i++) - { - Assert.Equal((byte)(i % 256), dataBuffer[i]); - } - - // Verify stream position is correct (29 + 5252 = 5281) - Assert.Equal(5281, stream.Position); - } - - [Fact] - public async ValueTask TestMultipleReadsExceedingBufferAfterRewindMemoryAsync() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 2048 bytes - for (int i = 0; i < 2048; i++) - { - bw.Write((byte)(i % 256)); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Record first 512 bytes - stream.StartRecording(); - var probeBuffer = new byte[512]; - await stream.ReadAsync(probeBuffer.AsMemory()).ConfigureAwait(false); - stream.Rewind(true); - - // Read in chunks that will exceed the buffer - var buffer = new byte[800]; - - // First read: 800 bytes (512 from buffer + 288 from underlying stream) - int bytesRead1 = await stream.ReadAsync(buffer.AsMemory()).ConfigureAwait(false); - Assert.Equal(800, bytesRead1); - - // Second read: 800 bytes (all from underlying stream) - int bytesRead2 = await stream.ReadAsync(buffer.AsMemory()).ConfigureAwait(false); - Assert.Equal(800, bytesRead2); - - // Third read: remaining 448 bytes - int bytesRead3 = await stream.ReadAsync(buffer.AsMemory()).ConfigureAwait(false); - Assert.Equal(448, bytesRead3); - - // Verify stream position - Assert.Equal(2048, stream.Position); - } -#endif -} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeAsyncTest.cs new file mode 100644 index 000000000..1c3b7d183 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeAsyncTest.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SharpCompressStreamEdgeAsyncTest +{ +#if !LEGACY_DOTNET + + [Fact] + public async ValueTask DisposeAsync_WithLeaveStreamOpenTrue_DoesNotDisposeUnderlying() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + await stream.DisposeAsync().ConfigureAwait(false); + Assert.Equal(0, ms.Position); + Assert.True(ms.CanRead); + } + + [Fact] + public async ValueTask DisposeAsync_WithLeaveStreamOpenFalse_DisposesUnderlying() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + await stream.DisposeAsync().ConfigureAwait(false); + Assert.Throws(() => ms.Read(new byte[1], 0, 1)); + } +#endif + + [Fact] + public async ValueTask ReadAsync_ZeroCount_ReturnsZero() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var buffer = new byte[10]; + int bytesRead = await stream.ReadAsync(buffer, 0, 0).ConfigureAwait(false); + Assert.Equal(0, bytesRead); + } + + [Fact] + public async ValueTask ReadAsync_AtEndOfStream_ReturnsZero() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var buffer = new byte[10]; + await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + Assert.Equal(0, bytesRead); + } + +#if !LEGACY_DOTNET + [Fact] + public async ValueTask ReadAsyncMemory_ZeroCount_ReturnsZero() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var buffer = new byte[10]; + int bytesRead = await stream.ReadAsync(buffer.AsMemory(0, 0)).ConfigureAwait(false); + Assert.Equal(0, bytesRead); + } + + [Fact] + public async ValueTask ReadAsyncMemory_AtEndOfStream_ReturnsZero() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var buffer = new byte[10]; + await stream.ReadAsync(buffer.AsMemory()).ConfigureAwait(false); + int bytesRead = await stream.ReadAsync(buffer.AsMemory()).ConfigureAwait(false); + Assert.Equal(0, bytesRead); + } +#endif + + [Fact] + public async ValueTask CopyToAsync_DelegatesToUnderlyingStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var destination = new MemoryStream(); + await stream.CopyToAsync(destination).ConfigureAwait(false); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, destination.ToArray()); + } + + [Fact] + public async ValueTask CopyToAsync_WithBufferSize_WorksCorrectly() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var destination = new MemoryStream(); + await stream.CopyToAsync(destination, 2).ConfigureAwait(false); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, destination.ToArray()); + } + +#if !LEGACY_DOTNET + [Fact] + public async ValueTask WriteAsyncMemory_DelegatesToUnderlying() + { + var ms = new MemoryStream(); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var data = new byte[] { 1, 2, 3, 4, 5 }; + await stream.WriteAsync(data.AsMemory()).ConfigureAwait(false); + Assert.Equal(data, ms.ToArray()); + } + + [Fact] + public async ValueTask FlushAsyncMemory_DelegatesToUnderlying() + { + var ms = new MemoryStream(); + var stream = SharpCompressStream.CreateNonDisposing(ms); + await stream.WriteAsync(new byte[] { 1, 2, 3 }).ConfigureAwait(false); + await stream.FlushAsync().ConfigureAwait(false); + Assert.Equal(3, ms.Length); + } +#endif +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeTest.cs new file mode 100644 index 000000000..c4409af8b --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeTest.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SharpCompressStreamEdgeTest +{ + [Fact] + public void Dispose_WithLeaveStreamOpenTrue_DoesNotDisposeUnderlying() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + stream.Dispose(); + Assert.Equal(0, ms.Position); + Assert.True(ms.CanRead); + } + + [Fact] + public void Dispose_WithLeaveStreamOpenFalse_DisposesUnderlying() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.Dispose(); + Assert.Throws(() => ms.Read(new byte[1], 0, 1)); + } + + [Fact] + public void Dispose_WithThrowOnDisposeTrue_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + stream.ThrowOnDispose = true; + Assert.Throws(() => stream.Dispose()); + } + + [Fact] + public void Read_ZeroCount_ReturnsZero() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var buffer = new byte[10]; + int bytesRead = stream.Read(buffer, 0, 0); + Assert.Equal(0, bytesRead); + } + + [Fact] + public void Read_AtEndOfStream_ReturnsZero() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var buffer = new byte[10]; + int bytesRead = stream.Read(buffer, 0, buffer.Length); + Assert.Equal(5, bytesRead); + bytesRead = stream.Read(buffer, 0, buffer.Length); + Assert.Equal(0, bytesRead); + } + + [Fact] + public void Position_InitialValue_IsZero() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + Assert.Equal(0, stream.Position); + } + + [Fact] + public void CanRead_AlwaysReturnsTrue() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.True(stream.CanRead); + } + + [Fact] + public void CanSeek_PassthroughMode_DelegatesToUnderlying() + { + var seekableMs = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(new MemoryStream(new byte[] { 1, 2, 3, 4, 5 })); + + var seekableStream = SharpCompressStream.CreateNonDisposing(seekableMs); + var nonSeekableStream = SharpCompressStream.CreateNonDisposing(nonSeekableMs); + + Assert.True(seekableStream.CanSeek); + Assert.False(nonSeekableStream.CanSeek); + } + + [Fact] + public void BaseStream_ReturnsUnderlyingStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Same(ms, stream.BaseStream()); + } +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorAsyncTest.cs new file mode 100644 index 000000000..ebd225c05 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorAsyncTest.cs @@ -0,0 +1,146 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.IO; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SharpCompressStreamErrorAsyncTest +{ + private class NonSeekableStreamWrapper : Stream + { + private readonly Stream _baseStream; + + public NonSeekableStreamWrapper(Stream baseStream) => _baseStream = baseStream; + + public override bool CanRead => _baseStream.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => _baseStream.CanWrite; + public override long Length => _baseStream.Length; + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() => _baseStream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + _baseStream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + + public override void SetLength(long value) => _baseStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => + _baseStream.Write(buffer, offset, count); + + protected override void Dispose(bool disposing) + { + if (disposing) + _baseStream.Dispose(); + base.Dispose(disposing); + } + } + +#if !LEGACY_DOTNET + [Fact] + public async ValueTask DisposeAsync_WithThrowOnDisposeTrue_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + stream.ThrowOnDispose = true; + await Assert + .ThrowsAsync(async () => + await stream.DisposeAsync().ConfigureAwait(false) + ) + .ConfigureAwait(false); + } +#endif + + [Fact] + public async ValueTask CreateNonDisposing_ReadAsync_ZeroCount_ReturnsZero() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var buffer = new byte[10]; + int bytesRead = await stream.ReadAsync(buffer, 0, 0).ConfigureAwait(false); + Assert.Equal(0, bytesRead); + } + + [Fact] + public async ValueTask CreateNonDisposing_ReadAsync_AtEndOfStream_ReturnsZero() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var buffer = new byte[10]; + await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + Assert.Equal(0, bytesRead); + } + + [Fact] + public async ValueTask Create_AsyncReadWithRecording_WorksCorrectly() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + var buffer = new byte[4]; + await stream.ReadAsync(buffer, 0, 4).ConfigureAwait(false); + Assert.Equal(4, stream.Position); + } + + [Fact] + public async ValueTask Create_AsyncReadWithBufferOverflow_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[256]); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 64); + stream.StartRecording(); + var buffer = new byte[32]; + for (int i = 0; i < 3; i++) + { + await stream.ReadExactAsync(buffer, 0, 32).ConfigureAwait(false); + } + Assert.Throws(() => stream.Rewind()); + } + + [Fact] + public async ValueTask FlushAsync_NonPassthrough_ThrowsNotSupported() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + await Assert + .ThrowsAsync(async () => + await stream.FlushAsync().ConfigureAwait(false) + ) + .ConfigureAwait(false); + } + + [Fact] + public async ValueTask WriteAsync_NonPassthrough_ThrowsNotSupported() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + await Assert + .ThrowsAsync(async () => + await stream.WriteAsync(new byte[] { 1 }, 0, 1).ConfigureAwait(false) + ) + .ConfigureAwait(false); + } + + [Fact] + public async ValueTask CopyToAsync_WithPassthrough_CopiesAllData() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var destination = new MemoryStream(); + await stream.CopyToAsync(destination).ConfigureAwait(false); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, destination.ToArray()); + } +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorTest.cs new file mode 100644 index 000000000..6ac0ee382 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorTest.cs @@ -0,0 +1,187 @@ +using System; +using System.IO; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SharpCompressStreamErrorTest +{ + private class NonSeekableStreamWrapper : Stream + { + private readonly Stream _baseStream; + + public NonSeekableStreamWrapper(Stream baseStream) + { + _baseStream = baseStream; + } + + public override bool CanRead => _baseStream.CanRead; + + public override bool CanSeek => false; + + public override bool CanWrite => _baseStream.CanWrite; + + public override long Length => _baseStream.Length; + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() => _baseStream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + _baseStream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + + public override void SetLength(long value) => _baseStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => + _baseStream.Write(buffer, offset, count); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _baseStream.Dispose(); + } + base.Dispose(disposing); + } + } + + [Fact] + public void Rewind_WithoutStartRecording_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + Assert.Throws(() => stream.Rewind()); + } + + [Fact] + public void Rewind_PassthroughMode_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Throws(() => stream.Rewind()); + } + + [Fact] + public void StartRecording_Twice_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + Assert.Throws(() => stream.StartRecording()); + } + + [Fact] + public void StartRecording_PassthroughMode_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Throws(() => stream.StartRecording()); + } + + [Fact] + public void StopRecording_WithoutRecording_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + Assert.Throws(() => stream.StopRecording()); + } + + [Fact] + public void StopRecording_PassthroughMode_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Throws(() => stream.StopRecording()); + } + + [Fact] + public void StopRecording_Twice_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + stream.Read(new byte[4], 0, 4); + stream.StopRecording(); + Assert.Throws(() => stream.StopRecording()); + } + + [Fact] + public void Seek_BeyondRecordedRange_ThrowsNotSupported() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + stream.Read(new byte[4], 0, 4); + Assert.Throws(() => stream.Position = 100); + } + + [Fact] + public void Seek_FromEnd_ThrowsNotSupported() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + Assert.Throws(() => stream.Seek(-1, SeekOrigin.End)); + } + + [Fact] + public void Position_SetNegative_Throws() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + stream.Read(new byte[4], 0, 4); + Assert.Throws(() => stream.Position = -1); + } + + [Fact] + public void Flush_NonPassthrough_ThrowsNotSupported() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + Assert.Throws(() => stream.Flush()); + } + + [Fact] + public void Write_NonPassthrough_ThrowsNotSupported() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + Assert.Throws(() => stream.Write(new byte[] { 1 }, 0, 1)); + } + + [Fact] + public void SetLength_NonPassthrough_ThrowsNotSupported() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + Assert.Throws(() => stream.SetLength(100)); + } + + [Fact] + public void Length_NonPassthroughWithoutBuffer_ThrowsNotSupported() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = new SharpCompressStream(nonSeekableMs); + Assert.Throws(() => stream.Length); + } +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamFactoryAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamFactoryAsyncTest.cs new file mode 100644 index 000000000..f5ba19097 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamFactoryAsyncTest.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SharpCompressStreamFactoryAsyncTest +{ + [Fact] + public async ValueTask Create_AsyncReadWithSeekableStream_WorksCorrectly() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.Create(ms); + var buffer = new byte[5]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + Assert.Equal(5, bytesRead); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, buffer); + } + + [Fact] + public async ValueTask Create_AsyncReadWithNonSeekableStream_BufferedCorrectly() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + var buffer = new byte[5]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + Assert.Equal(5, bytesRead); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, buffer); + } + + [Fact] + public async ValueTask Create_WithBufferAsync_WorksCorrectly() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + var buffer = new byte[4]; + await stream.ReadAsync(buffer, 0, 4).ConfigureAwait(false); + Assert.Equal(4, stream.Position); + } + + [Fact] + public async ValueTask Create_AsyncReadSeekable_PositionUpdatesCorrectly() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + var stream = SharpCompressStream.Create(ms); + var buffer = new byte[4]; + await stream.ReadAsync(buffer, 0, 4).ConfigureAwait(false); + Assert.Equal(4, stream.Position); + } +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamFactoryTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamFactoryTest.cs new file mode 100644 index 000000000..4a5d74a9d --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamFactoryTest.cs @@ -0,0 +1,148 @@ +using System; +using System.IO; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SharpCompressStreamFactoryTest +{ + private class IStreamStackMock : Stream, IStreamStack + { + private readonly Stream _baseStream; + + public IStreamStackMock(Stream baseStream) + { + _baseStream = baseStream; + } + + public Stream BaseStream() => _baseStream; + + public override bool CanRead => _baseStream.CanRead; + + public override bool CanSeek => _baseStream.CanSeek; + + public override bool CanWrite => _baseStream.CanWrite; + + public override long Length => _baseStream.Length; + + public override long Position + { + get => _baseStream.Position; + set => _baseStream.Position = value; + } + + public override void Flush() => _baseStream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + _baseStream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => + _baseStream.Seek(offset, origin); + + public override void SetLength(long value) => _baseStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => + _baseStream.Write(buffer, offset, count); + } + + [Fact] + public void Create_WithSeekableStream_ReturnsSeekableSharpCompressStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.Create(ms); + Assert.IsType(stream); + } + + [Fact] + public void Create_WithNonSeekableStream_ReturnsSharpCompressStreamWithBuffer() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs); + Assert.IsType(stream); + Assert.NotNull(stream); + } + + [Fact] + public void Create_WithSharpCompressStreamPassthrough_UnwrapsAndCreatesNew() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var passthroughStream = SharpCompressStream.CreateNonDisposing(ms); + var stream = SharpCompressStream.Create(passthroughStream); + Assert.NotSame(passthroughStream, stream); + Assert.IsType(stream); + } + + [Fact] + public void Create_WithSharpCompressStreamNonPassthrough_ReturnsSameInstance() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var sharpStream = SharpCompressStream.Create(nonSeekableMs, 128); + var stream = SharpCompressStream.Create(sharpStream); + Assert.Same(sharpStream, stream); + } + + [Fact] + public void Create_WithIStreamStack_UnwrapsSharpCompressStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var sharpStream = SharpCompressStream.CreateNonDisposing(ms); + var wrappedStream = new IStreamStackMock(sharpStream); + var stream = SharpCompressStream.Create(wrappedStream); + Assert.Same(sharpStream, stream); + } + + [Fact] + public void Create_WithBufferSize_UsesCustomBufferSize() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + Assert.NotNull(stream); + stream.StartRecording(); + var buffer = new byte[4]; + stream.Read(buffer, 0, 4); + Assert.Equal(4, stream.Position); + } + + [Fact] + public void Create_WithLeaveStreamOpenTrue_PreservesSetting() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var passthroughStream = SharpCompressStream.CreateNonDisposing(ms); + var stream = SharpCompressStream.Create(passthroughStream); + Assert.True(stream.LeaveStreamOpen); + } + + [Fact] + public void Create_WithSeekablePassthroughStream_CreatesSeekableWrapper() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var passthroughStream = SharpCompressStream.CreateNonDisposing(ms); + var stream = SharpCompressStream.Create(passthroughStream); + Assert.IsType(stream); + } + + [Fact] + public void Create_WithIStreamStack_ReturnsUnderlyingSharpCompressStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var sharpStream = SharpCompressStream.Create(ms); + var wrappedStream = new IStreamStackMock(sharpStream); + var result = SharpCompressStream.Create(wrappedStream); + Assert.Same(sharpStream, result); + } + + [Fact] + public void Create_WithNonSeekablePassthroughStream_CreatesBufferedWrapper() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var passthroughStream = SharpCompressStream.CreateNonDisposing(nonSeekableMs); + var stream = SharpCompressStream.Create(passthroughStream); + Assert.IsType(stream); + } +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughAsyncTest.cs new file mode 100644 index 000000000..a3972f33c --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughAsyncTest.cs @@ -0,0 +1,110 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SharpCompressStreamPassthroughAsyncTest +{ + [Fact] + public async ValueTask CreateNonDisposing_ReadAsync_DelegatesDirectly() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var buffer = new byte[5]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + Assert.Equal(5, bytesRead); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, buffer); + } + + [Fact] + public async ValueTask CreateNonDisposing_WriteAsync_DelegatesDirectly() + { + var ms = new MemoryStream(); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var data = new byte[] { 1, 2, 3, 4, 5 }; + await stream.WriteAsync(data, 0, data.Length).ConfigureAwait(false); + Assert.Equal(data, ms.ToArray()); + } + + [Fact] + public async ValueTask CreateNonDisposing_FlushAsync_DelegatesDirectly() + { + var ms = new MemoryStream(); + var stream = SharpCompressStream.CreateNonDisposing(ms); + await stream.WriteAsync(new byte[] { 1, 2, 3 }, 0, 3).ConfigureAwait(false); + await stream.FlushAsync().ConfigureAwait(false); + Assert.Equal(3, ms.Length); + } + + [Fact] + public async ValueTask CreateNonDisposing_CanReadAsync_ReturnsTrue() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.True(stream.CanRead); + } + + [Fact] + public async ValueTask CreateNonDisposing_ReadAsync_WithCancellationToken() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var buffer = new byte[5]; + var cts = new System.Threading.CancellationTokenSource(); + int bytesRead = await stream + .ReadAsync(buffer, 0, buffer.Length, cts.Token) + .ConfigureAwait(false); + Assert.Equal(5, bytesRead); + } + + [Fact] + public async ValueTask CreateNonDisposing_WriteAsync_WithCancellationToken() + { + var ms = new MemoryStream(); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var data = new byte[] { 1, 2, 3, 4, 5 }; + var cts = new System.Threading.CancellationTokenSource(); + await stream.WriteAsync(data, 0, data.Length, cts.Token).ConfigureAwait(false); + Assert.Equal(data, ms.ToArray()); + } + + [Fact] + public async ValueTask CreateNonDisposing_FlushAsync_WithCancellationToken() + { + var ms = new MemoryStream(); + var stream = SharpCompressStream.CreateNonDisposing(ms); + await stream.WriteAsync(new byte[] { 1, 2, 3 }, 0, 3).ConfigureAwait(false); + var cts = new System.Threading.CancellationTokenSource(); + await stream.FlushAsync(cts.Token).ConfigureAwait(false); + Assert.Equal(3, ms.Length); + } + +#if !LEGACY_DOTNET + [Fact] + public async ValueTask CreateNonDisposing_DoesNotDisposeUnderlying_Async() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + await stream.DisposeAsync().ConfigureAwait(false); + Assert.Equal(0, ms.Position); + Assert.True(ms.CanRead); + } + + [Fact] + public async ValueTask CreateNonDisposing_DisposeAsync_WithThrowOnDisposeTrue_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + stream.ThrowOnDispose = true; + await Assert + .ThrowsAsync(async () => + await stream.DisposeAsync().ConfigureAwait(false) + ) + .ConfigureAwait(false); + } +#endif +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughTest.cs new file mode 100644 index 000000000..154043e02 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughTest.cs @@ -0,0 +1,185 @@ +using System; +using System.IO; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SharpCompressStreamPassthroughTest +{ + [Fact] + public void CreateNonDisposing_LeaveStreamOpen_ReturnsTrue() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.True(stream.LeaveStreamOpen); + } + + [Fact] + public void CreateNonDisposing_IsPassthrough_ReturnsTrue() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.True(stream.IsPassthrough); + } + + [Fact] + public void CreateNonDisposing_CanRead_ReturnsTrue() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.True(stream.CanRead); + } + + [Fact] + public void CreateNonDisposing_CanSeek_DelegatesToUnderlyingSeekableStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Equal(ms.CanSeek, stream.CanSeek); + Assert.True(stream.CanSeek); + } + + [Fact] + public void CreateNonDisposing_CanSeek_DelegatesToUnderlyingNonSeekableStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.CreateNonDisposing(nonSeekableMs); + Assert.Equal(nonSeekableMs.CanSeek, stream.CanSeek); + Assert.False(stream.CanSeek); + } + + [Fact] + public void CreateNonDisposing_CanWrite_DelegatesToUnderlyingStream() + { + var ms = new MemoryStream(); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Equal(ms.CanWrite, stream.CanWrite); + Assert.True(stream.CanWrite); + } + + [Fact] + public void CreateNonDisposing_Read_DelegatesDirectlyWithoutBuffering() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var buffer = new byte[5]; + int bytesRead = stream.Read(buffer, 0, buffer.Length); + Assert.Equal(5, bytesRead); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, buffer); + } + + [Fact] + public void CreateNonDisposing_PositionGet_DelegatesToUnderlyingStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + ms.Position = 2; + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Equal(ms.Position, stream.Position); + Assert.Equal(2, stream.Position); + } + + [Fact] + public void CreateNonDisposing_PositionSet_DelegatesToUnderlyingStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + stream.Position = 3; + Assert.Equal(3, ms.Position); + Assert.Equal(3, stream.Position); + } + + [Fact] + public void CreateNonDisposing_DoesNotDisposeUnderlyingStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + stream.Dispose(); + Assert.Equal(0, ms.Position); + Assert.True(ms.CanRead); + } + + [Fact] + public void CreateNonDisposing_Length_DelegatesToUnderlyingStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Equal(ms.Length, stream.Length); + Assert.Equal(5, stream.Length); + } + + [Fact] + public void CreateNonDisposing_Seek_DelegatesToUnderlyingStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + long result = stream.Seek(3, SeekOrigin.Begin); + Assert.Equal(3, result); + Assert.Equal(3, ms.Position); + Assert.Equal(3, stream.Position); + } + + [Fact] + public void CreateNonDisposing_Flush_DelegatesToUnderlyingStream() + { + var ms = new MemoryStream(); + var stream = SharpCompressStream.CreateNonDisposing(ms); + stream.Write(new byte[] { 1, 2, 3 }, 0, 3); + stream.Flush(); + Assert.Equal(3, ms.Length); + } + + [Fact] + public void CreateNonDisposing_SetLength_DelegatesToUnderlyingStream() + { + var ms = new MemoryStream(); + var stream = SharpCompressStream.CreateNonDisposing(ms); + stream.SetLength(20); + Assert.Equal(20, stream.Length); + Assert.Equal(20, ms.Length); + } + + [Fact] + public void CreateNonDisposing_Write_DelegatesToUnderlyingStream() + { + var ms = new MemoryStream(); + var stream = SharpCompressStream.CreateNonDisposing(ms); + var data = new byte[] { 1, 2, 3, 4, 5 }; + stream.Write(data, 0, data.Length); + Assert.Equal(data, ms.ToArray()); + } + + [Fact] + public void CreateNonDisposing_StartRecording_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Throws(() => stream.StartRecording()); + } + + [Fact] + public void CreateNonDisposing_Rewind_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Throws(() => stream.Rewind()); + } + + [Fact] + public void CreateNonDisposing_StopRecording_ThrowsInvalidOperation() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Throws(() => stream.StopRecording()); + } + + [Fact] + public void CreateNonDisposing_IsRecording_AlwaysFalse() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.False(stream.IsRecording); + } +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamPropertyTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamPropertyTest.cs new file mode 100644 index 000000000..2262960dc --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamPropertyTest.cs @@ -0,0 +1,177 @@ +using System; +using System.IO; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SharpCompressStreamPropertyTest +{ + [Fact] + public void BaseStream_ReturnsUnderlyingStream() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.Same(ms, stream.BaseStream()); + } + + [Fact] + public void IsPassthrough_CreateNonDisposing_ReturnsTrue() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.True(stream.IsPassthrough); + } + + [Fact] + public void IsPassthrough_CreateWithBuffer_ReturnsFalse() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + Assert.False(stream.IsPassthrough); + } + + [Fact] + public void IsPassthrough_CreateSeekable_ReturnsFalse() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.Create(ms); + Assert.False(stream.IsPassthrough); + } + + [Fact] + public void IsRecording_AfterStartRecording_ReturnsTrue() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + Assert.True(stream.IsRecording); + } + + [Fact] + public void IsRecording_AfterStopRecording_ReturnsFalse() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + stream.Read(new byte[4], 0, 4); + stream.StopRecording(); + Assert.False(stream.IsRecording); + } + + [Fact] + public void IsRecording_AfterRewindWithStopRecording_ReturnsFalse() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + stream.Read(new byte[4], 0, 4); + stream.Rewind(true); + Assert.False(stream.IsRecording); + } + + [Fact] + public void LeaveStreamOpen_CreateNonDisposing_ReturnsTrue() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.True(stream.LeaveStreamOpen); + } + + [Fact] + public void LeaveStreamOpen_CreateWithBuffer_ReturnsFalse() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + Assert.False(stream.LeaveStreamOpen); + } + + [Fact] + public void LeaveStreamOpen_CreateSeekable_ReturnsFalse() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.Create(ms); + Assert.False(stream.LeaveStreamOpen); + } + + [Fact] + public void CanRead_AlwaysTrue() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.True(stream.CanRead); + } + + [Fact] + public void CanSeek_PassthroughWithSeekable_DelegatesTrue() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.True(stream.CanSeek); + } + + [Fact] + public void CanSeek_PassthroughWithNonSeekable_DelegatesFalse() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var nonSeekableMs = new ForwardOnlyStream(ms); + var stream = SharpCompressStream.CreateNonDisposing(nonSeekableMs); + Assert.False(stream.CanSeek); + } + + [Fact] + public void CanWrite_PassthroughWithWritable_DelegatesTrue() + { + var ms = new MemoryStream(); + var stream = SharpCompressStream.CreateNonDisposing(ms); + Assert.True(stream.CanWrite); + } + + [Fact] + public void CanWrite_PassthroughWithReadOnly_DelegatesFalse() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var readOnlyMs = new ReadOnlyStreamWrapper(ms); + var stream = SharpCompressStream.CreateNonDisposing(readOnlyMs); + Assert.False(stream.CanWrite); + } + + private class ReadOnlyStreamWrapper : Stream + { + private readonly Stream _baseStream; + + public ReadOnlyStreamWrapper(Stream baseStream) + { + _baseStream = baseStream; + } + + public override bool CanRead => _baseStream.CanRead; + public override bool CanSeek => _baseStream.CanSeek; + public override bool CanWrite => false; + public override long Length => _baseStream.Length; + public override long Position + { + get => _baseStream.Position; + set => _baseStream.Position = value; + } + + public override void Flush() => _baseStream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + _baseStream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => + _baseStream.Seek(offset, origin); + + public override void SetLength(long value) => _baseStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + } +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamSeekAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamSeekAsyncTest.cs new file mode 100644 index 000000000..9d17febf9 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamSeekAsyncTest.cs @@ -0,0 +1,141 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.IO; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SharpCompressStreamSeekAsyncTest +{ + private class NonSeekableStreamWrapper : Stream + { + private readonly Stream _baseStream; + + public NonSeekableStreamWrapper(Stream baseStream) => _baseStream = baseStream; + + public override bool CanRead => _baseStream.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => _baseStream.CanWrite; + public override long Length => _baseStream.Length; + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() => _baseStream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + _baseStream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + + public override void SetLength(long value) => _baseStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => + _baseStream.Write(buffer, offset, count); + + protected override void Dispose(bool disposing) + { + if (disposing) + _baseStream.Dispose(); + base.Dispose(disposing); + } + } + + [Fact] + public async ValueTask SeekAsync_AfterReadAsync_MaintainsPosition() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + var buffer = new byte[4]; + await stream.ReadAsync(buffer, 0, 4).ConfigureAwait(false); + Assert.Equal(4, stream.Position); + + stream.Seek(-2, SeekOrigin.Current); + Assert.Equal(2, stream.Position); + + await stream.ReadAsync(buffer, 0, 2).ConfigureAwait(false); + Assert.Equal(3, buffer[0]); + Assert.Equal(4, buffer[1]); + } + + [Fact] + public async ValueTask Position_Set_AfterAsyncRead_WorksCorrectly() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + var buffer = new byte[8]; + await stream.ReadAsync(buffer, 0, 8).ConfigureAwait(false); + + stream.Position = 2; + Assert.Equal(2, stream.Position); + + var readBuffer = new byte[2]; + await stream.ReadAsync(readBuffer, 0, 2).ConfigureAwait(false); + Assert.Equal(3, readBuffer[0]); + Assert.Equal(4, readBuffer[1]); + } + + [Fact] + public async ValueTask SeekAsync_ToRecordingStart_AfterAsyncRead_WorksCorrectly() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + var buffer = new byte[4]; + await stream.ReadAsync(buffer, 0, 4).ConfigureAwait(false); + + stream.Position = 0; + Assert.Equal(0, stream.Position); + + await stream.ReadAsync(buffer, 0, 4).ConfigureAwait(false); + Assert.Equal(1, buffer[0]); + Assert.Equal(2, buffer[1]); + Assert.Equal(3, buffer[2]); + Assert.Equal(4, buffer[3]); + } + + [Fact] + public async ValueTask SeekAsync_ZeroCurrentOrigin_DoesNotMove() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + var buffer = new byte[4]; + await stream.ReadAsync(buffer, 0, 4).ConfigureAwait(false); + Assert.Equal(4, stream.Position); + + stream.Seek(0, SeekOrigin.Current); + Assert.Equal(4, stream.Position); + } + + [Fact] + public async ValueTask SeekAsync_NegativeCurrent_MovesBackward() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + var buffer = new byte[6]; + await stream.ReadAsync(buffer, 0, 6).ConfigureAwait(false); + Assert.Equal(6, stream.Position); + + stream.Seek(-3, SeekOrigin.Current); + Assert.Equal(3, stream.Position); + + var readBuffer = new byte[3]; + await stream.ReadAsync(readBuffer, 0, 3).ConfigureAwait(false); + Assert.Equal(4, readBuffer[0]); + Assert.Equal(5, readBuffer[1]); + Assert.Equal(6, readBuffer[2]); + } +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamSeekTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamSeekTest.cs new file mode 100644 index 000000000..b2313489d --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamSeekTest.cs @@ -0,0 +1,127 @@ +using System; +using System.IO; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SharpCompressStreamSeekTest +{ + private class NonSeekableStreamWrapper : Stream + { + private readonly Stream _baseStream; + + public NonSeekableStreamWrapper(Stream baseStream) + { + _baseStream = baseStream; + } + + public override bool CanRead => _baseStream.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => _baseStream.CanWrite; + public override long Length => _baseStream.Length; + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() => _baseStream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + _baseStream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + + public override void SetLength(long value) => _baseStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => + _baseStream.Write(buffer, offset, count); + + protected override void Dispose(bool disposing) + { + if (disposing) + _baseStream.Dispose(); + base.Dispose(disposing); + } + } + + [Fact] + public void Seek_CurrentOrigin_MovesRelativeToCurrent() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + var buffer = new byte[4]; + stream.Read(buffer, 0, 4); + Assert.Equal(4, stream.Position); + + stream.Seek(-2, SeekOrigin.Current); + Assert.Equal(2, stream.Position); + + stream.Read(buffer, 0, 2); + Assert.Equal(3, buffer[0]); + Assert.Equal(4, buffer[1]); + } + + [Fact] + public void Seek_BeginOrigin_MovesToAbsolutePosition() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + var buffer = new byte[8]; + stream.Read(buffer, 0, 8); + + stream.Seek(2, SeekOrigin.Begin); + Assert.Equal(2, stream.Position); + + var readBuffer = new byte[2]; + stream.Read(readBuffer, 0, 2); + Assert.Equal(3, readBuffer[0]); + Assert.Equal(4, readBuffer[1]); + } + + [Fact] + public void Seek_ToExactBufferBoundary_Succeeds() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + var buffer = new byte[4]; + stream.Read(buffer, 0, 4); + + stream.Seek(4, SeekOrigin.Begin); + Assert.Equal(4, stream.Position); + + stream.Read(buffer, 0, 4); + Assert.Equal(5, buffer[0]); + Assert.Equal(6, buffer[1]); + Assert.Equal(7, buffer[2]); + Assert.Equal(8, buffer[3]); + } + + [Fact] + public void Position_SetWithinRecordedRange_Succeeds() + { + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 128); + stream.StartRecording(); + var buffer = new byte[8]; + stream.Read(buffer, 0, 8); + + stream.Position = 2; + Assert.Equal(2, stream.Position); + + var readBuffer = new byte[2]; + stream.Read(readBuffer, 0, 2); + Assert.Equal(3, readBuffer[0]); + Assert.Equal(4, readBuffer[1]); + } +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamTest.cs deleted file mode 100644 index 3e846f39e..000000000 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamTest.cs +++ /dev/null @@ -1,884 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using SharpCompress.IO; -using SharpCompress.Test.Mocks; -using Xunit; - -namespace SharpCompress.Test.Streams; - -public class SharpCompressStreamTest -{ - [Fact] - public void TestRewind() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - var br = new BinaryReader(stream); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - stream.Rewind(true); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - Assert.Equal(5, br.ReadInt32()); - Assert.Equal(6, br.ReadInt32()); - Assert.Equal(7, br.ReadInt32()); - } - - [Fact] - public void TestIncompleteRewind() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - var br = new BinaryReader(stream); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - stream.Rewind(true); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - Assert.Equal(5, br.ReadInt32()); - Assert.Equal(6, br.ReadInt32()); - Assert.Equal(7, br.ReadInt32()); - } - - [Fact] - public void TestRecording() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - var br = new BinaryReader(stream); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - stream.Rewind(false); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - } - - [Fact] - public void TestPosition() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - for (int i = 0; i < 10; i++) - { - bw.Write(i); - } - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - Assert.Equal(0, stream.Position); - - var buffer = new byte[4]; - stream.Read(buffer, 0, 4); - Assert.Equal(4, stream.Position); - - stream.StartRecording(); - stream.Read(buffer, 0, 4); - Assert.Equal(8, stream.Position); - - stream.Rewind(); - Assert.Equal(4, stream.Position); - } - - [Fact] - public void TestPositionSeek() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - for (int i = 0; i < 10; i++) - { - bw.Write(i); - } - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - stream.StartRecording(); - var br = new BinaryReader(stream); - - Assert.Equal(0, br.ReadInt32()); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - - stream.Position = 4; - Assert.Equal(1, br.ReadInt32()); - } - - [Fact] - public void TestDispose() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Flush(); - ms.Position = 0; - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - stream.Dispose(); - Assert.Throws(() => stream.Read(new byte[4], 0, 4)); - } - - [Fact] - public void TestStopRecordingBasic() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - stream.StartRecording(); - var br = new BinaryReader(stream); - - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - - stream.StopRecording(); - - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - Assert.Equal(5, br.ReadInt32()); - Assert.Equal(6, br.ReadInt32()); - Assert.Equal(7, br.ReadInt32()); - - Assert.False(stream.IsRecording); - } - - [Fact] - public void TestStopRecordingNoFurtherBuffering() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - stream.StartRecording(); - - var buffer = new byte[8]; - stream.Read(buffer, 0, 8); - - stream.StopRecording(); - - stream.Read(buffer, 0, 8); - Assert.Equal(BitConverter.GetBytes(1), buffer.Take(4).ToArray()); - Assert.Equal(BitConverter.GetBytes(2), buffer.Skip(4).Take(4).ToArray()); - - int bytesRead = stream.Read(buffer, 0, 8); - Assert.Equal(8, bytesRead); - - Assert.False(stream.IsRecording); - - bytesRead = stream.Read(buffer, 0, 8); - Assert.Equal(0, bytesRead); - } - -#if !LEGACY_DOTNET - [Fact] - public void TestStopRecordingWithSpan() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - - var buffer = new byte[8]; - stream.Read(buffer); - - stream.StopRecording(); - - stream.Read(buffer); - Assert.Equal(BitConverter.GetBytes(1), buffer.Take(4).ToArray()); - Assert.Equal(BitConverter.GetBytes(2), buffer.Skip(4).Take(4).ToArray()); - - int bytesRead = stream.Read(buffer); - Assert.Equal(8, bytesRead); - Assert.Equal(BitConverter.GetBytes(3), buffer.Take(4).ToArray()); - Assert.Equal(BitConverter.GetBytes(4), buffer.Skip(4).Take(4).ToArray()); - } -#endif - - [Fact] - public void TestNonSeekableStream_Rewind() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Flush(); - ms.Position = 0; - - var nonSeekableStream = new NonSeekableStreamWrapper(ms); - var stream = new SharpCompressStream(nonSeekableStream); - stream.StartRecording(); - var br = new BinaryReader(stream); - - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - stream.Rewind(true); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - Assert.Equal(5, br.ReadInt32()); - Assert.Equal(6, br.ReadInt32()); - Assert.Equal(7, br.ReadInt32()); - } - - [Fact] - public void TestNonSeekableStream_Recording() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Flush(); - ms.Position = 0; - - var nonSeekableStream = new NonSeekableStreamWrapper(ms); - var stream = new SharpCompressStream(nonSeekableStream); - stream.StartRecording(); - var br = new BinaryReader(stream); - - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - stream.Rewind(false); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - } - - [Fact] - public void TestNonSeekableStream_Position() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - for (int i = 0; i < 10; i++) - { - bw.Write(i); - } - bw.Flush(); - ms.Position = 0; - - var nonSeekableStream = new NonSeekableStreamWrapper(ms); - var stream = new SharpCompressStream(nonSeekableStream); - Assert.Equal(0, stream.Position); - Assert.True(stream.CanSeek); - - var buffer = new byte[4]; - stream.Read(buffer, 0, 4); - Assert.Equal(4, stream.Position); - - stream.StartRecording(); - stream.Read(buffer, 0, 4); - Assert.Equal(8, stream.Position); - - stream.Rewind(); - Assert.Equal(4, stream.Position); - } - - [Fact] - public void TestNonSeekableStream_PositionSet_WithinBuffer() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Flush(); - ms.Position = 0; - - var nonSeekableStream = new NonSeekableStreamWrapper(ms); - var stream = new SharpCompressStream(nonSeekableStream); - stream.StartRecording(); - - var buffer = new byte[4]; - stream.Read(buffer, 0, 4); - Assert.Equal(1, BitConverter.ToInt32(buffer, 0)); - - stream.Read(buffer, 0, 4); - Assert.Equal(2, BitConverter.ToInt32(buffer, 0)); - - stream.Position = 0; - Assert.Equal(0, stream.Position); - - stream.Read(buffer, 0, 4); - Assert.Equal(1, BitConverter.ToInt32(buffer, 0)); - - stream.Read(buffer, 0, 4); - Assert.Equal(2, BitConverter.ToInt32(buffer, 0)); - } - - [Fact] - public void TestNonSeekableStream_PositionSet_OutsideBuffer_Throws() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Flush(); - ms.Position = 0; - - var nonSeekableStream = new NonSeekableStreamWrapper(ms); - var stream = new SharpCompressStream(nonSeekableStream); - stream.StartRecording(); - - var buffer = new byte[4]; - stream.Read(buffer, 0, 4); - - Assert.Throws(() => stream.Position = 100); - } - - [Fact] - public void TestNonSeekableStream_StopRecordingBasic() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Flush(); - ms.Position = 0; - - var nonSeekableStream = new NonSeekableStreamWrapper(ms); - var stream = new SharpCompressStream(nonSeekableStream); - stream.StartRecording(); - var br = new BinaryReader(stream); - - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - - stream.StopRecording(); - - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - Assert.Equal(5, br.ReadInt32()); - Assert.Equal(6, br.ReadInt32()); - Assert.Equal(7, br.ReadInt32()); - - Assert.False(stream.IsRecording); - } - - [Fact] - public void TestStopRecordingThenRewind() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Write(8); - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - var br = new BinaryReader(new ForwardOnlyStream(stream)); - - // Read first 4 values (gets buffered) - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - - // Stop recording - stream.StopRecording(); - Assert.False(stream.IsRecording); - - // Rewind to start of buffer - stream.Rewind(true); - - // Should be able to read from buffer again - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - // Rewind to start of buffer - stream.Rewind(); - // Should be able to read from buffer again - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - - // Continue reading remaining data from underlying stream - Assert.Equal(5, br.ReadInt32()); - Assert.Equal(6, br.ReadInt32()); - Assert.Equal(7, br.ReadInt32()); - Assert.Equal(8, br.ReadInt32()); - } - - [Fact] - public void TestNonSeekableStream_StopRecordingThenRewind() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Write(8); - bw.Flush(); - ms.Position = 0; - - var nonSeekableStream = new NonSeekableStreamWrapper(ms); - var stream = new SharpCompressStream(nonSeekableStream); - stream.StartRecording(); - var br = new BinaryReader(stream); - - // Read first 4 values (gets buffered) - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - - // Stop recording - stream.StopRecording(); - Assert.False(stream.IsRecording); - - // Rewind to start of buffer - stream.Rewind(true); - - // Should be able to read from buffer again - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - - // Continue reading remaining data from underlying stream - Assert.Equal(5, br.ReadInt32()); - Assert.Equal(6, br.ReadInt32()); - Assert.Equal(7, br.ReadInt32()); - Assert.Equal(8, br.ReadInt32()); - } - - [Fact] - public void TestMultipleRewindsAfterStopRecording() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Write(3); - bw.Write(4); - bw.Write(5); - bw.Write(6); - bw.Write(7); - bw.Write(8); - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - var br = new BinaryReader(stream); - - // Read first 4 values (gets buffered) - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - - // Stop recording - stream.StopRecording(); - Assert.False(stream.IsRecording); - - // First rewind - read all buffered data, then continue with underlying stream - stream.Rewind(); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - Assert.Equal(5, br.ReadInt32()); - Assert.Equal(6, br.ReadInt32()); - - // Second rewind - should still be able to read from buffer - stream.Rewind(); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - - // Third rewind - still works - stream.Rewind(); - Assert.Equal(1, br.ReadInt32()); - Assert.Equal(2, br.ReadInt32()); - Assert.Equal(3, br.ReadInt32()); - Assert.Equal(4, br.ReadInt32()); - Assert.Equal(5, br.ReadInt32()); - Assert.Equal(6, br.ReadInt32()); - Assert.Equal(7, br.ReadInt32()); - Assert.Equal(8, br.ReadInt32()); - } - - [Fact] - public void TestStopRecordingTwiceThrows() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - bw.Write(1); - bw.Write(2); - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(ms); - stream.StartRecording(); - - var br = new BinaryReader(stream); - Assert.Equal(1, br.ReadInt32()); - - // First StopRecording should succeed - stream.StopRecording(); - Assert.False(stream.IsRecording); - - // Second StopRecording should throw - Assert.Throws(() => stream.StopRecording()); - } - - [Fact] - public void TestReadMoreThanBufferSizeAfterRewind() - { - // This test verifies the fix for the bug where reading more bytes than - // are in the buffer after a rewind would only return the buffered bytes - // instead of continuing to read from the underlying stream. - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 29 bytes (simulating Arc header) - for (int i = 0; i < 29; i++) - { - bw.Write((byte)i); - } - - // Write 5252 bytes (simulating Arc compressed data) - for (int i = 0; i < 5252; i++) - { - bw.Write((byte)(i % 256)); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Simulate factory detection: record first 512 bytes - stream.StartRecording(); - var probeBuffer = new byte[512]; - int probeRead = stream.Read(probeBuffer, 0, 512); - Assert.Equal(512, probeRead); - - // Stop recording and rewind (simulates what ReaderFactory does) - stream.Rewind(true); - - // Read header (29 bytes) - should come from buffer - var headerBuffer = new byte[29]; - int headerRead = stream.Read(headerBuffer, 0, 29); - Assert.Equal(29, headerRead); - - // Read compressed data (5252 bytes) - buffer has 483 bytes left, - // but we need 5252 bytes. This should read all 5252 bytes, not just 483. - var dataBuffer = new byte[5252]; - int dataRead = stream.Read(dataBuffer, 0, 5252); - Assert.Equal(5252, dataRead); - - // Verify we read the correct data - for (int i = 0; i < 5252; i++) - { - Assert.Equal((byte)(i % 256), dataBuffer[i]); - } - - // Verify stream position is correct (29 + 5252 = 5281) - Assert.Equal(5281, stream.Position); - } - - [Fact] - public void TestReadExactlyBufferSizeAfterRewind() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 1024 bytes - for (int i = 0; i < 1024; i++) - { - bw.Write((byte)(i % 256)); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Record first 512 bytes - stream.StartRecording(); - var probeBuffer = new byte[512]; - stream.Read(probeBuffer, 0, 512); - stream.Rewind(true); - - // Read exactly the buffer size (512 bytes) - var buffer = new byte[512]; - int bytesRead = stream.Read(buffer, 0, 512); - Assert.Equal(512, bytesRead); - - // Verify we read the correct data - for (int i = 0; i < 512; i++) - { - Assert.Equal((byte)(i % 256), buffer[i]); - } - } - - [Fact] - public void TestReadLessThanBufferSizeAfterRewind() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 1024 bytes - for (int i = 0; i < 1024; i++) - { - bw.Write((byte)(i % 256)); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Record first 512 bytes - stream.StartRecording(); - var probeBuffer = new byte[512]; - stream.Read(probeBuffer, 0, 512); - stream.Rewind(true); - - // Read less than buffer size (256 bytes) - var buffer = new byte[256]; - int bytesRead = stream.Read(buffer, 0, 256); - Assert.Equal(256, bytesRead); - - // Verify we read the correct data - for (int i = 0; i < 256; i++) - { - Assert.Equal((byte)(i % 256), buffer[i]); - } - } - - [Fact] - public void TestMultipleReadsExceedingBufferAfterRewind() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 2048 bytes - for (int i = 0; i < 2048; i++) - { - bw.Write((byte)(i % 256)); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Record first 512 bytes - stream.StartRecording(); - var probeBuffer = new byte[512]; - stream.Read(probeBuffer, 0, 512); - stream.Rewind(true); - - // Read in chunks that will exceed the buffer - var buffer = new byte[800]; - - // First read: 800 bytes (512 from buffer + 288 from underlying stream) - int bytesRead1 = stream.Read(buffer, 0, 800); - Assert.Equal(800, bytesRead1); - - // Second read: 800 bytes (all from underlying stream) - int bytesRead2 = stream.Read(buffer, 0, 800); - Assert.Equal(800, bytesRead2); - - // Third read: remaining 448 bytes - int bytesRead3 = stream.Read(buffer, 0, 800); - Assert.Equal(448, bytesRead3); - - // Verify stream position - Assert.Equal(2048, stream.Position); - } - - [Fact] - public void TestReadPartiallyFromBufferThenUnderlyingStream() - { - var ms = new MemoryStream(); - var bw = new BinaryWriter(ms); - - // Write 1000 bytes with specific pattern - for (int i = 0; i < 1000; i++) - { - bw.Write((byte)i); - } - - bw.Flush(); - ms.Position = 0; - - var stream = new SharpCompressStream(new ForwardOnlyStream(ms)); - - // Record first 100 bytes - stream.StartRecording(); - var probeBuffer = new byte[100]; - stream.Read(probeBuffer, 0, 100); - stream.Rewind(true); - - // Read 50 bytes (from buffer) - var buffer1 = new byte[50]; - int read1 = stream.Read(buffer1, 0, 50); - Assert.Equal(50, read1); - for (int i = 0; i < 50; i++) - { - Assert.Equal((byte)i, buffer1[i]); - } - - // Read 150 bytes (50 from buffer + 100 from underlying stream) - var buffer2 = new byte[150]; - int read2 = stream.Read(buffer2, 0, 150); - Assert.Equal(150, read2); - for (int i = 0; i < 150; i++) - { - Assert.Equal((byte)(i + 50), buffer2[i]); - } - - // Verify position - Assert.Equal(200, stream.Position); - } - - private class NonSeekableStreamWrapper : Stream - { - private readonly Stream _baseStream; - - public NonSeekableStreamWrapper(Stream baseStream) - { - _baseStream = baseStream; - } - - public override bool CanRead => _baseStream.CanRead; - - public override bool CanSeek => false; - - public override bool CanWrite => _baseStream.CanWrite; - - public override long Length => _baseStream.Length; - - public override long Position - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } - - public override void Flush() => _baseStream.Flush(); - - public override int Read(byte[] buffer, int offset, int count) => - _baseStream.Read(buffer, offset, count); - - public override long Seek(long offset, SeekOrigin origin) => - throw new NotSupportedException(); - - public override void SetLength(long value) => _baseStream.SetLength(value); - - public override void Write(byte[] buffer, int offset, int count) => - _baseStream.Write(buffer, offset, count); - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _baseStream.Dispose(); - } - base.Dispose(disposing); - } - } -} diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index 5aad3e8ca..34ceac02f 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -44,7 +44,7 @@ public async ValueTask Tar_Z_Reader_Async() => await ReadAsync("Tar.tar.Z", CompressionType.Lzw); [Fact] - public async ValueTask Tar_Async_Assert() => await AssertArchiveAsync("Tar.tar"); + public async ValueTask Tar_Async_Assert() => await AssertArchiveAsync("Tar.tar"); [Fact] public async ValueTask Tar_BZip2_Reader_Async() => diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index 786cc41dc..4b66c0d9c 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -91,7 +91,8 @@ await writer.WriteAllAsync( await using var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(SharpCompressStream.CreateNonDisposing(stream)), - readerOptions + readerOptions, + cancellationToken ); await reader.WriteAllToDirectoryAsync(SCRATCH_FILES_PATH, cancellationToken); }