Detect v Inspect is added. Detect is used only basic info but does not buffer as much as it can.

This commit is contained in:
Adam Hathcock
2026-08-04 11:45:13 +01:00
parent 858f5040df
commit 87f5616d1f
16 changed files with 2125 additions and 291 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
using System;
namespace SharpCompress.Archives;
/// <summary>
/// Specifies the APIs available for an archive format.
/// </summary>
[Flags]
public enum ArchiveAccessMode
{
/// <summary>
/// No archive access API is available.
/// </summary>
None = 0,
/// <summary>
/// The seekable <see cref="IArchive"/> API is available.
/// </summary>
Archive = 1,
/// <summary>
/// The forward-only <see cref="SharpCompress.Readers.IReader"/> API is available.
/// </summary>
Reader = 2,
}

View File

@@ -0,0 +1,43 @@
using SharpCompress.Common;
namespace SharpCompress.Archives;
/// <summary>
/// Identifies an archive format without enumerating its entries.
/// </summary>
public sealed class ArchiveDetection
{
internal ArchiveDetection(
ArchiveType? containerType,
string formatName,
CompressionType? outerCompressionType,
ArchiveAccessMode supportedApis
)
{
ContainerType = containerType;
FormatName = formatName;
OuterCompressionType = outerCompressionType;
SupportedApis = supportedApis;
}
/// <summary>
/// Gets the logical archive container type, when it is a built-in SharpCompress type.
/// </summary>
public ArchiveType? ContainerType { get; }
/// <summary>
/// Gets the detected format name.
/// </summary>
public string FormatName { get; }
/// <summary>
/// Gets the compression wrapper around <see cref="ContainerType"/>, if present.
/// For example, a tar.xz archive has a TAR container and XZ outer compression.
/// </summary>
public CompressionType? OuterCompressionType { get; }
/// <summary>
/// Gets the APIs supported by the detected format.
/// </summary>
public ArchiveAccessMode SupportedApis { get; }
}

View File

@@ -0,0 +1,25 @@
using System;
namespace SharpCompress.Archives;
/// <summary>
/// Identifies which archive data is encrypted.
/// </summary>
[Flags]
public enum ArchiveEncryptionScope
{
/// <summary>
/// No encryption is present.
/// </summary>
None = 0,
/// <summary>
/// One or more entry payloads are encrypted.
/// </summary>
EntryData = 1,
/// <summary>
/// Archive headers are encrypted.
/// </summary>
Headers = 2,
}

View File

