From d301cb365fad2db630bc47f5d2dfdb63aba4cdcd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:45:31 +0000 Subject: [PATCH] Add format-specific ArchiveInformation details --- docs/API.md | 4 +- .../Archives/ArchiveFactory.Detection.cs | 128 +++++++++++++++++- .../Archives/ArchiveInformation.cs | 12 ++ .../SharpCompress.Test/ArchiveFactoryTests.cs | 51 +++++++ 4 files changed, 192 insertions(+), 3 deletions(-) diff --git a/docs/API.md b/docs/API.md index 80143e83..b2145189 100644 --- a/docs/API.md +++ b/docs/API.md @@ -56,6 +56,8 @@ if (info is not null) { Console.WriteLine($"Type: {info.Type}"); Console.WriteLine($"Supports random access: {info.SupportsRandomAccess}"); + Console.WriteLine($"ZIP data descriptor entries: {info.ZipDataDescriptorEntryCount}"); + Console.WriteLine($"Solid stream count: {info.SolidStreamCount}"); } var asyncInfo = await ArchiveFactory.GetArchiveInformationAsync( @@ -73,7 +75,7 @@ using (var archive = ArchiveFactory.OpenArchive(parts)) } ``` -`ArchiveInformation.SupportsRandomAccess` is `true` when the detected format supports `IArchive` random access. It is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW, where `ReaderFactory.OpenReader` should be used instead. Compressed tar wrappers such as `.tar.gz` and `.tar.xz` are also reader-only; `ArchiveFactory.GetArchiveInformation` returns `null` for them and `ArchiveFactory.OpenArchive` does not open them as the outer compression wrapper. Use `ReaderFactory.OpenReader` or `TarReader.OpenReader` for those files. +`ArchiveInformation.SupportsRandomAccess` is `true` when the detected format supports `IArchive` random access. It is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW, where `ReaderFactory.OpenReader` should be used instead. Compressed tar wrappers such as `.tar.gz` and `.tar.xz` are also reader-only; `ArchiveFactory.GetArchiveInformation` returns `null` for them and `ArchiveFactory.OpenArchive` does not open them as the outer compression wrapper. Use `ReaderFactory.OpenReader` or `TarReader.OpenReader` for those files. Format-specific details are available through `ArchiveInformation.ZipDataDescriptorEntryCount` (ZIP) and `ArchiveInformation.SolidStreamCount` (solid-capable formats such as 7z and RAR). ### Creating Archives diff --git a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs index cf34e6e5..5674a462 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs @@ -2,8 +2,13 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Archives.Rar; +using SharpCompress.Archives.SevenZip; using SharpCompress.Archives.Tar; +using SharpCompress.Archives.Zip; using SharpCompress.Common; +using SharpCompress.Common.Zip; +using SharpCompress.Common.Zip.Headers; using SharpCompress.Factories; using SharpCompress.IO; using SharpCompress.Providers; @@ -86,7 +91,11 @@ public static partial class ArchiveFactory .ConfigureAwait(false); return factory is null ? null - : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + : BuildArchiveInformation( + stream, + readerOptions ?? ReaderOptions.ForExternalStream, + factory + ); } internal static ValueTask FindFactoryAsync( @@ -254,7 +263,11 @@ public static partial class ArchiveFactory var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream); return factory is null ? null - : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + : BuildArchiveInformation( + stream, + readerOptions ?? ReaderOptions.ForExternalStream, + factory + ); } /// @@ -411,4 +424,115 @@ public static partial class ArchiveFactory LzwFactory => CompressionType.Lzw, _ => null, }; + + private static ArchiveInformation BuildArchiveInformation( + Stream stream, + ReaderOptions readerOptions, + IFactory factory + ) + { + var info = new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + var startPosition = stream.Position; + + try + { + var probeReaderOptions = readerOptions with { LeaveStreamOpen = true }; + switch (factory) + { + case ZipFactory: + TryPopulateZipDetails(info, stream, probeReaderOptions); + break; + case SevenZipFactory: + TryPopulateSevenZipDetails(info, stream, probeReaderOptions); + break; + case RarFactory: + TryPopulateRarDetails(info, stream, probeReaderOptions); + break; + } + } + finally + { + stream.Seek(startPosition, SeekOrigin.Begin); + } + + return info; + } + + private static void TryPopulateZipDetails( + ArchiveInformation info, + Stream stream, + ReaderOptions readerOptions + ) + { + try + { + info.ZipDataDescriptorEntryCount = GetZipDataDescriptorEntryCount( + stream, + readerOptions + ); + } + catch + { + // Keep archive detection resilient even when format-specific probing fails. + } + } + + private static void TryPopulateSevenZipDetails( + ArchiveInformation info, + Stream stream, + ReaderOptions readerOptions + ) + { + try + { + info.SolidStreamCount = GetSevenZipSolidStreamCount(stream, readerOptions); + } + catch + { + // Keep archive detection resilient even when format-specific probing fails. + } + } + + private static void TryPopulateRarDetails( + ArchiveInformation info, + Stream stream, + ReaderOptions readerOptions + ) + { + try + { + info.SolidStreamCount = GetRarSolidStreamCount(stream, readerOptions); + } + catch + { + // Keep archive detection resilient even when format-specific probing fails. + } + } + + private static int GetZipDataDescriptorEntryCount(Stream stream, ReaderOptions readerOptions) + { + using var archive = ZipArchive.OpenArchive(stream, readerOptions); + return archive + .Entries.OfType() + .SelectMany(entry => entry.Parts.OfType()) + .Count(part => + FlagUtility.HasFlag(part.Header.Flags, HeaderFlags.UsePostDataDescriptor) + ); + } + + private static int GetSevenZipSolidStreamCount(Stream stream, ReaderOptions readerOptions) + { + using var archive = SevenZipArchive.OpenArchive(stream, readerOptions); + return archive + .Entries.OfType() + .Where(entry => !entry.IsDirectory && entry.FilePart.Folder is not null) + .GroupBy(entry => entry.FilePart.Folder) + .Count(group => group.Skip(1).Any()); + } + + private static int GetRarSolidStreamCount(Stream stream, ReaderOptions readerOptions) + { + using var archive = RarArchive.OpenArchive(stream, readerOptions); + return archive.IsSolid ? 1 : 0; + } } diff --git a/src/SharpCompress/Archives/ArchiveInformation.cs b/src/SharpCompress/Archives/ArchiveInformation.cs index 75d3cb5a..81d4fc6d 100644 --- a/src/SharpCompress/Archives/ArchiveInformation.cs +++ b/src/SharpCompress/Archives/ArchiveInformation.cs @@ -25,6 +25,18 @@ public record ArchiveInformation /// public bool SupportsRandomAccess { get; set; } + /// + /// For ZIP archives, the number of entries that use post-data descriptor trailers. + /// This value is for non-ZIP formats. + /// + public int? ZipDataDescriptorEntryCount { get; set; } + + /// + /// For solid-capable archive formats, the number of solid compressed streams present. + /// This value is for formats that do not support solid entries. + /// + public int? SolidStreamCount { get; set; } + /// /// Creates a new archive information instance. /// diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index 199dff47..2c8aa42c 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -760,6 +760,57 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(startPosition, stream.Position); } + [Theory] + [InlineData("Zip.none.datadescriptors.zip", true)] + [InlineData("Zip.deflate.zip", false)] + public void GetArchiveInformation_ZipDataDescriptorEntryCount( + string archiveName, + bool expectsDataDescriptorTrailers + ) + { + using var stream = File.OpenRead(GetTestArchivePath(archiveName)); + + var info = ArchiveFactory.GetArchiveInformation(stream); + + Assert.NotNull(info); + Assert.Equal(ArchiveType.Zip, info.Type); + Assert.NotNull(info.ZipDataDescriptorEntryCount); + Assert.Equal(expectsDataDescriptorTrailers, info.ZipDataDescriptorEntryCount > 0); + } + + [Theory] + [InlineData("7Zip.solid.7z", true)] + [InlineData("7Zip.nonsolid.7z", false)] + [InlineData("Rar.rar", false)] + public void GetArchiveInformation_SolidStreamCount(string archiveName, bool expectsSolidStreams) + { + using var stream = File.OpenRead(GetTestArchivePath(archiveName)); + + var info = ArchiveFactory.GetArchiveInformation(stream); + + Assert.NotNull(info); + Assert.NotNull(info.SolidStreamCount); + Assert.Equal(expectsSolidStreams, info.SolidStreamCount > 0); + } + + [Theory] + [InlineData("Zip.none.datadescriptors.zip", true)] + [InlineData("7Zip.solid.7z", true)] + public async ValueTask GetArchiveInformationAsync_IncludesFormatSpecificDetails( + string archiveName, + bool expectsAnyFormatSpecificDetails + ) + { + using var stream = File.OpenRead(GetTestArchivePath(archiveName)); + + var info = await ArchiveFactory.GetArchiveInformationAsync(stream); + + Assert.NotNull(info); + var hasZipDetail = info.ZipDataDescriptorEntryCount > 0; + var hasSolidDetail = info.SolidStreamCount > 0; + Assert.Equal(expectsAnyFormatSpecificDetails, hasZipDetail || hasSolidDetail); + } + private MemoryStream CreatePrefixedArchiveStream(string archiveName, int prefixLength) { var archiveBytes = File.ReadAllBytes(GetTestArchivePath(archiveName));