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
)
)