@@ -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
{
/// <summary>
/// Returns information about the archive at the given file path asynchronously,
/// or <see langword="null"/> if the file is not a recognized archive.
/// Identifies the archive at the given file path without enumerating its entries.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
public static async ValueTask<ArchiveDetection?> DetectArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
) =>
await GetArchiveInformationAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
await DetectArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
.ConfigureAwait(false);
/// <summary>
/// Returns information about the archive at the given file path asynchronously,
/// or <see langword="null"/> if the file is not a recognized archive.
/// Identifies the archive at the given file path without enumerating its entries.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
/// <param name="readerOptions">Options controlling archive detection.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
public static async ValueTask<ArchiveDetection?> 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
}
/// <summary>
/// Returns information about the archive in the given stream asynchronously,
/// or <see langword="null"/> if the stream is not a recognized archive.
/// Identifies the archive in the given stream without enumerating its entries.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
public static async ValueTask<ArchiveDetection?> DetectArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
) =>
await GetArchiveInformationAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
await DetectArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
/// <summary>
/// Returns information about the archive in the given stream asynchronously,
/// or <see langword="null"/> if the stream is not a recognized archive.
/// Identifies the archive in the given stream without enumerating its entries.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
/// <param name="readerOptions">Options controlling archive detection.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
public static async ValueTask<ArchiveDetection?> 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<T> FindFactoryAsync<T>(
@@ -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);
}
}
/// <summary>
/// Returns information about the archive at the given file path,
/// or <see langword="null"/> if the file is not a recognized archive.
/// Identifies the archive at the given file path without enumerating its entries.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
public static ArchiveInformation? GetArchiveInformation(string filePath) =>
GetArchiveInformation(filePath, ReaderOptions.ForFilePath);
public static ArchiveDetection? DetectArchive(string filePath) =>
DetectArchive(filePath, ReaderOptions.ForFilePath);
/// <summary>
/// Returns information about the archive at the given file path,
/// or <see langword="null"/> if the file is not a recognized archive.
/// Identifies the archive at the given file path without enumerating its entries.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
/// <param name="readerOptions">Options controlling archive detection.</param>
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);
}
/// <summary>
/// Returns information about the archive in the given stream,
/// or <see langword="null"/> if the stream is not a recognized archive.
/// Identifies the archive in the given stream without enumerating its entries.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
public static ArchiveInformation? GetArchiveInformation(Stream stream) =>
GetArchiveInformation(stream, ReaderOptions.ForExternalStream);
public static ArchiveDetection? DetectArchive(Stream stream) =>
DetectArchive(stream, ReaderOptions.ForExternalStream);
/// <summary>
/// Returns information about the archive in the given stream,
/// or <see langword="null"/> if the stream is not a recognized archive.
/// Identifies the archive in the given stream without enumerating its entries.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
/// <param name="readerOptions">Options controlling archive detection.</param>
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);
}
/// <summary>
@@ -278,7 +255,7 @@ public static partial class ArchiveFactory
/// <remarks>
/// This is the shared, seekable-stream detection core used by
/// <see cref="FindFactory{T}(Stream)"/>, <see cref="IsArchive(Stream, out ArchiveType?)"/>,
/// and <see cref="GetArchiveInformation(Stream)"/>.
/// and <see cref="DetectArchive(Stream)"/>.
/// <para>
/// <see cref="ReaderFactory.OpenReader(Stream, ReaderOptions)"/> uses a separate code path
/// based on <see cref="IO.SharpCompressStream"/> 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<ArchiveDetection?> 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<CompressionType?> 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<ZipArchiveEntry>()
.SelectMany(entry => entry.Parts.OfType<ZipFilePart>())
.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<SevenZipArchiveEntry>()
.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);
}

View File

