move detection and use more sync method

This commit is contained in:
Adam Hathcock
2026-08-06 15:17:32 +01:00
parent 839df5f5d9
commit 5908d9a7e6
5 changed files with 406 additions and 844 deletions

View File

@@ -0,0 +1,369 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Detection;
using SharpCompress.Factories;
using SharpCompress.IO;
using SharpCompress.Providers;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
public static partial class ArchiveFactory
{
/// <summary>
/// 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>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static async ValueTask<ArchiveDetection?> DetectArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
) =>
await DetectArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
.ConfigureAwait(false);
/// <summary>
/// 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>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static async ValueTask<ArchiveDetection?> DetectArchiveAsync(
string filePath,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return await DetectArchiveAsync(
stream,
readerOptions ?? ReaderOptions.ForFilePath,
cancellationToken
)
.ConfigureAwait(false);
}
/// <summary>
/// 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>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static async ValueTask<ArchiveDetection?> DetectArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
) =>
await DetectArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
/// <summary>
/// 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>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static async ValueTask<ArchiveDetection?> DetectArchiveAsync(
Stream stream,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
stream.RequireReadable();
stream.RequireSeekable();
return await TryDetectArchiveAsync(
stream,
readerOptions ?? ReaderOptions.ForExternalStream,
cancellationToken
)
.ConfigureAwait(false);
}
internal static ValueTask<T> FindFactoryAsync<T>(
string filePath,
CancellationToken cancellationToken = default
)
where T : IFactory
{
filePath.NotNullOrEmpty(nameof(filePath));
return FindFactoryAsync<T>(new FileInfo(filePath), cancellationToken);
}
internal static async ValueTask<T> FindFactoryAsync<T>(
FileInfo fileInfo,
CancellationToken cancellationToken = default
)
where T : IFactory
{
fileInfo.NotNull(nameof(fileInfo));
using Stream stream = fileInfo.OpenRead();
return await FindFactoryAsync<T>(stream, cancellationToken).ConfigureAwait(false);
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private static async ValueTask<T> FindFactoryAsync<T>(
FileInfo fileInfo,
ReaderOptions readerOptions,
CancellationToken cancellationToken
)
where T : IFactory
{
fileInfo.NotNull(nameof(fileInfo));
using Stream stream = fileInfo.OpenRead();
return await FindFactoryAsync<T>(stream, readerOptions, cancellationToken)
.ConfigureAwait(false);
}
internal static async ValueTask<T> FindFactoryAsync<T>(
Stream stream,
CancellationToken cancellationToken = default
)
where T : IFactory
{
stream.RequireReadable();
stream.RequireSeekable();
// Use the shared async detection loop over all factories. If the matched factory
// implements T we return it; otherwise (or if nothing matched) we fall through
// to the same "unsupported format" exception that the original code produced,
// listing the T-typed factories as the hint for the caller.
return await FindFactoryAsync<T>(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private static async ValueTask<T> FindFactoryAsync<T>(
Stream stream,
ReaderOptions readerOptions,
CancellationToken cancellationToken
)
where T : IFactory
{
stream.RequireReadable();
stream.RequireSeekable();
var factory = await TryFindFactoryAsync(stream, readerOptions, cancellationToken)
.ConfigureAwait(false);
if (factory is T typedFactory)
{
return typedFactory;
}
var extensions = string.Join(", ", Factory.Factories.OfType<T>().Select(item => item.Name));
throw new ArchiveOperationException(
$"Cannot determine compressed stream type. Supported Archive Formats: {extensions}"
);
}
/// <summary>
/// Iterates all registered factories and returns the first one whose
/// <see cref="IFactory.IsArchiveAsync"/> recognises the stream, or <see langword="null"/>.
/// Stream position is restored to its value at entry on both success and failure.
/// </summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private static async ValueTask<IFactory?> TryFindFactoryAsync(
Stream stream,
ReaderOptions readerOptions,
CancellationToken cancellationToken
)
{
var startPosition = stream.Position;
try
{
foreach (var factory in Factory.Factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
var isArchive = await factory
.IsArchiveAsync(stream, readerOptions, cancellationToken)
.ConfigureAwait(false);
if (isArchive)
{
stream.Seek(startPosition, SeekOrigin.Begin);
if (
await IsCompressedTarAsync(
stream,
factory,
readerOptions,
cancellationToken
)
.ConfigureAwait(false)
)
{
continue;
}
return factory;
}
}
return null;
}
finally
{
stream.Seek(startPosition, SeekOrigin.Begin);
}
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private static async ValueTask<bool> IsCompressedTarAsync(
Stream stream,
IFactory factory,
ReaderOptions readerOptions,
CancellationToken cancellationToken
) =>
GetCompressedTarType(factory) is { } compressionType
&& await IsCompressedTarAsync(stream, readerOptions, compressionType, cancellationToken)
.ConfigureAwait(false);
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private static async ValueTask<bool> IsCompressedTarAsync(
Stream stream,
ReaderOptions readerOptions,
CompressionType compressionType,
CancellationToken cancellationToken
)
{
using var nonDisposingStream = SharpCompressStream.CreateNonDisposing(stream);
var testStream =
compressionType == CompressionType.GZip
? await readerOptions
.Providers.CreateDecompressStreamAsync(
compressionType,
nonDisposingStream,
CompressionContext
.FromStream(nonDisposingStream)
.WithReaderOptions(readerOptions),
cancellationToken
)
.ConfigureAwait(false)
: await readerOptions
.Providers.CreateDecompressStreamAsync(
compressionType,
nonDisposingStream,
cancellationToken
)
.ConfigureAwait(false);
try
{
return await TarArchive
.IsTarFileAsync(testStream, cancellationToken)
.ConfigureAwait(false);
}
finally
{
DisposeProbeStream(testStream);
}
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private static async ValueTask<ArchiveDetection?> TryDetectArchiveAsync(
Stream stream,
ReaderOptions readerOptions,
CancellationToken cancellationToken
)
{
var startPosition = stream.Position;
try
{
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;
}
finally
{
stream.Seek(startPosition, SeekOrigin.Begin);
}
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private static async ValueTask<CompressionType?> TryDetectCompressedTarAsync(
Stream stream,
ReaderOptions readerOptions,
long startPosition,
CancellationToken cancellationToken
)
{
foreach (var wrapper in TarWrapper.Wrappers)
{
#if !SYNC_ONLY
cancellationToken.ThrowIfCancellationRequested();
#endif
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;
}
}
return null;
}
}

View File

@@ -1,391 +1,13 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Detection;
using SharpCompress.Factories;
using SharpCompress.IO;
using SharpCompress.Providers;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
public static partial class ArchiveFactory
{
/// <summary>
/// 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<ArchiveDetection?> DetectArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
) =>
await DetectArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
.ConfigureAwait(false);
/// <summary>
/// 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<ArchiveDetection?> DetectArchiveAsync(
string filePath,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return await DetectArchiveAsync(
stream,
readerOptions ?? ReaderOptions.ForFilePath,
cancellationToken
)
.ConfigureAwait(false);
}
/// <summary>
/// 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<ArchiveDetection?> DetectArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
) =>
await DetectArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
/// <summary>
/// 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<ArchiveDetection?> DetectArchiveAsync(
Stream stream,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
stream.RequireReadable();
stream.RequireSeekable();
return await TryDetectArchiveAsync(
stream,
readerOptions ?? ReaderOptions.ForExternalStream,
cancellationToken
)
.ConfigureAwait(false);
}
internal static ValueTask<T> FindFactoryAsync<T>(
string filePath,
CancellationToken cancellationToken = default
)
where T : IFactory
{
filePath.NotNullOrEmpty(nameof(filePath));
return FindFactoryAsync<T>(new FileInfo(filePath), cancellationToken);
}
internal static async ValueTask<T> FindFactoryAsync<T>(
FileInfo fileInfo,
CancellationToken cancellationToken = default
)
where T : IFactory
{
fileInfo.NotNull(nameof(fileInfo));
using Stream stream = fileInfo.OpenRead();
return await FindFactoryAsync<T>(stream, cancellationToken).ConfigureAwait(false);
}
private static async ValueTask<T> FindFactoryAsync<T>(
FileInfo fileInfo,
ReaderOptions readerOptions,
CancellationToken cancellationToken
)
where T : IFactory
{
fileInfo.NotNull(nameof(fileInfo));
using Stream stream = fileInfo.OpenRead();
return await FindFactoryAsync<T>(stream, readerOptions, cancellationToken)
.ConfigureAwait(false);
}
internal static async ValueTask<T> FindFactoryAsync<T>(
Stream stream,
CancellationToken cancellationToken = default
)
where T : IFactory
{
stream.RequireReadable();
stream.RequireSeekable();
// Use the shared async detection loop over all factories. If the matched factory
// implements T we return it; otherwise (or if nothing matched) we fall through
// to the same "unsupported format" exception that the original code produced,
// listing the T-typed factories as the hint for the caller.
return await FindFactoryAsync<T>(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
}
private static async ValueTask<T> FindFactoryAsync<T>(
Stream stream,
ReaderOptions readerOptions,
CancellationToken cancellationToken
)
where T : IFactory
{
stream.RequireReadable();
stream.RequireSeekable();
var factory = await TryFindFactoryAsync(stream, readerOptions, cancellationToken)
.ConfigureAwait(false);
if (factory is T typedFactory)
{
return typedFactory;
}
var extensions = string.Join(", ", Factory.Factories.OfType<T>().Select(item => item.Name));
throw new ArchiveOperationException(
$"Cannot determine compressed stream type. Supported Archive Formats: {extensions}"
);
}
/// <summary>
/// Async counterpart of the synchronous factory detection path.
/// Iterates all registered factories and returns the first one whose
/// <see cref="IFactory.IsArchiveAsync"/> recognises the stream, or <see langword="null"/>.
/// Stream position is restored to its value at entry on both success and failure.
/// </summary>
private static async ValueTask<IFactory?> TryFindFactoryAsync(
Stream stream,
ReaderOptions readerOptions,
CancellationToken cancellationToken
)
{
var startPosition = stream.Position;
try
{
foreach (var factory in Factory.Factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
var isArchive = await factory
.IsArchiveAsync(stream, readerOptions, cancellationToken)
.ConfigureAwait(false);
if (isArchive)
{
stream.Seek(startPosition, SeekOrigin.Begin);
if (
await IsCompressedTarAsync(
stream,
factory,
readerOptions,
cancellationToken
)
.ConfigureAwait(false)
)
{
continue;
}
return factory;
}
}
return null;
}
finally
{
stream.Seek(startPosition, SeekOrigin.Begin);
}
}
/// <summary>
/// Identifies the archive at the given file path without enumerating its entries.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
public static ArchiveDetection? DetectArchive(string filePath) =>
DetectArchive(filePath, ReaderOptions.ForFilePath);
/// <summary>
/// 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 ArchiveDetection? DetectArchive(string filePath, ReaderOptions? readerOptions)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return DetectArchive(stream, readerOptions ?? ReaderOptions.ForFilePath);
}
/// <summary>
/// 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 ArchiveDetection? DetectArchive(Stream stream) =>
DetectArchive(stream, ReaderOptions.ForExternalStream);
/// <summary>
/// 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 ArchiveDetection? DetectArchive(Stream stream, ReaderOptions? readerOptions)
{
stream.RequireReadable();
stream.RequireSeekable();
return TryDetectArchive(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
/// <summary>
/// Iterates all registered factories and returns the first one whose
/// <see cref="IFactory.IsArchive"/> recognises the stream, or <see langword="null"/>.
/// Stream position is restored to its value at entry on both success and failure.
/// </summary>
/// <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="DetectArchive(Stream)"/>.
/// <para>
/// <see cref="ReaderFactory.OpenReader(Stream, ReaderOptions)"/> uses a separate code path
/// based on <see cref="IO.SharpCompressStream"/> rewindable buffering, which supports
/// non-seekable streams and is therefore not unified with this helper.
/// </para>
/// </remarks>
private static IFactory? TryFindFactory(Stream stream) =>
TryFindFactory(stream, ReaderOptions.ForExternalStream);
private static IFactory? TryFindFactory(Stream stream, ReaderOptions readerOptions)
{
var startPosition = stream.Position;
try
{
foreach (var factory in Factory.Factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
var isArchive = factory.IsArchive(stream, readerOptions);
if (isArchive)
{
stream.Seek(startPosition, SeekOrigin.Begin);
if (IsCompressedTar(stream, factory, readerOptions))
{
continue;
}
return factory;
}
}
return null;
}
finally
{
stream.Seek(startPosition, SeekOrigin.Begin);
}
}
private static bool IsCompressedTar(
Stream stream,
IFactory factory,
ReaderOptions readerOptions
) =>
GetCompressedTarType(factory) is { } compressionType
&& IsCompressedTar(stream, readerOptions, compressionType);
private static bool IsCompressedTar(
Stream stream,
ReaderOptions readerOptions,
CompressionType compressionType
)
{
using var nonDisposingStream = SharpCompressStream.CreateNonDisposing(stream);
var testStream =
compressionType == CompressionType.GZip
? readerOptions.Providers.CreateDecompressStream(
compressionType,
nonDisposingStream,
CompressionContext
.FromStream(nonDisposingStream)
.WithReaderOptions(readerOptions)
)
: readerOptions.Providers.CreateDecompressStream(
compressionType,
nonDisposingStream
);
try
{
return TarArchive.IsTarFile(testStream);
}
finally
{
DisposeProbeStream(testStream);
}
}
private static async ValueTask<bool> IsCompressedTarAsync(
Stream stream,
IFactory factory,
ReaderOptions readerOptions,
CancellationToken cancellationToken
) =>
GetCompressedTarType(factory) is { } compressionType
&& await IsCompressedTarAsync(stream, readerOptions, compressionType, cancellationToken)
.ConfigureAwait(false);
private static async ValueTask<bool> IsCompressedTarAsync(
Stream stream,
ReaderOptions readerOptions,
CompressionType compressionType,
CancellationToken cancellationToken
)
{
using var nonDisposingStream = SharpCompressStream.CreateNonDisposing(stream);
var testStream =
compressionType == CompressionType.GZip
? await readerOptions
.Providers.CreateDecompressStreamAsync(
compressionType,
nonDisposingStream,
CompressionContext
.FromStream(nonDisposingStream)
.WithReaderOptions(readerOptions),
cancellationToken
)
.ConfigureAwait(false)
: await readerOptions
.Providers.CreateDecompressStreamAsync(
compressionType,
nonDisposingStream,
cancellationToken
)
.ConfigureAwait(false);
try
{
return await TarArchive
.IsTarFileAsync(testStream, cancellationToken)
.ConfigureAwait(false);
}
finally
{
DisposeProbeStream(testStream);
}
}
private static void DisposeProbeStream(Stream stream)
{
try
@@ -408,169 +30,6 @@ public static partial class ArchiveFactory
_ => null,
};
private static ArchiveDetection? TryDetectArchive(Stream stream, ReaderOptions readerOptions)
{
var startPosition = stream.Position;
try
{
foreach (var factory in Factory.Factories)
{
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);
}
}
private static async ValueTask<ArchiveDetection?> TryDetectArchiveAsync(
Stream stream,
ReaderOptions readerOptions,
CancellationToken cancellationToken
)
{
var startPosition = stream.Position;
try
{
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;
}
finally
{
stream.Seek(startPosition, SeekOrigin.Begin);
}
}
private static CompressionType? TryDetectCompressedTar(
Stream stream,
ReaderOptions readerOptions,
long startPosition
)
{
foreach (var wrapper in TarWrapper.Wrappers)
{
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 async ValueTask<CompressionType?> TryDetectCompressedTarAsync(
Stream stream,
ReaderOptions readerOptions,
long startPosition,
CancellationToken cancellationToken
)
{
foreach (var wrapper in TarWrapper.Wrappers)
{
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;
}
}
return null;
}
private static ArchiveDetection CreateDetection(IFactory factory)
{
var supportedApis = ArchiveAccessMode.None;

View File

@@ -20,11 +20,12 @@ namespace SharpCompress.Archives;
public static partial class ArchiveFactory
{
/// <summary>
/// Asynchronously collects metadata for the archive at the given file path.
/// 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>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static async ValueTask<ArchiveInformation?> InspectArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
@@ -33,12 +34,13 @@ public static partial class ArchiveFactory
.ConfigureAwait(false);
/// <summary>
/// Asynchronously collects metadata for the archive at the given file path.
/// 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>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static async ValueTask<ArchiveInformation?> InspectArchiveAsync(
string filePath,
ReaderOptions? readerOptions,
@@ -68,11 +70,12 @@ public static partial class ArchiveFactory
}
/// <summary>
/// Asynchronously collects metadata for the archive in the given stream.
/// 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>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static async ValueTask<ArchiveInformation?> InspectArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
@@ -81,13 +84,14 @@ public static partial class ArchiveFactory
.ConfigureAwait(false);
/// <summary>
/// Asynchronously collects metadata for the archive in the given stream.
/// 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>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static async ValueTask<ArchiveInformation?> InspectArchiveAsync(
Stream stream,
ReaderOptions? readerOptions,
@@ -96,7 +100,9 @@ public static partial class ArchiveFactory
{
stream.RequireReadable();
stream.RequireSeekable();
#if !SYNC_ONLY
cancellationToken.ThrowIfCancellationRequested();
#endif
var options = readerOptions ?? ReaderOptions.ForExternalStream;
var startPosition = stream.Position;
@@ -119,6 +125,10 @@ public static partial class ArchiveFactory
if ((detection.SupportedApis & ArchiveAccessMode.Archive) != 0)
{
#if SYNC_ONLY
using var archive = OpenArchive(archiveStream, inspectionOptions);
return InspectOpenedArchive(archive, detection, physicalSize, 1);
#else
await using var archive = await OpenAsyncArchive(
archiveStream,
inspectionOptions,
@@ -133,8 +143,14 @@ public static partial class ArchiveFactory
cancellationToken
)
.ConfigureAwait(false);
#endif
}
#if SYNC_ONLY
var aceHeader = ReadAceHeader(archiveStream, detection, inspectionOptions);
using var reader = ReaderFactory.OpenReader(archiveStream, inspectionOptions);
return InspectOpenedReader(reader, detection, physicalSize, 1, aceHeader);
#else
var aceHeader = await ReadAceHeaderAsync(
archiveStream,
detection,
@@ -154,6 +170,7 @@ public static partial class ArchiveFactory
cancellationToken
)
.ConfigureAwait(false);
#endif
}
catch (CryptographicException) when (string.IsNullOrEmpty(options.Password))
{
@@ -198,12 +215,13 @@ public static partial class ArchiveFactory
}
/// <summary>
/// Asynchronously collects metadata for an archive opened from multiple files.
/// 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>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static async ValueTask<ArchiveInformation?> InspectArchiveAsync(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
@@ -242,6 +260,10 @@ public static partial class ArchiveFactory
var physicalSize = GetPhysicalSize(fileInfos);
try
{
#if SYNC_ONLY
using var archive = OpenArchive(fileInfos, options);
return InspectOpenedArchive(archive, detection, physicalSize, fileInfos.Count);
#else
await using var archive = await OpenAsyncArchive(fileInfos, options, cancellationToken)
.ConfigureAwait(false);
return await InspectOpenedArchiveAsync(
@@ -252,6 +274,7 @@ public static partial class ArchiveFactory
cancellationToken
)
.ConfigureAwait(false);
#endif
}
catch (CryptographicException) when (string.IsNullOrEmpty(options.Password))
{
@@ -265,12 +288,13 @@ public static partial class ArchiveFactory
}
/// <summary>
/// Asynchronously collects metadata for an archive opened from multiple streams.
/// 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>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static async ValueTask<ArchiveInformation?> InspectArchiveAsync(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
@@ -317,6 +341,10 @@ public static partial class ArchiveFactory
archiveStreams.AddRange(
streams.Skip(1).Select(stream => new ArchiveOffsetStream(stream))
);
#if SYNC_ONLY
using var archive = OpenArchive(archiveStreams, inspectionOptions);
return InspectOpenedArchive(archive, detection, physicalSize, streams.Count);
#else
await using var archive = await OpenAsyncArchive(
archiveStreams,
inspectionOptions,
@@ -331,6 +359,7 @@ public static partial class ArchiveFactory
cancellationToken
)
.ConfigureAwait(false);
#endif
}
catch (CryptographicException) when (string.IsNullOrEmpty(options.Password))
{
@@ -466,6 +495,7 @@ public static partial class ArchiveFactory
);
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private static async ValueTask<AceMainHeader?> ReadAceHeaderAsync(
Stream stream,
ArchiveDetection detection,
@@ -566,6 +596,7 @@ public static partial class ArchiveFactory
return (new ZipArchiveInformation(deferredSizeEntryCount > 0), deferredSizeEntryCount);
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private static async ValueTask<ArchiveDetection?> RedetectArchiveAsync(
Stream stream,
long startPosition,

View File

@@ -2,8 +2,6 @@ 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;
@@ -12,7 +10,6 @@ using SharpCompress.Common.Rar;
using SharpCompress.Common.Zip;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.Detection;
using SharpCompress.IO;
using SharpCompress.Readers;
using AceMainHeader = SharpCompress.Common.Ace.Headers.AceMainHeader;
@@ -20,236 +17,6 @@ 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,
@@ -331,28 +98,6 @@ public static partial class ArchiveFactory
);
}
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,
@@ -486,17 +231,6 @@ public static partial class ArchiveFactory
}
}
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();

View File

@@ -123,37 +123,6 @@ public static partial class ArchiveFactory
public static T FindFactory<T>(Stream stream)
where T : IFactory => FindFactory<T>(stream, ReaderOptions.ForExternalStream);
private static T FindFactory<T>(FileInfo fileInfo, ReaderOptions readerOptions)
where T : IFactory
{
fileInfo.NotNull(nameof(fileInfo));
using Stream stream = fileInfo.OpenRead();
return FindFactory<T>(stream, readerOptions);
}
private static T FindFactory<T>(Stream stream, ReaderOptions readerOptions)
where T : IFactory
{
stream.RequireReadable();
stream.RequireSeekable();
// Use the shared detection loop over all factories. If the matched factory
// implements T we return it; otherwise (or if nothing matched) we fall through
// to the same "unsupported format" exception that the original code produced,
// listing the T-typed factories as the hint for the caller.
var factory = TryFindFactory(stream, readerOptions);
if (factory is T typedFactory)
{
return typedFactory;
}
var extensions = string.Join(", ", Factory.Factories.OfType<T>().Select(item => item.Name));
throw new ArchiveOperationException(
$"Cannot determine compressed stream type. Supported Archive Formats: {extensions}"
);
}
public static bool IsArchive(string filePath, out ArchiveType? type)
{
return IsArchive(filePath, ReaderOptions.ForFilePath, out type);