better zip streaming info

This commit is contained in:
Adam Hathcock
2026-08-05 16:20:08 +01:00
parent 87f5616d1f
commit ea7d97fcee
11 changed files with 192 additions and 59 deletions

View File

@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives.Rar;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Common.Ace.Headers;
using SharpCompress.Common.Rar;
@@ -389,7 +390,12 @@ public static partial class ArchiveFactory
var entryArray = entries.ToArray();
var volumeArray = volumes.ToArray();
var zip = GetZipInformation(entryArray);
var (zipInformation, deferredSizeEntryCount) = await GetZipInformationAsync(
archive.Type == ArchiveType.Zip,
entryArray,
cancellationToken
)
.ConfigureAwait(false);
var (isSolid, solidStreamCount) = await GetSolidInformationAsync(archive, entryArray)
.ConfigureAwait(false);
var isComplete = await archive.IsCompleteAsync().ConfigureAwait(false);
@@ -411,7 +417,7 @@ public static partial class ArchiveFactory
limitations,
GetFormatVersion(volumeArray),
entryArray.LongLength,
zip?.DataDescriptorEntryCount ?? 0,
deferredSizeEntryCount,
physicalSize,
isComplete ? GetCompressedPayloadSize(archive, entryArray) : null,
isComplete && archive.Type != ArchiveType.GZip
@@ -426,7 +432,7 @@ public static partial class ArchiveFactory
volumeArray.Length,
isComplete,
GetArchiveComment(volumeArray),
zip
zipInformation
);
}
@@ -522,6 +528,40 @@ public static partial class ArchiveFactory
_ => entries.Aggregate(0L, (total, entry) => total + entry.CompressedSize),
};
private static async ValueTask<(
ZipArchiveInformation? Information,
long DeferredSizeEntryCount
)> GetZipInformationAsync(
bool isZipArchive,
IEnumerable<IArchiveEntry> entries,
CancellationToken cancellationToken
)
{
if (!isZipArchive)
{
return (null, 0);
}
long deferredSizeEntryCount = 0;
foreach (var entry in entries.OfType<ZipArchiveEntry>())
{
cancellationToken.ThrowIfCancellationRequested();
var filePart = entry.Parts.OfType<SeekableZipFilePart>().Single();
if (!filePart.HasDeferredSizes)
{
continue;
}
var localHeader = await filePart.GetRawLocalHeaderAsync().ConfigureAwait(false);
if (localHeader.CompressedSize == 0 && localHeader.UncompressedSize == 0)
{
deferredSizeEntryCount++;
}
}
return (new ZipArchiveInformation(deferredSizeEntryCount > 0), deferredSizeEntryCount);
}
private static async ValueTask<ArchiveDetection?> RedetectArchiveAsync(
Stream stream,
long startPosition,

View File

@@ -258,7 +258,10 @@ public static partial class ArchiveFactory
{
var entries = archive.Entries.ToArray();
var volumes = archive.Volumes.ToArray();
var zip = GetZipInformation(entries);
var (zipInformation, deferredSizeEntryCount) = GetZipInformation(
archive.Type == ArchiveType.Zip,
entries
);
var (isSolid, solidStreamCount) = GetSolidInformation(archive, entries);
var isMultiVolume = GetIsMultiVolume(archive.Type, volumes);
var comment = GetArchiveComment(volumes);
@@ -281,7 +284,7 @@ public static partial class ArchiveFactory
limitations,
GetFormatVersion(volumes),
entries.LongLength,
zip?.DataDescriptorEntryCount ?? 0,
deferredSizeEntryCount,
physicalSize,
isComplete ? GetCompressedPayloadSize(archive, entries) : null,
isComplete && archive.Type != ArchiveType.GZip
@@ -296,7 +299,7 @@ public static partial class ArchiveFactory
volumes.Length,
isComplete,
comment,
zip
zipInformation
);
}
@@ -384,23 +387,33 @@ public static partial class ArchiveFactory
? ArchiveInformationStatus.Complete
: ArchiveInformationStatus.Partial;
private static ZipArchiveInformation? GetZipInformation(IEnumerable<IArchiveEntry> entries)
private static (
ZipArchiveInformation? Information,
long DeferredSizeEntryCount
) GetZipInformation(bool isZipArchive, IEnumerable<IArchiveEntry> entries)
{
var dataDescriptorEntryCount = entries
.OfType<ZipArchiveEntry>()
.LongCount(entry =>
entry
.Parts.OfType<ZipFilePart>()
.Any(part =>
FlagUtility.HasFlag(
part.Header.Flags,
SharpCompress.Common.Zip.Headers.HeaderFlags.UsePostDataDescriptor
)
)
);
return dataDescriptorEntryCount == 0 && !entries.OfType<ZipArchiveEntry>().Any()
? null
: new ZipArchiveInformation(dataDescriptorEntryCount);
if (!isZipArchive)
{
return (null, 0);
}
long deferredSizeEntryCount = 0;
foreach (var entry in entries.OfType<ZipArchiveEntry>())
{
var filePart = entry.Parts.OfType<SeekableZipFilePart>().Single();
if (!filePart.HasDeferredSizes)
{
continue;
}
var localHeader = filePart.GetRawLocalHeader();
if (localHeader.CompressedSize == 0 && localHeader.UncompressedSize == 0)
{
deferredSizeEntryCount++;
}
}
return (new ZipArchiveInformation(deferredSizeEntryCount > 0), deferredSizeEntryCount);
}
private static (bool IsSolid, long SolidStreamCount) GetSolidInformation(
@@ -563,7 +576,7 @@ public static partial class ArchiveFactory
private readonly ArchiveDetection detection;
private long? compressedPayloadSize = 0;
private long? uncompressedPayloadSize = 0;
private long dataDescriptorEntryCount;
private long deferredSizeEntryCount;
private long entriesWithUnknownSizeCount;
private bool isEncrypted;
private ArchiveInformationLimitations limitations;
@@ -595,15 +608,15 @@ public static partial class ArchiveFactory
EntryCount++;
isEncrypted |= entry.IsEncrypted;
var usesDataDescriptor = UsesZipDataDescriptor(entry);
if (usesDataDescriptor)
var hasDeferredSizes = HasDeferredSizes(entry);
if (hasDeferredSizes)
{
dataDescriptorEntryCount++;
deferredSizeEntryCount++;
}
if (
detection.ContainerType == ArchiveType.Lzw
|| usesDataDescriptor
|| hasDeferredSizes
|| !TryGetSize(entry, out var size)
)
{
@@ -650,7 +663,7 @@ public static partial class ArchiveFactory
true,
null,
detection.ContainerType == ArchiveType.Zip
? new ZipArchiveInformation(dataDescriptorEntryCount)
? new ZipArchiveInformation(deferredSizeEntryCount > 0)
: null
);
@@ -668,7 +681,7 @@ public static partial class ArchiveFactory
}
}
private static bool UsesZipDataDescriptor(IEntry entry) =>
private static bool HasDeferredSizes(IEntry entry) =>
entry is ZipEntry zipEntry
&& zipEntry
.Parts.OfType<ZipFilePart>()
@@ -677,6 +690,8 @@ public static partial class ArchiveFactory
part.Header.Flags,
SharpCompress.Common.Zip.Headers.HeaderFlags.UsePostDataDescriptor
)
&& part.Header.CompressedSize == 0
&& part.Header.UncompressedSize == 0
);
}
}

