From 76300b9c970fba1874b13bbe19d5dde1a054df02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:38:36 +0000 Subject: [PATCH 1/9] Initial plan From a176ffee20fc2d6f4e683d49c0174ab0a436796a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:42:21 +0000 Subject: [PATCH 2/9] initial plan Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/324eaee6-e733-4193-b619-6eb75b435ec2 Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/packages.lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index e82196f8..03c03a9a 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.6, )", - "resolved": "10.0.6", - "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.26, )", - "resolved": "8.0.26", - "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", From 6c59326628bd33f1c3438bfd4d9e8402c0682fef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:47:12 +0000 Subject: [PATCH 3/9] Add ArchiveInformation record and GetArchiveInformation API for consolidated archive detection Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/324eaee6-e733-4193-b619-6eb75b435ec2 Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Archives/ArchiveFactory.Async.cs | 48 +++++++++ src/SharpCompress/Archives/ArchiveFactory.cs | 38 +++++++ .../Archives/ArchiveInformation.cs | 22 ++++ src/SharpCompress/packages.lock.json | 12 +-- .../SharpCompress.Test/ArchiveFactoryTests.cs | 100 ++++++++++++++++++ 5 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 src/SharpCompress/Archives/ArchiveInformation.cs diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 84bb671d..f2c945c6 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -110,6 +110,54 @@ public static partial class ArchiveFactory .ConfigureAwait(false); } + /// + /// Returns information about the archive at the given file path asynchronously, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + string filePath, + CancellationToken cancellationToken = default + ) + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return await GetArchiveInformationAsync(stream, cancellationToken).ConfigureAwait(false); + } + + /// + /// Returns information about the archive in the given stream asynchronously, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + stream.RequireReadable(); + stream.RequireSeekable(); + + var startPosition = stream.Position; + + foreach (var factory in Factory.Factories) + { + var isArchive = await factory + .IsArchiveAsync(stream, cancellationToken: cancellationToken) + .ConfigureAwait(false); + stream.Position = startPosition; + + if (isArchive) + { + return new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + } + + return null; + } + internal static ValueTask FindFactoryAsync( string filePath, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 34cc5bb1..3767c7c9 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -176,6 +176,44 @@ public static partial class ArchiveFactory return false; } + /// + /// Returns information about the archive at the given file path, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + public static ArchiveInformation? GetArchiveInformation(string filePath) + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return GetArchiveInformation(stream); + } + + /// + /// Returns information about the archive in the given stream, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + public static ArchiveInformation? GetArchiveInformation(Stream stream) + { + stream.RequireReadable(); + stream.RequireSeekable(); + + var startPosition = stream.Position; + + foreach (var factory in Factory.Factories) + { + var isArchive = factory.IsArchive(stream); + stream.Position = startPosition; + + if (isArchive) + { + return new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + } + + return null; + } + public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( string filePath, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/ArchiveInformation.cs b/src/SharpCompress/Archives/ArchiveInformation.cs new file mode 100644 index 00000000..adc0dfa2 --- /dev/null +++ b/src/SharpCompress/Archives/ArchiveInformation.cs @@ -0,0 +1,22 @@ +using SharpCompress.Common; + +namespace SharpCompress.Archives; + +/// +/// Contains information about a detected archive, including its type and supported capabilities. +/// +/// +/// Use or +/// +/// to obtain an instance of this record. +/// +/// +/// The type of archive detected, or when the format is not a registered well-known type. +/// +/// +/// when this archive format supports random access via the API, +/// meaning the full file listing can be retrieved without decompressing the entire archive. +/// when only the API is available, +/// which reads entries sequentially and can only report per-entry progress. +/// +public record ArchiveInformation(ArchiveType? Type, bool SupportsRandomAccess); diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 03c03a9a..a401c702 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.5, )", - "resolved": "10.0.5", - "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.25, )", - "resolved": "8.0.25", - "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" + "requested": "[8.0.22, )", + "resolved": "8.0.22", + "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index d6326bad..867561d9 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -159,6 +159,106 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(0, stream.Position); } + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + public void GetArchiveInformation_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = ArchiveFactory.GetArchiveInformation( + Path.Combine(TEST_ARCHIVES_PATH, archiveName) + ); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + public async ValueTask GetArchiveInformationAsync_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = await ArchiveFactory.GetArchiveInformationAsync( + Path.Combine(TEST_ARCHIVES_PATH, archiveName) + ); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + + [Fact] + public void GetArchiveInformation_ReturnsNull_ForNonArchive() + { + using var stream = new MemoryStream(Encoding.ASCII.GetBytes("not an archive")); + + var info = ArchiveFactory.GetArchiveInformation(stream); + + Assert.Null(info); + } + + [Fact] + public async ValueTask GetArchiveInformationAsync_ReturnsNull_ForNonArchive() + { + using var stream = new MemoryStream(Encoding.ASCII.GetBytes("not an archive")); + + var info = await ArchiveFactory.GetArchiveInformationAsync(stream); + + Assert.Null(info); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + public void GetArchiveInformation_Stream_PreservesPosition( + string archiveName, + ArchiveType expectedType + ) + { + using var stream = CreatePrefixedArchiveStream(archiveName, 13); + var startPosition = stream.Position; + + var info = ArchiveFactory.GetArchiveInformation(stream); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(startPosition, stream.Position); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + public async ValueTask GetArchiveInformationAsync_Stream_PreservesPosition( + string archiveName, + ArchiveType expectedType + ) + { + using var stream = CreatePrefixedArchiveStream(archiveName, 13); + var startPosition = stream.Position; + + var info = await ArchiveFactory.GetArchiveInformationAsync(stream); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(startPosition, stream.Position); + } + private MemoryStream CreatePrefixedArchiveStream(string archiveName, int prefixLength) { var archiveBytes = File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); From f118935bb8c895e132c4e48c77753b95d9291771 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:13:00 +0000 Subject: [PATCH 4/9] Consolidate archive detection into shared TryFindFactory helpers Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/9c5bf06d-3e2f-4bb5-9130-f5b7cdd1d0a0 Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Archives/ArchiveFactory.Async.cs | 59 +++++++----- src/SharpCompress/Archives/ArchiveFactory.cs | 92 +++++++++---------- src/SharpCompress/packages.lock.json | 12 +-- 3 files changed, 81 insertions(+), 82 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index f2c945c6..a96bbefa 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -140,22 +140,10 @@ public static partial class ArchiveFactory stream.RequireReadable(); stream.RequireSeekable(); - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - var isArchive = await factory - .IsArchiveAsync(stream, cancellationToken: cancellationToken) - .ConfigureAwait(false); - stream.Position = startPosition; - - if (isArchive) - { - return new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); - } - } - - return null; + var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + return factory is null + ? null + : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); } internal static ValueTask FindFactoryAsync( @@ -188,14 +176,39 @@ public static partial class ArchiveFactory stream.RequireReadable(); stream.RequireSeekable(); - var factories = Factory.Factories.OfType(); + // Use the shared async detection loop over all factories. If the matched factory + // 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); + if (factory is T typedFactory) + { + return typedFactory; + } + var extensions = string.Join(", ", Factory.Factories.OfType().Select(item => item.Name)); + + throw new ArchiveOperationException( + $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" + ); + } + + /// + /// Async counterpart of . + /// 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 + ) + { var startPosition = stream.Position; - foreach (var factory in factories) + foreach (var factory in Factory.Factories) { stream.Seek(startPosition, SeekOrigin.Begin); - if ( await factory .IsArchiveAsync(stream, cancellationToken: cancellationToken) @@ -203,15 +216,11 @@ public static partial class ArchiveFactory ) { stream.Seek(startPosition, SeekOrigin.Begin); - return factory; } } - var extensions = string.Join(", ", factories.Select(item => item.Name)); - - throw new ArchiveOperationException( - $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" - ); + stream.Seek(startPosition, SeekOrigin.Begin); + return null; } } diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 3767c7c9..0f236a0e 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -123,23 +123,17 @@ public static partial class ArchiveFactory stream.RequireReadable(); stream.RequireSeekable(); - var factories = Factory.Factories.OfType(); - - var startPosition = stream.Position; - - foreach (var factory in factories) + // Use the shared detection loop over all factories. If the matched factory + // 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); + if (factory is T typedFactory) { - stream.Seek(startPosition, SeekOrigin.Begin); - - if (factory.IsArchive(stream)) - { - stream.Seek(startPosition, SeekOrigin.Begin); - - return factory; - } + return typedFactory; } - var extensions = string.Join(", ", factories.Select(item => item.Name)); + var extensions = string.Join(", ", Factory.Factories.OfType().Select(item => item.Name)); throw new ArchiveOperationException( $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" @@ -155,25 +149,12 @@ public static partial class ArchiveFactory public static bool IsArchive(Stream stream, out ArchiveType? type) { - type = null; stream.RequireReadable(); stream.RequireSeekable(); - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - var isArchive = factory.IsArchive(stream); - stream.Position = startPosition; - - if (isArchive) - { - type = factory.KnownArchiveType; - return true; - } - } - - return false; + var factory = TryFindFactory(stream); + type = factory?.KnownArchiveType; + return factory is not null; } /// @@ -198,19 +179,42 @@ public static partial class ArchiveFactory stream.RequireReadable(); stream.RequireSeekable(); + var factory = TryFindFactory(stream); + return factory is null + ? null + : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + + /// + /// 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. + /// + /// + /// This is the shared, seekable-stream detection core used by + /// , , + /// and . + /// + /// uses a separate code path + /// based on rewindable buffering, which supports + /// non-seekable streams and is therefore not unified with this helper. + /// + /// + private static IFactory? TryFindFactory(Stream stream) + { var startPosition = stream.Position; foreach (var factory in Factory.Factories) { - var isArchive = factory.IsArchive(stream); - stream.Position = startPosition; - - if (isArchive) + stream.Seek(startPosition, SeekOrigin.Begin); + if (factory.IsArchive(stream)) { - return new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + stream.Seek(startPosition, SeekOrigin.Begin); + return factory; } } + stream.Seek(startPosition, SeekOrigin.Begin); return null; } @@ -232,22 +236,8 @@ public static partial class ArchiveFactory stream.RequireReadable(); stream.RequireSeekable(); - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - var isArchive = await factory - .IsArchiveAsync(stream, cancellationToken: cancellationToken) - .ConfigureAwait(false); - stream.Position = startPosition; - - if (isArchive) - { - return (true, factory.KnownArchiveType); - } - } - - return (false, null); + var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + return (factory is not null, factory?.KnownArchiveType); } public static IEnumerable GetFileParts(string part1) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index a401c702..03c03a9a 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.22, )", - "resolved": "8.0.22", - "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", From d2c06e95d6b1d805907a5582def96bfe886abbf4 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 26 Apr 2026 11:07:11 +0100 Subject: [PATCH 5/9] Clean up and move detection to new file --- .../Archives/ArchiveFactory.Async.cs | 116 ----------- .../Archives/ArchiveFactory.Detection.cs | 187 ++++++++++++++++++ src/SharpCompress/Archives/ArchiveFactory.cs | 61 ------ 3 files changed, 187 insertions(+), 177 deletions(-) create mode 100644 src/SharpCompress/Archives/ArchiveFactory.Detection.cs diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index a96bbefa..6c45cb1f 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -1,11 +1,9 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; -using SharpCompress.Factories; using SharpCompress.Readers; namespace SharpCompress.Archives; @@ -109,118 +107,4 @@ public static partial class ArchiveFactory .OpenAsyncArchive(streamsArray, options, cancellationToken) .ConfigureAwait(false); } - - /// - /// Returns information about the archive at the given file path asynchronously, - /// or if the file is not a recognized archive. - /// - /// Path to the archive file. - /// Cancellation token. - public static async ValueTask GetArchiveInformationAsync( - string filePath, - CancellationToken cancellationToken = default - ) - { - filePath.NotNullOrEmpty(nameof(filePath)); - using Stream stream = File.OpenRead(filePath); - return await GetArchiveInformationAsync(stream, cancellationToken).ConfigureAwait(false); - } - - /// - /// Returns information about the archive in the given stream asynchronously, - /// or if the stream is not a recognized archive. - /// - /// A readable and seekable stream positioned at the start of the archive. - /// Cancellation token. - public static async ValueTask GetArchiveInformationAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - stream.RequireReadable(); - stream.RequireSeekable(); - - var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); - return factory is null - ? null - : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); - } - - internal static ValueTask FindFactoryAsync( - string filePath, - CancellationToken cancellationToken = default - ) - where T : IFactory - { - filePath.NotNullOrEmpty(nameof(filePath)); - return FindFactoryAsync(new FileInfo(filePath), cancellationToken); - } - - internal static async ValueTask FindFactoryAsync( - FileInfo finfo, - CancellationToken cancellationToken = default - ) - where T : IFactory - { - finfo.NotNull(nameof(finfo)); - using Stream stream = finfo.OpenRead(); - return await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); - } - - internal static async ValueTask FindFactoryAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - where T : IFactory - { - stream.RequireReadable(); - stream.RequireSeekable(); - - // Use the shared async detection loop over all factories. If the matched factory - // 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); - if (factory is T typedFactory) - { - return typedFactory; - } - - var extensions = string.Join(", ", Factory.Factories.OfType().Select(item => item.Name)); - - throw new ArchiveOperationException( - $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" - ); - } - - /// - /// Async counterpart of . - /// 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 - ) - { - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - stream.Seek(startPosition, SeekOrigin.Begin); - if ( - await factory - .IsArchiveAsync(stream, cancellationToken: cancellationToken) - .ConfigureAwait(false) - ) - { - stream.Seek(startPosition, SeekOrigin.Begin); - return factory; - } - } - - stream.Seek(startPosition, SeekOrigin.Begin); - return null; - } } diff --git a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs new file mode 100644 index 00000000..ae04e09f --- /dev/null +++ b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs @@ -0,0 +1,187 @@ +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Factories; +using SharpCompress.Readers; + +namespace SharpCompress.Archives; + +public static partial class ArchiveFactory +{ + /// + /// Returns information about the archive at the given file path asynchronously, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + string filePath, + CancellationToken cancellationToken = default + ) + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return await GetArchiveInformationAsync(stream, cancellationToken).ConfigureAwait(false); + } + + /// + /// Returns information about the archive in the given stream asynchronously, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + stream.RequireReadable(); + stream.RequireSeekable(); + + var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + return factory is null + ? null + : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + + internal static ValueTask FindFactoryAsync( + string filePath, + CancellationToken cancellationToken = default + ) + where T : IFactory + { + filePath.NotNullOrEmpty(nameof(filePath)); + return FindFactoryAsync(new FileInfo(filePath), cancellationToken); + } + + internal static async ValueTask FindFactoryAsync( + FileInfo fileInfo, + CancellationToken cancellationToken = default + ) + where T : IFactory + { + fileInfo.NotNull(nameof(fileInfo)); + using Stream stream = fileInfo.OpenRead(); + return await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + } + + internal static async ValueTask FindFactoryAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + where T : IFactory + { + stream.RequireReadable(); + stream.RequireSeekable(); + + // Use the shared async detection loop over all factories. If the matched factory + // 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); + if (factory is T typedFactory) + { + return typedFactory; + } + + var extensions = string.Join(", ", Factory.Factories.OfType().Select(item => item.Name)); + + throw new ArchiveOperationException( + $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" + ); + } + + /// + /// Async counterpart of . + /// 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 + ) + { + var startPosition = stream.Position; + + foreach (var factory in Factory.Factories) + { + stream.Seek(startPosition, SeekOrigin.Begin); + if ( + await factory + .IsArchiveAsync(stream, cancellationToken: cancellationToken) + .ConfigureAwait(false) + ) + { + stream.Seek(startPosition, SeekOrigin.Begin); + return factory; + } + } + + stream.Seek(startPosition, SeekOrigin.Begin); + return null; + } + + /// + /// Returns information about the archive at the given file path, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + public static ArchiveInformation? GetArchiveInformation(string filePath) + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return GetArchiveInformation(stream); + } + + /// + /// Returns information about the archive in the given stream, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + public static ArchiveInformation? GetArchiveInformation(Stream stream) + { + stream.RequireReadable(); + stream.RequireSeekable(); + + var factory = TryFindFactory(stream); + return factory is null + ? null + : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + + /// + /// 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. + /// + /// + /// This is the shared, seekable-stream detection core used by + /// , , + /// and . + /// + /// uses a separate code path + /// based on rewindable buffering, which supports + /// non-seekable streams and is therefore not unified with this helper. + /// + /// + private static IFactory? TryFindFactory(Stream stream) + { + var startPosition = stream.Position; + + foreach (var factory in Factory.Factories) + { + stream.Seek(startPosition, SeekOrigin.Begin); + if (factory.IsArchive(stream)) + { + stream.Seek(startPosition, SeekOrigin.Begin); + return factory; + } + } + + stream.Seek(startPosition, SeekOrigin.Begin); + return null; + } +} diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 0f236a0e..d6f08cc6 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -157,67 +157,6 @@ public static partial class ArchiveFactory return factory is not null; } - /// - /// Returns information about the archive at the given file path, - /// or if the file is not a recognized archive. - /// - /// Path to the archive file. - public static ArchiveInformation? GetArchiveInformation(string filePath) - { - filePath.NotNullOrEmpty(nameof(filePath)); - using Stream stream = File.OpenRead(filePath); - return GetArchiveInformation(stream); - } - - /// - /// Returns information about the archive in the given stream, - /// or if the stream is not a recognized archive. - /// - /// A readable and seekable stream positioned at the start of the archive. - public static ArchiveInformation? GetArchiveInformation(Stream stream) - { - stream.RequireReadable(); - stream.RequireSeekable(); - - var factory = TryFindFactory(stream); - return factory is null - ? null - : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); - } - - /// - /// 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. - /// - /// - /// This is the shared, seekable-stream detection core used by - /// , , - /// and . - /// - /// uses a separate code path - /// based on rewindable buffering, which supports - /// non-seekable streams and is therefore not unified with this helper. - /// - /// - private static IFactory? TryFindFactory(Stream stream) - { - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - stream.Seek(startPosition, SeekOrigin.Begin); - if (factory.IsArchive(stream)) - { - stream.Seek(startPosition, SeekOrigin.Begin); - return factory; - } - } - - stream.Seek(startPosition, SeekOrigin.Begin); - return null; - } - public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( string filePath, CancellationToken cancellationToken = default From 4fc08534c2c902a93d854b554d5e732f8ec96a58 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 26 Apr 2026 11:18:27 +0100 Subject: [PATCH 6/9] add detection test --- .../SharpCompress.Test/ArchiveFactoryTests.cs | 180 +++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index 867561d9..26a62acd 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -203,6 +203,173 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); } + [Theory] + [InlineData("64bitstream.zip.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARMT.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BZip2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Copy.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.EmptyStream.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Filters.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.IA64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPMd.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.RISCV.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.SPARC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Tar.tar", ArchiveType.Tar, true)] + [InlineData("7Zip.Tar.tar.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ZSTD.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.distance.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.encryptedFiles.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.eos.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.1block.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.encrypted.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.largefile.ace", ArchiveType.Ace, false)] + [InlineData("Arc.crunched.arc", ArchiveType.Arc, false)] + [InlineData("Arc.crunched.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arj.encrypted.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Issue_685.zip", ArchiveType.Zip, true)] + [InlineData("PrePostHeaders.zip", ArchiveType.Zip, true)] + [InlineData("Rar.Audio_program.rar", ArchiveType.Rar, true)] + [InlineData("Rar.Encrypted.rar", ArchiveType.Rar, true)] + [InlineData("Rar.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar.issue1050.rar", ArchiveType.Rar, true)] + [InlineData("Rar.malformed_512byte.rar", ArchiveType.Rar, true)] + [InlineData("Rar.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("Rar.solid.rar", ArchiveType.Rar, true)] + [InlineData("Rar.test_invalid_exttime.rar", ArchiveType.Rar, true)] + [InlineData("Rar15.rar", ArchiveType.Rar, true)] + [InlineData("Rar2.rar", ArchiveType.Rar, true)] + [InlineData("Rar4.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.crc_blake2.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.solid.rar", ArchiveType.Rar, true)] + [InlineData("Tar.ContainsRar.tar", ArchiveType.Tar, true)] + [InlineData("Tar.ContainsTarGz.tar", ArchiveType.Tar, true)] + [InlineData("Tar.Empty.tar", ArchiveType.Tar, true)] + [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)] + [InlineData("WinZip27_XZ.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip27_ZSTD.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.644.zip", ArchiveType.Zip, true)] + [InlineData("Zip.EntryComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.Evil.zip", ArchiveType.Zip, true)] + [InlineData("Zip.LongComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.UnicodePathExtra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.badlocalextra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd-.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.implode.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.empty.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.datadescriptors.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.encrypted.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.issue86.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce1.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce3.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce4.zip", ArchiveType.Zip, true)] + [InlineData("Zip.shrink.zip", ArchiveType.Zip, true)] + [InlineData("Zip.uncompressed.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.compressedonly.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.zstd.WinzipAES.mixed.zip", ArchiveType.Zip, true)] + [InlineData("large_test.txt.Z", ArchiveType.Lzw, false)] + [InlineData("test_477.zip", ArchiveType.Zip, true)] + [InlineData("ustar with long names.tar", ArchiveType.Tar, true)] + [InlineData("very long filename.tar", ArchiveType.Tar, true)] + [InlineData("zipcrypto.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.AES.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted2.zip", ArchiveType.Zip, true)] + public async ValueTask GetArchiveInformationAsync_DetectsSingleFileTestArchives( + string archiveName, + ArchiveType expectedType, + bool expectedSeekable + ) + { + var info = await ArchiveFactory.GetArchiveInformationAsync(GetTestArchivePath(archiveName)); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedSeekable, info.SupportsRandomAccess); + } + [Fact] public void GetArchiveInformation_ReturnsNull_ForNonArchive() { @@ -261,7 +428,7 @@ public class ArchiveFactoryTests : TestBase private MemoryStream CreatePrefixedArchiveStream(string archiveName, int prefixLength) { - var archiveBytes = File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + var archiveBytes = File.ReadAllBytes(GetTestArchivePath(archiveName)); var buffer = new byte[prefixLength + archiveBytes.Length]; archiveBytes.CopyTo(buffer, prefixLength); @@ -270,4 +437,15 @@ public class ArchiveFactoryTests : TestBase stream.Position = prefixLength; return stream; } + + private static string GetTestArchivePath(string archiveName) + { + var archivesPath = Path.Combine(TEST_ARCHIVES_PATH, archiveName); + if (File.Exists(archivesPath)) + { + return archivesPath; + } + + return Path.GetFullPath(Path.Combine(TEST_ARCHIVES_PATH, "..", archiveName)); + } } From ac8203e957d65ab18795a3c051df5c9ec50ce122 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 26 Apr 2026 11:30:56 +0100 Subject: [PATCH 7/9] add sync tests --- .../SharpCompress.Test/ArchiveFactoryTests.cs | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index 26a62acd..f19496ee 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -181,6 +181,22 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); } + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)] + public void GetArchiveInformation_WithLookForHeader_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = ArchiveFactory.GetArchiveInformation(GetTestArchivePath(archiveName), true); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + [Theory] [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] @@ -203,6 +219,192 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); } + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)] + public async ValueTask GetArchiveInformationAsync_WithLookForHeader_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = await ArchiveFactory.GetArchiveInformationAsync( + GetTestArchivePath(archiveName), + true + ); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + + [Theory] + [InlineData("64bitstream.zip.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARMT.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BZip2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Copy.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.EmptyStream.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Filters.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.IA64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPMd.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.RISCV.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.SPARC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Tar.tar", ArchiveType.Tar, true)] + [InlineData("7Zip.Tar.tar.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ZSTD.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.distance.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.encryptedFiles.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.eos.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.1block.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.encrypted.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.largefile.ace", ArchiveType.Ace, false)] + [InlineData("Arc.crunched.arc", ArchiveType.Arc, false)] + [InlineData("Arc.crunched.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arj.encrypted.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Issue_685.zip", ArchiveType.Zip, true)] + [InlineData("PrePostHeaders.zip", ArchiveType.Zip, true)] + [InlineData("Rar.Audio_program.rar", ArchiveType.Rar, true)] + [InlineData("Rar.Encrypted.rar", ArchiveType.Rar, true)] + [InlineData("Rar.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar.issue1050.rar", ArchiveType.Rar, true)] + [InlineData("Rar.malformed_512byte.rar", ArchiveType.Rar, true)] + [InlineData("Rar.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("Rar.solid.rar", ArchiveType.Rar, true)] + [InlineData("Rar.test_invalid_exttime.rar", ArchiveType.Rar, true)] + [InlineData("Rar15.rar", ArchiveType.Rar, true)] + [InlineData("Rar2.rar", ArchiveType.Rar, true)] + [InlineData("Rar4.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.crc_blake2.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.solid.rar", ArchiveType.Rar, true)] + [InlineData("Tar.ContainsRar.tar", ArchiveType.Tar, true)] + [InlineData("Tar.ContainsTarGz.tar", ArchiveType.Tar, true)] + [InlineData("Tar.Empty.tar", ArchiveType.Tar, true)] + [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)] + [InlineData("WinZip27_XZ.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip27_ZSTD.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.644.zip", ArchiveType.Zip, true)] + [InlineData("Zip.EntryComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.Evil.zip", ArchiveType.Zip, true)] + [InlineData("Zip.LongComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.UnicodePathExtra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.badlocalextra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd-.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.implode.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.empty.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.datadescriptors.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.encrypted.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.issue86.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce1.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce3.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce4.zip", ArchiveType.Zip, true)] + [InlineData("Zip.shrink.zip", ArchiveType.Zip, true)] + [InlineData("Zip.uncompressed.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.compressedonly.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.zstd.WinzipAES.mixed.zip", ArchiveType.Zip, true)] + [InlineData("large_test.txt.Z", ArchiveType.Lzw, false)] + [InlineData("test_477.zip", ArchiveType.Zip, true)] + [InlineData("ustar with long names.tar", ArchiveType.Tar, true)] + [InlineData("very long filename.tar", ArchiveType.Tar, true)] + [InlineData("zipcrypto.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.AES.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted2.zip", ArchiveType.Zip, true)] + public void GetArchiveInformation_DetectsSingleFileTestArchives( + string archiveName, + ArchiveType expectedType, + bool expectedSeekable + ) + { + var info = ArchiveFactory.GetArchiveInformation(GetTestArchivePath(archiveName)); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedSeekable, info.SupportsRandomAccess); + } + [Theory] [InlineData("64bitstream.zip.7z", ArchiveType.SevenZip, true)] [InlineData("7Zip.ARM.7z", ArchiveType.SevenZip, true)] From aae3502a8be7856da367636f73661652a2fb0b08 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 27 Apr 2026 16:43:06 +0100 Subject: [PATCH 8/9] intermediate commit --- .../Archives/ArchiveFactory.Detection.cs | 107 ++++++++++++++++-- .../Archives/Rar/RarArchive.Factory.cs | 4 +- .../SevenZip/SevenZipArchive.Factory.cs | 71 ++++++++++-- src/SharpCompress/Factories/Factory.cs | 9 ++ src/SharpCompress/Factories/RarFactory.cs | 9 ++ .../Factories/SevenZipFactory.cs | 9 ++ .../SharpCompress.Test/ArchiveFactoryTests.cs | 12 +- 7 files changed, 194 insertions(+), 27 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs index ae04e09f..c8b9f37d 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs @@ -19,11 +19,31 @@ public static partial class ArchiveFactory public static async ValueTask GetArchiveInformationAsync( string filePath, CancellationToken cancellationToken = default + ) => + await GetArchiveInformationAsync(filePath, ReaderOptions.ForFilePath, cancellationToken) + .ConfigureAwait(false); + + /// + /// Returns information about the archive at the given file path asynchronously, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + /// Options controlling archive detection. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + string filePath, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream stream = File.OpenRead(filePath); - return await GetArchiveInformationAsync(stream, cancellationToken).ConfigureAwait(false); + return await GetArchiveInformationAsync( + stream, + readerOptions ?? ReaderOptions.ForFilePath, + cancellationToken + ) + .ConfigureAwait(false); } /// @@ -35,12 +55,32 @@ public static partial class ArchiveFactory public static async ValueTask GetArchiveInformationAsync( Stream stream, CancellationToken cancellationToken = default + ) => + await GetArchiveInformationAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + + /// + /// Returns information about the archive in the given stream asynchronously, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + /// Options controlling archive detection. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + Stream stream, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { stream.RequireReadable(); stream.RequireSeekable(); - var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + var factory = await TryFindFactoryAsync( + stream, + readerOptions ?? ReaderOptions.ForExternalStream, + cancellationToken + ) + .ConfigureAwait(false); return factory is null ? null : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); @@ -102,6 +142,14 @@ public static partial class ArchiveFactory 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, + CancellationToken cancellationToken ) { var startPosition = stream.Position; @@ -109,11 +157,15 @@ public static partial class ArchiveFactory foreach (var factory in Factory.Factories) { stream.Seek(startPosition, SeekOrigin.Begin); - if ( - await factory - .IsArchiveAsync(stream, cancellationToken: cancellationToken) + var isArchive = factory is Factory concreteFactory + ? await concreteFactory + .IsArchiveAsyncWithOptions(stream, readerOptions, cancellationToken) .ConfigureAwait(false) - ) + : await factory + .IsArchiveAsync(stream, readerOptions.Password, cancellationToken) + .ConfigureAwait(false); + + if (isArchive) { stream.Seek(startPosition, SeekOrigin.Begin); return factory; @@ -129,11 +181,23 @@ public static partial class ArchiveFactory /// or if the file is not a recognized archive. /// /// Path to the archive file. - public static ArchiveInformation? GetArchiveInformation(string filePath) + public static ArchiveInformation? GetArchiveInformation(string filePath) => + GetArchiveInformation(filePath, ReaderOptions.ForFilePath); + + /// + /// Returns information about the archive at the given file path, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + /// Options controlling archive detection. + public static ArchiveInformation? GetArchiveInformation( + string filePath, + ReaderOptions? readerOptions + ) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream stream = File.OpenRead(filePath); - return GetArchiveInformation(stream); + return GetArchiveInformation(stream, readerOptions ?? ReaderOptions.ForFilePath); } /// @@ -141,12 +205,24 @@ public static partial class ArchiveFactory /// or if the stream is not a recognized archive. /// /// A readable and seekable stream positioned at the start of the archive. - public static ArchiveInformation? GetArchiveInformation(Stream stream) + public static ArchiveInformation? GetArchiveInformation(Stream stream) => + GetArchiveInformation(stream, ReaderOptions.ForExternalStream); + + /// + /// Returns information about the archive in the given stream, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + /// Options controlling archive detection. + public static ArchiveInformation? GetArchiveInformation( + Stream stream, + ReaderOptions? readerOptions + ) { stream.RequireReadable(); stream.RequireSeekable(); - var factory = TryFindFactory(stream); + var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream); return factory is null ? null : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); @@ -167,14 +243,21 @@ public static partial class ArchiveFactory /// non-seekable streams and is therefore not unified with this helper. /// /// - private static IFactory? TryFindFactory(Stream stream) + private static IFactory? TryFindFactory(Stream stream) => + TryFindFactory(stream, ReaderOptions.ForExternalStream); + + private static IFactory? TryFindFactory(Stream stream, ReaderOptions readerOptions) { var startPosition = stream.Position; foreach (var factory in Factory.Factories) { stream.Seek(startPosition, SeekOrigin.Begin); - if (factory.IsArchive(stream)) + var isArchive = factory is Factory concreteFactory + ? concreteFactory.IsArchiveWithOptions(stream, readerOptions) + : factory.IsArchive(stream, readerOptions.Password); + + if (isArchive) { stream.Seek(startPosition, SeekOrigin.Begin); return factory; diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index 2c333c5c..a8a0448a 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -153,7 +153,7 @@ public partial class RarArchive { try { - MarkHeader.Read(stream, true, false); + MarkHeader.Read(stream, true, options?.LookForHeader ?? false); return true; } catch @@ -172,7 +172,7 @@ public partial class RarArchive try { await MarkHeader - .ReadAsync(stream, true, false, cancellationToken) + .ReadAsync(stream, true, options?.LookForHeader ?? false, cancellationToken) .ConfigureAwait(false); return true; } diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index a338e363..8c177a0f 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -143,11 +143,14 @@ public partial class SevenZipArchive return IsSevenZipFile(stream); } - public static bool IsSevenZipFile(Stream stream) + public static bool IsSevenZipFile(Stream stream) => + IsSevenZipFile(stream, ReaderOptions.ForExternalStream); + + public static bool IsSevenZipFile(Stream stream, ReaderOptions? readerOptions) { try { - return SignatureMatch(stream); + return SignatureMatch(stream, readerOptions?.LookForHeader ?? false); } catch { @@ -158,12 +161,25 @@ public partial class SevenZipArchive public static async ValueTask IsSevenZipFileAsync( Stream stream, CancellationToken cancellationToken = default + ) => + await IsSevenZipFileAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + + public static async ValueTask IsSevenZipFileAsync( + Stream stream, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); try { - return await SignatureMatchAsync(stream, cancellationToken).ConfigureAwait(false); + return await SignatureMatchAsync( + stream, + readerOptions?.LookForHeader ?? false, + cancellationToken + ) + .ConfigureAwait(false); } catch { @@ -173,13 +189,29 @@ public partial class SevenZipArchive private static ReadOnlySpan Signature => [(byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C]; - private static bool SignatureMatch(Stream stream) + private static bool SignatureMatch(Stream stream, bool lookForHeader) { var buffer = ArrayPool.Shared.Rent(6); try { - stream.ReadExact(buffer, 0, 6); - return buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature); + var maxScanOffset = lookForHeader ? 0x80000 - 20 : 0; + for (var offset = 0; offset <= maxScanOffset; offset++) + { + stream.ReadExact(buffer, 0, 6); + if (buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature)) + { + return true; + } + + if (!lookForHeader || !stream.CanSeek || stream.Length - stream.Position < 6) + { + return false; + } + + stream.Position -= 5; + } + + return false; } finally { @@ -189,18 +221,39 @@ public partial class SevenZipArchive private static async ValueTask SignatureMatchAsync( Stream stream, + bool lookForHeader, CancellationToken cancellationToken ) { var buffer = ArrayPool.Shared.Rent(6); try { - if (!await stream.ReadFullyAsync(buffer, 0, 6, cancellationToken).ConfigureAwait(false)) + var maxScanOffset = lookForHeader ? 0x80000 - 20 : 0; + for (var offset = 0; offset <= maxScanOffset; offset++) { - return false; + if ( + !await stream + .ReadFullyAsync(buffer, 0, 6, cancellationToken) + .ConfigureAwait(false) + ) + { + return false; + } + + if (buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature)) + { + return true; + } + + if (!lookForHeader || !stream.CanSeek || stream.Length - stream.Position < 6) + { + return false; + } + + stream.Position -= 5; } - return buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature); + return false; } finally { diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 8c8cbde2..633d3056 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -56,12 +56,21 @@ public abstract class Factory : IFactory /// public abstract bool IsArchive(Stream stream, string? password = null); + internal virtual bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => + IsArchive(stream, readerOptions.Password); + public abstract ValueTask IsArchiveAsync( Stream stream, string? password = null, CancellationToken cancellationToken = default ); + internal virtual ValueTask IsArchiveAsyncWithOptions( + Stream stream, + ReaderOptions readerOptions, + CancellationToken cancellationToken = default + ) => IsArchiveAsync(stream, readerOptions.Password, cancellationToken); + /// public virtual FileInfo? GetFilePart(int index, FileInfo part1) => null; diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index 615f7fb4..2d7f1f86 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -34,6 +34,9 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade public override bool IsArchive(Stream stream, string? password = null) => RarArchive.IsRarFile(stream); + internal override bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => + RarArchive.IsRarFile(stream, readerOptions); + /// public override ValueTask IsArchiveAsync( Stream stream, @@ -41,6 +44,12 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade CancellationToken cancellationToken = default ) => RarArchive.IsRarFileAsync(stream, cancellationToken: cancellationToken); + internal override ValueTask IsArchiveAsyncWithOptions( + Stream stream, + ReaderOptions readerOptions, + CancellationToken cancellationToken = default + ) => RarArchive.IsRarFileAsync(stream, readerOptions, cancellationToken); + /// public override FileInfo? GetFilePart(int index, FileInfo part1) => RarArchiveVolumeFactory.GetFilePart(index, part1); diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index adc26074..a2eb46c5 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -37,6 +37,9 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory, I public override bool IsArchive(Stream stream, string? password = null) => SevenZipArchive.IsSevenZipFile(stream); + internal override bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => + SevenZipArchive.IsSevenZipFile(stream, readerOptions); + /// public override ValueTask IsArchiveAsync( Stream stream, @@ -44,6 +47,12 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory, I CancellationToken cancellationToken = default ) => SevenZipArchive.IsSevenZipFileAsync(stream, cancellationToken); + internal override ValueTask IsArchiveAsyncWithOptions( + Stream stream, + ReaderOptions readerOptions, + CancellationToken cancellationToken = default + ) => SevenZipArchive.IsSevenZipFileAsync(stream, readerOptions, cancellationToken); + #endregion #region IArchiveFactory diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index f19496ee..314e0cff 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Common; using SharpCompress.Factories; +using SharpCompress.Readers; using SharpCompress.Test.Mocks; using Xunit; @@ -184,13 +185,16 @@ public class ArchiveFactoryTests : TestBase [Theory] [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)] [InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)] - public void GetArchiveInformation_WithLookForHeader_ReturnsExpectedInfo( + public void GetArchiveInformation_WithReaderOptions_ReturnsExpectedInfo( string archiveName, ArchiveType expectedType, bool expectedRandomAccess ) { - var info = ArchiveFactory.GetArchiveInformation(GetTestArchivePath(archiveName), true); + var info = ArchiveFactory.GetArchiveInformation( + GetTestArchivePath(archiveName), + ReaderOptions.ForFilePath.WithLookForHeader(true) + ); Assert.NotNull(info); Assert.Equal(expectedType, info.Type); @@ -222,7 +226,7 @@ public class ArchiveFactoryTests : TestBase [Theory] [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)] [InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)] - public async ValueTask GetArchiveInformationAsync_WithLookForHeader_ReturnsExpectedInfo( + public async ValueTask GetArchiveInformationAsync_WithReaderOptions_ReturnsExpectedInfo( string archiveName, ArchiveType expectedType, bool expectedRandomAccess @@ -230,7 +234,7 @@ public class ArchiveFactoryTests : TestBase { var info = await ArchiveFactory.GetArchiveInformationAsync( GetTestArchivePath(archiveName), - true + ReaderOptions.ForFilePath.WithLookForHeader(true) ); Assert.NotNull(info); From 472765b5e325530d9d8324cfc940be434c057cc2 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 27 Apr 2026 17:17:31 +0100 Subject: [PATCH 9/9] fix up overloads and usages --- AGENTS.md | 1 + .../Archives/ArchiveFactory.Detection.cs | 14 ++-- src/SharpCompress/Archives/ArchiveFactory.cs | 48 ++++++++++++-- src/SharpCompress/Factories/AceFactory.cs | 4 +- src/SharpCompress/Factories/ArcFactory.cs | 4 +- src/SharpCompress/Factories/ArjFactory.cs | 4 +- src/SharpCompress/Factories/Factory.cs | 21 ++---- src/SharpCompress/Factories/GZipFactory.cs | 4 +- src/SharpCompress/Factories/IFactory.cs | 8 +-- src/SharpCompress/Factories/LzwFactory.cs | 4 +- src/SharpCompress/Factories/RarFactory.cs | 11 +--- .../Factories/SevenZipFactory.cs | 11 +--- src/SharpCompress/Factories/TarFactory.cs | 8 +-- .../Factories/ZStandardFactory.cs | 4 +- src/SharpCompress/Factories/ZipFactory.cs | 12 ++-- .../SharpCompress.Test/ArchiveFactoryTests.cs | 66 +++++++++++++++++++ tests/SharpCompress.Test/ReaderTests.cs | 3 +- 17 files changed, 149 insertions(+), 78 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a47121a0..79480854 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,6 +190,7 @@ SharpCompress supports multiple archive and compression formats: ### Validation Expectations - Run targeted tests for the changed area first. +- On non-Windows machines, avoid net48 test runs unless Mono is installed; use framework-specific validation such as `--framework net10.0` instead. - Run `dotnet csharpier format .` after code edits. - Run `dotnet csharpier check .` before handing off changes. diff --git a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs index c8b9f37d..a0084914 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs @@ -157,13 +157,9 @@ public static partial class ArchiveFactory foreach (var factory in Factory.Factories) { stream.Seek(startPosition, SeekOrigin.Begin); - var isArchive = factory is Factory concreteFactory - ? await concreteFactory - .IsArchiveAsyncWithOptions(stream, readerOptions, cancellationToken) - .ConfigureAwait(false) - : await factory - .IsArchiveAsync(stream, readerOptions.Password, cancellationToken) - .ConfigureAwait(false); + var isArchive = await factory + .IsArchiveAsync(stream, readerOptions, cancellationToken) + .ConfigureAwait(false); if (isArchive) { @@ -253,9 +249,7 @@ public static partial class ArchiveFactory foreach (var factory in Factory.Factories) { stream.Seek(startPosition, SeekOrigin.Begin); - var isArchive = factory is Factory concreteFactory - ? concreteFactory.IsArchiveWithOptions(stream, readerOptions) - : factory.IsArchive(stream, readerOptions.Password); + var isArchive = factory.IsArchive(stream, readerOptions); if (isArchive) { diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index d6f08cc6..49e2f9b4 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -141,18 +141,32 @@ public static partial class ArchiveFactory } public static bool IsArchive(string filePath, out ArchiveType? type) + { + return IsArchive(filePath, ReaderOptions.ForFilePath, out type); + } + + public static bool IsArchive( + string filePath, + ReaderOptions? readerOptions, + out ArchiveType? type + ) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream s = File.OpenRead(filePath); - return IsArchive(s, out type); + return IsArchive(s, readerOptions ?? ReaderOptions.ForFilePath, out type); } public static bool IsArchive(Stream stream, out ArchiveType? type) + { + return IsArchive(stream, ReaderOptions.ForExternalStream, out type); + } + + public static bool IsArchive(Stream stream, ReaderOptions? readerOptions, out ArchiveType? type) { stream.RequireReadable(); stream.RequireSeekable(); - var factory = TryFindFactory(stream); + var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream); type = factory?.KnownArchiveType; return factory is not null; } @@ -160,22 +174,48 @@ public static partial class ArchiveFactory public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( string filePath, CancellationToken cancellationToken = default + ) => + await IsArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken) + .ConfigureAwait(false); + + public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( + string filePath, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream stream = File.OpenRead(filePath); - return await IsArchiveAsync(stream, cancellationToken).ConfigureAwait(false); + return await IsArchiveAsync( + stream, + readerOptions ?? ReaderOptions.ForFilePath, + cancellationToken + ) + .ConfigureAwait(false); } public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( Stream stream, CancellationToken cancellationToken = default + ) => + await IsArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + + public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( + Stream stream, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { stream.RequireReadable(); stream.RequireSeekable(); - var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + var factory = await TryFindFactoryAsync( + stream, + readerOptions ?? ReaderOptions.ForExternalStream, + cancellationToken + ) + .ConfigureAwait(false); return (factory is not null, factory?.KnownArchiveType); } diff --git a/src/SharpCompress/Factories/AceFactory.cs b/src/SharpCompress/Factories/AceFactory.cs index f2fdfaa6..5117045e 100644 --- a/src/SharpCompress/Factories/AceFactory.cs +++ b/src/SharpCompress/Factories/AceFactory.cs @@ -23,12 +23,12 @@ public class AceFactory : Factory, IReaderFactory yield return "ace"; } - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => AceHeader.IsArchive(stream); public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => AceHeader.IsArchiveAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index 46c12666..cbc95e01 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -25,7 +25,7 @@ public class ArcFactory : Factory, IReaderFactory yield return "arc"; } - public override bool IsArchive(Stream stream, string? password = null) + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) { //You may have to use some(paranoid) checks to ensure that you actually are //processing an ARC file, since other archivers also adopted the idea of putting @@ -63,7 +63,7 @@ public class ArcFactory : Factory, IReaderFactory public override async ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs index d599d81d..de1a8382 100644 --- a/src/SharpCompress/Factories/ArjFactory.cs +++ b/src/SharpCompress/Factories/ArjFactory.cs @@ -23,12 +23,12 @@ public class ArjFactory : Factory, IReaderFactory yield return "arj"; } - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => ArjHeader.IsArchive(stream); public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => ArjHeader.IsArchiveAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 633d3056..115c6e49 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -54,22 +54,12 @@ public abstract class Factory : IFactory public abstract IEnumerable GetSupportedExtensions(); /// - public abstract bool IsArchive(Stream stream, string? password = null); - - internal virtual bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => - IsArchive(stream, readerOptions.Password); - + public abstract bool IsArchive(Stream stream, ReaderOptions readerOptions); public abstract ValueTask IsArchiveAsync( - Stream stream, - string? password = null, - CancellationToken cancellationToken = default - ); - - internal virtual ValueTask IsArchiveAsyncWithOptions( Stream stream, ReaderOptions readerOptions, CancellationToken cancellationToken = default - ) => IsArchiveAsync(stream, readerOptions.Password, cancellationToken); + ); /// public virtual FileInfo? GetFilePart(int index, FileInfo part1) => null; @@ -95,7 +85,7 @@ public abstract class Factory : IFactory if (this is IReaderFactory readerFactory) { stream.Rewind(); - if (IsArchive(stream, options.Password)) + if (IsArchive(stream, options)) { stream.Rewind(true); reader = readerFactory.OpenReader(stream, options); @@ -115,10 +105,7 @@ public abstract class Factory : IFactory if (this is IReaderFactory readerFactory) { stream.Rewind(); - if ( - await IsArchiveAsync(stream, options.Password, cancellationToken) - .ConfigureAwait(false) - ) + if (await IsArchiveAsync(stream, options, cancellationToken).ConfigureAwait(false)) { stream.Rewind(true); return await readerFactory diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index c49b8f8e..dd870d51 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -44,13 +44,13 @@ public class GZipFactory } /// - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => GZipArchive.IsGZipFile(stream); /// public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => GZipArchive.IsGZipFileAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/IFactory.cs b/src/SharpCompress/Factories/IFactory.cs index 2f0b1ab0..76bddb64 100644 --- a/src/SharpCompress/Factories/IFactory.cs +++ b/src/SharpCompress/Factories/IFactory.cs @@ -37,18 +37,18 @@ public interface IFactory /// Returns true if the stream represents an archive of the format defined by this type. /// /// A stream, pointing to the beginning of the archive. - /// optional password - bool IsArchive(Stream stream, string? password = null); + /// Options controlling archive detection. + bool IsArchive(Stream stream, ReaderOptions readerOptions); /// /// Returns true if the stream represents an archive of the format defined by this type asynchronously. /// /// A stream, pointing to the beginning of the archive. - /// optional password + /// Options controlling archive detection. /// cancellation token ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ); diff --git a/src/SharpCompress/Factories/LzwFactory.cs b/src/SharpCompress/Factories/LzwFactory.cs index cbfabb60..5bc333c6 100644 --- a/src/SharpCompress/Factories/LzwFactory.cs +++ b/src/SharpCompress/Factories/LzwFactory.cs @@ -32,13 +32,13 @@ public class LzwFactory : Factory, IReaderFactory } /// - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => LzwStream.IsLzwStream(stream); /// public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => LzwStream.IsLzwStreamAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index 2d7f1f86..2efc920d 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -31,20 +31,11 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade } /// - public override bool IsArchive(Stream stream, string? password = null) => - RarArchive.IsRarFile(stream); - - internal override bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => RarArchive.IsRarFile(stream, readerOptions); /// public override ValueTask IsArchiveAsync( - Stream stream, - string? password = null, - CancellationToken cancellationToken = default - ) => RarArchive.IsRarFileAsync(stream, cancellationToken: cancellationToken); - - internal override ValueTask IsArchiveAsyncWithOptions( Stream stream, ReaderOptions readerOptions, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index a2eb46c5..bac7b533 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -34,20 +34,11 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory, I } /// - public override bool IsArchive(Stream stream, string? password = null) => - SevenZipArchive.IsSevenZipFile(stream); - - internal override bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => SevenZipArchive.IsSevenZipFile(stream, readerOptions); /// public override ValueTask IsArchiveAsync( - Stream stream, - string? password = null, - CancellationToken cancellationToken = default - ) => SevenZipArchive.IsSevenZipFileAsync(stream, cancellationToken); - - internal override ValueTask IsArchiveAsyncWithOptions( Stream stream, ReaderOptions readerOptions, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 56d0a73b..eaa6e706 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -48,9 +48,9 @@ public class TarFactory } /// - public override bool IsArchive(Stream stream, string? password = null) + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) { - var providers = CompressionProviderRegistry.Default; + var providers = readerOptions.Providers; var sharpCompressStream = new SharpCompressStream(stream); sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize); foreach (var wrapper in TarWrapper.Wrappers) @@ -78,11 +78,11 @@ public class TarFactory /// public override async ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) { - var providers = CompressionProviderRegistry.Default; + var providers = readerOptions.Providers; var sharpCompressStream = new SharpCompressStream(stream); sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize); foreach (var wrapper in TarWrapper.Wrappers) diff --git a/src/SharpCompress/Factories/ZStandardFactory.cs b/src/SharpCompress/Factories/ZStandardFactory.cs index e45b2454..6ee920dc 100644 --- a/src/SharpCompress/Factories/ZStandardFactory.cs +++ b/src/SharpCompress/Factories/ZStandardFactory.cs @@ -21,12 +21,12 @@ internal class ZStandardFactory : Factory yield return "zstd"; } - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => ZStandardStream.IsZStandard(stream); public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => ZStandardStream.IsZStandardAsync(stream, cancellationToken); } diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index b5dbdff3..a9f97052 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -43,10 +43,10 @@ public class ZipFactory } /// - public override bool IsArchive(Stream stream, string? password = null) + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) { var startPosition = stream.CanSeek ? stream.Position : -1; - if (ZipArchive.IsZipFile(stream, password)) + if (ZipArchive.IsZipFile(stream, readerOptions.Password)) { return true; } @@ -61,7 +61,7 @@ public class ZipFactory stream.Position = startPosition; //test the zip (last) file of a multipart zip - if (ZipArchive.IsZipMulti(stream, password)) + if (ZipArchive.IsZipMulti(stream, readerOptions.Password)) { return true; } @@ -74,7 +74,7 @@ public class ZipFactory /// public override async ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) { @@ -84,7 +84,7 @@ public class ZipFactory // probe for single volume zip if ( await ZipArchive - .IsZipFileAsync(stream, password, cancellationToken) + .IsZipFileAsync(stream, readerOptions.Password, cancellationToken) .ConfigureAwait(false) ) { @@ -102,7 +102,7 @@ public class ZipFactory //test the zip (last) file of a multipart zip if ( await ZipArchive - .IsZipMultiAsync(stream, password, cancellationToken) + .IsZipMultiAsync(stream, readerOptions.Password, cancellationToken) .ConfigureAwait(false) ) { diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index 314e0cff..ba9acd7e 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -112,6 +112,72 @@ public class ArchiveFactoryTests : TestBase ); } + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + [InlineData("Rar.rar", ArchiveType.Rar)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip)] + public void IsArchive_String_ReturnsExpectedType(string archiveName, ArchiveType expectedType) + { + var result = ArchiveFactory.IsArchive( + Path.Combine(TEST_ARCHIVES_PATH, archiveName), + out var type + ); + + Assert.True(result); + Assert.Equal(expectedType, type); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + public void IsArchive_Stream_PreservesPosition(string archiveName, ArchiveType expectedType) + { + using var stream = CreatePrefixedArchiveStream(archiveName, 11); + var startPosition = stream.Position; + + var result = ArchiveFactory.IsArchive(stream, out var type); + + Assert.True(result); + Assert.Equal(expectedType, type); + Assert.Equal(startPosition, stream.Position); + } + + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar)] + public void IsArchive_WithReaderOptions_ReturnsExpectedType( + string archiveName, + ArchiveType expectedType + ) + { + var result = ArchiveFactory.IsArchive( + GetTestArchivePath(archiveName), + ReaderOptions.ForFilePath.WithLookForHeader(true), + out var type + ); + + Assert.True(result); + Assert.Equal(expectedType, type); + } + + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar)] + public async ValueTask IsArchiveAsync_WithReaderOptions_ReturnsExpectedType( + string archiveName, + ArchiveType expectedType + ) + { + var result = await ArchiveFactory.IsArchiveAsync( + GetTestArchivePath(archiveName), + ReaderOptions.ForFilePath.WithLookForHeader(true) + ); + + Assert.True(result.IsArchive); + Assert.Equal(expectedType, result.Type); + } + [Theory] [InlineData("Zip.deflate.zip", ArchiveType.Zip)] [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 72ab5f47..4c9024c9 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -103,7 +103,7 @@ public abstract class ReaderTests : TestBase ( await factory.IsArchiveAsync( new FileInfo(testArchive).OpenRead(), - null, + ReaderOptions.ForExternalStream, cancellationToken ) ) @@ -112,6 +112,7 @@ public abstract class ReaderTests : TestBase ( await factory.IsArchiveAsync( new FileInfo(testArchive).OpenRead(), + ReaderOptions.ForExternalStream, cancellationToken: cancellationToken ) )