diff --git a/AGENTS.md b/AGENTS.md index a47121a0..79480854 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,6 +190,7 @@ SharpCompress supports multiple archive and compression formats: ### Validation Expectations - Run targeted tests for the changed area first. +- On non-Windows machines, avoid net48 test runs unless Mono is installed; use framework-specific validation such as `--framework net10.0` instead. - Run `dotnet csharpier format .` after code edits. - Run `dotnet csharpier check .` before handing off changes. diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 84bb671d..6c45cb1f 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -1,11 +1,9 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; -using SharpCompress.Factories; using SharpCompress.Readers; namespace SharpCompress.Archives; @@ -109,61 +107,4 @@ public static partial class ArchiveFactory .OpenAsyncArchive(streamsArray, options, cancellationToken) .ConfigureAwait(false); } - - internal static ValueTask FindFactoryAsync( - string filePath, - CancellationToken cancellationToken = default - ) - where T : IFactory - { - filePath.NotNullOrEmpty(nameof(filePath)); - return FindFactoryAsync(new FileInfo(filePath), cancellationToken); - } - - internal static async ValueTask FindFactoryAsync( - FileInfo finfo, - CancellationToken cancellationToken = default - ) - where T : IFactory - { - finfo.NotNull(nameof(finfo)); - using Stream stream = finfo.OpenRead(); - return await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); - } - - internal static async ValueTask FindFactoryAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - where T : IFactory - { - stream.RequireReadable(); - stream.RequireSeekable(); - - var factories = Factory.Factories.OfType(); - - var startPosition = stream.Position; - - foreach (var factory in factories) - { - stream.Seek(startPosition, SeekOrigin.Begin); - - if ( - await factory - .IsArchiveAsync(stream, cancellationToken: cancellationToken) - .ConfigureAwait(false) - ) - { - stream.Seek(startPosition, SeekOrigin.Begin); - - return factory; - } - } - - var extensions = string.Join(", ", factories.Select(item => item.Name)); - - throw new ArchiveOperationException( - $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" - ); - } } diff --git a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs new file mode 100644 index 00000000..a0084914 --- /dev/null +++ b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs @@ -0,0 +1,264 @@ +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Factories; +using SharpCompress.Readers; + +namespace SharpCompress.Archives; + +public static partial class ArchiveFactory +{ + /// + /// Returns information about the archive at the given file path asynchronously, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + string filePath, + CancellationToken cancellationToken = default + ) => + await GetArchiveInformationAsync(filePath, ReaderOptions.ForFilePath, cancellationToken) + .ConfigureAwait(false); + + /// + /// Returns information about the archive at the given file path asynchronously, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + /// Options controlling archive detection. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + string filePath, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default + ) + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return await GetArchiveInformationAsync( + stream, + readerOptions ?? ReaderOptions.ForFilePath, + cancellationToken + ) + .ConfigureAwait(false); + } + + /// + /// Returns information about the archive in the given stream asynchronously, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + Stream stream, + CancellationToken cancellationToken = default + ) => + await GetArchiveInformationAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + + /// + /// Returns information about the archive in the given stream asynchronously, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + /// Options controlling archive detection. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + Stream stream, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default + ) + { + stream.RequireReadable(); + stream.RequireSeekable(); + + var factory = await TryFindFactoryAsync( + stream, + readerOptions ?? ReaderOptions.ForExternalStream, + cancellationToken + ) + .ConfigureAwait(false); + return factory is null + ? null + : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + + internal static ValueTask FindFactoryAsync( + string filePath, + CancellationToken cancellationToken = default + ) + where T : IFactory + { + filePath.NotNullOrEmpty(nameof(filePath)); + return FindFactoryAsync(new FileInfo(filePath), cancellationToken); + } + + internal static async ValueTask FindFactoryAsync( + FileInfo fileInfo, + CancellationToken cancellationToken = default + ) + where T : IFactory + { + fileInfo.NotNull(nameof(fileInfo)); + using Stream stream = fileInfo.OpenRead(); + return await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + } + + internal static async ValueTask FindFactoryAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + where T : IFactory + { + stream.RequireReadable(); + stream.RequireSeekable(); + + // Use the shared async detection loop over all factories. If the matched factory + // implements T we return it; otherwise (or if nothing matched) we fall through + // to the same "unsupported format" exception that the original code produced, + // listing the T-typed factories as the hint for the caller. + var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + if (factory is T typedFactory) + { + return typedFactory; + } + + var extensions = string.Join(", ", Factory.Factories.OfType().Select(item => item.Name)); + + throw new ArchiveOperationException( + $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" + ); + } + + /// + /// Async counterpart of . + /// Iterates all registered factories and returns the first one whose + /// recognises the stream, or . + /// Stream position is restored to its value at entry on both success and failure. + /// + private static async ValueTask TryFindFactoryAsync( + Stream stream, + CancellationToken cancellationToken + ) => + await TryFindFactoryAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + + private static async ValueTask TryFindFactoryAsync( + Stream stream, + ReaderOptions readerOptions, + CancellationToken cancellationToken + ) + { + var startPosition = stream.Position; + + 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); + return factory; + } + } + + stream.Seek(startPosition, SeekOrigin.Begin); + return null; + } + + /// + /// Returns information about the archive at the given file path, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + public static ArchiveInformation? GetArchiveInformation(string filePath) => + GetArchiveInformation(filePath, ReaderOptions.ForFilePath); + + /// + /// Returns information about the archive at the given file path, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + /// Options controlling archive detection. + public static ArchiveInformation? GetArchiveInformation( + string filePath, + ReaderOptions? readerOptions + ) + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return GetArchiveInformation(stream, readerOptions ?? ReaderOptions.ForFilePath); + } + + /// + /// Returns information about the archive in the given stream, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + public static ArchiveInformation? GetArchiveInformation(Stream stream) => + GetArchiveInformation(stream, ReaderOptions.ForExternalStream); + + /// + /// Returns information about the archive in the given stream, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + /// Options controlling archive detection. + public static ArchiveInformation? GetArchiveInformation( + Stream stream, + ReaderOptions? readerOptions + ) + { + stream.RequireReadable(); + stream.RequireSeekable(); + + var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream); + return factory is null + ? null + : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + + /// + /// Iterates all registered factories and returns the first one whose + /// recognises the stream, or . + /// Stream position is restored to its value at entry on both success and failure. + /// + /// + /// This is the shared, seekable-stream detection core used by + /// , , + /// and . + /// + /// uses a separate code path + /// based on rewindable buffering, which supports + /// non-seekable streams and is therefore not unified with this helper. + /// + /// + private static IFactory? TryFindFactory(Stream stream) => + TryFindFactory(stream, ReaderOptions.ForExternalStream); + + private static IFactory? TryFindFactory(Stream stream, ReaderOptions readerOptions) + { + var startPosition = stream.Position; + + foreach (var factory in Factory.Factories) + { + stream.Seek(startPosition, SeekOrigin.Begin); + var isArchive = factory.IsArchive(stream, readerOptions); + + if (isArchive) + { + stream.Seek(startPosition, SeekOrigin.Begin); + return factory; + } + } + + stream.Seek(startPosition, SeekOrigin.Begin); + return null; + } +} diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 34cc5bb1..49e2f9b4 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -123,23 +123,17 @@ public static partial class ArchiveFactory stream.RequireReadable(); stream.RequireSeekable(); - var factories = Factory.Factories.OfType(); - - var startPosition = stream.Position; - - foreach (var factory in factories) + // Use the shared detection loop over all factories. If the matched factory + // implements T we return it; otherwise (or if nothing matched) we fall through + // to the same "unsupported format" exception that the original code produced, + // listing the T-typed factories as the hint for the caller. + var factory = TryFindFactory(stream); + if (factory is T typedFactory) { - stream.Seek(startPosition, SeekOrigin.Begin); - - if (factory.IsArchive(stream)) - { - stream.Seek(startPosition, SeekOrigin.Begin); - - return factory; - } + return typedFactory; } - var extensions = string.Join(", ", factories.Select(item => item.Name)); + var extensions = string.Join(", ", Factory.Factories.OfType().Select(item => item.Name)); throw new ArchiveOperationException( $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" @@ -147,69 +141,82 @@ public static partial class ArchiveFactory } public static bool IsArchive(string filePath, out ArchiveType? type) + { + return IsArchive(filePath, ReaderOptions.ForFilePath, out type); + } + + public static bool IsArchive( + string filePath, + ReaderOptions? readerOptions, + out ArchiveType? type + ) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream s = File.OpenRead(filePath); - return IsArchive(s, out type); + return IsArchive(s, readerOptions ?? ReaderOptions.ForFilePath, out type); } public static bool IsArchive(Stream stream, out ArchiveType? type) { - type = null; + return IsArchive(stream, ReaderOptions.ForExternalStream, out type); + } + + public static bool IsArchive(Stream stream, ReaderOptions? readerOptions, out ArchiveType? type) + { stream.RequireReadable(); stream.RequireSeekable(); - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - var isArchive = factory.IsArchive(stream); - stream.Position = startPosition; - - if (isArchive) - { - type = factory.KnownArchiveType; - return true; - } - } - - return false; + var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream); + type = factory?.KnownArchiveType; + return factory is not null; } public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( string filePath, CancellationToken cancellationToken = default + ) => + await IsArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken) + .ConfigureAwait(false); + + public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( + string filePath, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream stream = File.OpenRead(filePath); - return await IsArchiveAsync(stream, cancellationToken).ConfigureAwait(false); + return await IsArchiveAsync( + stream, + readerOptions ?? ReaderOptions.ForFilePath, + cancellationToken + ) + .ConfigureAwait(false); } public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( Stream stream, CancellationToken cancellationToken = default + ) => + await IsArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + + public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( + Stream stream, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { stream.RequireReadable(); stream.RequireSeekable(); - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - var isArchive = await factory - .IsArchiveAsync(stream, cancellationToken: cancellationToken) - .ConfigureAwait(false); - stream.Position = startPosition; - - if (isArchive) - { - return (true, factory.KnownArchiveType); - } - } - - return (false, null); + var factory = await TryFindFactoryAsync( + stream, + readerOptions ?? ReaderOptions.ForExternalStream, + cancellationToken + ) + .ConfigureAwait(false); + return (factory is not null, factory?.KnownArchiveType); } public static IEnumerable GetFileParts(string part1) diff --git a/src/SharpCompress/Archives/ArchiveInformation.cs b/src/SharpCompress/Archives/ArchiveInformation.cs new file mode 100644 index 00000000..adc0dfa2 --- /dev/null +++ b/src/SharpCompress/Archives/ArchiveInformation.cs @@ -0,0 +1,22 @@ +using SharpCompress.Common; + +namespace SharpCompress.Archives; + +/// +/// Contains information about a detected archive, including its type and supported capabilities. +/// +/// +/// Use or +/// +/// to obtain an instance of this record. +/// +/// +/// The type of archive detected, or when the format is not a registered well-known type. +/// +/// +/// when this archive format supports random access via the API, +/// meaning the full file listing can be retrieved without decompressing the entire archive. +/// when only the API is available, +/// which reads entries sequentially and can only report per-entry progress. +/// +public record ArchiveInformation(ArchiveType? Type, bool SupportsRandomAccess); diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index 2c333c5c..a8a0448a 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -153,7 +153,7 @@ public partial class RarArchive { try { - MarkHeader.Read(stream, true, false); + MarkHeader.Read(stream, true, options?.LookForHeader ?? false); return true; } catch @@ -172,7 +172,7 @@ public partial class RarArchive try { await MarkHeader - .ReadAsync(stream, true, false, cancellationToken) + .ReadAsync(stream, true, options?.LookForHeader ?? false, cancellationToken) .ConfigureAwait(false); return true; } diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index a338e363..8c177a0f 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -143,11 +143,14 @@ public partial class SevenZipArchive return IsSevenZipFile(stream); } - public static bool IsSevenZipFile(Stream stream) + public static bool IsSevenZipFile(Stream stream) => + IsSevenZipFile(stream, ReaderOptions.ForExternalStream); + + public static bool IsSevenZipFile(Stream stream, ReaderOptions? readerOptions) { try { - return SignatureMatch(stream); + return SignatureMatch(stream, readerOptions?.LookForHeader ?? false); } catch { @@ -158,12 +161,25 @@ public partial class SevenZipArchive public static async ValueTask IsSevenZipFileAsync( Stream stream, CancellationToken cancellationToken = default + ) => + await IsSevenZipFileAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + + public static async ValueTask IsSevenZipFileAsync( + Stream stream, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); try { - return await SignatureMatchAsync(stream, cancellationToken).ConfigureAwait(false); + return await SignatureMatchAsync( + stream, + readerOptions?.LookForHeader ?? false, + cancellationToken + ) + .ConfigureAwait(false); } catch { @@ -173,13 +189,29 @@ public partial class SevenZipArchive private static ReadOnlySpan Signature => [(byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C]; - private static bool SignatureMatch(Stream stream) + private static bool SignatureMatch(Stream stream, bool lookForHeader) { var buffer = ArrayPool.Shared.Rent(6); try { - stream.ReadExact(buffer, 0, 6); - return buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature); + var maxScanOffset = lookForHeader ? 0x80000 - 20 : 0; + for (var offset = 0; offset <= maxScanOffset; offset++) + { + stream.ReadExact(buffer, 0, 6); + if (buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature)) + { + return true; + } + + if (!lookForHeader || !stream.CanSeek || stream.Length - stream.Position < 6) + { + return false; + } + + stream.Position -= 5; + } + + return false; } finally { @@ -189,18 +221,39 @@ public partial class SevenZipArchive private static async ValueTask SignatureMatchAsync( Stream stream, + bool lookForHeader, CancellationToken cancellationToken ) { var buffer = ArrayPool.Shared.Rent(6); try { - if (!await stream.ReadFullyAsync(buffer, 0, 6, cancellationToken).ConfigureAwait(false)) + var maxScanOffset = lookForHeader ? 0x80000 - 20 : 0; + for (var offset = 0; offset <= maxScanOffset; offset++) { - return false; + if ( + !await stream + .ReadFullyAsync(buffer, 0, 6, cancellationToken) + .ConfigureAwait(false) + ) + { + return false; + } + + if (buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature)) + { + return true; + } + + if (!lookForHeader || !stream.CanSeek || stream.Length - stream.Position < 6) + { + return false; + } + + stream.Position -= 5; } - return buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature); + return false; } finally { diff --git a/src/SharpCompress/Factories/AceFactory.cs b/src/SharpCompress/Factories/AceFactory.cs index f2fdfaa6..5117045e 100644 --- a/src/SharpCompress/Factories/AceFactory.cs +++ b/src/SharpCompress/Factories/AceFactory.cs @@ -23,12 +23,12 @@ public class AceFactory : Factory, IReaderFactory yield return "ace"; } - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => AceHeader.IsArchive(stream); public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => AceHeader.IsArchiveAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index 46c12666..cbc95e01 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -25,7 +25,7 @@ public class ArcFactory : Factory, IReaderFactory yield return "arc"; } - public override bool IsArchive(Stream stream, string? password = null) + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) { //You may have to use some(paranoid) checks to ensure that you actually are //processing an ARC file, since other archivers also adopted the idea of putting @@ -63,7 +63,7 @@ public class ArcFactory : Factory, IReaderFactory public override async ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs index d599d81d..de1a8382 100644 --- a/src/SharpCompress/Factories/ArjFactory.cs +++ b/src/SharpCompress/Factories/ArjFactory.cs @@ -23,12 +23,12 @@ public class ArjFactory : Factory, IReaderFactory yield return "arj"; } - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => ArjHeader.IsArchive(stream); public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => ArjHeader.IsArchiveAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 8c8cbde2..115c6e49 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -54,11 +54,10 @@ public abstract class Factory : IFactory public abstract IEnumerable GetSupportedExtensions(); /// - public abstract bool IsArchive(Stream stream, string? password = null); - + public abstract bool IsArchive(Stream stream, ReaderOptions readerOptions); public abstract ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ); @@ -86,7 +85,7 @@ public abstract class Factory : IFactory if (this is IReaderFactory readerFactory) { stream.Rewind(); - if (IsArchive(stream, options.Password)) + if (IsArchive(stream, options)) { stream.Rewind(true); reader = readerFactory.OpenReader(stream, options); @@ -106,10 +105,7 @@ public abstract class Factory : IFactory if (this is IReaderFactory readerFactory) { stream.Rewind(); - if ( - await IsArchiveAsync(stream, options.Password, cancellationToken) - .ConfigureAwait(false) - ) + if (await IsArchiveAsync(stream, options, cancellationToken).ConfigureAwait(false)) { stream.Rewind(true); return await readerFactory diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index c49b8f8e..dd870d51 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -44,13 +44,13 @@ public class GZipFactory } /// - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => GZipArchive.IsGZipFile(stream); /// public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => GZipArchive.IsGZipFileAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/IFactory.cs b/src/SharpCompress/Factories/IFactory.cs index 2f0b1ab0..76bddb64 100644 --- a/src/SharpCompress/Factories/IFactory.cs +++ b/src/SharpCompress/Factories/IFactory.cs @@ -37,18 +37,18 @@ public interface IFactory /// Returns true if the stream represents an archive of the format defined by this type. /// /// A stream, pointing to the beginning of the archive. - /// optional password - bool IsArchive(Stream stream, string? password = null); + /// Options controlling archive detection. + bool IsArchive(Stream stream, ReaderOptions readerOptions); /// /// Returns true if the stream represents an archive of the format defined by this type asynchronously. /// /// A stream, pointing to the beginning of the archive. - /// optional password + /// Options controlling archive detection. /// cancellation token ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ); diff --git a/src/SharpCompress/Factories/LzwFactory.cs b/src/SharpCompress/Factories/LzwFactory.cs index cbfabb60..5bc333c6 100644 --- a/src/SharpCompress/Factories/LzwFactory.cs +++ b/src/SharpCompress/Factories/LzwFactory.cs @@ -32,13 +32,13 @@ public class LzwFactory : Factory, IReaderFactory } /// - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => LzwStream.IsLzwStream(stream); /// public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => LzwStream.IsLzwStreamAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index 615f7fb4..2efc920d 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -31,15 +31,15 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade } /// - public override bool IsArchive(Stream stream, string? password = null) => - RarArchive.IsRarFile(stream); + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => + RarArchive.IsRarFile(stream, readerOptions); /// public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default - ) => RarArchive.IsRarFileAsync(stream, cancellationToken: cancellationToken); + ) => RarArchive.IsRarFileAsync(stream, readerOptions, cancellationToken); /// public override FileInfo? GetFilePart(int index, FileInfo part1) => diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index adc26074..bac7b533 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -34,15 +34,15 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory, I } /// - public override bool IsArchive(Stream stream, string? password = null) => - SevenZipArchive.IsSevenZipFile(stream); + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => + SevenZipArchive.IsSevenZipFile(stream, readerOptions); /// public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default - ) => SevenZipArchive.IsSevenZipFileAsync(stream, cancellationToken); + ) => SevenZipArchive.IsSevenZipFileAsync(stream, readerOptions, cancellationToken); #endregion diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 56d0a73b..eaa6e706 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -48,9 +48,9 @@ public class TarFactory } /// - public override bool IsArchive(Stream stream, string? password = null) + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) { - var providers = CompressionProviderRegistry.Default; + var providers = readerOptions.Providers; var sharpCompressStream = new SharpCompressStream(stream); sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize); foreach (var wrapper in TarWrapper.Wrappers) @@ -78,11 +78,11 @@ public class TarFactory /// public override async ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) { - var providers = CompressionProviderRegistry.Default; + var providers = readerOptions.Providers; var sharpCompressStream = new SharpCompressStream(stream); sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize); foreach (var wrapper in TarWrapper.Wrappers) diff --git a/src/SharpCompress/Factories/ZStandardFactory.cs b/src/SharpCompress/Factories/ZStandardFactory.cs index e45b2454..6ee920dc 100644 --- a/src/SharpCompress/Factories/ZStandardFactory.cs +++ b/src/SharpCompress/Factories/ZStandardFactory.cs @@ -21,12 +21,12 @@ internal class ZStandardFactory : Factory yield return "zstd"; } - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => ZStandardStream.IsZStandard(stream); public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => ZStandardStream.IsZStandardAsync(stream, cancellationToken); } diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index b5dbdff3..a9f97052 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -43,10 +43,10 @@ public class ZipFactory } /// - public override bool IsArchive(Stream stream, string? password = null) + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) { var startPosition = stream.CanSeek ? stream.Position : -1; - if (ZipArchive.IsZipFile(stream, password)) + if (ZipArchive.IsZipFile(stream, readerOptions.Password)) { return true; } @@ -61,7 +61,7 @@ public class ZipFactory stream.Position = startPosition; //test the zip (last) file of a multipart zip - if (ZipArchive.IsZipMulti(stream, password)) + if (ZipArchive.IsZipMulti(stream, readerOptions.Password)) { return true; } @@ -74,7 +74,7 @@ public class ZipFactory /// public override async ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) { @@ -84,7 +84,7 @@ public class ZipFactory // probe for single volume zip if ( await ZipArchive - .IsZipFileAsync(stream, password, cancellationToken) + .IsZipFileAsync(stream, readerOptions.Password, cancellationToken) .ConfigureAwait(false) ) { @@ -102,7 +102,7 @@ public class ZipFactory //test the zip (last) file of a multipart zip if ( await ZipArchive - .IsZipMultiAsync(stream, password, cancellationToken) + .IsZipMultiAsync(stream, readerOptions.Password, cancellationToken) .ConfigureAwait(false) ) { diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index e82196f8..03c03a9a 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.6, )", - "resolved": "10.0.6", - "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.26, )", - "resolved": "8.0.26", - "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index d6326bad..ba9acd7e 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Common; using SharpCompress.Factories; +using SharpCompress.Readers; using SharpCompress.Test.Mocks; using Xunit; @@ -111,6 +112,72 @@ public class ArchiveFactoryTests : TestBase ); } + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + [InlineData("Rar.rar", ArchiveType.Rar)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip)] + public void IsArchive_String_ReturnsExpectedType(string archiveName, ArchiveType expectedType) + { + var result = ArchiveFactory.IsArchive( + Path.Combine(TEST_ARCHIVES_PATH, archiveName), + out var type + ); + + Assert.True(result); + Assert.Equal(expectedType, type); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + public void IsArchive_Stream_PreservesPosition(string archiveName, ArchiveType expectedType) + { + using var stream = CreatePrefixedArchiveStream(archiveName, 11); + var startPosition = stream.Position; + + var result = ArchiveFactory.IsArchive(stream, out var type); + + Assert.True(result); + Assert.Equal(expectedType, type); + Assert.Equal(startPosition, stream.Position); + } + + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar)] + public void IsArchive_WithReaderOptions_ReturnsExpectedType( + string archiveName, + ArchiveType expectedType + ) + { + var result = ArchiveFactory.IsArchive( + GetTestArchivePath(archiveName), + ReaderOptions.ForFilePath.WithLookForHeader(true), + out var type + ); + + Assert.True(result); + Assert.Equal(expectedType, type); + } + + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar)] + public async ValueTask IsArchiveAsync_WithReaderOptions_ReturnsExpectedType( + string archiveName, + ArchiveType expectedType + ) + { + var result = await ArchiveFactory.IsArchiveAsync( + GetTestArchivePath(archiveName), + ReaderOptions.ForFilePath.WithLookForHeader(true) + ); + + Assert.True(result.IsArchive); + Assert.Equal(expectedType, result.Type); + } + [Theory] [InlineData("Zip.deflate.zip", ArchiveType.Zip)] [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] @@ -159,9 +226,481 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(0, stream.Position); } + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + public void GetArchiveInformation_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = ArchiveFactory.GetArchiveInformation( + Path.Combine(TEST_ARCHIVES_PATH, archiveName) + ); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)] + public void GetArchiveInformation_WithReaderOptions_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = ArchiveFactory.GetArchiveInformation( + GetTestArchivePath(archiveName), + ReaderOptions.ForFilePath.WithLookForHeader(true) + ); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + public async ValueTask GetArchiveInformationAsync_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = await ArchiveFactory.GetArchiveInformationAsync( + Path.Combine(TEST_ARCHIVES_PATH, archiveName) + ); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)] + public async ValueTask GetArchiveInformationAsync_WithReaderOptions_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = await ArchiveFactory.GetArchiveInformationAsync( + GetTestArchivePath(archiveName), + ReaderOptions.ForFilePath.WithLookForHeader(true) + ); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + + [Theory] + [InlineData("64bitstream.zip.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARMT.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BZip2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Copy.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.EmptyStream.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Filters.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.IA64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPMd.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.RISCV.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.SPARC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Tar.tar", ArchiveType.Tar, true)] + [InlineData("7Zip.Tar.tar.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ZSTD.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.distance.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.encryptedFiles.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.eos.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.1block.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.encrypted.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.largefile.ace", ArchiveType.Ace, false)] + [InlineData("Arc.crunched.arc", ArchiveType.Arc, false)] + [InlineData("Arc.crunched.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arj.encrypted.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Issue_685.zip", ArchiveType.Zip, true)] + [InlineData("PrePostHeaders.zip", ArchiveType.Zip, true)] + [InlineData("Rar.Audio_program.rar", ArchiveType.Rar, true)] + [InlineData("Rar.Encrypted.rar", ArchiveType.Rar, true)] + [InlineData("Rar.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar.issue1050.rar", ArchiveType.Rar, true)] + [InlineData("Rar.malformed_512byte.rar", ArchiveType.Rar, true)] + [InlineData("Rar.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("Rar.solid.rar", ArchiveType.Rar, true)] + [InlineData("Rar.test_invalid_exttime.rar", ArchiveType.Rar, true)] + [InlineData("Rar15.rar", ArchiveType.Rar, true)] + [InlineData("Rar2.rar", ArchiveType.Rar, true)] + [InlineData("Rar4.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.crc_blake2.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.solid.rar", ArchiveType.Rar, true)] + [InlineData("Tar.ContainsRar.tar", ArchiveType.Tar, true)] + [InlineData("Tar.ContainsTarGz.tar", ArchiveType.Tar, true)] + [InlineData("Tar.Empty.tar", ArchiveType.Tar, true)] + [InlineData("Tar.LongPathsWithLongNameExtension.tar", ArchiveType.Tar, true)] + [InlineData("Tar.mod.tar", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar.bz2", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar.lz", ArchiveType.Tar, true)] + [InlineData("Tar.oldgnu.tar.gz", ArchiveType.Tar, true)] + [InlineData("Tar.tar", ArchiveType.Tar, true)] + [InlineData("Tar.tar.Z", ArchiveType.Tar, true)] + [InlineData("Tar.tar.bz2", ArchiveType.Tar, true)] + [InlineData("Tar.tar.gz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.lz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.xz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.zst", ArchiveType.Tar, true)] + [InlineData("TarCorrupted.tar", ArchiveType.Tar, true)] + [InlineData("TarWithSymlink.tar.gz", ArchiveType.Tar, true)] + [InlineData("WinZip26.zip", ArchiveType.Zip, true)] + [InlineData("WinZip26_BZip2.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip26_LZMA.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip27_XZ.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip27_ZSTD.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.644.zip", ArchiveType.Zip, true)] + [InlineData("Zip.EntryComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.Evil.zip", ArchiveType.Zip, true)] + [InlineData("Zip.LongComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.UnicodePathExtra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.badlocalextra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd-.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.implode.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.empty.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.datadescriptors.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.encrypted.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.issue86.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce1.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce3.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce4.zip", ArchiveType.Zip, true)] + [InlineData("Zip.shrink.zip", ArchiveType.Zip, true)] + [InlineData("Zip.uncompressed.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.compressedonly.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.zstd.WinzipAES.mixed.zip", ArchiveType.Zip, true)] + [InlineData("large_test.txt.Z", ArchiveType.Lzw, false)] + [InlineData("test_477.zip", ArchiveType.Zip, true)] + [InlineData("ustar with long names.tar", ArchiveType.Tar, true)] + [InlineData("very long filename.tar", ArchiveType.Tar, true)] + [InlineData("zipcrypto.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.AES.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted2.zip", ArchiveType.Zip, true)] + public void GetArchiveInformation_DetectsSingleFileTestArchives( + string archiveName, + ArchiveType expectedType, + bool expectedSeekable + ) + { + var info = ArchiveFactory.GetArchiveInformation(GetTestArchivePath(archiveName)); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedSeekable, info.SupportsRandomAccess); + } + + [Theory] + [InlineData("64bitstream.zip.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARMT.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BZip2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Copy.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.EmptyStream.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Filters.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.IA64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPMd.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.RISCV.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.SPARC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Tar.tar", ArchiveType.Tar, true)] + [InlineData("7Zip.Tar.tar.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ZSTD.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.distance.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.encryptedFiles.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.eos.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.1block.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.encrypted.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.largefile.ace", ArchiveType.Ace, false)] + [InlineData("Arc.crunched.arc", ArchiveType.Arc, false)] + [InlineData("Arc.crunched.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arj.encrypted.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Issue_685.zip", ArchiveType.Zip, true)] + [InlineData("PrePostHeaders.zip", ArchiveType.Zip, true)] + [InlineData("Rar.Audio_program.rar", ArchiveType.Rar, true)] + [InlineData("Rar.Encrypted.rar", ArchiveType.Rar, true)] + [InlineData("Rar.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar.issue1050.rar", ArchiveType.Rar, true)] + [InlineData("Rar.malformed_512byte.rar", ArchiveType.Rar, true)] + [InlineData("Rar.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("Rar.solid.rar", ArchiveType.Rar, true)] + [InlineData("Rar.test_invalid_exttime.rar", ArchiveType.Rar, true)] + [InlineData("Rar15.rar", ArchiveType.Rar, true)] + [InlineData("Rar2.rar", ArchiveType.Rar, true)] + [InlineData("Rar4.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.crc_blake2.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.solid.rar", ArchiveType.Rar, true)] + [InlineData("Tar.ContainsRar.tar", ArchiveType.Tar, true)] + [InlineData("Tar.ContainsTarGz.tar", ArchiveType.Tar, true)] + [InlineData("Tar.Empty.tar", ArchiveType.Tar, true)] + [InlineData("Tar.LongPathsWithLongNameExtension.tar", ArchiveType.Tar, true)] + [InlineData("Tar.mod.tar", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar.bz2", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar.lz", ArchiveType.Tar, true)] + [InlineData("Tar.oldgnu.tar.gz", ArchiveType.Tar, true)] + [InlineData("Tar.tar", ArchiveType.Tar, true)] + [InlineData("Tar.tar.Z", ArchiveType.Tar, true)] + [InlineData("Tar.tar.bz2", ArchiveType.Tar, true)] + [InlineData("Tar.tar.gz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.lz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.xz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.zst", ArchiveType.Tar, true)] + [InlineData("TarCorrupted.tar", ArchiveType.Tar, true)] + [InlineData("TarWithSymlink.tar.gz", ArchiveType.Tar, true)] + [InlineData("WinZip26.zip", ArchiveType.Zip, true)] + [InlineData("WinZip26_BZip2.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip26_LZMA.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip27_XZ.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip27_ZSTD.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.644.zip", ArchiveType.Zip, true)] + [InlineData("Zip.EntryComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.Evil.zip", ArchiveType.Zip, true)] + [InlineData("Zip.LongComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.UnicodePathExtra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.badlocalextra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd-.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.implode.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.empty.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.datadescriptors.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.encrypted.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.issue86.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce1.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce3.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce4.zip", ArchiveType.Zip, true)] + [InlineData("Zip.shrink.zip", ArchiveType.Zip, true)] + [InlineData("Zip.uncompressed.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.compressedonly.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.zstd.WinzipAES.mixed.zip", ArchiveType.Zip, true)] + [InlineData("large_test.txt.Z", ArchiveType.Lzw, false)] + [InlineData("test_477.zip", ArchiveType.Zip, true)] + [InlineData("ustar with long names.tar", ArchiveType.Tar, true)] + [InlineData("very long filename.tar", ArchiveType.Tar, true)] + [InlineData("zipcrypto.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.AES.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted2.zip", ArchiveType.Zip, true)] + public async ValueTask GetArchiveInformationAsync_DetectsSingleFileTestArchives( + string archiveName, + ArchiveType expectedType, + bool expectedSeekable + ) + { + var info = await ArchiveFactory.GetArchiveInformationAsync(GetTestArchivePath(archiveName)); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedSeekable, info.SupportsRandomAccess); + } + + [Fact] + public void GetArchiveInformation_ReturnsNull_ForNonArchive() + { + using var stream = new MemoryStream(Encoding.ASCII.GetBytes("not an archive")); + + var info = ArchiveFactory.GetArchiveInformation(stream); + + Assert.Null(info); + } + + [Fact] + public async ValueTask GetArchiveInformationAsync_ReturnsNull_ForNonArchive() + { + using var stream = new MemoryStream(Encoding.ASCII.GetBytes("not an archive")); + + var info = await ArchiveFactory.GetArchiveInformationAsync(stream); + + Assert.Null(info); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + public void GetArchiveInformation_Stream_PreservesPosition( + string archiveName, + ArchiveType expectedType + ) + { + using var stream = CreatePrefixedArchiveStream(archiveName, 13); + var startPosition = stream.Position; + + var info = ArchiveFactory.GetArchiveInformation(stream); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(startPosition, stream.Position); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + public async ValueTask GetArchiveInformationAsync_Stream_PreservesPosition( + string archiveName, + ArchiveType expectedType + ) + { + using var stream = CreatePrefixedArchiveStream(archiveName, 13); + var startPosition = stream.Position; + + var info = await ArchiveFactory.GetArchiveInformationAsync(stream); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(startPosition, stream.Position); + } + private MemoryStream CreatePrefixedArchiveStream(string archiveName, int prefixLength) { - var archiveBytes = File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + var archiveBytes = File.ReadAllBytes(GetTestArchivePath(archiveName)); var buffer = new byte[prefixLength + archiveBytes.Length]; archiveBytes.CopyTo(buffer, prefixLength); @@ -170,4 +709,15 @@ public class ArchiveFactoryTests : TestBase stream.Position = prefixLength; return stream; } + + private static string GetTestArchivePath(string archiveName) + { + var archivesPath = Path.Combine(TEST_ARCHIVES_PATH, archiveName); + if (File.Exists(archivesPath)) + { + return archivesPath; + } + + return Path.GetFullPath(Path.Combine(TEST_ARCHIVES_PATH, "..", archiveName)); + } } diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 72ab5f47..4c9024c9 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -103,7 +103,7 @@ public abstract class ReaderTests : TestBase ( await factory.IsArchiveAsync( new FileInfo(testArchive).OpenRead(), - null, + ReaderOptions.ForExternalStream, cancellationToken ) ) @@ -112,6 +112,7 @@ public abstract class ReaderTests : TestBase ( await factory.IsArchiveAsync( new FileInfo(testArchive).OpenRead(), + ReaderOptions.ForExternalStream, cancellationToken: cancellationToken ) )