View File

@@ -5,11 +5,12 @@ namespace SharpCompress.Archives;
/// </summary>
public sealed class ZipArchiveInformation
{
internal ZipArchiveInformation(long dataDescriptorEntryCount) =>
DataDescriptorEntryCount = dataDescriptorEntryCount;
internal ZipArchiveInformation(bool hasEntriesWithDeferredSizes) =>
HasEntriesWithDeferredSizes = hasEntriesWithDeferredSizes;
/// <summary>
/// Gets the number of entries whose local header defers its CRC and sizes to a data descriptor.
/// Gets whether any entry sizes are available only after reading the entry data.
/// When <see langword="true"/>, a forward-only reader can initially report zero sizes for those entries.
/// </summary>
public long DataDescriptorEntryCount { get; }
public bool HasEntriesWithDeferredSizes { get; }
}

View File

@@ -23,6 +23,9 @@ internal partial class SeekableZipFilePart
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default) =>
Header = await _headerFactory
.GetLocalHeaderAsync(BaseStream, (DirectoryEntryHeader)Header)
.GetLocalHeaderAsync(BaseStream, _directoryEntryHeader)
.ConfigureAwait(false);
internal ValueTask<LocalEntryHeader> GetRawLocalHeaderAsync() =>
_headerFactory.GetRawLocalHeaderAsync(BaseStream, _directoryEntryHeader);
}

View File

