From b8a7cc18d9394a5dad4cd51c0ca0f1f294fdbb38 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 24 Jun 2026 16:27:57 +0100 Subject: [PATCH] intermediate commit --- .../tar-format/references/tar-format.md | 2 +- docs/API.md | 2 +- docs/FORMATS.md | 2 +- docs/TAR_SPEC.md | 2 +- .../SharpCompress.Test/ArchiveFactoryTests.cs | 54 ++++++++++++++++++- .../Tar/TarArchiveAsyncTests.cs | 20 +++++-- .../SharpCompress.Test/Tar/TarArchiveTests.cs | 19 ++++--- 7 files changed, 84 insertions(+), 17 deletions(-) diff --git a/.agents/skills/tar-format/references/tar-format.md b/.agents/skills/tar-format/references/tar-format.md index 22d4bbb9..72b9c510 100644 --- a/.agents/skills/tar-format/references/tar-format.md +++ b/.agents/skills/tar-format/references/tar-format.md @@ -280,7 +280,7 @@ Archive API: - `TarArchive.OpenArchive(Stream)` and `TarArchive.OpenAsyncArchive(Stream)` require seekable raw tar streams. - File/path overloads own the opened file stream. -- Compressed tar wrappers are supported through `TarReader`, not `TarArchive`. +- 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: diff --git a/docs/API.md b/docs/API.md index 39a194de..80143e83 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. Compressed tar wrappers such as `.tar.gz` and `.tar.xz` are also reader-only; use `ReaderFactory.OpenReader` or `TarReader.OpenReader` for those files. +`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 fdb8a3ea..22a7080a 100644 --- a/docs/FORMATS.md +++ b/docs/FORMATS.md @@ -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. Compressed tar wrappers are supported by `ReaderFactory`/`TarReader`, not by `ArchiveFactory`/`TarArchive`. +`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 f3d7238c..b2714445 100644 --- a/docs/TAR_SPEC.md +++ b/docs/TAR_SPEC.md @@ -84,7 +84,7 @@ 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 wrapper detection is supported through the recording and rewind mechanism on the reader path. -- `ArchiveFactory`/`TarArchive` do not open compressed tar wrappers; use `ReaderFactory`/`TarReader` for `.tar.gz`, `.tar.bz2`, `.tar.xz`, `.tar.zst`, `.tar.lz`, and `.tar.Z`. +- `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. diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index de09dc72..1f8b6df0 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -409,7 +409,6 @@ public class ArchiveFactoryTests : TestBase [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] [InlineData("Tar.tar", ArchiveType.Tar, true)] [InlineData("TarCorrupted.tar", ArchiveType.Tar, true)] - [InlineData("TarWithSymlink.tar.gz", ArchiveType.GZip, true)] [InlineData("WinZip26.zip", ArchiveType.Zip, true)] [InlineData("WinZip26_BZip2.zipx", ArchiveType.Zip, true)] [InlineData("WinZip26_LZMA.zipx", ArchiveType.Zip, true)] @@ -567,7 +566,6 @@ public class ArchiveFactoryTests : TestBase [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] [InlineData("Tar.tar", ArchiveType.Tar, true)] [InlineData("TarCorrupted.tar", ArchiveType.Tar, true)] - [InlineData("TarWithSymlink.tar.gz", ArchiveType.GZip, true)] [InlineData("WinZip26.zip", ArchiveType.Zip, true)] [InlineData("WinZip26_BZip2.zipx", ArchiveType.Zip, true)] [InlineData("WinZip26_LZMA.zipx", ArchiveType.Zip, true)] @@ -658,6 +656,58 @@ public class ArchiveFactoryTests : TestBase Assert.Null(info); } + [Theory] + [InlineData("Tar.tar.gz")] + [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.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.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.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/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 88c7e4cc..5f5976c7 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -35,16 +35,30 @@ public class TarArchiveAsyncTests : ArchiveTests ); } - [Fact] - public async ValueTask TarArchiveOpenAsyncArchive_RejectsCompressedTar() + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.Z")] + public async ValueTask TarArchiveOpenAsyncArchive_RejectsCompressedTar(string archiveName) { - using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); + 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.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 21b6cead..41aa47ac 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -486,19 +486,22 @@ public class TarArchiveTests : ArchiveTests Assert.False(isTar); } - [Fact] - public void ArchiveFactoryStreamRead_Autodetect_CompressedTar_AsGZip() + [Theory] + [InlineData("Tar.tar.gz")] + [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.Equal(ArchiveType.GZip, archive.Type); + Assert.Throws(() => ArchiveFactory.OpenArchive(stream)); } - [Fact] - public void TarArchiveOpenArchive_RejectsCompressedTar() + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.Z")] + public void TarArchiveOpenArchive_RejectsCompressedTar(string archiveName) { - using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); Assert.Throws(() => TarArchive.OpenArchive(stream)); }