diff --git a/.agents/skills/tar-format/references/tar-format.md b/.agents/skills/tar-format/references/tar-format.md index 1ee10c1a8..72b9c5100 100644 --- a/.agents/skills/tar-format/references/tar-format.md +++ b/.agents/skills/tar-format/references/tar-format.md @@ -249,15 +249,15 @@ GNU tar uses base-256 binary fields for out-of-range numeric values. SharpCompre Wrapper detection is defined in `TarWrapper.Wrappers`. Detection is content-based: wrapper detection is followed by a tar-header probe of the decompressed payload. -| Wrapper | Extensions | Read | Write | -| ------- | ---------- | ---- | ----- | -| Plain tar | `tar` | Yes | Yes | -| Tar + GZip | `tar.gz`, `taz`, `tgz` | Yes | Yes | -| Tar + BZip2 | `tar.bz2`, `tb2`, `tbz`, `tbz2`, `tz2` | Yes | Yes | -| Tar + LZip | `tar.lz` | Yes | Yes | -| Tar + XZ | `tar.xz`, `txz` | Yes | No | -| Tar + ZStandard | `tar.zst`, `tar.zstd`, `tzst`, `tzstd` | Yes | No | -| Tar + LZW compress | `tar.Z`, `tZ`, `taZ` | Yes | No | +| Wrapper | Extensions | Archive Read | Reader Read | Write | +| ------- | ---------- | ------------ | ----------- | ----- | +| Plain tar | `tar` | Yes | Yes | Yes | +| Tar + GZip | `tar.gz`, `taz`, `tgz` | No | Yes | Yes | +| Tar + BZip2 | `tar.bz2`, `tb2`, `tbz`, `tbz2`, `tz2` | No | Yes | Yes | +| Tar + LZip | `tar.lz` | No | Yes | Yes | +| Tar + XZ | `tar.xz`, `txz` | No | Yes | No | +| Tar + ZStandard | `tar.zst`, `tar.zstd`, `tzst`, `tzstd` | No | Yes | No | +| Tar + LZW compress | `tar.Z`, `tZ`, `taZ` | No | Yes | No | Writer support currently accepts only these compression types: @@ -278,9 +278,9 @@ Reader API: Archive API: -- `TarArchive.OpenArchive(Stream)` and `TarArchive.OpenAsyncArchive(Stream)` require seekable streams. +- `TarArchive.OpenArchive(Stream)` and `TarArchive.OpenAsyncArchive(Stream)` require seekable raw tar streams. - File/path overloads own the opened file stream. -- Compressed tar archive access follows streaming semantics over the decompressed stream rather than full random-access semantics. +- Compressed tar wrappers are supported through `TarReader`, not `TarArchive` or `ArchiveFactory`; archive detection blocks them instead of opening the outer compression wrapper as a standalone archive. Parsed metadata surfaced through entries includes: @@ -323,7 +323,7 @@ Keep these limitations explicit in code comments, docs, and tests: - No write support for `tar.xz`, `tar.zst`, or `tar.Z`. - PAX read support is limited to `path`, `linkpath`, `size`, `mtime`, `uid`, `gid`, and `mode`. - Unknown PAX keys are ignored. -- Stream-based `TarArchive` open requires seekable input. +- Stream-based `TarArchive` open requires seekable raw tar input. ## Test Fixtures diff --git a/docs/API.md b/docs/API.md index db807f557..80143e833 100644 --- a/docs/API.md +++ b/docs/API.md @@ -73,7 +73,7 @@ using (var archive = ArchiveFactory.OpenArchive(parts)) } ``` -`ArchiveInformation.SupportsRandomAccess` is `true` when the detected format supports `IArchive` random access. It is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW, where `ReaderFactory.OpenReader` should be used instead. +`ArchiveInformation.SupportsRandomAccess` is `true` when the detected format supports `IArchive` random access. It is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW, where `ReaderFactory.OpenReader` should be used instead. Compressed tar wrappers such as `.tar.gz` and `.tar.xz` are also reader-only; `ArchiveFactory.GetArchiveInformation` returns `null` for them and `ArchiveFactory.OpenArchive` does not open them as the outer compression wrapper. Use `ReaderFactory.OpenReader` or `TarReader.OpenReader` for those files. ### Creating Archives diff --git a/docs/FORMATS.md b/docs/FORMATS.md index 3504d659e..22a7080a3 100644 --- a/docs/FORMATS.md +++ b/docs/FORMATS.md @@ -16,12 +16,12 @@ | Rar | Rar | Decompress | RarArchive | RarReader | N/A | | Zip (2) | None, Shrink, Reduce, Implode, DEFLATE, Deflate64, BZip2, LZMA, PPMd, ZStandard, XZ | Both | ZipArchive | ZipReader | ZipWriter | | Tar | None | Both | TarArchive | TarReader | TarWriter (3) | -| Tar.GZip | DEFLATE | Both | TarArchive | TarReader | TarWriter (3) | -| Tar.BZip2 | BZip2 | Both | TarArchive | TarReader | TarWriter (3) | -| Tar.Zstandard | ZStandard | Decompress | TarArchive | TarReader | N/A | -| Tar.LZip | LZMA | Both | TarArchive | TarReader | TarWriter (3) | -| Tar.XZ | LZMA2 | Decompress | TarArchive | TarReader | N/A | -| Tar.LZW | LZW | Decompress | TarArchive | TarReader | N/A | +| Tar.GZip | DEFLATE | Both | N/A | TarReader | TarWriter (3) | +| Tar.BZip2 | BZip2 | Both | N/A | TarReader | TarWriter (3) | +| Tar.Zstandard | ZStandard | Decompress | N/A | TarReader | N/A | +| Tar.LZip | LZMA | Both | N/A | TarReader | TarWriter (3) | +| Tar.XZ | LZMA2 | Decompress | N/A | TarReader | N/A | +| Tar.LZW | LZW | Decompress | N/A | TarReader | N/A | | GZip (single file) | DEFLATE | Both | GZipArchive | GZipReader | GZipWriter | | 7Zip (4) | LZMA, LZMA2, BZip2, PPMd, BCJ, BCJ2, Deflate | Both | SevenZipArchive | N/A | SevenZipWriter | @@ -31,7 +31,7 @@ 4. The 7Zip format doesn't allow for reading as a forward-only stream, so 7Zip read support is only through the Archive API. Writing is supported through SevenZipWriter for non-solid archives with LZMA/LZMA2 and requires a seekable output stream. See [7Zip Format Notes](#7zip-format-notes) for details on async extraction behavior. 5. LZip has no support for extra data like the file name or timestamp. There is a default filename used when looking at the entry Key on the archive. -`ArchiveFactory.GetArchiveInformation(...).SupportsRandomAccess` is `true` when the detected format has an Archive API in this table. It is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW. +`ArchiveFactory.GetArchiveInformation(...).SupportsRandomAccess` is `true` when the detected format has an Archive API in this table. It is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW. Compressed tar wrappers are supported by `ReaderFactory`/`TarReader`, not by `ArchiveFactory`/`TarArchive`; ArchiveFactory detection blocks them instead of opening the outer compression wrapper as a standalone archive. ### Zip Format Notes diff --git a/docs/TAR_SPEC.md b/docs/TAR_SPEC.md index b1870004d..c59071a1d 100644 --- a/docs/TAR_SPEC.md +++ b/docs/TAR_SPEC.md @@ -57,18 +57,18 @@ Tar wrapper detection is defined by `TarWrapper.Wrappers` in `src/SharpCompress/ | Wrapper | Detection | `TarArchive` read | `TarReader` read | `TarWriter` write | | ------- | --------- | ----------------- | ---------------- | ----------------- | | Plain tar | Yes | Yes | Yes | Yes | -| Tar + GZip | Yes | Yes | Yes | Yes | -| Tar + BZip2 | Yes | Yes | Yes | Yes | -| Tar + LZip | Yes | Yes | Yes | Yes | -| Tar + XZ | Yes | Yes | Yes | No | -| Tar + ZStandard | Yes | Yes | Yes | No | -| Tar + LZW compress | Yes | Yes | Yes | No | +| Tar + GZip | Reader only | No | Yes | Yes | +| Tar + BZip2 | Reader only | No | Yes | Yes | +| Tar + LZip | Reader only | No | Yes | Yes | +| Tar + XZ | Reader only | No | Yes | No | +| Tar + ZStandard | Reader only | No | Yes | No | +| Tar + LZW compress | Reader only | No | Yes | No | Write support is implemented in `src/SharpCompress/Writers/Tar/TarWriter.cs` and currently accepts only `CompressionType.None`, `CompressionType.GZip`, `CompressionType.BZip2`, and `CompressionType.LZip`. ## Detection Behavior -Tar detection is implemented in `TarFactory.IsArchive`, `TarFactory.IsArchiveAsync`, `TarFactory.GetCompressionType`, and `TarFactory.GetCompressionTypeAsync`. +Tar reader detection is implemented in `TarFactory.TryOpenReader`, `TarFactory.TryOpenReaderAsync`, `TarFactory.GetCompressionType`, and `TarFactory.GetCompressionTypeAsync`. Archive detection through `TarFactory.IsArchive` and `TarFactory.IsArchiveAsync` accepts raw tar only. Detection behavior is: @@ -77,13 +77,14 @@ Detection behavior is: 3. Probe each registered wrapper in order. 4. If a wrapper matches, create a decompression stream for that wrapper. 5. Call `TarArchive.IsTarFile` or `TarArchive.IsTarFileAsync` on the decompressed stream. -6. If the tar probe succeeds, treat the stream as tar with that wrapper compression. +6. If the tar probe succeeds, open `TarReader` with that wrapper compression. Implications: - Tar detection is content-based, not extension-based. - Wrapper detection is not sufficient by itself. The decompressed payload must also parse as tar. -- Non-seekable detection is supported through the recording and rewind mechanism. +- Non-seekable wrapper detection is supported through the recording and rewind mechanism on the reader path. +- `ArchiveFactory`/`TarArchive` do not open compressed tar wrappers and do not fall through to the outer compression wrapper archive; use `ReaderFactory`/`TarReader` for `.tar.gz`, `.tar.bz2`, `.tar.xz`, `.tar.zst`, `.tar.lz`, and `.tar.Z`. - The largest rewind requirement currently comes from BZip2, which declares a larger minimum probe buffer in `TarWrapper`. `TarArchive.IsTarFile` and `TarArchive.IsTarFileAsync` attempt to read a single tar header and return `false` on any exception. They also treat an all-zero empty archive block as a valid empty tar archive when the entry type is defined. @@ -151,20 +152,15 @@ Implementation files: `TarArchive.OpenArchive(Stream)` and `TarArchive.OpenAsyncArchive(Stream)` require a seekable stream and throw `ArgumentException` when `CanSeek` is `false`. -`TarArchive.OpenArchive(FileInfo)` and the list-based overloads use `SourceStream` and determine wrapper compression by calling `TarFactory.GetCompressionType`. +`TarArchive.OpenArchive(FileInfo)` and the list-based overloads use `SourceStream` and validate the input as raw tar. -Asynchronous `OpenAsyncArchive` overloads use `TarFactory.GetCompressionTypeAsync` for wrapper detection. +Asynchronous `OpenAsyncArchive` overloads perform the same raw-tar validation. ### Entry Loading -`TarArchive.LoadEntries` and `LoadEntriesAsync` parse entries differently depending on wrapper compression: +`TarArchive.LoadEntries` and `LoadEntriesAsync` parse raw tar streams in `StreamingMode.Seekable`. The header stores `DataStartPosition`, and entries reopen data through `TarFilePart` by seeking back to the data position. -- Uncompressed tar uses `StreamingMode.Seekable`. -- Wrapped tar uses `StreamingMode.Streaming` because the decompressed stream is not treated as random-access. - -When seekable mode is used, the header stores `DataStartPosition`, and entries reopen data through `TarFilePart` by seeking back to the data position. - -When streaming mode is used, the header stores a `PackedStream`, and entry access follows streaming semantics over the decompressed stream. +Wrapped tar streams use `TarReader`, whose forward-only entry access follows streaming semantics over the decompressed stream. ### Archive Rewrite Behavior @@ -418,7 +414,7 @@ This section documents current implementation limits, not desired future behavio ### Archive behavior limitations - Stream-based archive open requires a seekable input stream -- Compressed tar archive access is not full random-access in the same sense as uncompressed seekable tar +- Compressed tar wrappers are supported through `TarReader`, not `TarArchive` ## Test Coverage Map diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 6c45cb1f4..db054211d 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -17,7 +17,11 @@ public static async ValueTask OpenAsyncArchive( ) { readerOptions ??= ReaderOptions.ForExternalStream; - var factory = await FindFactoryAsync(stream, cancellationToken) + var factory = await FindFactoryAsync( + stream, + readerOptions, + cancellationToken + ) .ConfigureAwait(false); return await factory .OpenAsyncArchive(stream, readerOptions, cancellationToken) @@ -46,7 +50,7 @@ public static async ValueTask OpenAsyncArchive( { options ??= ReaderOptions.ForFilePath; - var factory = await FindFactoryAsync(fileInfo, cancellationToken) + var factory = await FindFactoryAsync(fileInfo, options, cancellationToken) .ConfigureAwait(false); return await factory .OpenAsyncArchive(fileInfo, options, cancellationToken) @@ -76,7 +80,11 @@ public static async ValueTask OpenAsyncArchive( fileInfo.NotNull(nameof(fileInfo)); options ??= ReaderOptions.ForFilePath; - var factory = await FindFactoryAsync(fileInfo, cancellationToken) + var factory = await FindFactoryAsync( + fileInfo, + options, + cancellationToken + ) .ConfigureAwait(false); return await factory .OpenAsyncArchive(filesArray, options, cancellationToken) @@ -101,7 +109,11 @@ public static async ValueTask OpenAsyncArchive( firstStream.NotNull(nameof(firstStream)); options ??= ReaderOptions.ForExternalStream; - var factory = await FindFactoryAsync(firstStream, cancellationToken) + var factory = await FindFactoryAsync( + firstStream, + options, + cancellationToken + ) .ConfigureAwait(false); return await factory .OpenAsyncArchive(streamsArray, options, cancellationToken) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs index a0084914b..cf34e6e51 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs @@ -2,8 +2,11 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Archives.Tar; using SharpCompress.Common; using SharpCompress.Factories; +using SharpCompress.IO; +using SharpCompress.Providers; using SharpCompress.Readers; namespace SharpCompress.Archives; @@ -107,6 +110,19 @@ internal static async ValueTask FindFactoryAsync( return await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); } + private static async ValueTask FindFactoryAsync( + FileInfo fileInfo, + ReaderOptions readerOptions, + CancellationToken cancellationToken + ) + where T : IFactory + { + fileInfo.NotNull(nameof(fileInfo)); + using Stream stream = fileInfo.OpenRead(); + return await FindFactoryAsync(stream, readerOptions, cancellationToken) + .ConfigureAwait(false); + } + internal static async ValueTask FindFactoryAsync( Stream stream, CancellationToken cancellationToken = default @@ -120,7 +136,22 @@ internal static async ValueTask FindFactoryAsync( // implements T we return it; otherwise (or if nothing matched) we fall through // to the same "unsupported format" exception that the original code produced, // listing the T-typed factories as the hint for the caller. - var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + return await FindFactoryAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + } + + private static async ValueTask FindFactoryAsync( + Stream stream, + ReaderOptions readerOptions, + CancellationToken cancellationToken + ) + where T : IFactory + { + stream.RequireReadable(); + stream.RequireSeekable(); + + var factory = await TryFindFactoryAsync(stream, readerOptions, cancellationToken) + .ConfigureAwait(false); if (factory is T typedFactory) { return typedFactory; @@ -134,18 +165,11 @@ internal static async ValueTask FindFactoryAsync( } /// - /// Async counterpart of . + /// Async counterpart of the synchronous factory detection path. /// Iterates all registered factories and returns the first one whose /// recognises the stream, or . /// Stream position is restored to its value at entry on both success and failure. /// - private static async ValueTask TryFindFactoryAsync( - Stream stream, - CancellationToken cancellationToken - ) => - await TryFindFactoryAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) - .ConfigureAwait(false); - private static async ValueTask TryFindFactoryAsync( Stream stream, ReaderOptions readerOptions, @@ -163,6 +187,15 @@ CancellationToken cancellationToken if (isArchive) { + stream.Seek(startPosition, SeekOrigin.Begin); + if ( + await IsCompressedTarAsync(stream, factory, readerOptions, cancellationToken) + .ConfigureAwait(false) + ) + { + continue; + } + stream.Seek(startPosition, SeekOrigin.Begin); return factory; } @@ -253,6 +286,12 @@ CancellationToken cancellationToken if (isArchive) { + stream.Seek(startPosition, SeekOrigin.Begin); + if (IsCompressedTar(stream, factory, readerOptions)) + { + continue; + } + stream.Seek(startPosition, SeekOrigin.Begin); return factory; } @@ -261,4 +300,115 @@ CancellationToken cancellationToken stream.Seek(startPosition, SeekOrigin.Begin); return null; } + + private static bool IsCompressedTar( + Stream stream, + IFactory factory, + ReaderOptions readerOptions + ) => + GetCompressedTarType(factory) is { } compressionType + && IsCompressedTar(stream, readerOptions, compressionType); + + private static bool IsCompressedTar( + Stream stream, + ReaderOptions readerOptions, + CompressionType compressionType + ) + { + using var nonDisposingStream = SharpCompressStream.CreateNonDisposing(stream); + var testStream = + compressionType == CompressionType.GZip + ? readerOptions.Providers.CreateDecompressStream( + compressionType, + nonDisposingStream, + CompressionContext + .FromStream(nonDisposingStream) + .WithReaderOptions(readerOptions) + ) + : readerOptions.Providers.CreateDecompressStream( + compressionType, + nonDisposingStream + ); + + try + { + return TarArchive.IsTarFile(testStream); + } + finally + { + DisposeProbeStream(testStream); + } + } + + private static async ValueTask IsCompressedTarAsync( + Stream stream, + IFactory factory, + ReaderOptions readerOptions, + CancellationToken cancellationToken + ) => + GetCompressedTarType(factory) is { } compressionType + && await IsCompressedTarAsync(stream, readerOptions, compressionType, cancellationToken) + .ConfigureAwait(false); + + private static async ValueTask IsCompressedTarAsync( + Stream stream, + ReaderOptions readerOptions, + CompressionType compressionType, + CancellationToken cancellationToken + ) + { + using var nonDisposingStream = SharpCompressStream.CreateNonDisposing(stream); + var testStream = + compressionType == CompressionType.GZip + ? await readerOptions + .Providers.CreateDecompressStreamAsync( + compressionType, + nonDisposingStream, + CompressionContext + .FromStream(nonDisposingStream) + .WithReaderOptions(readerOptions), + cancellationToken + ) + .ConfigureAwait(false) + : await readerOptions + .Providers.CreateDecompressStreamAsync( + compressionType, + nonDisposingStream, + cancellationToken + ) + .ConfigureAwait(false); + + try + { + return await TarArchive + .IsTarFileAsync(testStream, cancellationToken) + .ConfigureAwait(false); + } + finally + { + DisposeProbeStream(testStream); + } + } + + private static void DisposeProbeStream(Stream stream) + { + try + { +#pragma warning disable VSTHRD103 // Probe streams may validate unread trailers during disposal. + stream.Dispose(); +#pragma warning restore VSTHRD103 + } + catch + { + // Probes intentionally read only enough data to identify tar content. + } + } + + private static CompressionType? GetCompressedTarType(IFactory factory) => + factory switch + { + GZipFactory => CompressionType.GZip, + LzwFactory => CompressionType.Lzw, + _ => null, + }; } diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 49e2f9b43..63b639549 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -16,7 +16,8 @@ public static partial class ArchiveFactory public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { readerOptions ??= ReaderOptions.ForExternalStream; - return FindFactory(stream).OpenArchive(stream, readerOptions); + return FindFactory(stream, readerOptions) + .OpenArchive(stream, readerOptions); } public static IWritableArchive CreateArchive() @@ -44,7 +45,7 @@ public static IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? options = n { options ??= ReaderOptions.ForFilePath; - return FindFactory(fileInfo).OpenArchive(fileInfo, options); + return FindFactory(fileInfo, options).OpenArchive(fileInfo, options); } public static IArchive OpenArchive( @@ -68,7 +69,8 @@ public static IArchive OpenArchive( fileInfo.NotNull(nameof(fileInfo)); options ??= ReaderOptions.ForFilePath; - return FindFactory(fileInfo).OpenArchive(filesArray, options); + return FindFactory(fileInfo, options) + .OpenArchive(filesArray, options); } public static IArchive OpenArchive(IReadOnlyList streams, ReaderOptions? options = null) @@ -88,7 +90,8 @@ public static IArchive OpenArchive(IReadOnlyList streams, ReaderOptions? firstStream.NotNull(nameof(firstStream)); options ??= ReaderOptions.ForExternalStream; - return FindFactory(firstStream).OpenArchive(streamsArray, options); + return FindFactory(firstStream, options) + .OpenArchive(streamsArray, options); } public static void WriteToDirectory( @@ -118,6 +121,17 @@ public static T FindFactory(FileInfo finfo) } public static T FindFactory(Stream stream) + where T : IFactory => FindFactory(stream, ReaderOptions.ForExternalStream); + + private static T FindFactory(FileInfo fileInfo, ReaderOptions readerOptions) + where T : IFactory + { + fileInfo.NotNull(nameof(fileInfo)); + using Stream stream = fileInfo.OpenRead(); + return FindFactory(stream, readerOptions); + } + + private static T FindFactory(Stream stream, ReaderOptions readerOptions) where T : IFactory { stream.RequireReadable(); @@ -127,7 +141,7 @@ public static T FindFactory(Stream stream) // implements T we return it; otherwise (or if nothing matched) we fall through // to the same "unsupported format" exception that the original code produced, // listing the T-typed factories as the hint for the caller. - var factory = TryFindFactory(stream); + var factory = TryFindFactory(stream, readerOptions); if (factory is T typedFactory) { return typedFactory; diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs index 5d8833af5..d8991d43e 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -4,12 +4,10 @@ using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; -using SharpCompress.Common.Options; using SharpCompress.Common.Tar; using SharpCompress.IO; using SharpCompress.Readers; using SharpCompress.Readers.Tar; -using SharpCompress.Writers; using SharpCompress.Writers.Tar; namespace SharpCompress.Archives.Tar; @@ -85,7 +83,7 @@ protected override ValueTask CreateReaderForSolidExtractionAsync() { var stream = Volumes.Single().Stream; stream.Position = 0; - return new((IAsyncReader)new TarReader(stream, ReaderOptions, _compressionType)); + return new((IAsyncReader)new TarReader(stream, ReaderOptions, CompressionType.None)); } protected override async IAsyncEnumerable LoadEntriesAsync( @@ -93,20 +91,15 @@ IAsyncEnumerable volumes ) { var sourceStream = (await volumes.SingleAsync().ConfigureAwait(false)).Stream; - var stream = await GetStreamAsync(sourceStream).ConfigureAwait(false); + var stream = sourceStream; if (stream.CanSeek) { stream.Position = 0; } - var streamingMode = - _compressionType == CompressionType.None - ? StreamingMode.Seekable - : StreamingMode.Streaming; - await foreach ( var header in TarHeaderFactory.ReadHeaderAsync( - streamingMode, + StreamingMode.Seekable, stream, ReaderOptions.ArchiveEncoding ) @@ -116,10 +109,7 @@ var header in TarHeaderFactory.ReadHeaderAsync( { yield return new TarArchiveEntry( this, - new TarFilePart( - header, - _compressionType == CompressionType.None ? stream : null - ), + new TarFilePart(header, stream), CompressionType.None, ReaderOptions ); diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index b63a6c228..181e2a263 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Tar.Headers; -using SharpCompress.Factories; using SharpCompress.IO; using SharpCompress.Readers; using SharpCompress.Writers.Tar; @@ -53,12 +52,7 @@ public static IWritableArchive OpenArchive( i => i < files.Count ? files[i] : null, readerOptions ?? ReaderOptions.ForFilePath ); - var compressionType = TarFactory.GetCompressionType( - sourceStream, - sourceStream.ReaderOptions - ); - sourceStream.Seek(0, SeekOrigin.Begin); - return new TarArchive(sourceStream, compressionType); + return OpenValidatedArchive(sourceStream); } public static IWritableArchive OpenArchive( @@ -72,12 +66,7 @@ public static IWritableArchive OpenArchive( i => i < strms.Count ? strms[i] : null, readerOptions ?? ReaderOptions.ForExternalStream ); - var compressionType = TarFactory.GetCompressionType( - sourceStream, - sourceStream.ReaderOptions - ); - sourceStream.Seek(0, SeekOrigin.Begin); - return new TarArchive(sourceStream, compressionType); + return OpenValidatedArchive(sourceStream); } public static IWritableArchive OpenArchive( @@ -104,11 +93,8 @@ public static async ValueTask> OpenAsync i => null, readerOptions ?? ReaderOptions.ForExternalStream ); - var compressionType = await TarFactory - .GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken) + return await OpenValidatedArchiveAsync(sourceStream, cancellationToken) .ConfigureAwait(false); - sourceStream.Seek(0, SeekOrigin.Begin); - return new TarArchive(sourceStream, compressionType); } public static ValueTask> OpenAsyncArchive( @@ -132,11 +118,8 @@ public static async ValueTask> OpenAsync fileInfo.NotNull(nameof(fileInfo)); readerOptions ??= ReaderOptions.ForFilePath; var sourceStream = new SourceStream(fileInfo, i => null, readerOptions); - var compressionType = await TarFactory - .GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken) + return await OpenValidatedArchiveAsync(sourceStream, cancellationToken) .ConfigureAwait(false); - sourceStream.Seek(0, SeekOrigin.Begin); - return new TarArchive(sourceStream, compressionType); } public static async ValueTask> OpenAsyncArchive( @@ -152,11 +135,8 @@ public static async ValueTask> OpenAsync i => i < strms.Count ? strms[i] : null, readerOptions ?? ReaderOptions.ForExternalStream ); - var compressionType = await TarFactory - .GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken) + return await OpenValidatedArchiveAsync(sourceStream, cancellationToken) .ConfigureAwait(false); - sourceStream.Seek(0, SeekOrigin.Begin); - return new TarArchive(sourceStream, compressionType); } public static async ValueTask> OpenAsyncArchive( @@ -173,11 +153,8 @@ public static async ValueTask> OpenAsync i => i < files.Count ? files[i] : null, readerOptions ?? ReaderOptions.ForFilePath ); - var compressionType = await TarFactory - .GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken) + return await OpenValidatedArchiveAsync(sourceStream, cancellationToken) .ConfigureAwait(false); - sourceStream.Seek(0, SeekOrigin.Begin); - return new TarArchive(sourceStream, compressionType); } public static bool IsTarFile(string filePath) => IsTarFile(new FileInfo(filePath)); @@ -226,14 +203,16 @@ public static async ValueTask IsTarFileAsync( #else using var reader = new AsyncBinaryReader(stream, leaveOpen: true); #endif - var readSucceeded = await tarHeader.ReadAsync(reader).ConfigureAwait(false); + var readSucceeded = await tarHeader + .ReadAsync(reader, cancellationToken: cancellationToken) + .ConfigureAwait(false); var isEmptyArchive = tarHeader.Name?.Length == 0 && tarHeader.Size == 0 && IsDefined(tarHeader.EntryType); return readSucceeded || isEmptyArchive; } - catch (Exception) + catch (Exception ex) when (ex is not OperationCanceledException) { // Catch all exceptions during tar header reading to determine if this is a valid tar file // Invalid tar files or corrupted streams will throw various exceptions @@ -246,6 +225,66 @@ public static async ValueTask IsTarFileAsync( public static ValueTask> CreateAsyncArchive() => new(new TarArchive()); + private static TarArchive OpenValidatedArchive(SourceStream sourceStream) + { + try + { + EnsureRawTarFile(sourceStream); + return new TarArchive(sourceStream); + } + catch + { + sourceStream.Dispose(); + throw; + } + } + + private static async ValueTask OpenValidatedArchiveAsync( + SourceStream sourceStream, + CancellationToken cancellationToken + ) + { + try + { + await EnsureRawTarFileAsync(sourceStream, cancellationToken).ConfigureAwait(false); + return new TarArchive(sourceStream); + } + catch + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + await sourceStream.DisposeAsync().ConfigureAwait(false); +#else + sourceStream.Dispose(); +#endif + throw; + } + } + + private static void EnsureRawTarFile(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + if (!IsTarFile(stream)) + { + stream.Seek(0, SeekOrigin.Begin); + throw new InvalidFormatException("Not a tar file."); + } + stream.Seek(0, SeekOrigin.Begin); + } + + private static async ValueTask EnsureRawTarFileAsync( + Stream stream, + CancellationToken cancellationToken + ) + { + stream.Seek(0, SeekOrigin.Begin); + if (!await IsTarFileAsync(stream, cancellationToken).ConfigureAwait(false)) + { + stream.Seek(0, SeekOrigin.Begin); + throw new InvalidFormatException("Not a tar file."); + } + stream.Seek(0, SeekOrigin.Begin); + } + private static bool IsDefined(EntryType value) { #if LEGACY_DOTNET diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index 038fd32d9..c7b7fd6cd 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -2,12 +2,9 @@ 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.IO; -using SharpCompress.Providers; using SharpCompress.Readers; using SharpCompress.Readers.Tar; using SharpCompress.Writers.Tar; @@ -17,108 +14,28 @@ namespace SharpCompress.Archives.Tar; public partial class TarArchive : AbstractWritableArchive { - private readonly CompressionType _compressionType; - protected override IEnumerable LoadVolumes(SourceStream sourceStream) { sourceStream.NotNull("SourceStream is null").LoadAllParts(); return new TarVolume(sourceStream, ReaderOptions, 1).AsEnumerable(); } - internal TarArchive(SourceStream sourceStream, CompressionType compressionType) - : base(ArchiveType.Tar, sourceStream) - { - _compressionType = compressionType; - } + internal TarArchive(SourceStream sourceStream) + : base(ArchiveType.Tar, sourceStream) { } private TarArchive() : base(ArchiveType.Tar) { } - private Stream GetStream(Stream stream) => - _compressionType switch - { - 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); + var stream = volumes.Single().Stream; if (stream.CanSeek) { stream.Position = 0; } foreach ( var header in TarHeaderFactory.ReadHeader( - _compressionType == CompressionType.None - ? StreamingMode.Seekable - : StreamingMode.Streaming, + StreamingMode.Seekable, stream, ReaderOptions.ArchiveEncoding ) @@ -128,10 +45,7 @@ var header in TarHeaderFactory.ReadHeader( { yield return new TarArchiveEntry( this, - new TarFilePart( - header, - _compressionType == CompressionType.None ? stream : null - ), + new TarFilePart(header, stream), CompressionType.None, ReaderOptions ); @@ -197,6 +111,6 @@ protected override IReader CreateReaderForSolidExtraction() { var stream = Volumes.Single().Stream; stream.Position = 0; - return new TarReader(stream, ReaderOptions, _compressionType); + return new TarReader(stream, ReaderOptions, CompressionType.None); } } diff --git a/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs b/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs index 42f6678db..f1c81813b 100644 --- a/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs +++ b/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs @@ -202,7 +202,8 @@ await output internal async ValueTask ReadAsync( AsyncBinaryReader reader, - PaxMetadata? globalPaxMetadata = null + PaxMetadata? globalPaxMetadata = null, + CancellationToken cancellationToken = default ) { globalPaxMetadata ??= new PaxMetadata(); @@ -213,35 +214,45 @@ internal async ValueTask ReadAsync( { while (true) { - await reader.ReadBytesAsync(buffer, 0, BLOCK_SIZE).ConfigureAwait(false); + await reader + .ReadBytesAsync(buffer, 0, BLOCK_SIZE, cancellationToken) + .ConfigureAwait(false); entryType = ReadEntryType(buffer); // LongName and LongLink headers can follow each other and need // to apply to the header that follows them. if (entryType == EntryType.LongName) { - pendingMetadata.Name = await ReadLongNameAsync(reader, buffer) + pendingMetadata.Name = await ReadLongNameAsync( + reader, + buffer, + cancellationToken + ) .ConfigureAwait(false); continue; } if (entryType == EntryType.LongLink) { - pendingMetadata.LinkName = await ReadLongNameAsync(reader, buffer) + pendingMetadata.LinkName = await ReadLongNameAsync( + reader, + buffer, + cancellationToken + ) .ConfigureAwait(false); continue; } if (entryType == EntryType.LocalExtendedHeader) { - await ReadPaxMetadataAsync(reader, buffer, pendingMetadata) + await ReadPaxMetadataAsync(reader, buffer, pendingMetadata, cancellationToken) .ConfigureAwait(false); continue; } if (entryType == EntryType.GlobalExtendedHeader) { - await ReadPaxMetadataAsync(reader, buffer, globalPaxMetadata) + await ReadPaxMetadataAsync(reader, buffer, globalPaxMetadata, cancellationToken) .ConfigureAwait(false); pendingMetadata = globalPaxMetadata.Clone(); continue; @@ -306,12 +317,16 @@ await ReadPaxMetadataAsync(reader, buffer, globalPaxMetadata) return true; } - private static async ValueTask ReadLengthAsync(AsyncBinaryReader reader, int length) + private static async ValueTask ReadLengthAsync( + AsyncBinaryReader reader, + int length, + CancellationToken cancellationToken + ) { var buffer = ArrayPool.Shared.Rent(length); try { - await reader.ReadBytesAsync(buffer, 0, length).ConfigureAwait(false); + await reader.ReadBytesAsync(buffer, 0, length, cancellationToken).ConfigureAwait(false); } finally { @@ -319,13 +334,18 @@ private static async ValueTask ReadLengthAsync(AsyncBinaryReader reader, int len } } - private async ValueTask ReadLongNameAsync(AsyncBinaryReader reader, byte[] buffer) + private async ValueTask ReadLongNameAsync( + AsyncBinaryReader reader, + byte[] buffer, + CancellationToken cancellationToken + ) { var nameBytes = await ReadMetadataPayloadAsync( reader, buffer, MAX_LONG_NAME_SIZE, - "Long name" + "Long name", + cancellationToken ) .ConfigureAwait(false); @@ -335,14 +355,16 @@ private async ValueTask ReadLongNameAsync(AsyncBinaryReader reader, byte private async ValueTask ReadPaxMetadataAsync( AsyncBinaryReader reader, byte[] buffer, - PaxMetadata pendingMetadata + PaxMetadata pendingMetadata, + CancellationToken cancellationToken ) { var payload = await ReadMetadataPayloadAsync( reader, buffer, MAX_PAX_HEADER_SIZE, - "PAX header" + "PAX header", + cancellationToken ) .ConfigureAwait(false); @@ -353,7 +375,8 @@ private async ValueTask ReadMetadataPayloadAsync( AsyncBinaryReader reader, byte[] buffer, int maxSize, - string payloadName + string payloadName, + CancellationToken cancellationToken ) { var size = ReadSize(buffer); @@ -368,12 +391,14 @@ string payloadName var payloadLength = (int)size; var payload = new byte[payloadLength]; - await reader.ReadBytesAsync(payload, 0, payloadLength).ConfigureAwait(false); + await reader + .ReadBytesAsync(payload, 0, payloadLength, cancellationToken) + .ConfigureAwait(false); var paddingLength = GetPaddingLength(payloadLength); if (paddingLength > 0) { - await ReadLengthAsync(reader, paddingLength).ConfigureAwait(false); + await ReadLengthAsync(reader, paddingLength, cancellationToken).ConfigureAwait(false); } return payload; diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index dd870d515..92a8bbef3 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -161,6 +161,36 @@ out IReader? reader return false; } + internal override async ValueTask TryOpenReaderAsync( + SharpCompressStream sharpCompressStream, + ReaderOptions options, + CancellationToken cancellationToken = default + ) + { + if ( + !await GZipArchive + .IsGZipFileAsync(sharpCompressStream, cancellationToken) + .ConfigureAwait(false) + ) + { + sharpCompressStream.Rewind(); + return null; + } + + sharpCompressStream.Rewind(); + var tarReader = await new TarFactory() + .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) + .ConfigureAwait(false); + if (tarReader is not null) + { + return tarReader; + } + + sharpCompressStream.StopRecording(); + return await OpenAsyncReader(sharpCompressStream, options, cancellationToken) + .ConfigureAwait(false); + } + /// public IReader OpenReader(Stream stream, ReaderOptions? options) => GZipReader.OpenReader(stream, options); diff --git a/src/SharpCompress/Factories/LzwFactory.cs b/src/SharpCompress/Factories/LzwFactory.cs index 5bc333c69..bd5569d11 100644 --- a/src/SharpCompress/Factories/LzwFactory.cs +++ b/src/SharpCompress/Factories/LzwFactory.cs @@ -80,6 +80,36 @@ out IReader? reader return false; } + internal override async ValueTask TryOpenReaderAsync( + SharpCompressStream sharpCompressStream, + ReaderOptions options, + CancellationToken cancellationToken = default + ) + { + if ( + !await LzwStream + .IsLzwStreamAsync(sharpCompressStream, cancellationToken) + .ConfigureAwait(false) + ) + { + sharpCompressStream.Rewind(); + return null; + } + + sharpCompressStream.Rewind(); + var tarReader = await new TarFactory() + .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) + .ConfigureAwait(false); + if (tarReader is not null) + { + return tarReader; + } + + sharpCompressStream.StopRecording(); + return await OpenAsyncReader(sharpCompressStream, options, cancellationToken) + .ConfigureAwait(false); + } + /// public IReader OpenReader(Stream stream, ReaderOptions? options) => LzwReader.OpenReader(stream, options); diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 182a33e4c..7c88b1d6b 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -50,28 +50,7 @@ public override IEnumerable GetSupportedExtensions() /// public override bool IsArchive(Stream stream, ReaderOptions readerOptions) { - var providers = readerOptions.Providers; - var sharpCompressStream = new SharpCompressStream(stream); - sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize); - foreach (var wrapper in TarWrapper.Wrappers) - { - sharpCompressStream.Rewind(); - if (wrapper.IsMatch(sharpCompressStream)) - { - sharpCompressStream.Rewind(); - var decompressedStream = CreateProbeDecompressionStream( - sharpCompressStream, - wrapper.CompressionType - ); - if (TarArchive.IsTarFile(decompressedStream)) - { - sharpCompressStream.Rewind(); - return true; - } - } - } - - return false; + return TarArchive.IsTarFile(stream); } /// @@ -81,38 +60,7 @@ public override async ValueTask IsArchiveAsync( CancellationToken cancellationToken = default ) { - var providers = readerOptions.Providers; - var sharpCompressStream = new SharpCompressStream(stream); - sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize); - foreach (var wrapper in TarWrapper.Wrappers) - { - sharpCompressStream.Rewind(); - if ( - await wrapper - .IsMatchAsync(sharpCompressStream, cancellationToken) - .ConfigureAwait(false) - ) - { - sharpCompressStream.Rewind(); - var decompressedStream = await CreateProbeDecompressionStreamAsync( - sharpCompressStream, - wrapper.CompressionType, - cancellationToken: cancellationToken - ) - .ConfigureAwait(false); - if ( - await TarArchive - .IsTarFileAsync(decompressedStream, cancellationToken) - .ConfigureAwait(false) - ) - { - sharpCompressStream.Rewind(); - return true; - } - } - } - - return false; + return await TarArchive.IsTarFileAsync(stream, cancellationToken).ConfigureAwait(false); } #endregion @@ -234,6 +182,44 @@ await TarArchive throw new InvalidFormatException("Not a tar file."); } + internal override bool TryOpenReader( + SharpCompressStream stream, + ReaderOptions options, + out IReader? reader + ) + { + try + { + stream.Rewind(); + reader = OpenReader(stream, options); + return true; + } + catch (InvalidFormatException) + { + stream.Rewind(); + reader = null; + return false; + } + } + + internal override async ValueTask TryOpenReaderAsync( + SharpCompressStream stream, + ReaderOptions options, + CancellationToken cancellationToken = default + ) + { + try + { + stream.Rewind(); + return await OpenAsyncReader(stream, options, cancellationToken).ConfigureAwait(false); + } + catch (InvalidFormatException) + { + stream.Rewind(); + return null; + } + } + #region IArchiveFactory /// @@ -378,8 +364,7 @@ await TarArchive } } - sharpCompressStream.Rewind(); - return (IAsyncReader)TarReader.OpenReader(sharpCompressStream, options); + throw new InvalidFormatException("Not a tar file."); } #endregion diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index 15d53cc5b..199dff47b 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -407,18 +407,8 @@ bool expectedRandomAccess [InlineData("Tar.LongPathsWithLongNameExtension.tar", ArchiveType.Tar, true)] [InlineData("Tar.mod.tar", ArchiveType.Tar, true)] [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] - [InlineData("Tar.noEmptyDirs.tar.bz2", ArchiveType.Tar, true)] - [InlineData("Tar.noEmptyDirs.tar.lz", ArchiveType.Tar, true)] - [InlineData("Tar.oldgnu.tar.gz", ArchiveType.Tar, true)] [InlineData("Tar.tar", ArchiveType.Tar, true)] - [InlineData("Tar.tar.Z", ArchiveType.Tar, true)] - [InlineData("Tar.tar.bz2", ArchiveType.Tar, true)] - [InlineData("Tar.tar.gz", ArchiveType.Tar, true)] - [InlineData("Tar.tar.lz", ArchiveType.Tar, true)] - [InlineData("Tar.tar.xz", ArchiveType.Tar, true)] - [InlineData("Tar.tar.zst", ArchiveType.Tar, true)] [InlineData("TarCorrupted.tar", ArchiveType.Tar, true)] - [InlineData("TarWithSymlink.tar.gz", ArchiveType.Tar, true)] [InlineData("WinZip26.zip", ArchiveType.Zip, true)] [InlineData("WinZip26_BZip2.zipx", ArchiveType.Zip, true)] [InlineData("WinZip26_LZMA.zipx", ArchiveType.Zip, true)] @@ -574,18 +564,8 @@ bool expectedSeekable [InlineData("Tar.LongPathsWithLongNameExtension.tar", ArchiveType.Tar, true)] [InlineData("Tar.mod.tar", ArchiveType.Tar, true)] [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] - [InlineData("Tar.noEmptyDirs.tar.bz2", ArchiveType.Tar, true)] - [InlineData("Tar.noEmptyDirs.tar.lz", ArchiveType.Tar, true)] - [InlineData("Tar.oldgnu.tar.gz", ArchiveType.Tar, true)] [InlineData("Tar.tar", ArchiveType.Tar, true)] - [InlineData("Tar.tar.Z", ArchiveType.Tar, true)] - [InlineData("Tar.tar.bz2", ArchiveType.Tar, true)] - [InlineData("Tar.tar.gz", ArchiveType.Tar, true)] - [InlineData("Tar.tar.lz", ArchiveType.Tar, true)] - [InlineData("Tar.tar.xz", ArchiveType.Tar, true)] - [InlineData("Tar.tar.zst", ArchiveType.Tar, true)] [InlineData("TarCorrupted.tar", ArchiveType.Tar, true)] - [InlineData("TarWithSymlink.tar.gz", ArchiveType.Tar, true)] [InlineData("WinZip26.zip", ArchiveType.Zip, true)] [InlineData("WinZip26_BZip2.zipx", ArchiveType.Zip, true)] [InlineData("WinZip26_LZMA.zipx", ArchiveType.Zip, true)] @@ -676,6 +656,74 @@ public async ValueTask GetArchiveInformationAsync_ReturnsNull_ForNonArchive() Assert.Null(info); } + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.lz")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + [InlineData("Tar.tar.Z")] + public void IsArchive_ReturnsFalse_ForCompressedTar(string archiveName) + { + using var stream = File.OpenRead(GetTestArchivePath(archiveName)); + + var isArchive = ArchiveFactory.IsArchive(stream, out var archiveType); + + Assert.False(isArchive); + Assert.Null(archiveType); + } + + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.lz")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + [InlineData("Tar.tar.Z")] + public async ValueTask IsArchiveAsync_ReturnsFalse_ForCompressedTar(string archiveName) + { + using var stream = File.OpenRead(GetTestArchivePath(archiveName)); + + var (isArchive, archiveType) = await ArchiveFactory.IsArchiveAsync(stream); + + Assert.False(isArchive); + Assert.Null(archiveType); + } + + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.lz")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + [InlineData("Tar.tar.Z")] + public void GetArchiveInformation_ReturnsNull_ForCompressedTar(string archiveName) + { + using var stream = File.OpenRead(GetTestArchivePath(archiveName)); + + var info = ArchiveFactory.GetArchiveInformation(stream); + + Assert.Null(info); + } + + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.lz")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + [InlineData("Tar.tar.Z")] + public async ValueTask GetArchiveInformationAsync_ReturnsNull_ForCompressedTar( + string archiveName + ) + { + using var stream = File.OpenRead(GetTestArchivePath(archiveName)); + + var info = await ArchiveFactory.GetArchiveInformationAsync(stream); + + Assert.Null(info); + } + [Theory] [InlineData("Zip.deflate.zip", ArchiveType.Zip)] [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] diff --git a/tests/SharpCompress.Test/AsyncParityAndCancellationTests.cs b/tests/SharpCompress.Test/AsyncParityAndCancellationTests.cs index 49beacfab..8bad2b1d2 100644 --- a/tests/SharpCompress.Test/AsyncParityAndCancellationTests.cs +++ b/tests/SharpCompress.Test/AsyncParityAndCancellationTests.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using SharpCompress.Archives; +using SharpCompress.Archives.Tar; using SharpCompress.Common; using SharpCompress.Readers; using SharpCompress.Test.Mocks; @@ -19,7 +20,6 @@ public class AsyncParityAndCancellationTests : TestBase [Theory] [InlineData("Zip.deflate.zip")] [InlineData("Tar.tar")] - [InlineData("Tar.tar.gz")] [InlineData("Rar.rar")] [InlineData("7Zip.nonsolid.7z")] public async Task ArchiveAsyncEntries_ShouldMatchSyncEntries(string archiveName) @@ -74,6 +74,34 @@ await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH, cancellationToken: cts.T ); } + [Fact] + public async Task TarArchiveOpenAsyncArchive_ShouldRespectCancellationBeforeValidationAsync() + { + await using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")); + using var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + + await Assert.ThrowsAnyAsync(async () => + await TarArchive.OpenAsyncArchive(stream, cancellationToken: cts.Token) + ); + } + + [Fact] + public async Task TarArchiveOpenAsyncArchive_ShouldRespectCancellationDuringValidationAsync() + { + var archiveBytes = CreateLargeTarArchive(); + using var cts = new CancellationTokenSource(); + await using var stream = new CancelAfterBytesReadStream( + new MemoryStream(archiveBytes), + cts, + cancelAfterBytes: 128 + ); + + await Assert.ThrowsAnyAsync(async () => + await TarArchive.OpenAsyncArchive(stream, cancellationToken: cts.Token) + ); + } + [Fact] public async Task AsyncReaderExtraction_ShouldRespectCancellationDuringRead() { diff --git a/tests/SharpCompress.Test/CompressionProviderTests.cs b/tests/SharpCompress.Test/CompressionProviderTests.cs index 1b043efd4..fc183359b 100644 --- a/tests/SharpCompress.Test/CompressionProviderTests.cs +++ b/tests/SharpCompress.Test/CompressionProviderTests.cs @@ -1,13 +1,11 @@ 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.Archives; using SharpCompress.Common; -using SharpCompress.Common.Options; using SharpCompress.Compressors; using SharpCompress.IO; using SharpCompress.Providers; @@ -524,7 +522,7 @@ public void ReaderOptions_WithProviders_CanBeCloned() } [Fact] - public void TarArchive_OpenArchive_UsesCustomGZipProvider() + public void TarReader_OpenReader_UsesCustomGZipProvider() { using var archiveStream = new MemoryStream(); using ( @@ -543,9 +541,9 @@ public void TarArchive_OpenArchive_UsesCustomGZipProvider() var readOptions = ReaderOptions.ForExternalStream.WithProviders(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 reader = TarReader.OpenReader(archiveStream, readOptions); + reader.MoveToNextEntry().Should().BeTrue(); + using var entryStream = reader.OpenEntryStream(); using var resultStream = new MemoryStream(); entryStream.CopyTo(resultStream); @@ -553,7 +551,7 @@ public void TarArchive_OpenArchive_UsesCustomGZipProvider() } [Fact] - public async Task TarArchive_OpenAsyncArchive_UsesCustomGZipProvider() + public async Task TarReader_OpenAsyncReader_UsesCustomGZipProvider() { using var archiveStream = new MemoryStream(); using ( @@ -572,23 +570,71 @@ public async Task TarArchive_OpenAsyncArchive_UsesCustomGZipProvider() var readOptions = ReaderOptions.ForExternalStream.WithProviders(registry); archiveStream.Position = 0; - await using var archive = await TarArchive.OpenAsyncArchive(archiveStream, readOptions); - await foreach (var entry in archive.EntriesAsync) - { - if (entry.IsDirectory) - { - continue; - } + await using var reader = await TarReader.OpenAsyncReader(archiveStream, readOptions); + (await reader.MoveToNextEntryAsync()).Should().BeTrue(); + using var entryStream = await reader.OpenEntryStreamAsync(); + using var resultStream = new MemoryStream(); + await entryStream.CopyToAsync(resultStream); - using var entryStream = await entry.OpenEntryStreamAsync(); - using var resultStream = new MemoryStream(); - await entryStream.CopyToAsync(resultStream); - break; - } + trackingProvider.AsyncDecompressionCalls.Should().BeGreaterThan(0); + } + + [Fact] + public void ArchiveFactory_OpenArchive_UsesCustomProviderWhenRejectingCompressedTar() + { + using var archiveStream = CreateGZipTarStream(); + var trackingProvider = new TrackingCompressionProvider(new GZipCompressionProvider()); + var options = ReaderOptions.ForExternalStream.WithProviders( + CompressionProviderRegistry.Default.With(trackingProvider) + ); + + Action open = () => ArchiveFactory.OpenArchive(archiveStream, options); + + open.Should().Throw(); + trackingProvider.DecompressionCalls.Should().BeGreaterThan(0); + } + + [Fact] + public async Task ArchiveFactory_OpenAsyncArchive_UsesCustomProviderWhenRejectingCompressedTarAsync() + { + using var archiveStream = CreateGZipTarStream(); + var trackingProvider = new TrackingCompressionProvider(new GZipCompressionProvider()); + var options = ReaderOptions.ForExternalStream.WithProviders( + CompressionProviderRegistry.Default.With(trackingProvider) + ); + Func open = async () => + await ArchiveFactory.OpenAsyncArchive( + archiveStream, + options, + TestContext.Current.CancellationToken + ); + + await open.Should().ThrowAsync(); trackingProvider.AsyncDecompressionCalls.Should().BeGreaterThan(0); } + private static MemoryStream CreateGZipTarStream() + { + var archiveStream = new MemoryStream(); + using ( + var writer = new TarWriter( + archiveStream, + new TarWriterOptions(CompressionType.GZip, true) + ) + ) + { + writer.Write( + "test.txt", + new MemoryStream(Encoding.UTF8.GetBytes("tar archive provider usage")), + DateTime.Now + ); + } + + archiveStream.Position = 0; + return archiveStream; + } + [Fact] public async Task ZipReader_OpenEntryStreamAsync_UsesCustomDeflateProvider() { diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 28947e97d..3a1b95ccb 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -8,7 +8,6 @@ using SharpCompress.Archives.Tar; using SharpCompress.Common; using SharpCompress.Readers; -using SharpCompress.Readers.Tar; using SharpCompress.Test.Mocks; using SharpCompress.Writers; using SharpCompress.Writers.Tar; @@ -35,6 +34,38 @@ await TarArchive.OpenAsyncArchive(new AsyncOnlyStream(stream)) ); } + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.lz")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + [InlineData("Tar.tar.Z")] + public async ValueTask TarArchiveOpenAsyncArchive_RejectsCompressedTar(string archiveName) + { + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + + await Assert.ThrowsAsync(async () => + await TarArchive.OpenAsyncArchive(stream) + ); + } + + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.lz")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + [InlineData("Tar.tar.Z")] + public async ValueTask ArchiveFactoryOpenAsyncArchive_RejectsCompressedTar(string archiveName) + { + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + + await Assert.ThrowsAsync(async () => + await ArchiveFactory.OpenAsyncArchive(stream) + ); + } + [Fact] public async ValueTask TarArchiveOpenAsyncStream_Throws_On_Unreadable_Stream() { diff --git a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs index 7959967e7..1b4cd0bc4 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -486,14 +486,53 @@ public void Tar_Detect_Test() Assert.False(isTar); } - [Fact] - public void TarArchiveStreamRead_Autodetect_CompressedTar() + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.lz")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + [InlineData("Tar.tar.Z")] + public void ArchiveFactoryStreamRead_Autodetect_RejectsCompressedTar(string archiveName) { - using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); - using var archive = ArchiveFactory.OpenArchive(stream); + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + + Assert.Throws(() => ArchiveFactory.OpenArchive(stream)); + } + + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.lz")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + [InlineData("Tar.tar.Z")] + public void TarArchiveOpenArchive_RejectsCompressedTar(string archiveName) + { + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); - Assert.Equal(ArchiveType.Tar, archive.Type); - Assert.NotEmpty(archive.Entries); + Assert.Throws(() => TarArchive.OpenArchive(stream)); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public void TarArchiveOpenArchive_RejectionHonorsLeaveStreamOpen( + bool leaveStreamOpen, + bool expectedDisposed + ) + { + using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); + var stream = new TestStream(file); + var options = ReaderOptions.ForExternalStream.WithLeaveStreamOpen(leaveStreamOpen); + + Assert.Throws(() => TarArchive.OpenArchive(stream, options)); + + Assert.Equal(expectedDisposed, stream.IsDisposed); + if (!stream.IsDisposed) + { + stream.Dispose(); + } } [Fact] diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index d0104cbbe..90bce78eb 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -46,6 +46,25 @@ public async ValueTask Tar_Skip_Async() public async ValueTask Tar_Z_Reader_Async() => await ReadAsync("Tar.tar.Z", CompressionType.Lzw); + [Theory] + [InlineData("Tar.tar.gz", "gz", CompressionType.GZip)] + [InlineData("Tar.tar.Z", "z", CompressionType.Lzw)] + public async ValueTask ReaderFactory_ExtensionHint_PreservesCompressedTarDetection_Async( + string archiveName, + string extensionHint, + CompressionType compressionType + ) + { + using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + var options = ReaderOptions.ForExternalStream.WithExtensionHint(extensionHint); + + await using var reader = await ReaderFactory.OpenAsyncReader(stream, options); + + Assert.Equal(ArchiveType.Tar, reader.Type); + Assert.True(await reader.MoveToNextEntryAsync()); + Assert.Equal(compressionType, reader.Entry.CompressionType); + } + [Fact] public async ValueTask Tar_Async_Assert() => await AssertArchiveAsync("Tar.tar");