@@ -8,6 +8,7 @@ internal partial class SeekableZipFilePart : ZipFilePart
{
private bool _isLocalHeaderLoaded;
private readonly SeekableZipHeaderFactory _headerFactory;
private readonly DirectoryEntryHeader _directoryEntryHeader;
internal SeekableZipFilePart(
SeekableZipHeaderFactory headerFactory,
@@ -15,7 +16,17 @@ internal partial class SeekableZipFilePart : ZipFilePart
Stream stream,
CompressionProviderRegistry compressionProviders
)
: base(header, stream, compressionProviders) => _headerFactory = headerFactory;
: base(header, stream, compressionProviders)
{
_headerFactory = headerFactory;
_directoryEntryHeader = header;
}
internal LocalEntryHeader GetRawLocalHeader() =>
_headerFactory.GetRawLocalHeader(BaseStream, _directoryEntryHeader);
internal bool HasDeferredSizes =>
FlagUtility.HasFlag(_directoryEntryHeader.Flags, HeaderFlags.UsePostDataDescriptor);
protected override Stream CreateBaseStream()
{

View File

@@ -146,17 +146,27 @@ internal sealed partial class SeekableZipHeaderFactory
throw new ArchiveOperationException();
}
// populate fields only known from the DirectoryEntryHeader
localEntryHeader.HasData = directoryEntryHeader.HasData;
localEntryHeader.ExternalFileAttributes = directoryEntryHeader.ExternalFileAttributes;
localEntryHeader.Comment = directoryEntryHeader.Comment;
if (FlagUtility.HasFlag(localEntryHeader.Flags, HeaderFlags.UsePostDataDescriptor))
{
localEntryHeader.Crc = directoryEntryHeader.Crc;
localEntryHeader.CompressedSize = directoryEntryHeader.CompressedSize;
localEntryHeader.UncompressedSize = directoryEntryHeader.UncompressedSize;
}
PopulateDirectoryEntryMetadata(localEntryHeader, directoryEntryHeader);
return localEntryHeader;
}
internal async ValueTask<LocalEntryHeader> GetRawLocalHeaderAsync(
Stream stream,
DirectoryEntryHeader directoryEntryHeader
)
{
stream.Seek(directoryEntryHeader.RelativeOffsetOfEntryHeader, SeekOrigin.Begin);
#if NET8_0_OR_GREATER
await using var reader = new AsyncBinaryReader(stream, leaveOpen: true);
#else
using var reader = new AsyncBinaryReader(stream, leaveOpen: true);
#endif
var signature = await reader.ReadUInt32Async().ConfigureAwait(false);
if (signature != ENTRY_HEADER_BYTES)
{
throw new ArchiveOperationException();
}
return await ReadLocalHeader(reader).ConfigureAwait(false);
}
}

View File

@@ -151,6 +151,31 @@ internal sealed partial class SeekableZipHeaderFactory : ZipHeaderFactory
throw new ArchiveOperationException();
}
PopulateDirectoryEntryMetadata(localEntryHeader, directoryEntryHeader);
return localEntryHeader;
}
internal LocalEntryHeader GetRawLocalHeader(
Stream stream,
DirectoryEntryHeader directoryEntryHeader
)
{
stream.Seek(directoryEntryHeader.RelativeOffsetOfEntryHeader, SeekOrigin.Begin);
var reader = new BinaryReader(stream);
var signature = reader.ReadUInt32();
if (signature != ENTRY_HEADER_BYTES)
{
throw new ArchiveOperationException();
}
return ReadLocalHeader(reader);
}
private static void PopulateDirectoryEntryMetadata(
LocalEntryHeader localEntryHeader,
DirectoryEntryHeader directoryEntryHeader
)
{
// populate fields only known from the DirectoryEntryHeader
localEntryHeader.HasData = directoryEntryHeader.HasData;
localEntryHeader.ExternalFileAttributes = directoryEntryHeader.ExternalFileAttributes;
@@ -163,6 +188,5 @@ internal sealed partial class SeekableZipHeaderFactory : ZipHeaderFactory
localEntryHeader.CompressedSize = directoryEntryHeader.CompressedSize;
localEntryHeader.UncompressedSize = directoryEntryHeader.UncompressedSize;
}
return localEntryHeader;
}
}

View File

@@ -20,8 +20,7 @@ internal partial class ZipHeaderFactory
{
case ENTRY_HEADER_BYTES:
{
var entryHeader = new LocalEntryHeader(_archiveEncoding);
await entryHeader.Read(reader).ConfigureAwait(false);
var entryHeader = await ReadLocalHeader(reader).ConfigureAwait(false);
await LoadHeaderAsync(entryHeader, reader.BaseStream).ConfigureAwait(false);
_lastEntryHeader = entryHeader;
@@ -87,6 +86,13 @@ internal partial class ZipHeaderFactory
}
}
protected async ValueTask<LocalEntryHeader> ReadLocalHeader(AsyncBinaryReader reader)
{
var entryHeader = new LocalEntryHeader(_archiveEncoding);
await entryHeader.Read(reader).ConfigureAwait(false);
return entryHeader;
}
/// <summary>
/// Loads encryption metadata and stream positioning for a header using async reads where needed.
/// </summary>

View File

@@ -42,8 +42,7 @@ internal partial class ZipHeaderFactory
{
case ENTRY_HEADER_BYTES:
{
var entryHeader = new LocalEntryHeader(_archiveEncoding);
entryHeader.Read(reader);
var entryHeader = ReadLocalHeader(reader);
LoadHeader(entryHeader, reader.BaseStream);
_lastEntryHeader = entryHeader;
@@ -109,6 +108,13 @@ internal partial class ZipHeaderFactory
}
}
protected LocalEntryHeader ReadLocalHeader(BinaryReader reader)
{
var entryHeader = new LocalEntryHeader(_archiveEncoding);
entryHeader.Read(reader);
return entryHeader;
}
internal static bool IsHeader(uint headerBytes)
{
switch (headerBytes)