diff --git a/docs/API.md b/docs/API.md
index cda03364..1536c9c6 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -50,20 +50,26 @@ if (ArchiveFactory.IsArchive("archive.zip", out var archiveType))
Console.WriteLine($"Detected {archiveType}");
}
-// Detect capabilities before choosing Archive API vs Reader API
-var info = ArchiveFactory.GetArchiveInformation("archive.arc");
-if (info is not null)
+// Detect the format and choose an API without enumerating entries.
+var detection = ArchiveFactory.DetectArchive("archive.tar.xz");
+if (detection is not null)
{
- Console.WriteLine($"Type: {info.Type}");
- Console.WriteLine($"Supports random access: {info.SupportsRandomAccess}");
- Console.WriteLine($"ZIP data descriptor entries: {info.ZipDataDescriptorEntryCount}");
- Console.WriteLine($"Solid stream count: {info.SolidStreamCount}");
+ Console.WriteLine($"Container: {detection.ContainerType}");
+ Console.WriteLine($"Outer compression: {detection.OuterCompressionType}");
+ Console.WriteLine($"Supported APIs: {detection.SupportedApis}");
}
-var asyncInfo = await ArchiveFactory.GetArchiveInformationAsync(
- "archive.zip",
- cancellationToken
-);
+// InspectArchive parses complete archive metadata. It can be expensive for
+// sequential formats such as TAR and compressed TAR.
+var information = ArchiveFactory.InspectArchive("archive.zip");
+if (information is not null)
+{
+ Console.WriteLine($"Entries: {information.EntryCount}");
+ Console.WriteLine($"Solid streams: {information.SolidStreamCount}");
+ Console.WriteLine($"ZIP data descriptor entries: {information.Zip?.DataDescriptorEntryCount}");
+}
+
+var asyncInformation = await ArchiveFactory.InspectArchiveAsync("archive.zip", cancellationToken);
// Multi-volume archives
var parts = ArchiveFactory.GetFileParts("archive.part1.rar")
@@ -75,7 +81,23 @@ using (var archive = ArchiveFactory.OpenArchive(parts))
}
```
-`ArchiveInformation.SupportsRandomAccess` is `true` when the detected format supports `IArchive` random access. It is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW, where `ReaderFactory.OpenReader` should be used instead. Compressed tar wrappers such as `.tar.gz` and `.tar.xz` are also reader-only; `ArchiveFactory.GetArchiveInformation` returns `null` for them and `ArchiveFactory.OpenArchive` does not open them as the outer compression wrapper. Use `ReaderFactory.OpenReader` or `TarReader.OpenReader` for those files. Format-specific details are available through `ArchiveInformation.ZipDataDescriptorEntryCount` (ZIP) and `ArchiveInformation.SolidStreamCount` (solid-capable formats such as 7z and RAR).
+`DetectArchive` identifies the logical container, its outer compression wrapper, and whether the Archive and Reader APIs are available without enumerating entries. Compressed TAR wrappers such as `.tar.gz` and `.tar.xz` are identified as `ArchiveType.Tar` with an outer compression type and `ArchiveAccessMode.Reader`.
+
+`InspectArchive` enumerates metadata and returns `ArchiveInformation`. It reports `Partial` status for missing volumes or encrypted headers without a password; malformed archives and incorrect passwords throw. ZIP-specific metadata is exposed through `ArchiveInformation.Zip`.
+
+The stream overloads of `DetectArchive` and `InspectArchive` preserve the supplied stream's position and leave it open, including when `ReaderOptions.LeaveStreamOpen` is `false`.
+
+#### Migrating from `GetArchiveInformation`
+
+| Removed API | Replacement |
+| --- | --- |
+| `GetArchiveInformation(...)` used only to identify a format | `DetectArchive(...)` |
+| `GetArchiveInformation(...)` used to enumerate archive details | `InspectArchive(...)` |
+| `GetArchiveInformationAsync(...)` | `DetectArchiveAsync(...)` or `InspectArchiveAsync(...)` |
+| `Type` | `Detection.ContainerType` |
+| `SupportsRandomAccess` | `Detection.SupportedApis.HasFlag(ArchiveAccessMode.Archive)` |
+| `ZipDataDescriptorEntryCount` | `Zip.DataDescriptorEntryCount` |
+| `SolidStreamCount` | `SolidStreamCount` |
### Creating Archives
diff --git a/docs/FORMATS.md b/docs/FORMATS.md
index 22a7080a..a9309c06 100644
--- a/docs/FORMATS.md
+++ b/docs/FORMATS.md
@@ -31,7 +31,7 @@
4. The 7Zip format doesn't allow for reading as a forward-only stream, so 7Zip read support is only through the Archive API. Writing is supported through SevenZipWriter for non-solid archives with LZMA/LZMA2 and requires a seekable output stream. See [7Zip Format Notes](#7zip-format-notes) for details on async extraction behavior.
5. LZip has no support for extra data like the file name or timestamp. There is a default filename used when looking at the entry Key on the archive.
-`ArchiveFactory.GetArchiveInformation(...).SupportsRandomAccess` is `true` when the detected format has an Archive API in this table. It is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW. Compressed tar wrappers are supported by `ReaderFactory`/`TarReader`, not by `ArchiveFactory`/`TarArchive`; ArchiveFactory detection blocks them instead of opening the outer compression wrapper as a standalone archive.
+`ArchiveFactory.DetectArchive(...)` reports which APIs in this table are available through `ArchiveDetection.SupportedApis`. Reader-only formats include Ace, Arc, Arj, and standalone LZW. Compressed TAR wrappers are detected as a TAR container with an outer compression type and support the Reader API, not the Archive API. Use `ArchiveFactory.InspectArchive(...)` when complete archive metadata is required.
### Zip Format Notes
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 9666d43f..838cd16f 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -227,16 +227,16 @@ using (var archive = ArchiveFactory.OpenArchive("archive.zip"))
}
```
-### Use ArchiveInformation to choose the right API
+### Detect the format and choose the right API
```C#
var archivePath = "archive.arc";
-var info = ArchiveFactory.GetArchiveInformation(archivePath);
-if (info is null)
+var detection = ArchiveFactory.DetectArchive(archivePath);
+if (detection is null)
{
Console.WriteLine("Not a supported archive");
}
-else if (info.SupportsRandomAccess)
+else if (detection.SupportedApis.HasFlag(ArchiveAccessMode.Archive))
{
using var archive = ArchiveFactory.OpenArchive(archivePath);
archive.WriteToDirectory(@"D:\output");
@@ -248,7 +248,23 @@ else
}
```
-`SupportsRandomAccess` is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW. Use the Reader API for those formats.
+`DetectArchive` does not enumerate entries. It reports both Archive and Reader API availability; reader-only formats include Ace, Arc, Arj, standalone LZW, and compressed TAR wrappers.
+
+### Inspect archive metadata
+
+```C#
+var information = ArchiveFactory.InspectArchive(archivePath);
+if (information is not null)
+{
+ Console.WriteLine($"Entries: {information.EntryCount}");
+ Console.WriteLine($"Compressed bytes: {information.CompressedPayloadSize}");
+ Console.WriteLine($"Solid streams: {information.SolidStreamCount}");
+}
+```
+
+`InspectArchive` parses archive metadata, which can require a complete sequential scan for TAR and compressed TAR files. It returns partial information for encrypted headers without a password or missing archive parts; inspect `Status` and `Limitations` before using nullable metadata values.
+
+The stream overload preserves the caller's current position and leaves the supplied stream open.
### Open multi-volume archives
diff --git a/src/SharpCompress/Archives/ArchiveAccessMode.cs b/src/SharpCompress/Archives/ArchiveAccessMode.cs
new file mode 100644
index 00000000..a86afdb2
--- /dev/null
+++ b/src/SharpCompress/Archives/ArchiveAccessMode.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace SharpCompress.Archives;
+
+///
+/// Specifies the APIs available for an archive format.
+///
+[Flags]
+public enum ArchiveAccessMode
+{
+ ///
+ /// No archive access API is available.
+ ///
+ None = 0,
+
+ ///
+ /// The seekable API is available.
+ ///
+ Archive = 1,
+
+ ///
+ /// The forward-only API is available.
+ ///
+ Reader = 2,
+}
diff --git a/src/SharpCompress/Archives/ArchiveDetection.cs b/src/SharpCompress/Archives/ArchiveDetection.cs
new file mode 100644
index 00000000..fe961af8
--- /dev/null
+++ b/src/SharpCompress/Archives/ArchiveDetection.cs
@@ -0,0 +1,43 @@
+using SharpCompress.Common;
+
+namespace SharpCompress.Archives;
+
+///
+/// Identifies an archive format without enumerating its entries.
+///
+public sealed class ArchiveDetection
+{
+ internal ArchiveDetection(
+ ArchiveType? containerType,
+ string formatName,
+ CompressionType? outerCompressionType,
+ ArchiveAccessMode supportedApis
+ )
+ {
+ ContainerType = containerType;
+ FormatName = formatName;
+ OuterCompressionType = outerCompressionType;
+ SupportedApis = supportedApis;
+ }
+
+ ///
+ /// Gets the logical archive container type, when it is a built-in SharpCompress type.
+ ///
+ public ArchiveType? ContainerType { get; }
+
+ ///
+ /// Gets the detected format name.
+ ///
+ public string FormatName { get; }
+
+ ///
+ /// Gets the compression wrapper around , if present.
+ /// For example, a tar.xz archive has a TAR container and XZ outer compression.
+ ///
+ public CompressionType? OuterCompressionType { get; }
+
+ ///
+ /// Gets the APIs supported by the detected format.
+ ///
+ public ArchiveAccessMode SupportedApis { get; }
+}
diff --git a/src/SharpCompress/Archives/ArchiveEncryptionScope.cs b/src/SharpCompress/Archives/ArchiveEncryptionScope.cs
new file mode 100644
index 00000000..5299e9bf
--- /dev/null
+++ b/src/SharpCompress/Archives/ArchiveEncryptionScope.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace SharpCompress.Archives;
+
+///
+/// Identifies which archive data is encrypted.
+///
+[Flags]
+public enum ArchiveEncryptionScope
+{
+ ///
+ /// No encryption is present.
+ ///
+ None = 0,
+
+ ///
+ /// One or more entry payloads are encrypted.
+ ///
+ EntryData = 1,
+
+ ///
+ /// Archive headers are encrypted.
+ ///
+ Headers = 2,
+}
diff --git a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs
index 5674a462..caedc7fd 100644
--- a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs
+++ b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs
@@ -2,13 +2,8 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using SharpCompress.Archives.Rar;
-using SharpCompress.Archives.SevenZip;
using SharpCompress.Archives.Tar;
-using SharpCompress.Archives.Zip;
using SharpCompress.Common;
-using SharpCompress.Common.Zip;
-using SharpCompress.Common.Zip.Headers;
using SharpCompress.Factories;
using SharpCompress.IO;
using SharpCompress.Providers;
@@ -19,26 +14,24 @@ 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.
+ /// Identifies the archive at the given file path without enumerating its entries.
///
/// Path to the archive file.
/// Cancellation token.
- public static async ValueTask GetArchiveInformationAsync(
+ public static async ValueTask DetectArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
) =>
- await GetArchiveInformationAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
+ await DetectArchiveAsync(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.
+ /// Identifies the archive at the given file path without enumerating its entries.
///
/// Path to the archive file.
/// Options controlling archive detection.
/// Cancellation token.
- public static async ValueTask GetArchiveInformationAsync(
+ public static async ValueTask DetectArchiveAsync(
string filePath,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
@@ -46,7 +39,7 @@ public static partial class ArchiveFactory
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
- return await GetArchiveInformationAsync(
+ return await DetectArchiveAsync(
stream,
readerOptions ?? ReaderOptions.ForFilePath,
cancellationToken
@@ -55,26 +48,24 @@ public static partial class ArchiveFactory
}
///
- /// Returns information about the archive in the given stream asynchronously,
- /// or if the stream is not a recognized archive.
+ /// Identifies the archive in the given stream without enumerating its entries.
///
/// A readable and seekable stream positioned at the start of the archive.
/// Cancellation token.
- public static async ValueTask GetArchiveInformationAsync(
+ public static async ValueTask DetectArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
) =>
- await GetArchiveInformationAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
+ await DetectArchiveAsync(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.
+ /// Identifies the archive in the given stream without enumerating its entries.
///
/// A readable and seekable stream positioned at the start of the archive.
/// Options controlling archive detection.
/// Cancellation token.
- public static async ValueTask GetArchiveInformationAsync(
+ public static async ValueTask DetectArchiveAsync(
Stream stream,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
@@ -83,19 +74,12 @@ public static partial class ArchiveFactory
stream.RequireReadable();
stream.RequireSeekable();
- var factory = await TryFindFactoryAsync(
+ return await TryDetectArchiveAsync(
stream,
readerOptions ?? ReaderOptions.ForExternalStream,
cancellationToken
)
.ConfigureAwait(false);
- return factory is null
- ? null
- : BuildArchiveInformation(
- stream,
- readerOptions ?? ReaderOptions.ForExternalStream,
- factory
- );
}
internal static ValueTask FindFactoryAsync(
@@ -187,87 +171,80 @@ public static partial class ArchiveFactory
{
var startPosition = stream.Position;
- foreach (var factory in Factory.Factories)
+ try
{
- stream.Seek(startPosition, SeekOrigin.Begin);
- var isArchive = await factory
- .IsArchiveAsync(stream, readerOptions, cancellationToken)
- .ConfigureAwait(false);
-
- if (isArchive)
+ foreach (var factory in Factory.Factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
- if (
- await IsCompressedTarAsync(stream, factory, readerOptions, cancellationToken)
- .ConfigureAwait(false)
- )
+ var isArchive = await factory
+ .IsArchiveAsync(stream, readerOptions, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (isArchive)
{
- continue;
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ if (
+ await IsCompressedTarAsync(
+ stream,
+ factory,
+ readerOptions,
+ cancellationToken
+ )
+ .ConfigureAwait(false)
+ )
+ {
+ continue;
+ }
+
+ return factory;
}
-
- stream.Seek(startPosition, SeekOrigin.Begin);
- return factory;
}
- }
- stream.Seek(startPosition, SeekOrigin.Begin);
- return null;
+ return null;
+ }
+ finally
+ {
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ }
}
///
- /// Returns information about the archive at the given file path,
- /// or if the file is not a recognized archive.
+ /// Identifies the archive at the given file path without enumerating its entries.
///
/// Path to the archive file.
- public static ArchiveInformation? GetArchiveInformation(string filePath) =>
- GetArchiveInformation(filePath, ReaderOptions.ForFilePath);
+ public static ArchiveDetection? DetectArchive(string filePath) =>
+ DetectArchive(filePath, ReaderOptions.ForFilePath);
///
- /// Returns information about the archive at the given file path,
- /// or if the file is not a recognized archive.
+ /// Identifies the archive at the given file path without enumerating its entries.
///
/// Path to the archive file.
/// Options controlling archive detection.
- public static ArchiveInformation? GetArchiveInformation(
- string filePath,
- ReaderOptions? readerOptions
- )
+ public static ArchiveDetection? DetectArchive(string filePath, ReaderOptions? readerOptions)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
- return GetArchiveInformation(stream, readerOptions ?? ReaderOptions.ForFilePath);
+ return DetectArchive(stream, readerOptions ?? ReaderOptions.ForFilePath);
}
///
- /// Returns information about the archive in the given stream,
- /// or if the stream is not a recognized archive.
+ /// Identifies the archive in the given stream without enumerating its entries.
///
/// A readable and seekable stream positioned at the start of the archive.
- public static ArchiveInformation? GetArchiveInformation(Stream stream) =>
- GetArchiveInformation(stream, ReaderOptions.ForExternalStream);
+ public static ArchiveDetection? DetectArchive(Stream stream) =>
+ DetectArchive(stream, ReaderOptions.ForExternalStream);
///
- /// Returns information about the archive in the given stream,
- /// or if the stream is not a recognized archive.
+ /// Identifies the archive in the given stream without enumerating its entries.
///
/// A readable and seekable stream positioned at the start of the archive.
/// Options controlling archive detection.
- public static ArchiveInformation? GetArchiveInformation(
- Stream stream,
- ReaderOptions? readerOptions
- )
+ public static ArchiveDetection? DetectArchive(Stream stream, ReaderOptions? readerOptions)
{
stream.RequireReadable();
stream.RequireSeekable();
- var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream);
- return factory is null
- ? null
- : BuildArchiveInformation(
- stream,
- readerOptions ?? ReaderOptions.ForExternalStream,
- factory
- );
+ return TryDetectArchive(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
///
@@ -278,7 +255,7 @@ public static partial class ArchiveFactory
///
/// This is the shared, seekable-stream detection core used by
/// , ,
- /// and .
+ /// and .
///
/// uses a separate code path
/// based on rewindable buffering, which supports
@@ -292,26 +269,31 @@ public static partial class ArchiveFactory
{
var startPosition = stream.Position;
- foreach (var factory in Factory.Factories)
+ try
{
- stream.Seek(startPosition, SeekOrigin.Begin);
- var isArchive = factory.IsArchive(stream, readerOptions);
-
- if (isArchive)
+ foreach (var factory in Factory.Factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
- if (IsCompressedTar(stream, factory, readerOptions))
+ var isArchive = factory.IsArchive(stream, readerOptions);
+
+ if (isArchive)
{
- continue;
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ if (IsCompressedTar(stream, factory, readerOptions))
+ {
+ continue;
+ }
+
+ return factory;
}
-
- stream.Seek(startPosition, SeekOrigin.Begin);
- return factory;
}
- }
- stream.Seek(startPosition, SeekOrigin.Begin);
- return null;
+ return null;
+ }
+ finally
+ {
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ }
}
private static bool IsCompressedTar(
@@ -425,114 +407,184 @@ public static partial class ArchiveFactory
_ => null,
};
- private static ArchiveInformation BuildArchiveInformation(
- Stream stream,
- ReaderOptions readerOptions,
- IFactory factory
- )
+ private static ArchiveDetection? TryDetectArchive(Stream stream, ReaderOptions readerOptions)
{
- var info = new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory);
var startPosition = stream.Position;
try
{
- var probeReaderOptions = readerOptions with { LeaveStreamOpen = true };
- switch (factory)
+ foreach (var factory in Factory.Factories)
{
- case ZipFactory:
- TryPopulateZipDetails(info, stream, probeReaderOptions);
- break;
- case SevenZipFactory:
- TryPopulateSevenZipDetails(info, stream, probeReaderOptions);
- break;
- case RarFactory:
- TryPopulateRarDetails(info, stream, probeReaderOptions);
- break;
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ if (!factory.IsArchive(stream, readerOptions))
+ {
+ continue;
+ }
+
+ if (GetCompressedTarType(factory) is { } compressionType)
+ {
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ if (IsCompressedTar(stream, readerOptions, compressionType))
+ {
+ return CreateCompressedTarDetection(compressionType);
+ }
+ }
+
+ return CreateDetection(factory);
}
+
+ return
+ TryDetectCompressedTar(stream, readerOptions, startPosition)
+ is { } compressedTarType
+ ? CreateCompressedTarDetection(compressedTarType)
+ : null;
}
finally
{
stream.Seek(startPosition, SeekOrigin.Begin);
}
-
- return info;
}
- private static void TryPopulateZipDetails(
- ArchiveInformation info,
+ private static async ValueTask TryDetectArchiveAsync(
Stream stream,
- ReaderOptions readerOptions
+ ReaderOptions readerOptions,
+ CancellationToken cancellationToken
)
{
+ var startPosition = stream.Position;
+
try
{
- info.ZipDataDescriptorEntryCount = GetZipDataDescriptorEntryCount(
- stream,
- readerOptions
- );
+ foreach (var factory in Factory.Factories)
+ {
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ if (
+ !await factory
+ .IsArchiveAsync(stream, readerOptions, cancellationToken)
+ .ConfigureAwait(false)
+ )
+ {
+ continue;
+ }
+
+ if (GetCompressedTarType(factory) is { } compressionType)
+ {
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ if (
+ await IsCompressedTarAsync(
+ stream,
+ readerOptions,
+ compressionType,
+ cancellationToken
+ )
+ .ConfigureAwait(false)
+ )
+ {
+ return CreateCompressedTarDetection(compressionType);
+ }
+ }
+
+ return CreateDetection(factory);
+ }
+
+ var compressedTarType = await TryDetectCompressedTarAsync(
+ stream,
+ readerOptions,
+ startPosition,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ return compressedTarType is { } value ? CreateCompressedTarDetection(value) : null;
}
- catch
+ finally
{
- // Keep archive detection resilient even when format-specific probing fails.
+ stream.Seek(startPosition, SeekOrigin.Begin);
}
}
- private static void TryPopulateSevenZipDetails(
- ArchiveInformation info,
+ private static CompressionType? TryDetectCompressedTar(
Stream stream,
- ReaderOptions readerOptions
+ ReaderOptions readerOptions,
+ long startPosition
)
{
- try
+ foreach (var wrapper in TarWrapper.Wrappers)
{
- info.SolidStreamCount = GetSevenZipSolidStreamCount(stream, readerOptions);
- }
- catch
- {
- // Keep archive detection resilient even when format-specific probing fails.
+ if (wrapper.CompressionType == CompressionType.None)
+ {
+ continue;
+ }
+
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ if (!wrapper.IsMatch(stream))
+ {
+ continue;
+ }
+
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ if (IsCompressedTar(stream, readerOptions, wrapper.CompressionType))
+ {
+ return wrapper.CompressionType;
+ }
}
+
+ return null;
}
- private static void TryPopulateRarDetails(
- ArchiveInformation info,
+ private static async ValueTask TryDetectCompressedTarAsync(
Stream stream,
- ReaderOptions readerOptions
+ ReaderOptions readerOptions,
+ long startPosition,
+ CancellationToken cancellationToken
)
{
- try
+ foreach (var wrapper in TarWrapper.Wrappers)
{
- info.SolidStreamCount = GetRarSolidStreamCount(stream, readerOptions);
+ cancellationToken.ThrowIfCancellationRequested();
+ if (wrapper.CompressionType == CompressionType.None)
+ {
+ continue;
+ }
+
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ if (!await wrapper.IsMatchAsync(stream, cancellationToken).ConfigureAwait(false))
+ {
+ continue;
+ }
+
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ if (
+ await IsCompressedTarAsync(
+ stream,
+ readerOptions,
+ wrapper.CompressionType,
+ cancellationToken
+ )
+ .ConfigureAwait(false)
+ )
+ {
+ return wrapper.CompressionType;
+ }
}
- catch
+
+ return null;
+ }
+
+ private static ArchiveDetection CreateDetection(IFactory factory)
+ {
+ var supportedApis = ArchiveAccessMode.None;
+ if (factory is IArchiveFactory)
{
- // Keep archive detection resilient even when format-specific probing fails.
+ supportedApis |= ArchiveAccessMode.Archive;
}
+ if (factory is IReaderFactory)
+ {
+ supportedApis |= ArchiveAccessMode.Reader;
+ }
+
+ return new ArchiveDetection(factory.KnownArchiveType, factory.Name, null, supportedApis);
}
- private static int GetZipDataDescriptorEntryCount(Stream stream, ReaderOptions readerOptions)
- {
- using var archive = ZipArchive.OpenArchive(stream, readerOptions);
- return archive
- .Entries.OfType()
- .SelectMany(entry => entry.Parts.OfType())
- .Count(part =>
- FlagUtility.HasFlag(part.Header.Flags, HeaderFlags.UsePostDataDescriptor)
- );
- }
-
- private static int GetSevenZipSolidStreamCount(Stream stream, ReaderOptions readerOptions)
- {
- using var archive = SevenZipArchive.OpenArchive(stream, readerOptions);
- return archive
- .Entries.OfType()
- .Where(entry => !entry.IsDirectory && entry.FilePart.Folder is not null)
- .GroupBy(entry => entry.FilePart.Folder)
- .Count(group => group.Skip(1).Any());
- }
-
- private static int GetRarSolidStreamCount(Stream stream, ReaderOptions readerOptions)
- {
- using var archive = RarArchive.OpenArchive(stream, readerOptions);
- return archive.IsSolid ? 1 : 0;
- }
+ private static ArchiveDetection CreateCompressedTarDetection(CompressionType compressionType) =>
+ new(ArchiveType.Tar, "Tar", compressionType, ArchiveAccessMode.Reader);
}
diff --git a/src/SharpCompress/Archives/ArchiveFactory.Information.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Information.Async.cs
new file mode 100644
index 00000000..9c8076ef
--- /dev/null
+++ b/src/SharpCompress/Archives/ArchiveFactory.Information.Async.cs
@@ -0,0 +1,537 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using SharpCompress.Archives.Rar;
+using SharpCompress.Archives.SevenZip;
+using SharpCompress.Common;
+using SharpCompress.Common.Ace.Headers;
+using SharpCompress.Common.Rar;
+using SharpCompress.Common.Zip;
+using SharpCompress.IO;
+using SharpCompress.Readers;
+
+namespace SharpCompress.Archives;
+
+public static partial class ArchiveFactory
+{
+ ///
+ /// Asynchronously collects metadata for the archive at the given file path.
+ ///
+ /// Path to the archive file.
+ /// Cancellation token.
+ /// Archive metadata, or when the file is not a supported archive.
+ public static async ValueTask InspectArchiveAsync(
+ string filePath,
+ CancellationToken cancellationToken = default
+ ) =>
+ await InspectArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
+ .ConfigureAwait(false);
+
+ ///
+ /// Asynchronously collects metadata for the archive at the given file path.
+ ///
+ /// Path to the archive file.
+ /// Options controlling archive inspection.
+ /// Cancellation token.
+ /// Archive metadata, or when the file is not a supported archive.
+ public static async ValueTask InspectArchiveAsync(
+ string filePath,
+ ReaderOptions? readerOptions,
+ CancellationToken cancellationToken = default
+ )
+ {
+ filePath.NotNullOrEmpty(nameof(filePath));
+ var options = readerOptions ?? ReaderOptions.ForFilePath;
+ var detection = await DetectArchiveAsync(filePath, options, cancellationToken)
+ .ConfigureAwait(false);
+ if (detection is null)
+ {
+ return null;
+ }
+ if ((detection.SupportedApis & ArchiveAccessMode.Archive) != 0)
+ {
+ var fileInfos = GetArchiveFileParts(new FileInfo(filePath), options);
+ if (fileInfos.Length > 1)
+ {
+ return await InspectArchiveAsync(fileInfos, options, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+
+ using Stream stream = File.OpenRead(filePath);
+ return await InspectArchiveAsync(stream, options, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously collects metadata for the archive in the given stream.
+ ///
+ /// A readable and seekable stream positioned at the start of the archive.
+ /// Cancellation token.
+ /// Archive metadata, or when the stream is not a supported archive.
+ public static async ValueTask InspectArchiveAsync(
+ Stream stream,
+ CancellationToken cancellationToken = default
+ ) =>
+ await InspectArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
+ .ConfigureAwait(false);
+
+ ///
+ /// Asynchronously collects metadata for the archive in the given stream.
+ ///
+ /// A readable and seekable stream positioned at the start of the archive.
+ /// Options controlling archive inspection.
+ /// Cancellation token.
+ /// Archive metadata, or when the stream is not a supported archive.
+ /// The supplied stream remains open and is restored to its original position.
+ public static async ValueTask InspectArchiveAsync(
+ Stream stream,
+ ReaderOptions? readerOptions,
+ CancellationToken cancellationToken = default
+ )
+ {
+ stream.RequireReadable();
+ stream.RequireSeekable();
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var options = readerOptions ?? ReaderOptions.ForExternalStream;
+ var startPosition = stream.Position;
+ var physicalSize = GetPhysicalSize(stream, startPosition);
+
+ try
+ {
+ using var archiveStream = new ArchiveOffsetStream(stream);
+ var inspectionOptions = options with { LeaveStreamOpen = true };
+ var detection = await TryDetectArchiveAsync(
+ archiveStream,
+ inspectionOptions,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ if (detection is null)
+ {
+ return null;
+ }
+
+ if ((detection.SupportedApis & ArchiveAccessMode.Archive) != 0)
+ {
+ await using var archive = await OpenAsyncArchive(
+ archiveStream,
+ inspectionOptions,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ return await InspectOpenedArchiveAsync(
+ archive,
+ detection,
+ physicalSize,
+ 1,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ }
+
+ var aceHeader = await ReadAceHeaderAsync(
+ archiveStream,
+ detection,
+ inspectionOptions,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ await using var reader = await ReaderFactory
+ .OpenAsyncReader(archiveStream, inspectionOptions, cancellationToken)
+ .ConfigureAwait(false);
+ return await InspectOpenedReaderAsync(
+ reader,
+ detection,
+ physicalSize,
+ 1,
+ aceHeader,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ }
+ catch (CryptographicException) when (string.IsNullOrEmpty(options.Password))
+ {
+ var detection = await RedetectArchiveAsync(
+ stream,
+ startPosition,
+ options,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ return detection is null
+ ? null
+ : CreatePartialInformation(
+ detection,
+ physicalSize,
+ 1,
+ ArchiveInformationLimitations.EncryptedHeaders
+ );
+ }
+ catch (MultipartStreamRequiredException)
+ {
+ var detection = await RedetectArchiveAsync(
+ stream,
+ startPosition,
+ options,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ return detection is null
+ ? null
+ : CreatePartialInformation(
+ detection,
+ physicalSize,
+ 1,
+ ArchiveInformationLimitations.MissingVolumes
+ );
+ }
+ finally
+ {
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ }
+ }
+
+ ///
+ /// Asynchronously collects metadata for an archive opened from multiple files.
+ ///
+ /// Archive source files in archive order.
+ /// Options controlling archive inspection.
+ /// Cancellation token.
+ /// Archive metadata, or when the files are not a supported archive.
+ public static async ValueTask InspectArchiveAsync(
+ IReadOnlyList fileInfos,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ fileInfos.NotNull(nameof(fileInfos));
+ if (fileInfos.Count == 0)
+ {
+ throw new ArchiveOperationException("No files to inspect");
+ }
+ if (fileInfos.Count == 1)
+ {
+ return await InspectArchiveAsync(
+ fileInfos[0].FullName,
+ readerOptions,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ }
+
+ var options = readerOptions ?? ReaderOptions.ForFilePath;
+ var detection = await DetectArchiveAsync(fileInfos[0].FullName, options, cancellationToken)
+ .ConfigureAwait(false);
+ if (detection is null)
+ {
+ return null;
+ }
+ if ((detection.SupportedApis & ArchiveAccessMode.Archive) == 0)
+ {
+ throw new NotSupportedException(
+ "Inspecting multiple source files is supported only for formats with an Archive API."
+ );
+ }
+
+ var physicalSize = GetPhysicalSize(fileInfos);
+ try
+ {
+ await using var archive = await OpenAsyncArchive(fileInfos, options, cancellationToken)
+ .ConfigureAwait(false);
+ return await InspectOpenedArchiveAsync(
+ archive,
+ detection,
+ physicalSize,
+ fileInfos.Count,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ }
+ catch (CryptographicException) when (string.IsNullOrEmpty(options.Password))
+ {
+ return CreatePartialInformation(
+ detection,
+ physicalSize,
+ fileInfos.Count,
+ ArchiveInformationLimitations.EncryptedHeaders
+ );
+ }
+ }
+
+ ///
+ /// Asynchronously collects metadata for an archive opened from multiple streams.
+ ///
+ /// Archive source streams in archive order.
+ /// Options controlling archive inspection.
+ /// Cancellation token.
+ /// Archive metadata, or when the streams are not a supported archive.
+ public static async ValueTask InspectArchiveAsync(
+ IReadOnlyList streams,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ streams.NotNull(nameof(streams));
+ if (streams.Count == 0)
+ {
+ throw new ArchiveOperationException("No streams to inspect");
+ }
+ if (streams.Count == 1)
+ {
+ return await InspectArchiveAsync(streams[0], readerOptions, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ var options = readerOptions ?? ReaderOptions.ForExternalStream;
+ var startPositions = streams.Select(stream => stream.Position).ToArray();
+ var physicalSize = GetPhysicalSize(streams, startPositions);
+
+ try
+ {
+ using var firstArchiveStream = new ArchiveOffsetStream(streams[0]);
+ var inspectionOptions = options with { LeaveStreamOpen = true };
+ var detection = await TryDetectArchiveAsync(
+ firstArchiveStream,
+ inspectionOptions,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ if (detection is null)
+ {
+ return null;
+ }
+ if ((detection.SupportedApis & ArchiveAccessMode.Archive) == 0)
+ {
+ throw new NotSupportedException(
+ "Inspecting multiple source streams is supported only for formats with an Archive API."
+ );
+ }
+
+ var archiveStreams = new List { firstArchiveStream };
+ archiveStreams.AddRange(
+ streams.Skip(1).Select(stream => new ArchiveOffsetStream(stream))
+ );
+ await using var archive = await OpenAsyncArchive(
+ archiveStreams,
+ inspectionOptions,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ return await InspectOpenedArchiveAsync(
+ archive,
+ detection,
+ physicalSize,
+ streams.Count,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ }
+ catch (CryptographicException) when (string.IsNullOrEmpty(options.Password))
+ {
+ var detection = await RedetectArchiveAsync(
+ streams[0],
+ startPositions[0],
+ options,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ return detection is null
+ ? null
+ : CreatePartialInformation(
+ detection,
+ physicalSize,
+ streams.Count,
+ ArchiveInformationLimitations.EncryptedHeaders
+ );
+ }
+ finally
+ {
+ for (var i = 0; i < streams.Count; i++)
+ {
+ streams[i].Seek(startPositions[i], SeekOrigin.Begin);
+ }
+ }
+ }
+
+ private static async ValueTask InspectOpenedArchiveAsync(
+ IAsyncArchive archive,
+ ArchiveDetection detection,
+ long? physicalSize,
+ int physicalPartCount,
+ CancellationToken cancellationToken
+ )
+ {
+ var entries = new List();
+ await foreach (
+ var entry in archive
+ .EntriesAsync.WithCancellation(cancellationToken)
+ .ConfigureAwait(false)
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ entries.Add(entry);
+ }
+
+ var volumes = new List();
+ await foreach (
+ var volume in archive
+ .VolumesAsync.WithCancellation(cancellationToken)
+ .ConfigureAwait(false)
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ volumes.Add(volume);
+ }
+
+ var entryArray = entries.ToArray();
+ var volumeArray = volumes.ToArray();
+ var zip = GetZipInformation(entryArray);
+ var (isSolid, solidStreamCount) = await GetSolidInformationAsync(archive, entryArray)
+ .ConfigureAwait(false);
+ var isComplete = await archive.IsCompleteAsync().ConfigureAwait(false);
+ var limitations = isComplete
+ ? ArchiveInformationLimitations.None
+ : ArchiveInformationLimitations.MissingVolumes;
+ if (archive.Type == ArchiveType.GZip)
+ {
+ limitations |= ArchiveInformationLimitations.UnavailableMetadata;
+ }
+ var hasEncryptedHeaders = volumeArray
+ .OfType()
+ .Any(volume => volume.IsHeaderEncrypted);
+ var hasEncryptedEntries = entryArray.Any(entry => entry.IsEncrypted);
+
+ return new ArchiveInformation(
+ detection,
+ GetStatus(limitations),
+ limitations,
+ GetFormatVersion(volumeArray),
+ entryArray.LongLength,
+ zip?.DataDescriptorEntryCount ?? 0,
+ physicalSize,
+ isComplete ? GetCompressedPayloadSize(archive, entryArray) : null,
+ isComplete && archive.Type != ArchiveType.GZip
+ ? entryArray.Aggregate(0L, (total, entry) => total + entry.Size)
+ : null,
+ isSolid,
+ solidStreamCount,
+ GetEncryptionScope(hasEncryptedHeaders, hasEncryptedEntries),
+ hasEncryptedHeaders || hasEncryptedEntries,
+ GetIsMultiVolume(archive.Type, volumeArray),
+ physicalPartCount,
+ volumeArray.Length,
+ isComplete,
+ GetArchiveComment(volumeArray),
+ zip
+ );
+ }
+
+ private static async ValueTask InspectOpenedReaderAsync(
+ IAsyncReader reader,
+ ArchiveDetection detection,
+ long? physicalSize,
+ int physicalPartCount,
+ AceMainHeader? aceHeader,
+ CancellationToken cancellationToken
+ )
+ {
+ var inspection = new ReaderInspection(detection);
+ while (await reader.MoveToNextEntryAsync(cancellationToken).ConfigureAwait(false))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ inspection.Add(reader.Entry);
+ }
+
+ var isSolid = aceHeader?.IsSolid ?? false;
+ var isMultiVolume = aceHeader?.IsMultiVolume ?? false;
+ var formatVersion = aceHeader is null ? null : $"ACE {aceHeader.AceVersion / 10.0:0.0}";
+ return inspection.CreateInformation(
+ physicalSize,
+ physicalPartCount,
+ isSolid,
+ isSolid && inspection.EntryCount > 1 ? 1 : 0,
+ isMultiVolume,
+ formatVersion
+ );
+ }
+
+ private static async ValueTask ReadAceHeaderAsync(
+ Stream stream,
+ ArchiveDetection detection,
+ ReaderOptions options,
+ CancellationToken cancellationToken
+ )
+ {
+ if (detection.ContainerType != ArchiveType.Ace)
+ {
+ return null;
+ }
+
+ try
+ {
+ stream.Position = 0;
+ return await new AceMainHeader(options.ArchiveEncoding)
+ .ReadAsync(stream, cancellationToken)
+ .ConfigureAwait(false) as AceMainHeader;
+ }
+ finally
+ {
+ stream.Position = 0;
+ }
+ }
+
+ private static async ValueTask<(bool IsSolid, long SolidStreamCount)> GetSolidInformationAsync(
+ IAsyncArchive archive,
+ IReadOnlyCollection entries
+ )
+ {
+ if (archive.Type == ArchiveType.SevenZip)
+ {
+ var solidStreamCount = entries
+ .OfType()
+ .Where(entry => !entry.IsDirectory && entry.FilePart.Folder is not null)
+ .GroupBy(entry => entry.FilePart.Folder)
+ .LongCount(group => group.Skip(1).Any());
+ return (solidStreamCount > 0, solidStreamCount);
+ }
+
+ if (archive.Type == ArchiveType.Rar)
+ {
+ return (
+ await archive.IsSolidAsync().ConfigureAwait(false),
+ CountRarSolidStreams(entries)
+ );
+ }
+
+ return (false, 0);
+ }
+
+ private static long? GetCompressedPayloadSize(
+ IAsyncArchive archive,
+ IReadOnlyCollection entries
+ ) =>
+ archive.Type switch
+ {
+ ArchiveType.GZip => null,
+ ArchiveType.SevenZip when archive is SevenZipArchive sevenZipArchive =>
+ sevenZipArchive.TotalSize,
+ _ => entries.Aggregate(0L, (total, entry) => total + entry.CompressedSize),
+ };
+
+ private static async ValueTask RedetectArchiveAsync(
+ Stream stream,
+ long startPosition,
+ ReaderOptions options,
+ CancellationToken cancellationToken
+ )
+ {
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ using var archiveStream = new ArchiveOffsetStream(stream);
+ return await TryDetectArchiveAsync(archiveStream, options, cancellationToken)
+ .ConfigureAwait(false);
+ }
+}
diff --git a/src/SharpCompress/Archives/ArchiveFactory.Information.cs b/src/SharpCompress/Archives/ArchiveFactory.Information.cs
new file mode 100644
index 00000000..1179a41a
--- /dev/null
+++ b/src/SharpCompress/Archives/ArchiveFactory.Information.cs
@@ -0,0 +1,682 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using SharpCompress.Archives.Rar;
+using SharpCompress.Archives.SevenZip;
+using SharpCompress.Archives.Zip;
+using SharpCompress.Common;
+using SharpCompress.Common.Rar;
+using SharpCompress.Common.Zip;
+using SharpCompress.Common.Zip.Headers;
+using SharpCompress.IO;
+using SharpCompress.Readers;
+using AceMainHeader = SharpCompress.Common.Ace.Headers.AceMainHeader;
+
+namespace SharpCompress.Archives;
+
+public static partial class ArchiveFactory
+{
+ ///
+ /// Collects metadata for the archive at the given file path.
+ ///
+ /// Path to the archive file.
+ /// Archive metadata, or when the file is not a supported archive.
+ public static ArchiveInformation? InspectArchive(string filePath) =>
+ InspectArchive(filePath, ReaderOptions.ForFilePath);
+
+ ///
+ /// Collects metadata for the archive at the given file path.
+ ///
+ /// Path to the archive file.
+ /// Options controlling archive inspection.
+ /// Archive metadata, or when the file is not a supported archive.
+ public static ArchiveInformation? InspectArchive(string filePath, ReaderOptions? readerOptions)
+ {
+ filePath.NotNullOrEmpty(nameof(filePath));
+ var options = readerOptions ?? ReaderOptions.ForFilePath;
+ var detection = DetectArchive(filePath, options);
+ if (detection is null)
+ {
+ return null;
+ }
+ if ((detection.SupportedApis & ArchiveAccessMode.Archive) != 0)
+ {
+ var fileInfos = GetArchiveFileParts(new FileInfo(filePath), options);
+ if (fileInfos.Length > 1)
+ {
+ return InspectArchive(fileInfos, options);
+ }
+ }
+
+ using Stream stream = File.OpenRead(filePath);
+ return InspectArchive(stream, options);
+ }
+
+ ///
+ /// Collects metadata for the archive in the given stream.
+ ///
+ /// A readable and seekable stream positioned at the start of the archive.
+ /// Archive metadata, or when the stream is not a supported archive.
+ public static ArchiveInformation? InspectArchive(Stream stream) =>
+ InspectArchive(stream, ReaderOptions.ForExternalStream);
+
+ ///
+ /// Collects metadata for the archive in the given stream.
+ ///
+ /// A readable and seekable stream positioned at the start of the archive.
+ /// Options controlling archive inspection.
+ /// Archive metadata, or when the stream is not a supported archive.
+ /// The supplied stream remains open and is restored to its original position.
+ public static ArchiveInformation? InspectArchive(Stream stream, ReaderOptions? readerOptions)
+ {
+ stream.RequireReadable();
+ stream.RequireSeekable();
+
+ var options = readerOptions ?? ReaderOptions.ForExternalStream;
+ var startPosition = stream.Position;
+ var physicalSize = GetPhysicalSize(stream, startPosition);
+
+ try
+ {
+ using var archiveStream = new ArchiveOffsetStream(stream);
+ var inspectionOptions = options with { LeaveStreamOpen = true };
+ var detection = TryDetectArchive(archiveStream, inspectionOptions);
+ if (detection is null)
+ {
+ return null;
+ }
+
+ if ((detection.SupportedApis & ArchiveAccessMode.Archive) != 0)
+ {
+ using var archive = OpenArchive(archiveStream, inspectionOptions);
+ return InspectOpenedArchive(archive, detection, physicalSize, 1);
+ }
+
+ var aceHeader = ReadAceHeader(archiveStream, detection, inspectionOptions);
+ using var reader = ReaderFactory.OpenReader(archiveStream, inspectionOptions);
+ return InspectOpenedReader(reader, detection, physicalSize, 1, aceHeader);
+ }
+ catch (CryptographicException) when (string.IsNullOrEmpty(options.Password))
+ {
+ var detection = RedetectArchive(stream, startPosition, options);
+ return detection is null
+ ? null
+ : CreatePartialInformation(
+ detection,
+ physicalSize,
+ 1,
+ ArchiveInformationLimitations.EncryptedHeaders
+ );
+ }
+ catch (MultipartStreamRequiredException)
+ {
+ var detection = RedetectArchive(stream, startPosition, options);
+ return detection is null
+ ? null
+ : CreatePartialInformation(
+ detection,
+ physicalSize,
+ 1,
+ ArchiveInformationLimitations.MissingVolumes
+ );
+ }
+ finally
+ {
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ }
+ }
+
+ ///
+ /// Collects metadata for an archive opened from multiple files.
+ ///
+ /// Archive source files in archive order.
+ /// Options controlling archive inspection.
+ /// Archive metadata, or when the files are not a supported archive.
+ public static ArchiveInformation? InspectArchive(
+ IReadOnlyList fileInfos,
+ ReaderOptions? readerOptions = null
+ )
+ {
+ fileInfos.NotNull(nameof(fileInfos));
+ if (fileInfos.Count == 0)
+ {
+ throw new ArchiveOperationException("No files to inspect");
+ }
+ if (fileInfos.Count == 1)
+ {
+ return InspectArchive(fileInfos[0].FullName, readerOptions);
+ }
+
+ var options = readerOptions ?? ReaderOptions.ForFilePath;
+ var detection = DetectArchive(fileInfos[0].FullName, options);
+ if (detection is null)
+ {
+ return null;
+ }
+ if ((detection.SupportedApis & ArchiveAccessMode.Archive) == 0)
+ {
+ throw new NotSupportedException(
+ "Inspecting multiple source files is supported only for formats with an Archive API."
+ );
+ }
+
+ var physicalSize = GetPhysicalSize(fileInfos);
+ try
+ {
+ using var archive = OpenArchive(fileInfos, options);
+ return InspectOpenedArchive(archive, detection, physicalSize, fileInfos.Count);
+ }
+ catch (CryptographicException) when (string.IsNullOrEmpty(options.Password))
+ {
+ return CreatePartialInformation(
+ detection,
+ physicalSize,
+ fileInfos.Count,
+ ArchiveInformationLimitations.EncryptedHeaders
+ );
+ }
+ }
+
+ ///
+ /// Collects metadata for an archive opened from multiple streams.
+ ///
+ /// Archive source streams in archive order.
+ /// Options controlling archive inspection.
+ /// Archive metadata, or when the streams are not a supported archive.
+ public static ArchiveInformation? InspectArchive(
+ IReadOnlyList streams,
+ ReaderOptions? readerOptions = null
+ )
+ {
+ streams.NotNull(nameof(streams));
+ if (streams.Count == 0)
+ {
+ throw new ArchiveOperationException("No streams to inspect");
+ }
+ if (streams.Count == 1)
+ {
+ return InspectArchive(streams[0], readerOptions);
+ }
+
+ var options = readerOptions ?? ReaderOptions.ForExternalStream;
+ var startPositions = streams.Select(stream => stream.Position).ToArray();
+ var physicalSize = GetPhysicalSize(streams, startPositions);
+
+ try
+ {
+ using var firstArchiveStream = new ArchiveOffsetStream(streams[0]);
+ var inspectionOptions = options with { LeaveStreamOpen = true };
+ var detection = TryDetectArchive(firstArchiveStream, inspectionOptions);
+ if (detection is null)
+ {
+ return null;
+ }
+ if ((detection.SupportedApis & ArchiveAccessMode.Archive) == 0)
+ {
+ throw new NotSupportedException(
+ "Inspecting multiple source streams is supported only for formats with an Archive API."
+ );
+ }
+
+ var archiveStreams = new List { firstArchiveStream };
+ archiveStreams.AddRange(
+ streams.Skip(1).Select(stream => new ArchiveOffsetStream(stream))
+ );
+ using var archive = OpenArchive(archiveStreams, inspectionOptions);
+ return InspectOpenedArchive(archive, detection, physicalSize, streams.Count);
+ }
+ catch (CryptographicException) when (string.IsNullOrEmpty(options.Password))
+ {
+ var detection = RedetectArchive(streams[0], startPositions[0], options);
+ return detection is null
+ ? null
+ : CreatePartialInformation(
+ detection,
+ physicalSize,
+ streams.Count,
+ ArchiveInformationLimitations.EncryptedHeaders
+ );
+ }
+ finally
+ {
+ for (var i = 0; i < streams.Count; i++)
+ {
+ streams[i].Seek(startPositions[i], SeekOrigin.Begin);
+ }
+ }
+ }
+
+ private static ArchiveInformation InspectOpenedArchive(
+ IArchive archive,
+ ArchiveDetection detection,
+ long? physicalSize,
+ int physicalPartCount
+ )
+ {
+ var entries = archive.Entries.ToArray();
+ var volumes = archive.Volumes.ToArray();
+ var zip = GetZipInformation(entries);
+ var (isSolid, solidStreamCount) = GetSolidInformation(archive, entries);
+ var isMultiVolume = GetIsMultiVolume(archive.Type, volumes);
+ var comment = GetArchiveComment(volumes);
+ var hasEncryptedHeaders = volumes
+ .OfType()
+ .Any(volume => volume.IsHeaderEncrypted);
+ var hasEncryptedEntries = entries.Any(entry => entry.IsEncrypted);
+ var isComplete = archive.IsComplete;
+ var limitations = isComplete
+ ? ArchiveInformationLimitations.None
+ : ArchiveInformationLimitations.MissingVolumes;
+ if (archive.Type == ArchiveType.GZip)
+ {
+ limitations |= ArchiveInformationLimitations.UnavailableMetadata;
+ }
+
+ return new ArchiveInformation(
+ detection,
+ GetStatus(limitations),
+ limitations,
+ GetFormatVersion(volumes),
+ entries.LongLength,
+ zip?.DataDescriptorEntryCount ?? 0,
+ physicalSize,
+ isComplete ? GetCompressedPayloadSize(archive, entries) : null,
+ isComplete && archive.Type != ArchiveType.GZip
+ ? entries.Aggregate(0L, (total, entry) => total + entry.Size)
+ : null,
+ isSolid,
+ solidStreamCount,
+ GetEncryptionScope(hasEncryptedHeaders, hasEncryptedEntries),
+ hasEncryptedHeaders || hasEncryptedEntries,
+ isMultiVolume,
+ physicalPartCount,
+ volumes.Length,
+ isComplete,
+ comment,
+ zip
+ );
+ }
+
+ private static ArchiveInformation InspectOpenedReader(
+ IReader reader,
+ ArchiveDetection detection,
+ long? physicalSize,
+ int physicalPartCount,
+ AceMainHeader? aceHeader
+ )
+ {
+ var inspection = new ReaderInspection(detection);
+ while (reader.MoveToNextEntry())
+ {
+ inspection.Add(reader.Entry);
+ }
+
+ var isSolid = aceHeader?.IsSolid ?? false;
+ var isMultiVolume = aceHeader?.IsMultiVolume ?? false;
+ var formatVersion = aceHeader is null ? null : $"ACE {aceHeader.AceVersion / 10.0:0.0}";
+ return inspection.CreateInformation(
+ physicalSize,
+ physicalPartCount,
+ isSolid,
+ isSolid && inspection.EntryCount > 1 ? 1 : 0,
+ isMultiVolume,
+ formatVersion
+ );
+ }
+
+ private static AceMainHeader? ReadAceHeader(
+ Stream stream,
+ ArchiveDetection detection,
+ ReaderOptions options
+ )
+ {
+ if (detection.ContainerType != ArchiveType.Ace)
+ {
+ return null;
+ }
+
+ try
+ {
+ stream.Position = 0;
+ return new AceMainHeader(options.ArchiveEncoding).Read(stream) as AceMainHeader;
+ }
+ finally
+ {
+ stream.Position = 0;
+ }
+ }
+
+ private static ArchiveInformation CreatePartialInformation(
+ ArchiveDetection detection,
+ long? physicalSize,
+ int physicalPartCount,
+ ArchiveInformationLimitations limitations
+ ) =>
+ new(
+ detection,
+ ArchiveInformationStatus.Partial,
+ limitations,
+ null,
+ null,
+ null,
+ physicalSize,
+ null,
+ null,
+ null,
+ null,
+ limitations.HasFlag(ArchiveInformationLimitations.EncryptedHeaders)
+ ? ArchiveEncryptionScope.Headers
+ : null,
+ limitations.HasFlag(ArchiveInformationLimitations.EncryptedHeaders) ? true : null,
+ null,
+ physicalPartCount,
+ null,
+ null,
+ null,
+ null
+ );
+
+ private static ArchiveInformationStatus GetStatus(ArchiveInformationLimitations limitations) =>
+ limitations == ArchiveInformationLimitations.None
+ ? ArchiveInformationStatus.Complete
+ : ArchiveInformationStatus.Partial;
+
+ private static ZipArchiveInformation? GetZipInformation(IEnumerable entries)
+ {
+ var dataDescriptorEntryCount = entries
+ .OfType()
+ .LongCount(entry =>
+ entry
+ .Parts.OfType()
+ .Any(part =>
+ FlagUtility.HasFlag(
+ part.Header.Flags,
+ SharpCompress.Common.Zip.Headers.HeaderFlags.UsePostDataDescriptor
+ )
+ )
+ );
+ return dataDescriptorEntryCount == 0 && !entries.OfType().Any()
+ ? null
+ : new ZipArchiveInformation(dataDescriptorEntryCount);
+ }
+
+ private static (bool IsSolid, long SolidStreamCount) GetSolidInformation(
+ IArchive archive,
+ IReadOnlyCollection entries
+ )
+ {
+ if (archive.Type == ArchiveType.SevenZip)
+ {
+ var solidStreamCount = entries
+ .OfType()
+ .Where(entry => !entry.IsDirectory && entry.FilePart.Folder is not null)
+ .GroupBy(entry => entry.FilePart.Folder)
+ .LongCount(group => group.Skip(1).Any());
+ return (solidStreamCount > 0, solidStreamCount);
+ }
+
+ if (archive.Type == ArchiveType.Rar)
+ {
+ var solidStreamCount = CountRarSolidStreams(entries);
+ return (archive.IsSolid, solidStreamCount);
+ }
+
+ return (false, 0);
+ }
+
+ private static long CountRarSolidStreams(IEnumerable entries)
+ {
+ var wasSolid = false;
+ long count = 0;
+ foreach (var entry in entries.Where(entry => !entry.IsDirectory))
+ {
+ if (entry.IsSolid && !wasSolid)
+ {
+ count++;
+ }
+ wasSolid = entry.IsSolid;
+ }
+ return count;
+ }
+
+ private static bool GetIsMultiVolume(ArchiveType type, IVolume[] volumes) =>
+ type == ArchiveType.Rar
+ ? volumes.OfType().Any(volume => volume.IsMultiVolume)
+ : volumes.Length > 1;
+
+ private static string? GetArchiveComment(IEnumerable volumes) =>
+ volumes.OfType().LastOrDefault()?.Comment
+ ?? volumes
+ .OfType()
+ .Select(volume => volume.Comment)
+ .FirstOrDefault(comment => comment is not null);
+
+ private static string? GetFormatVersion(IEnumerable volumes)
+ {
+ var rarVolume = volumes.OfType().FirstOrDefault();
+ return rarVolume is null ? null : $"RAR {rarVolume.MinVersion}-{rarVolume.MaxVersion}";
+ }
+
+ private static ArchiveEncryptionScope GetEncryptionScope(
+ bool hasEncryptedHeaders,
+ bool hasEncryptedEntries
+ )
+ {
+ var encryption = ArchiveEncryptionScope.None;
+ if (hasEncryptedHeaders)
+ {
+ encryption |= ArchiveEncryptionScope.Headers;
+ }
+ if (hasEncryptedEntries)
+ {
+ encryption |= ArchiveEncryptionScope.EntryData;
+ }
+ return encryption;
+ }
+
+ private static long? GetCompressedPayloadSize(
+ IArchive archive,
+ IReadOnlyCollection entries
+ ) =>
+ archive.Type switch
+ {
+ ArchiveType.GZip => null,
+ ArchiveType.SevenZip when archive is SevenZipArchive sevenZipArchive =>
+ sevenZipArchive.TotalSize,
+ _ => entries.Aggregate(0L, (total, entry) => total + entry.CompressedSize),
+ };
+
+ private static long? GetPhysicalSize(Stream stream, long startPosition)
+ {
+ try
+ {
+ return stream.Length - startPosition;
+ }
+ catch (NotSupportedException)
+ {
+ return null;
+ }
+ }
+
+ private static ArchiveDetection? RedetectArchive(
+ Stream stream,
+ long startPosition,
+ ReaderOptions options
+ )
+ {
+ stream.Seek(startPosition, SeekOrigin.Begin);
+ using var archiveStream = new ArchiveOffsetStream(stream);
+ return TryDetectArchive(archiveStream, options);
+ }
+
+ private static FileInfo[] GetArchiveFileParts(FileInfo firstPart, ReaderOptions options)
+ {
+ using Stream stream = firstPart.OpenRead();
+ var factory = TryFindFactory(stream, options);
+ if (factory is null)
+ {
+ return [firstPart];
+ }
+
+ var parts = new List { firstPart };
+ for (var index = 1; factory.GetFilePart(index, firstPart) is { } part; index++)
+ {
+ parts.Add(part);
+ }
+ return parts.ToArray();
+ }
+
+ private static long? GetPhysicalSize(IEnumerable fileInfos)
+ {
+ try
+ {
+ return fileInfos.Aggregate(0L, (total, fileInfo) => total + fileInfo.Length);
+ }
+ catch (IOException)
+ {
+ return null;
+ }
+ }
+
+ private static long? GetPhysicalSize(IReadOnlyList streams, long[] startPositions)
+ {
+ try
+ {
+ long total = 0;
+ for (var i = 0; i < streams.Count; i++)
+ {
+ total += streams[i].Length - startPositions[i];
+ }
+ return total;
+ }
+ catch (NotSupportedException)
+ {
+ return null;
+ }
+ }
+
+ private sealed class ReaderInspection
+ {
+ private readonly ArchiveDetection detection;
+ private long? compressedPayloadSize = 0;
+ private long? uncompressedPayloadSize = 0;
+ private long dataDescriptorEntryCount;
+ private long entriesWithUnknownSizeCount;
+ private bool isEncrypted;
+ private ArchiveInformationLimitations limitations;
+
+ public ReaderInspection(ArchiveDetection detection)
+ {
+ this.detection = detection;
+ if (
+ detection.ContainerType == ArchiveType.Lzw
+ || (
+ detection.ContainerType == ArchiveType.Tar
+ && detection.OuterCompressionType is not null
+ )
+ )
+ {
+ compressedPayloadSize = null;
+ limitations |= ArchiveInformationLimitations.UnavailableMetadata;
+ }
+ if (detection.ContainerType == ArchiveType.Lzw)
+ {
+ uncompressedPayloadSize = null;
+ }
+ }
+
+ public long EntryCount { get; private set; }
+
+ public void Add(IEntry entry)
+ {
+ EntryCount++;
+ isEncrypted |= entry.IsEncrypted;
+
+ var usesDataDescriptor = UsesZipDataDescriptor(entry);
+ if (usesDataDescriptor)
+ {
+ dataDescriptorEntryCount++;
+ }
+
+ if (
+ detection.ContainerType == ArchiveType.Lzw
+ || usesDataDescriptor
+ || !TryGetSize(entry, out var size)
+ )
+ {
+ entriesWithUnknownSizeCount++;
+ uncompressedPayloadSize = null;
+ limitations |= ArchiveInformationLimitations.UnavailableMetadata;
+ }
+ else if (uncompressedPayloadSize is { } totalSize)
+ {
+ uncompressedPayloadSize = totalSize + size;
+ }
+
+ if (compressedPayloadSize is { } totalCompressedSize)
+ {
+ compressedPayloadSize = totalCompressedSize + entry.CompressedSize;
+ }
+ }
+
+ public ArchiveInformation CreateInformation(
+ long? physicalSize,
+ int physicalPartCount,
+ bool isSolid,
+ long solidStreamCount,
+ bool isMultiVolume,
+ string? formatVersion
+ ) =>
+ new(
+ detection,
+ GetStatus(limitations),
+ limitations,
+ formatVersion,
+ EntryCount,
+ entriesWithUnknownSizeCount,
+ physicalSize,
+ compressedPayloadSize,
+ uncompressedPayloadSize,
+ isSolid,
+ solidStreamCount,
+ isEncrypted ? ArchiveEncryptionScope.EntryData : ArchiveEncryptionScope.None,
+ isEncrypted,
+ isMultiVolume,
+ physicalPartCount,
+ 1,
+ true,
+ null,
+ detection.ContainerType == ArchiveType.Zip
+ ? new ZipArchiveInformation(dataDescriptorEntryCount)
+ : null
+ );
+
+ private static bool TryGetSize(IEntry entry, out long size)
+ {
+ try
+ {
+ size = entry.Size;
+ return size >= 0;
+ }
+ catch (NotImplementedException)
+ {
+ size = 0;
+ return false;
+ }
+ }
+
+ private static bool UsesZipDataDescriptor(IEntry entry) =>
+ entry is ZipEntry zipEntry
+ && zipEntry
+ .Parts.OfType()
+ .Any(part =>
+ FlagUtility.HasFlag(
+ part.Header.Flags,
+ SharpCompress.Common.Zip.Headers.HeaderFlags.UsePostDataDescriptor
+ )
+ );
+ }
+}
diff --git a/src/SharpCompress/Archives/ArchiveInformation.cs b/src/SharpCompress/Archives/ArchiveInformation.cs
index 81d4fc6d..68468771 100644
--- a/src/SharpCompress/Archives/ArchiveInformation.cs
+++ b/src/SharpCompress/Archives/ArchiveInformation.cs
@@ -1,50 +1,145 @@
-using SharpCompress.Common;
-
namespace SharpCompress.Archives;
///
-/// Contains information about a detected archive, including its type and supported capabilities.
+/// Contains metadata collected by fully inspecting an archive.
///
-///
-/// Use or
-///
-/// to obtain an instance of this record.
-///
-public record ArchiveInformation
+public sealed class ArchiveInformation
{
- ///
- /// The type of archive detected, or when the format is not a registered well-known type.
- ///
- public ArchiveType? Type { get; set; }
-
- ///
- /// 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 bool SupportsRandomAccess { get; set; }
-
- ///
- /// For ZIP archives, the number of entries that use post-data descriptor trailers.
- /// This value is for non-ZIP formats.
- ///
- public int? ZipDataDescriptorEntryCount { get; set; }
-
- ///
- /// For solid-capable archive formats, the number of solid compressed streams present.
- /// This value is for formats that do not support solid entries.
- ///
- public int? SolidStreamCount { get; set; }
-
- ///
- /// Creates a new archive information instance.
- ///
- /// The detected archive type.
- /// Whether the detected format supports random access.
- public ArchiveInformation(ArchiveType? type, bool supportsRandomAccess)
+ internal ArchiveInformation(
+ ArchiveDetection detection,
+ ArchiveInformationStatus status,
+ ArchiveInformationLimitations limitations,
+ string? formatVersion,
+ long? entryCount,
+ long? entriesWithUnknownSizeCount,
+ long? physicalSize,
+ long? compressedPayloadSize,
+ long? uncompressedPayloadSize,
+ bool? isSolid,
+ long? solidStreamCount,
+ ArchiveEncryptionScope? encryption,
+ bool? isEncrypted,
+ bool? isMultiVolume,
+ int? physicalPartCount,
+ int? logicalVolumeCount,
+ bool? isComplete,
+ string? comment,
+ ZipArchiveInformation? zip
+ )
{
- Type = type;
- SupportsRandomAccess = supportsRandomAccess;
+ Detection = detection;
+ Status = status;
+ Limitations = limitations;
+ FormatVersion = formatVersion;
+ EntryCount = entryCount;
+ EntriesWithUnknownSizeCount = entriesWithUnknownSizeCount;
+ PhysicalSize = physicalSize;
+ CompressedPayloadSize = compressedPayloadSize;
+ UncompressedPayloadSize = uncompressedPayloadSize;
+ IsSolid = isSolid;
+ SolidStreamCount = solidStreamCount;
+ Encryption = encryption;
+ IsEncrypted = isEncrypted;
+ IsMultiVolume = isMultiVolume;
+ PhysicalPartCount = physicalPartCount;
+ LogicalVolumeCount = logicalVolumeCount;
+ IsComplete = isComplete;
+ Comment = comment;
+ Zip = zip;
}
+
+ ///
+ /// Gets the format and API capabilities identified before inspection.
+ ///
+ public ArchiveDetection Detection { get; }
+
+ ///
+ /// Gets whether metadata collection completed.
+ ///
+ public ArchiveInformationStatus Status { get; }
+
+ ///
+ /// Gets conditions that prevented complete metadata collection.
+ ///
+ public ArchiveInformationLimitations Limitations { get; }
+
+ ///
+ /// Gets the format version when exposed by the archive format.
+ ///
+ public string? FormatVersion { get; }
+
+ ///
+ /// Gets the number of entries, excluding format control records.
+ ///
+ public long? EntryCount { get; }
+
+ ///
+ /// Gets the number of entries for which a forward-only reader cannot know the uncompressed size before reading entry data.
+ ///
+ public long? EntriesWithUnknownSizeCount { get; }
+
+ ///
+ /// Gets the number of bytes in the supplied source parts, when known.
+ ///
+ public long? PhysicalSize { get; }
+
+ ///
+ /// Gets the aggregate stored payload size, when the format exposes it reliably.
+ ///
+ public long? CompressedPayloadSize { get; }
+
+ ///
+ /// Gets the aggregate uncompressed entry size, when the format exposes it reliably.
+ ///
+ public long? UncompressedPayloadSize { get; }
+
+ ///
+ /// Gets whether entries share compression state.
+ ///
+ public bool? IsSolid { get; }
+
+ ///
+ /// Gets the number of independent shared compression streams, when known.
+ ///
+ public long? SolidStreamCount { get; }
+
+ ///
+ /// Gets the scopes protected by encryption, when known.
+ ///
+ public ArchiveEncryptionScope? Encryption { get; }
+
+ ///
+ /// Gets whether archive headers or entry data are encrypted, when known.
+ ///
+ public bool? IsEncrypted { get; }
+
+ ///
+ /// Gets whether the archive is part of a multi-volume set, when known.
+ ///
+ public bool? IsMultiVolume { get; }
+
+ ///
+ /// Gets the number of source parts supplied for inspection.
+ ///
+ public int? PhysicalPartCount { get; }
+
+ ///
+ /// Gets the number of logical volumes exposed by the archive, when known.
+ ///
+ public int? LogicalVolumeCount { get; }
+
+ ///
+ /// Gets whether all discovered entries are complete, when known.
+ ///
+ public bool? IsComplete { get; }
+
+ ///
+ /// Gets the archive comment, when supported by the format.
+ ///
+ public string? Comment { get; }
+
+ ///
+ /// Gets ZIP-specific metadata when the detected container is ZIP.
+ ///
+ public ZipArchiveInformation? Zip { get; }
}
diff --git a/src/SharpCompress/Archives/ArchiveInformationLimitations.cs b/src/SharpCompress/Archives/ArchiveInformationLimitations.cs
new file mode 100644
index 00000000..26f2cd3a
--- /dev/null
+++ b/src/SharpCompress/Archives/ArchiveInformationLimitations.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace SharpCompress.Archives;
+
+///
+/// Identifies conditions that prevented complete archive metadata inspection.
+///
+[Flags]
+public enum ArchiveInformationLimitations
+{
+ ///
+ /// No known inspection limitations apply.
+ ///
+ None = 0,
+
+ ///
+ /// Archive headers are encrypted and no password was supplied.
+ ///
+ EncryptedHeaders = 1,
+
+ ///
+ /// One or more archive volumes are unavailable.
+ ///
+ MissingVolumes = 2,
+
+ ///
+ /// The format does not expose a requested metadata value.
+ ///
+ UnavailableMetadata = 4,
+}
diff --git a/src/SharpCompress/Archives/ArchiveInformationStatus.cs b/src/SharpCompress/Archives/ArchiveInformationStatus.cs
new file mode 100644
index 00000000..dabee58b
--- /dev/null
+++ b/src/SharpCompress/Archives/ArchiveInformationStatus.cs
@@ -0,0 +1,17 @@
+namespace SharpCompress.Archives;
+
+///
+/// Describes whether archive metadata could be collected completely.
+///
+public enum ArchiveInformationStatus
+{
+ ///
+ /// All metadata supported by the format was collected.
+ ///
+ Complete,
+
+ ///
+ /// Some metadata could not be collected. See .
+ ///
+ Partial,
+}
diff --git a/src/SharpCompress/Archives/ZipArchiveInformation.cs b/src/SharpCompress/Archives/ZipArchiveInformation.cs
new file mode 100644
index 00000000..c7160903
--- /dev/null
+++ b/src/SharpCompress/Archives/ZipArchiveInformation.cs
@@ -0,0 +1,15 @@
+namespace SharpCompress.Archives;
+
+///
+/// Contains ZIP-specific metadata.
+///
+public sealed class ZipArchiveInformation
+{
+ internal ZipArchiveInformation(long dataDescriptorEntryCount) =>
+ DataDescriptorEntryCount = dataDescriptorEntryCount;
+
+ ///
+ /// Gets the number of entries whose local header defers its CRC and sizes to a data descriptor.
+ ///
+ public long DataDescriptorEntryCount { get; }
+}
diff --git a/src/SharpCompress/Common/Rar/RarVolume.cs b/src/SharpCompress/Common/Rar/RarVolume.cs
index 1b7de043..f3913d2c 100644
--- a/src/SharpCompress/Common/Rar/RarVolume.cs
+++ b/src/SharpCompress/Common/Rar/RarVolume.cs
@@ -25,6 +25,8 @@ public abstract class RarVolume : Volume
private ArchiveHeader? ArchiveHeader { get; set; }
+ internal bool IsHeaderEncrypted => _headerFactory.IsEncrypted;
+
private StreamingMode Mode => _headerFactory.StreamingMode;
internal abstract IEnumerable ReadFileParts();
diff --git a/src/SharpCompress/IO/ArchiveOffsetStream.cs b/src/SharpCompress/IO/ArchiveOffsetStream.cs
new file mode 100644
index 00000000..9c7bc03e
--- /dev/null
+++ b/src/SharpCompress/IO/ArchiveOffsetStream.cs
@@ -0,0 +1,87 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SharpCompress.IO;
+
+///
+/// Exposes the remainder of a seekable stream as an archive that begins at position zero.
+///
+internal sealed class ArchiveOffsetStream : Stream
+{
+ private readonly Stream stream;
+ private readonly long origin;
+
+ public ArchiveOffsetStream(Stream stream)
+ {
+ this.stream = stream;
+ origin = stream.Position;
+ }
+
+ public override bool CanRead => stream.CanRead;
+ public override bool CanSeek => stream.CanSeek;
+ public override bool CanWrite => false;
+ public override long Length => stream.Length - origin;
+
+ public override long Position
+ {
+ get => stream.Position - origin;
+ set => stream.Position = origin + value;
+ }
+
+ public override void Flush() { }
+
+ public override int Read(byte[] buffer, int offset, int count) =>
+ stream.Read(buffer, offset, count);
+
+#if !LEGACY_DOTNET
+ public override int Read(Span buffer) => stream.Read(buffer);
+#endif
+
+ public override int ReadByte() => stream.ReadByte();
+
+ public override Task ReadAsync(
+ byte[] buffer,
+ int offset,
+ int count,
+ CancellationToken cancellationToken
+ ) => stream.ReadAsync(buffer, offset, count, cancellationToken);
+
+#if !NET48 && !NETSTANDARD2_0
+ public override ValueTask ReadAsync(
+ Memory buffer,
+ CancellationToken cancellationToken = default
+ ) => stream.ReadAsync(buffer, cancellationToken);
+#endif
+
+ public override long Seek(long offset, SeekOrigin seekOrigin)
+ {
+ var position = seekOrigin switch
+ {
+ SeekOrigin.Begin => offset,
+ SeekOrigin.Current => Position + offset,
+ SeekOrigin.End => Length + offset,
+ _ => throw new ArgumentOutOfRangeException(nameof(seekOrigin)),
+ };
+
+ if (position < 0)
+ {
+ throw new IOException("Attempted to seek before the archive origin.");
+ }
+
+ stream.Seek(origin + position, SeekOrigin.Begin);
+ return position;
+ }
+
+ public override void SetLength(long value) => throw new NotSupportedException();
+
+ public override void Write(byte[] buffer, int offset, int count) =>
+ throw new NotSupportedException();
+
+ protected override void Dispose(bool disposing)
+ {
+ // The caller owns the source stream.
+ base.Dispose(disposing);
+ }
+}
diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs
index 2c8aa42c..5ea9d3c0 100644
--- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs
+++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs
@@ -247,38 +247,36 @@ public class ArchiveFactoryTests : TestBase
[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(
+ public void DetectArchive_ReturnsExpectedInfo(
string archiveName,
ArchiveType expectedType,
bool expectedRandomAccess
)
{
- var info = ArchiveFactory.GetArchiveInformation(
- Path.Combine(TEST_ARCHIVES_PATH, archiveName)
- );
+ var info = ArchiveFactory.DetectArchive(Path.Combine(TEST_ARCHIVES_PATH, archiveName));
Assert.NotNull(info);
- Assert.Equal(expectedType, info.Type);
- Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess);
+ Assert.Equal(expectedType, info.ContainerType);
+ Assert.Equal(expectedRandomAccess, info.SupportedApis.HasFlag(ArchiveAccessMode.Archive));
}
[Theory]
[InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)]
[InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)]
- public void GetArchiveInformation_WithReaderOptions_ReturnsExpectedInfo(
+ public void DetectArchive_WithReaderOptions_ReturnsExpectedInfo(
string archiveName,
ArchiveType expectedType,
bool expectedRandomAccess
)
{
- var info = ArchiveFactory.GetArchiveInformation(
+ var info = ArchiveFactory.DetectArchive(
GetTestArchivePath(archiveName),
ReaderOptions.ForFilePath.WithLookForHeader(true)
);
Assert.NotNull(info);
- Assert.Equal(expectedType, info.Type);
- Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess);
+ Assert.Equal(expectedType, info.ContainerType);
+ Assert.Equal(expectedRandomAccess, info.SupportedApis.HasFlag(ArchiveAccessMode.Archive));
}
[Theory]
@@ -288,38 +286,38 @@ public class ArchiveFactoryTests : TestBase
[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(
+ public async ValueTask DetectArchiveAsync_ReturnsExpectedInfo(
string archiveName,
ArchiveType expectedType,
bool expectedRandomAccess
)
{
- var info = await ArchiveFactory.GetArchiveInformationAsync(
+ var info = await ArchiveFactory.DetectArchiveAsync(
Path.Combine(TEST_ARCHIVES_PATH, archiveName)
);
Assert.NotNull(info);
- Assert.Equal(expectedType, info.Type);
- Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess);
+ Assert.Equal(expectedType, info.ContainerType);
+ Assert.Equal(expectedRandomAccess, info.SupportedApis.HasFlag(ArchiveAccessMode.Archive));
}
[Theory]
[InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)]
[InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)]
- public async ValueTask GetArchiveInformationAsync_WithReaderOptions_ReturnsExpectedInfo(
+ public async ValueTask DetectArchiveAsync_WithReaderOptions_ReturnsExpectedInfo(
string archiveName,
ArchiveType expectedType,
bool expectedRandomAccess
)
{
- var info = await ArchiveFactory.GetArchiveInformationAsync(
+ var info = await ArchiveFactory.DetectArchiveAsync(
GetTestArchivePath(archiveName),
ReaderOptions.ForFilePath.WithLookForHeader(true)
);
Assert.NotNull(info);
- Assert.Equal(expectedType, info.Type);
- Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess);
+ Assert.Equal(expectedType, info.ContainerType);
+ Assert.Equal(expectedRandomAccess, info.SupportedApis.HasFlag(ArchiveAccessMode.Archive));
}
[Theory]
@@ -466,17 +464,17 @@ public class ArchiveFactoryTests : TestBase
[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(
+ public void DetectArchive_DetectsSingleFileTestArchives(
string archiveName,
ArchiveType expectedType,
bool expectedSeekable
)
{
- var info = ArchiveFactory.GetArchiveInformation(GetTestArchivePath(archiveName));
+ var info = ArchiveFactory.DetectArchive(GetTestArchivePath(archiveName));
Assert.NotNull(info);
- Assert.Equal(expectedType, info.Type);
- Assert.Equal(expectedSeekable, info.SupportsRandomAccess);
+ Assert.Equal(expectedType, info.ContainerType);
+ Assert.Equal(expectedSeekable, info.SupportedApis.HasFlag(ArchiveAccessMode.Archive));
}
[Theory]
@@ -623,35 +621,35 @@ public class ArchiveFactoryTests : TestBase
[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(
+ public async ValueTask DetectArchiveAsync_DetectsSingleFileTestArchives(
string archiveName,
ArchiveType expectedType,
bool expectedSeekable
)
{
- var info = await ArchiveFactory.GetArchiveInformationAsync(GetTestArchivePath(archiveName));
+ var info = await ArchiveFactory.DetectArchiveAsync(GetTestArchivePath(archiveName));
Assert.NotNull(info);
- Assert.Equal(expectedType, info.Type);
- Assert.Equal(expectedSeekable, info.SupportsRandomAccess);
+ Assert.Equal(expectedType, info.ContainerType);
+ Assert.Equal(expectedSeekable, info.SupportedApis.HasFlag(ArchiveAccessMode.Archive));
}
[Fact]
- public void GetArchiveInformation_ReturnsNull_ForNonArchive()
+ public void DetectArchive_ReturnsNull_ForNonArchive()
{
using var stream = new MemoryStream(Encoding.ASCII.GetBytes("not an archive"));
- var info = ArchiveFactory.GetArchiveInformation(stream);
+ var info = ArchiveFactory.DetectArchive(stream);
Assert.Null(info);
}
[Fact]
- public async ValueTask GetArchiveInformationAsync_ReturnsNull_ForNonArchive()
+ public async ValueTask DetectArchiveAsync_ReturnsNull_ForNonArchive()
{
using var stream = new MemoryStream(Encoding.ASCII.GetBytes("not an archive"));
- var info = await ArchiveFactory.GetArchiveInformationAsync(stream);
+ var info = await ArchiveFactory.DetectArchiveAsync(stream);
Assert.Null(info);
}
@@ -697,13 +695,16 @@ public class ArchiveFactoryTests : TestBase
[InlineData("Tar.tar.xz")]
[InlineData("Tar.tar.zst")]
[InlineData("Tar.tar.Z")]
- public void GetArchiveInformation_ReturnsNull_ForCompressedTar(string archiveName)
+ public void DetectArchive_RecognizesCompressedTar(string archiveName)
{
using var stream = File.OpenRead(GetTestArchivePath(archiveName));
- var info = ArchiveFactory.GetArchiveInformation(stream);
+ var info = ArchiveFactory.DetectArchive(stream);
- Assert.Null(info);
+ Assert.NotNull(info);
+ Assert.Equal(ArchiveType.Tar, info.ContainerType);
+ Assert.NotNull(info.OuterCompressionType);
+ Assert.Equal(ArchiveAccessMode.Reader, info.SupportedApis);
}
[Theory]
@@ -713,21 +714,37 @@ public class ArchiveFactoryTests : TestBase
[InlineData("Tar.tar.xz")]
[InlineData("Tar.tar.zst")]
[InlineData("Tar.tar.Z")]
- public async ValueTask GetArchiveInformationAsync_ReturnsNull_ForCompressedTar(
- string archiveName
- )
+ public async ValueTask DetectArchiveAsync_RecognizesCompressedTar(string archiveName)
{
using var stream = File.OpenRead(GetTestArchivePath(archiveName));
- var info = await ArchiveFactory.GetArchiveInformationAsync(stream);
+ var info = await ArchiveFactory.DetectArchiveAsync(stream);
- Assert.Null(info);
+ Assert.NotNull(info);
+ Assert.Equal(ArchiveType.Tar, info.ContainerType);
+ Assert.NotNull(info.OuterCompressionType);
+ Assert.Equal(ArchiveAccessMode.Reader, info.SupportedApis);
}
[Theory]
[InlineData("Zip.deflate.zip", ArchiveType.Zip)]
[InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)]
- public void GetArchiveInformation_Stream_PreservesPosition(
+ public void DetectArchive_Stream_PreservesPosition(string archiveName, ArchiveType expectedType)
+ {
+ using var stream = CreatePrefixedArchiveStream(archiveName, 13);
+ var startPosition = stream.Position;
+
+ var info = ArchiveFactory.DetectArchive(stream);
+
+ Assert.NotNull(info);
+ Assert.Equal(expectedType, info.ContainerType);
+ Assert.Equal(startPosition, stream.Position);
+ }
+
+ [Theory]
+ [InlineData("Zip.deflate.zip", ArchiveType.Zip)]
+ [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)]
+ public async ValueTask DetectArchiveAsync_Stream_PreservesPosition(
string archiveName,
ArchiveType expectedType
)
@@ -735,17 +752,76 @@ public class ArchiveFactoryTests : TestBase
using var stream = CreatePrefixedArchiveStream(archiveName, 13);
var startPosition = stream.Position;
- var info = ArchiveFactory.GetArchiveInformation(stream);
+ var info = await ArchiveFactory.DetectArchiveAsync(stream);
Assert.NotNull(info);
- Assert.Equal(expectedType, info.Type);
+ Assert.Equal(expectedType, info.ContainerType);
Assert.Equal(startPosition, stream.Position);
}
+ [Theory]
+ [InlineData("Zip.none.datadescriptors.zip", 2)]
+ [InlineData("Zip.deflate.zip", 0)]
+ public void InspectArchive_ZipDataDescriptorEntryCount(
+ string archiveName,
+ long expectedDataDescriptorEntryCount
+ )
+ {
+ using var stream = File.OpenRead(GetTestArchivePath(archiveName));
+
+ var info = ArchiveFactory.InspectArchive(stream);
+
+ Assert.NotNull(info);
+ Assert.Equal(ArchiveType.Zip, info.Detection.ContainerType);
+ Assert.NotNull(info.Zip);
+ Assert.Equal(expectedDataDescriptorEntryCount, info.Zip.DataDescriptorEntryCount);
+ Assert.Equal(expectedDataDescriptorEntryCount, info.EntriesWithUnknownSizeCount);
+ }
+
+ [Theory]
+ [InlineData("7Zip.solid.7z", true, 1)]
+ [InlineData("7Zip.nonsolid.7z", false, 0)]
+ [InlineData("Rar.rar", false, 0)]
+ [InlineData("Rar.solid.rar", true, 1)]
+ public void InspectArchive_SolidStreamCount(
+ string archiveName,
+ bool expectedSolid,
+ long expectedSolidStreamCount
+ )
+ {
+ using var stream = File.OpenRead(GetTestArchivePath(archiveName));
+
+ var info = ArchiveFactory.InspectArchive(stream);
+
+ Assert.NotNull(info);
+ Assert.Equal(expectedSolid, info.IsSolid);
+ Assert.Equal(expectedSolidStreamCount, info.SolidStreamCount);
+ }
+
+ [Theory]
+ [InlineData("Zip.none.datadescriptors.zip", ArchiveType.Zip)]
+ [InlineData("7Zip.solid.7z", ArchiveType.SevenZip)]
+ public async ValueTask InspectArchiveAsync_ReturnsMetadata(
+ string archiveName,
+ ArchiveType expectedType
+ )
+ {
+ using var stream = File.OpenRead(GetTestArchivePath(archiveName));
+
+ var info = await ArchiveFactory.InspectArchiveAsync(stream);
+
+ Assert.NotNull(info);
+ Assert.Equal(expectedType, info.Detection.ContainerType);
+ Assert.Equal(ArchiveInformationStatus.Complete, info.Status);
+ Assert.NotNull(info.EntryCount);
+ }
+
[Theory]
[InlineData("Zip.deflate.zip", ArchiveType.Zip)]
[InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)]
- public async ValueTask GetArchiveInformationAsync_Stream_PreservesPosition(
+ [InlineData("Rar.rar", ArchiveType.Rar)]
+ [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip)]
+ public void InspectArchive_Stream_PreservesPosition(
string archiveName,
ArchiveType expectedType
)
@@ -753,62 +829,172 @@ public class ArchiveFactoryTests : TestBase
using var stream = CreatePrefixedArchiveStream(archiveName, 13);
var startPosition = stream.Position;
- var info = await ArchiveFactory.GetArchiveInformationAsync(stream);
+ var info = ArchiveFactory.InspectArchive(stream);
Assert.NotNull(info);
- Assert.Equal(expectedType, info.Type);
+ Assert.Equal(expectedType, info.Detection.ContainerType);
Assert.Equal(startPosition, stream.Position);
}
[Theory]
- [InlineData("Zip.none.datadescriptors.zip", true)]
- [InlineData("Zip.deflate.zip", false)]
- public void GetArchiveInformation_ZipDataDescriptorEntryCount(
+ [InlineData("Zip.deflate.zip", ArchiveType.Zip)]
+ [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)]
+ [InlineData("Rar.rar", ArchiveType.Rar)]
+ [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip)]
+ public async ValueTask InspectArchiveAsync_Stream_PreservesPosition(
string archiveName,
- bool expectsDataDescriptorTrailers
+ ArchiveType expectedType
)
{
- using var stream = File.OpenRead(GetTestArchivePath(archiveName));
+ using var stream = CreatePrefixedArchiveStream(archiveName, 13);
+ var startPosition = stream.Position;
- var info = ArchiveFactory.GetArchiveInformation(stream);
+ var info = await ArchiveFactory.InspectArchiveAsync(stream);
Assert.NotNull(info);
- Assert.Equal(ArchiveType.Zip, info.Type);
- Assert.NotNull(info.ZipDataDescriptorEntryCount);
- Assert.Equal(expectsDataDescriptorTrailers, info.ZipDataDescriptorEntryCount > 0);
+ Assert.Equal(expectedType, info.Detection.ContainerType);
+ Assert.Equal(startPosition, stream.Position);
+ }
+
+ [Fact]
+ public void InspectArchive_CompressedTar_ReportsContainerAndWrapper()
+ {
+ var info = ArchiveFactory.InspectArchive(GetTestArchivePath("Tar.tar.xz"));
+
+ Assert.NotNull(info);
+ Assert.Equal(ArchiveType.Tar, info.Detection.ContainerType);
+ Assert.Equal(CompressionType.Xz, info.Detection.OuterCompressionType);
+ Assert.Equal(ArchiveInformationStatus.Partial, info.Status);
+ Assert.Null(info.CompressedPayloadSize);
+ Assert.NotNull(info.EntryCount);
}
[Theory]
- [InlineData("7Zip.solid.7z", true)]
- [InlineData("7Zip.nonsolid.7z", false)]
- [InlineData("Rar.rar", false)]
- public void GetArchiveInformation_SolidStreamCount(string archiveName, bool expectsSolidStreams)
+ [InlineData("Ace.method1-solid.ace", true)]
+ [InlineData("Ace.method1.ace", false)]
+ public void InspectArchive_Ace_ReportsSolidMetadata(string archiveName, bool expectedSolid)
{
- using var stream = File.OpenRead(GetTestArchivePath(archiveName));
-
- var info = ArchiveFactory.GetArchiveInformation(stream);
+ var info = ArchiveFactory.InspectArchive(GetTestArchivePath(archiveName));
Assert.NotNull(info);
- Assert.NotNull(info.SolidStreamCount);
- Assert.Equal(expectsSolidStreams, info.SolidStreamCount > 0);
+ Assert.Equal(expectedSolid, info.IsSolid);
+ Assert.Equal(expectedSolid ? 1 : 0, info.SolidStreamCount);
+ }
+
+ [Fact]
+ public void InspectArchive_Arc_ReportsUnavailableUncompressedSizes()
+ {
+ var info = ArchiveFactory.InspectArchive(GetTestArchivePath("Arc.uncompressed.arc"));
+
+ Assert.NotNull(info);
+ Assert.Equal(ArchiveInformationStatus.Partial, info.Status);
+ Assert.True(info.Limitations.HasFlag(ArchiveInformationLimitations.UnavailableMetadata));
+ Assert.Null(info.UncompressedPayloadSize);
+ Assert.True(info.EntriesWithUnknownSizeCount > 0);
}
[Theory]
- [InlineData("Zip.none.datadescriptors.zip", true)]
- [InlineData("7Zip.solid.7z", true)]
- public async ValueTask GetArchiveInformationAsync_IncludesFormatSpecificDetails(
+ [InlineData("Arj.store.arj", ArchiveType.Arj, ArchiveInformationStatus.Complete)]
+ [InlineData("large_test.txt.Z", ArchiveType.Lzw, ArchiveInformationStatus.Partial)]
+ public void InspectArchive_ReaderOnlyFormats_ReturnsMetadata(
string archiveName,
- bool expectsAnyFormatSpecificDetails
+ ArchiveType expectedType,
+ ArchiveInformationStatus expectedStatus
)
{
- using var stream = File.OpenRead(GetTestArchivePath(archiveName));
-
- var info = await ArchiveFactory.GetArchiveInformationAsync(stream);
+ var info = ArchiveFactory.InspectArchive(GetTestArchivePath(archiveName));
Assert.NotNull(info);
- var hasZipDetail = info.ZipDataDescriptorEntryCount > 0;
- var hasSolidDetail = info.SolidStreamCount > 0;
- Assert.Equal(expectsAnyFormatSpecificDetails, hasZipDetail || hasSolidDetail);
+ Assert.Equal(expectedType, info.Detection.ContainerType);
+ Assert.Equal(expectedStatus, info.Status);
+ Assert.NotNull(info.EntryCount);
+ }
+
+ [Fact]
+ public void InspectArchive_EncryptedHeaders_ReturnsPartialInformation()
+ {
+ var info = ArchiveFactory.InspectArchive(
+ GetTestArchivePath("Rar.encrypted_filesAndHeader.rar")
+ );
+
+ Assert.NotNull(info);
+ Assert.Equal(ArchiveInformationStatus.Partial, info.Status);
+ Assert.True(info.Limitations.HasFlag(ArchiveInformationLimitations.EncryptedHeaders));
+ Assert.Equal(ArchiveEncryptionScope.Headers, info.Encryption);
+ Assert.True(info.IsEncrypted);
+ }
+
+ [Fact]
+ public void InspectArchive_EncryptedHeadersWithPassword_ReportsBothEncryptionScopes()
+ {
+ var info = ArchiveFactory.InspectArchive(
+ GetTestArchivePath("Rar.encrypted_filesAndHeader.rar"),
+ ReaderOptions.ForFilePath.WithPassword("test")
+ );
+
+ Assert.NotNull(info);
+ Assert.Equal(ArchiveInformationStatus.Complete, info.Status);
+ Assert.Equal(
+ ArchiveEncryptionScope.Headers | ArchiveEncryptionScope.EntryData,
+ info.Encryption
+ );
+ Assert.True(info.IsEncrypted);
+ }
+
+ [Fact]
+ public void InspectArchive_IncompleteRarStream_ReturnsPartialInformation()
+ {
+ using var stream = File.OpenRead(GetTestArchivePath("Rar.multi.solid.part01.rar"));
+
+ var info = ArchiveFactory.InspectArchive(stream);
+
+ Assert.NotNull(info);
+ Assert.Equal(ArchiveInformationStatus.Partial, info.Status);
+ Assert.True(info.Limitations.HasFlag(ArchiveInformationLimitations.MissingVolumes));
+ Assert.Null(info.CompressedPayloadSize);
+ Assert.Null(info.UncompressedPayloadSize);
+ }
+
+ [Fact]
+ public async ValueTask InspectArchiveAsync_CompressedTar_ReturnsMetadata()
+ {
+ var info = await ArchiveFactory.InspectArchiveAsync(GetTestArchivePath("Tar.tar.xz"));
+
+ Assert.NotNull(info);
+ Assert.Equal(ArchiveType.Tar, info.Detection.ContainerType);
+ Assert.Equal(CompressionType.Xz, info.Detection.OuterCompressionType);
+ Assert.NotNull(info.EntryCount);
+ }
+
+ [Fact]
+ public void InspectArchive_MultiVolumeRar_ReturnsVolumeMetadata()
+ {
+ FileInfo[] parts =
+ [
+ new(GetTestArchivePath("Rar.multi.solid.part01.rar")),
+ new(GetTestArchivePath("Rar.multi.solid.part02.rar")),
+ new(GetTestArchivePath("Rar.multi.solid.part03.rar")),
+ new(GetTestArchivePath("Rar.multi.solid.part04.rar")),
+ new(GetTestArchivePath("Rar.multi.solid.part05.rar")),
+ new(GetTestArchivePath("Rar.multi.solid.part06.rar")),
+ ];
+
+ var info = ArchiveFactory.InspectArchive(parts);
+
+ Assert.NotNull(info);
+ Assert.Equal(ArchiveType.Rar, info.Detection.ContainerType);
+ Assert.Equal(parts.Length, info.PhysicalPartCount);
+ Assert.True(info.IsMultiVolume);
+ }
+
+ [Fact]
+ public void InspectArchive_MultiVolumeRarPath_DiscoversAllParts()
+ {
+ var info = ArchiveFactory.InspectArchive(GetTestArchivePath("Rar.multi.solid.part01.rar"));
+
+ Assert.NotNull(info);
+ Assert.Equal(6, info.PhysicalPartCount);
+ Assert.True(info.IsMultiVolume);
}
private MemoryStream CreatePrefixedArchiveStream(string archiveName, int prefixLength)