@@ -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
{
/// <summary>
/// Asynchronously collects metadata for the archive at the given file path.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the file is not a supported archive.</returns>
public static async ValueTask<ArchiveInformation?> InspectArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
) =>
await InspectArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
.ConfigureAwait(false);
/// <summary>
/// Asynchronously collects metadata for the archive at the given file path.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
/// <param name="readerOptions">Options controlling archive inspection.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the file is not a supported archive.</returns>
public static async ValueTask<ArchiveInformation?> 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);
}
/// <summary>
/// Asynchronously collects metadata for the archive in the given stream.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the stream is not a supported archive.</returns>
public static async ValueTask<ArchiveInformation?> InspectArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
) =>
await InspectArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
/// <summary>
/// Asynchronously collects metadata for the archive in the given stream.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
/// <param name="readerOptions">Options controlling archive inspection.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the stream is not a supported archive.</returns>
/// <remarks>The supplied stream remains open and is restored to its original position.</remarks>
public static async ValueTask<ArchiveInformation?> 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);
}
}
/// <summary>
/// Asynchronously collects metadata for an archive opened from multiple files.
/// </summary>
/// <param name="fileInfos">Archive source files in archive order.</param>
/// <param name="readerOptions">Options controlling archive inspection.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the files are not a supported archive.</returns>
public static async ValueTask<ArchiveInformation?> InspectArchiveAsync(
IReadOnlyList<FileInfo> 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
);
}
}
/// <summary>
/// Asynchronously collects metadata for an archive opened from multiple streams.
/// </summary>
/// <param name="streams">Archive source streams in archive order.</param>
/// <param name="readerOptions">Options controlling archive inspection.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the streams are not a supported archive.</returns>
public static async ValueTask<ArchiveInformation?> InspectArchiveAsync(
IReadOnlyList<Stream> 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<Stream> { 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<ArchiveInformation> InspectOpenedArchiveAsync(
IAsyncArchive archive,
ArchiveDetection detection,
long? physicalSize,
int physicalPartCount,
CancellationToken cancellationToken
)
{
var entries = new List<IArchiveEntry>();
await foreach (
var entry in archive
.EntriesAsync.WithCancellation(cancellationToken)
.ConfigureAwait(false)
)
{
cancellationToken.ThrowIfCancellationRequested();
entries.Add(entry);
}
var volumes = new List<IVolume>();
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<RarVolume>()
.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<ArchiveInformation> 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<AceMainHeader?> 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<IArchiveEntry> entries
)
{
if (archive.Type == ArchiveType.SevenZip)
{
var solidStreamCount = entries
.OfType<SevenZipArchiveEntry>()
.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<IArchiveEntry> 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<ArchiveDetection?> 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);
}
}

View File

@@ -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
{
/// <summary>
/// Collects metadata for the archive at the given file path.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the file is not a supported archive.</returns>
public static ArchiveInformation? InspectArchive(string filePath) =>
InspectArchive(filePath, ReaderOptions.ForFilePath);
/// <summary>
/// Collects metadata for the archive at the given file path.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
/// <param name="readerOptions">Options controlling archive inspection.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the file is not a supported archive.</returns>
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);
}
/// <summary>
/// Collects metadata for the archive in the given stream.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the stream is not a supported archive.</returns>
public static ArchiveInformation? InspectArchive(Stream stream) =>
InspectArchive(stream, ReaderOptions.ForExternalStream);
/// <summary>
/// Collects metadata for the archive in the given stream.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
/// <param name="readerOptions">Options controlling archive inspection.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the stream is not a supported archive.</returns>
/// <remarks>The supplied stream remains open and is restored to its original position.</remarks>
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);
}
}
/// <summary>
/// Collects metadata for an archive opened from multiple files.
/// </summary>
/// <param name="fileInfos">Archive source files in archive order.</param>
/// <param name="readerOptions">Options controlling archive inspection.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the files are not a supported archive.</returns>
public static ArchiveInformation? InspectArchive(
IReadOnlyList<FileInfo> 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
);
}
}
/// <summary>
/// Collects metadata for an archive opened from multiple streams.
/// </summary>
/// <param name="streams">Archive source streams in archive order.</param>
/// <param name="readerOptions">Options controlling archive inspection.</param>
/// <returns>Archive metadata, or <see langword="null"/> when the streams are not a supported archive.</returns>
public static ArchiveInformation? InspectArchive(
IReadOnlyList<Stream> 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<Stream> { 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<RarVolume>()
.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<IArchiveEntry> entries)
{
var dataDescriptorEntryCount = entries
.OfType<ZipArchiveEntry>()
.LongCount(entry =>
entry
.Parts.OfType<ZipFilePart>()
.Any(part =>
FlagUtility.HasFlag(
part.Header.Flags,
SharpCompress.Common.Zip.Headers.HeaderFlags.UsePostDataDescriptor
)
)
);
return dataDescriptorEntryCount == 0 && !entries.OfType<ZipArchiveEntry>().Any()
? null
: new ZipArchiveInformation(dataDescriptorEntryCount);
}
private static (bool IsSolid, long SolidStreamCount) GetSolidInformation(
IArchive archive,
IReadOnlyCollection<IArchiveEntry> entries
)
{
if (archive.Type == ArchiveType.SevenZip)
{
var solidStreamCount = entries
.OfType<SevenZipArchiveEntry>()
.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<IArchiveEntry> 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<RarVolume>().Any(volume => volume.IsMultiVolume)
: volumes.Length > 1;
private static string? GetArchiveComment(IEnumerable<IVolume> volumes) =>
volumes.OfType<ZipVolume>().LastOrDefault()?.Comment
?? volumes
.OfType<RarVolume>()
.Select(volume => volume.Comment)
.FirstOrDefault(comment => comment is not null);
private static string? GetFormatVersion(IEnumerable<IVolume> volumes)
{
var rarVolume = volumes.OfType<RarVolume>().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<IArchiveEntry> 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<FileInfo> { firstPart };
for (var index = 1; factory.GetFilePart(index, firstPart) is { } part; index++)
{
parts.Add(part);
}
return parts.ToArray();
}
private static long? GetPhysicalSize(IEnumerable<FileInfo> fileInfos)
{
try
{
return fileInfos.Aggregate(0L, (total, fileInfo) => total + fileInfo.Length);
}
catch (IOException)
{
return null;
}
}
private static long? GetPhysicalSize(IReadOnlyList<Stream> 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<ZipFilePart>()
.Any(part =>
FlagUtility.HasFlag(
part.Header.Flags,
SharpCompress.Common.Zip.Headers.HeaderFlags.UsePostDataDescriptor
)
);
}
}

View File

@@ -1,50 +1,145 @@
using SharpCompress.Common;
namespace SharpCompress.Archives;
/// <summary>
/// Contains information about a detected archive, including its type and supported capabilities.
/// Contains metadata collected by fully inspecting an archive.
/// </summary>
/// <remarks>
/// Use <see cref="ArchiveFactory.GetArchiveInformation(System.IO.Stream)"/> or
/// <see cref="ArchiveFactory.GetArchiveInformationAsync(System.IO.Stream,System.Threading.CancellationToken)"/>
/// to obtain an instance of this record.
/// </remarks>
public record ArchiveInformation
public sealed class ArchiveInformation
{
/// <summary>
/// The type of archive detected, or <see langword="null"/> when the format is not a registered well-known type.
/// </summary>
public ArchiveType? Type { get; set; }
/// <summary>
/// <see langword="true"/> when this archive format supports random access via the <see cref="IArchive"/> API,
/// meaning the full file listing can be retrieved without decompressing the entire archive.
/// <see langword="false"/> when only the <see cref="SharpCompress.Readers.IReader"/> API is available,
/// which reads entries sequentially and can only report per-entry progress.
/// </summary>
public bool SupportsRandomAccess { get; set; }
/// <summary>
/// For ZIP archives, the number of entries that use post-data descriptor trailers.
/// This value is <see langword="null"/> for non-ZIP formats.
/// </summary>
public int? ZipDataDescriptorEntryCount { get; set; }
/// <summary>
/// For solid-capable archive formats, the number of solid compressed streams present.
/// This value is <see langword="null"/> for formats that do not support solid entries.
/// </summary>
public int? SolidStreamCount { get; set; }
/// <summary>
/// Creates a new archive information instance.
/// </summary>
/// <param name="type">The detected archive type.</param>
/// <param name="supportsRandomAccess">Whether the detected format supports random access.</param>
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;
}
/// <summary>
/// Gets the format and API capabilities identified before inspection.
/// </summary>
public ArchiveDetection Detection { get; }
/// <summary>
/// Gets whether metadata collection completed.
/// </summary>
public ArchiveInformationStatus Status { get; }
/// <summary>
/// Gets conditions that prevented complete metadata collection.
/// </summary>
public ArchiveInformationLimitations Limitations { get; }
/// <summary>
/// Gets the format version when exposed by the archive format.
/// </summary>
public string? FormatVersion { get; }
/// <summary>
/// Gets the number of entries, excluding format control records.
/// </summary>
public long? EntryCount { get; }
/// <summary>
/// Gets the number of entries for which a forward-only reader cannot know the uncompressed size before reading entry data.
/// </summary>
public long? EntriesWithUnknownSizeCount { get; }
/// <summary>
/// Gets the number of bytes in the supplied source parts, when known.
/// </summary>
public long? PhysicalSize { get; }
/// <summary>
/// Gets the aggregate stored payload size, when the format exposes it reliably.
/// </summary>
public long? CompressedPayloadSize { get; }
/// <summary>
/// Gets the aggregate uncompressed entry size, when the format exposes it reliably.
/// </summary>
public long? UncompressedPayloadSize { get; }
/// <summary>
/// Gets whether entries share compression state.
/// </summary>
public bool? IsSolid { get; }
/// <summary>
/// Gets the number of independent shared compression streams, when known.
/// </summary>
public long? SolidStreamCount { get; }
/// <summary>
/// Gets the scopes protected by encryption, when known.
/// </summary>
public ArchiveEncryptionScope? Encryption { get; }
/// <summary>
/// Gets whether archive headers or entry data are encrypted, when known.
/// </summary>
public bool? IsEncrypted { get; }
/// <summary>
/// Gets whether the archive is part of a multi-volume set, when known.
/// </summary>
public bool? IsMultiVolume { get; }
/// <summary>
/// Gets the number of source parts supplied for inspection.
/// </summary>
public int? PhysicalPartCount { get; }
/// <summary>
/// Gets the number of logical volumes exposed by the archive, when known.
/// </summary>
public int? LogicalVolumeCount { get; }
/// <summary>
/// Gets whether all discovered entries are complete, when known.
/// </summary>
public bool? IsComplete { get; }
/// <summary>
/// Gets the archive comment, when supported by the format.
/// </summary>
public string? Comment { get; }
/// <summary>
/// Gets ZIP-specific metadata when the detected container is ZIP.
/// </summary>
public ZipArchiveInformation? Zip { get; }
}

View File

@@ -0,0 +1,30 @@
using System;
namespace SharpCompress.Archives;
/// <summary>
/// Identifies conditions that prevented complete archive metadata inspection.
/// </summary>
[Flags]
public enum ArchiveInformationLimitations
{
/// <summary>
/// No known inspection limitations apply.
/// </summary>
None = 0,
/// <summary>
/// Archive headers are encrypted and no password was supplied.
/// </summary>
EncryptedHeaders = 1,
/// <summary>
/// One or more archive volumes are unavailable.
/// </summary>
MissingVolumes = 2,
/// <summary>
/// The format does not expose a requested metadata value.
/// </summary>
UnavailableMetadata = 4,
}

View File

@@ -0,0 +1,17 @@
namespace SharpCompress.Archives;
/// <summary>
/// Describes whether archive metadata could be collected completely.
/// </summary>
public enum ArchiveInformationStatus
{
/// <summary>
/// All metadata supported by the format was collected.
/// </summary>
Complete,
/// <summary>
/// Some metadata could not be collected. See <see cref="ArchiveInformation.Limitations"/>.
/// </summary>
Partial,
}

View File

@@ -0,0 +1,15 @@
namespace SharpCompress.Archives;
/// <summary>
/// Contains ZIP-specific metadata.
/// </summary>
public sealed class ZipArchiveInformation
{
internal ZipArchiveInformation(long dataDescriptorEntryCount) =>
DataDescriptorEntryCount = dataDescriptorEntryCount;
/// <summary>
/// Gets the number of entries whose local header defers its CRC and sizes to a data descriptor.
/// </summary>
public long DataDescriptorEntryCount { get; }
}

View File

@@ -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<RarFilePart> ReadFileParts();

View File

@@ -0,0 +1,87 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.IO;
/// <summary>
/// Exposes the remainder of a seekable stream as an archive that begins at position zero.
/// </summary>
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<byte> buffer) => stream.Read(buffer);
#endif
public override int ReadByte() => stream.ReadByte();
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
) => stream.ReadAsync(buffer, offset, count, cancellationToken);
#if !NET48 && !NETSTANDARD2_0
public override ValueTask<int> ReadAsync(
Memory<byte> 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);
}
}

View File

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