mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-22 06:54:40 +00:00
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:
46
docs/API.md
46
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
25
src/SharpCompress/Archives/ArchiveAccessMode.cs
Normal file
25
src/SharpCompress/Archives/ArchiveAccessMode.cs
Normal 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,
|
||||
}
|
||||
43
src/SharpCompress/Archives/ArchiveDetection.cs
Normal file
43
src/SharpCompress/Archives/ArchiveDetection.cs
Normal 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; }
|
||||
}
|
||||
25
src/SharpCompress/Archives/ArchiveEncryptionScope.cs
Normal file
25
src/SharpCompress/Archives/ArchiveEncryptionScope.cs
Normal 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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
537
src/SharpCompress/Archives/ArchiveFactory.Information.Async.cs
Normal file
537
src/SharpCompress/Archives/ArchiveFactory.Information.Async.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
682
src/SharpCompress/Archives/ArchiveFactory.Information.cs
Normal file
682
src/SharpCompress/Archives/ArchiveFactory.Information.cs
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
30
src/SharpCompress/Archives/ArchiveInformationLimitations.cs
Normal file
30
src/SharpCompress/Archives/ArchiveInformationLimitations.cs
Normal 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,
|
||||
}
|
||||
17
src/SharpCompress/Archives/ArchiveInformationStatus.cs
Normal file
17
src/SharpCompress/Archives/ArchiveInformationStatus.cs
Normal 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,
|
||||
}
|
||||
15
src/SharpCompress/Archives/ZipArchiveInformation.cs
Normal file
15
src/SharpCompress/Archives/ZipArchiveInformation.cs
Normal 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; }
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
87
src/SharpCompress/IO/ArchiveOffsetStream.cs
Normal file
87
src/SharpCompress/IO/ArchiveOffsetStream.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user