First pass with zip

This commit is contained in:
Adam Hathcock
2026-06-15 09:17:05 +01:00
parent c36a916c84
commit 2866bfbd54
24 changed files with 509 additions and 30 deletions

View File

@@ -18,11 +18,12 @@ public static class IArchiveEntryExtensions
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
public void WriteTo(Stream streamToWriteTo, IProgress<ProgressReport>? progress = null) =>
archiveEntry.WriteTo(streamToWriteTo, null, progress);
archiveEntry.WriteTo(streamToWriteTo, null, progress: progress);
private void WriteTo(
Stream streamToWriteTo,
int? bufferSize,
ExtractionOptions? options = null,
IProgress<ProgressReport>? progress = null
)
{
@@ -32,7 +33,10 @@ public static class IArchiveEntryExtensions
}
using var entryStream = archiveEntry.OpenEntryStream();
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
var checkedStream = options is null
? entryStream
: IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options);
var sourceStream = WrapWithProgress(checkedStream, archiveEntry, progress);
sourceStream.CopyTo(streamToWriteTo, bufferSize ?? Constants.BufferSize);
}
@@ -49,13 +53,19 @@ public static class IArchiveEntryExtensions
)
{
await archiveEntry
.WriteToAsync(streamToWriteTo, Constants.BufferSize, progress, cancellationToken)
.WriteToAsync(
streamToWriteTo,
Constants.BufferSize,
progress: progress,
cancellationToken: cancellationToken
)
.ConfigureAwait(false);
}
private async ValueTask WriteToAsync(
Stream streamToWriteTo,
int? bufferSize,
ExtractionOptions? options = null,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
)
@@ -74,7 +84,10 @@ public static class IArchiveEntryExtensions
.OpenEntryStreamAsync(cancellationToken)
.ConfigureAwait(false);
#endif
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
var checkedStream = options is null
? entryStream
: IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options);
var sourceStream = WrapWithProgress(checkedStream, archiveEntry, progress);
await sourceStream
.CopyToAsync(streamToWriteTo, bufferSize ?? Constants.BufferSize, cancellationToken)
.ConfigureAwait(false);
@@ -154,13 +167,14 @@ public static class IArchiveEntryExtensions
/// </summary>
public void WriteToFile(string destinationFileName, ExtractionOptions? options = null)
{
options ??= new ExtractionOptions();
entry.WriteEntryToFile(
destinationFileName,
options,
(x, fm) =>
{
using var fs = File.Open(x, fm);
entry.WriteTo(fs, options?.BufferSize ?? Constants.BufferSize);
entry.WriteTo(fs, options?.BufferSize ?? Constants.BufferSize, options, null);
}
);
}
@@ -172,7 +186,9 @@ public static class IArchiveEntryExtensions
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
)
{
options ??= new ExtractionOptions();
await entry
.WriteEntryToFileAsync(
destinationFileName,
@@ -181,11 +197,12 @@ public static class IArchiveEntryExtensions
{
using var fs = File.Open(x, fm);
await entry
.WriteToAsync(fs, options?.BufferSize, null, ct)
.WriteToAsync(fs, options.BufferSize, options, null, ct)
.ConfigureAwait(false);
},
cancellationToken
)
.ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,12 @@
namespace SharpCompress.Common;
internal enum ChecksumKind
{
Crc32,
}
internal readonly record struct ChecksumDescriptor(
ChecksumKind Kind,
long ExpectedValue,
bool IsAvailable
);

View File

@@ -0,0 +1,140 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Crypto;
namespace SharpCompress.Common;
internal sealed class ChecksumValidationStream : Stream
{
private readonly Stream _stream;
private readonly ChecksumDescriptor _checksum;
private readonly string _entryName;
private readonly uint[] _crc32Table;
private uint _seed = Crc32Stream.DEFAULT_SEED;
private bool _validated;
internal ChecksumValidationStream(Stream stream, ChecksumDescriptor checksum, string? entryName)
{
_stream = stream;
_checksum = checksum;
_entryName = string.IsNullOrEmpty(entryName) ? "Entry" : entryName!;
_crc32Table = Crc32Stream.InitializeTable(Crc32Stream.DEFAULT_POLYNOMIAL);
}
public override bool CanRead => _stream.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => _stream.Length;
public override long Position
{
get => _stream.Position;
set => throw new NotSupportedException();
}
public override void Flush() => _stream.Flush();
public override Task FlushAsync(CancellationToken cancellationToken) =>
_stream.FlushAsync(cancellationToken);
public override int Read(byte[] buffer, int offset, int count)
{
var read = _stream.Read(buffer, offset, count);
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
return read;
}
#if !LEGACY_DOTNET
public override int Read(Span<byte> buffer)
{
var read = _stream.Read(buffer);
UpdateAndValidateAtEof(buffer[..read], read);
return read;
}
#endif
public override int ReadByte()
{
var value = _stream.ReadByte();
if (value == -1)
{
Validate();
}
else
{
_seed = Crc32Stream.CalculateCrc(_crc32Table, _seed, (byte)value);
}
return value;
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
var read = await _stream
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
return read;
}
#if !LEGACY_DOTNET
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
var read = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
UpdateAndValidateAtEof(buffer.Span[..read], read);
return read;
}
#endif
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
private void UpdateAndValidateAtEof(ReadOnlySpan<byte> buffer, int read)
{
if (read > 0)
{
_seed = Crc32Stream.CalculateCrc(_crc32Table, _seed, buffer);
return;
}
Validate();
}
private void Validate()
{
if (_validated)
{
return;
}
_validated = true;
if (_checksum.Kind != ChecksumKind.Crc32)
{
return;
}
var actual = ~_seed;
var expected = unchecked((uint)_checksum.ExpectedValue);
if (actual != expected)
{
throw new InvalidFormatException(
$"CRC mismatch for entry '{_entryName}'. Expected 0x{expected:X8}, actual 0x{actual:X8}."
);
}
}
}

View File

@@ -84,6 +84,8 @@ public abstract class Entry : IEntry
internal virtual void Close() { }
internal virtual ChecksumDescriptor Checksum => default;
/// <summary>
/// Entry file attribute.
/// </summary>

View File

@@ -43,6 +43,15 @@ public sealed record ExtractionOptions : IExtractionOptions
/// </summary>
public int BufferSize { get; set; } = Constants.BufferSize;
/// <summary>
/// Validate archive entry checksums during extraction when checksum metadata is available.
/// </summary>
/// <remarks>
/// Formats without payload checksums skip this validation. Compression-format integrity
/// checks that are required to decode data may still fail even when this is disabled.
/// </remarks>
public bool CheckCrc { get; set; } = true;
/// <summary>
/// Delegate for writing symbolic links to disk.
/// The first parameter is the source path (where the symlink is created).

View File

@@ -5,6 +5,24 @@ namespace SharpCompress.Common;
internal static partial class IEntryExtensions
{
internal static Stream WrapWithChecksumValidation(
IEntry entry,
Stream source,
ExtractionOptions? options
)
{
if (options?.CheckCrc != false && entry is Entry typedEntry)
{
var checksum = typedEntry.Checksum;
if (checksum.IsAvailable)
{
return new ChecksumValidationStream(source, checksum, entry.Key);
}
}
return source;
}
extension(IEntry entry)
{
/// <summary>

View File

@@ -22,6 +22,7 @@ internal partial class DirectoryEntryHeader
.ReadUInt16Async()
.ConfigureAwait(false);
Crc = await reader.ReadUInt32Async().ConfigureAwait(false);
IsCrcAvailable = true;
CompressedSize = await reader.ReadUInt32Async().ConfigureAwait(false);
UncompressedSize = await reader.ReadUInt32Async().ConfigureAwait(false);
var nameLength = await reader.ReadUInt16Async().ConfigureAwait(false);

View File

@@ -17,6 +17,7 @@ internal partial class DirectoryEntryHeader : ZipFileEntry
OriginalLastModifiedTime = LastModifiedTime = reader.ReadUInt16();
OriginalLastModifiedDate = LastModifiedDate = reader.ReadUInt16();
Crc = reader.ReadUInt32();
IsCrcAvailable = true;
CompressedSize = reader.ReadUInt32();
UncompressedSize = reader.ReadUInt32();
var nameLength = reader.ReadUInt16();

View File

@@ -20,6 +20,7 @@ internal partial class LocalEntryHeader
.ReadUInt16Async()
.ConfigureAwait(false);
Crc = await reader.ReadUInt32Async().ConfigureAwait(false);
IsCrcAvailable = !Flags.HasFlag(HeaderFlags.UsePostDataDescriptor);
CompressedSize = await reader.ReadUInt32Async().ConfigureAwait(false);
UncompressedSize = await reader.ReadUInt32Async().ConfigureAwait(false);
var nameLength = await reader.ReadUInt16Async().ConfigureAwait(false);

View File

@@ -16,6 +16,7 @@ internal partial class LocalEntryHeader : ZipFileEntry
OriginalLastModifiedTime = LastModifiedTime = reader.ReadUInt16();
OriginalLastModifiedDate = LastModifiedDate = reader.ReadUInt16();
Crc = reader.ReadUInt32();
IsCrcAvailable = !Flags.HasFlag(HeaderFlags.UsePostDataDescriptor);
CompressedSize = reader.ReadUInt32();
UncompressedSize = reader.ReadUInt32();
var nameLength = reader.ReadUInt16();

View File

@@ -80,6 +80,8 @@ internal abstract partial class ZipFileEntry(ZipHeaderType type, IArchiveEncodin
internal uint Crc { get; set; }
internal bool IsCrcAvailable { get; set; }
protected void LoadExtra(byte[] extra)
{
for (var i = 0; i < extra.Length; )

View File

@@ -159,6 +159,7 @@ internal sealed partial class SeekableZipHeaderFactory : ZipHeaderFactory
if (FlagUtility.HasFlag(localEntryHeader.Flags, HeaderFlags.UsePostDataDescriptor))
{
localEntryHeader.Crc = directoryEntryHeader.Crc;
localEntryHeader.IsCrcAvailable = true;
localEntryHeader.CompressedSize = directoryEntryHeader.CompressedSize;
localEntryHeader.UncompressedSize = directoryEntryHeader.UncompressedSize;
}

View File

@@ -124,6 +124,7 @@ internal sealed partial class StreamingZipHeaderFactory
.ConfigureAwait(false);
}
lastEntryHeader.Crc = crc;
lastEntryHeader.IsCrcAvailable = true;
//attempt 32bit read
ulong compressedSize = await _reader
@@ -205,6 +206,7 @@ internal sealed partial class StreamingZipHeaderFactory
.ConfigureAwait(false);
}
lastEntryHeader.Crc = crc;
lastEntryHeader.IsCrcAvailable = true;
// The DataDescriptor can be either 64bit or 32bit
var compressedSize = await _reader
@@ -283,6 +285,7 @@ internal sealed partial class StreamingZipHeaderFactory
localHeader.UncompressedSize = directoryHeader.Size;
localHeader.CompressedSize = directoryHeader.CompressedSize;
localHeader.Crc = (uint)directoryHeader.Crc;
localHeader.IsCrcAvailable = true;
}
// If we have CompressedSize, there is data to be read

View File

@@ -59,6 +59,7 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory
crc = reader.ReadUInt32();
}
_lastEntryHeader.Crc = crc;
_lastEntryHeader.IsCrcAvailable = true;
//attempt 32bit read
ulong compSize = reader.ReadUInt32();
@@ -121,6 +122,7 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory
crc = reader.ReadUInt32();
}
_lastEntryHeader.Crc = crc;
_lastEntryHeader.IsCrcAvailable = true;
// The DataDescriptor can be either 64bit or 32bit
var compressed_size = reader.ReadUInt32();
@@ -188,6 +190,7 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory
local_header.UncompressedSize = dir_header.Size;
local_header.CompressedSize = dir_header.CompressedSize;
local_header.Crc = (uint)dir_header.Crc;
local_header.IsCrcAvailable = true;
}
// If we have CompressedSize, there is data to be read

View File

@@ -89,6 +89,49 @@ public class ZipEntry : Entry
public override long Crc => _filePart?.Header.Crc ?? 0;
internal override ChecksumDescriptor Checksum
{
get
{
if (
_filePart is null
|| IsDirectory
|| !_filePart.Header.IsCrcAvailable
|| !IsReliableCrcMetadata(_filePart.Header)
)
{
return default;
}
return new ChecksumDescriptor(
ChecksumKind.Crc32,
_filePart.Header.Crc,
IsAvailable: true
);
}
}
private static bool IsReliableCrcMetadata(ZipFileEntry header)
{
if (header.CompressionMethod != ZipCompressionMethod.WinzipAes)
{
return true;
}
var aesExtraData = header.Extra.FirstOrDefault(x => x.Type == ExtraDataType.WinZipAes);
if (aesExtraData is null || aesExtraData.DataBytes.Length < MinimumWinZipAesExtraDataLength)
{
return false;
}
var vendorVersion = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(
aesExtraData.DataBytes
);
// WinZip AES AE-2 stores a zero CRC field by design and relies on AES authentication.
return vendorVersion == 0x0001;
}
public override string? Key => _filePart?.Header.Name;
public override string? LinkTarget => null;

View File

@@ -44,6 +44,7 @@ internal partial class ZipHeaderFactory
)
{
_lastEntryHeader.Crc = await reader.ReadUInt32Async().ConfigureAwait(false);
_lastEntryHeader.IsCrcAvailable = true;
_lastEntryHeader.CompressedSize = zip64
? (long)await reader.ReadUInt64Async().ConfigureAwait(false)
: await reader.ReadUInt32Async().ConfigureAwait(false);

View File

@@ -66,6 +66,7 @@ internal partial class ZipHeaderFactory
)
{
_lastEntryHeader.Crc = reader.ReadUInt32();
_lastEntryHeader.IsCrcAvailable = true;
_lastEntryHeader.CompressedSize = zip64
? (long)reader.ReadUInt64()
: reader.ReadUInt32();

View File

@@ -119,7 +119,7 @@ public sealed class Crc32Stream : Stream
public static uint Compute(uint polynomial, uint seed, ReadOnlySpan<byte> buffer) =>
~CalculateCrc(InitializeTable(polynomial), seed, buffer);
private static uint[] InitializeTable(uint polynomial)
internal static uint[] InitializeTable(uint polynomial)
{
if (polynomial == DEFAULT_POLYNOMIAL && _defaultTable != null)
{
@@ -153,7 +153,7 @@ public sealed class Crc32Stream : Stream
return createTable;
}
private static uint CalculateCrc(uint[] table, uint crc, ReadOnlySpan<byte> buffer)
internal static uint CalculateCrc(uint[] table, uint crc, ReadOnlySpan<byte> buffer)
{
unchecked
{
@@ -165,6 +165,6 @@ public sealed class Crc32Stream : Stream
return crc;
}
private static uint CalculateCrc(uint[] table, uint crc, byte b) =>
internal static uint CalculateCrc(uint[] table, uint crc, byte b) =>
(crc >> 8) ^ table[(crc ^ b) & 0xFF];
}

View File

@@ -36,7 +36,9 @@ public static class IAsyncReaderExtensions
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
)
{
options ??= new ExtractionOptions();
await reader
.Entry.WriteEntryToFileAsync(
destinationFileName,
@@ -44,12 +46,12 @@ public static class IAsyncReaderExtensions
async (x, fm, ct) =>
{
using var fs = File.Open(x, fm);
await CopyEntryToAsync(reader, fs, options?.BufferSize, ct)
.ConfigureAwait(false);
await CopyEntryToAsync(reader, fs, options, ct).ConfigureAwait(false);
},
cancellationToken
)
.ConfigureAwait(false);
}
/// <summary>
/// Extract all remaining unread entries to specific directory asynchronously, retaining filename
@@ -72,7 +74,9 @@ public static class IAsyncReaderExtensions
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
)
{
options ??= new ExtractionOptions();
await reader
.Entry.WriteEntryToFileAsync(
destinationFileName,
@@ -80,12 +84,12 @@ public static class IAsyncReaderExtensions
async (x, fm, ct) =>
{
using var fs = File.Open(x, fm);
await CopyEntryToAsync(reader, fs, options?.BufferSize, ct)
.ConfigureAwait(false);
await CopyEntryToAsync(reader, fs, options, ct).ConfigureAwait(false);
},
cancellationToken
)
.ConfigureAwait(false);
}
public async ValueTask WriteEntryToAsync(
FileInfo destinationFileInfo,
@@ -100,7 +104,7 @@ public static class IAsyncReaderExtensions
private static async ValueTask CopyEntryToAsync(
IAsyncReader reader,
Stream writableStream,
int? bufferSize,
ExtractionOptions options,
CancellationToken cancellationToken
)
{
@@ -113,9 +117,14 @@ public static class IAsyncReaderExtensions
.OpenEntryStreamAsync(cancellationToken)
.ConfigureAwait(false);
#endif
var sourceStream = WrapWithProgress(entryStream, reader.Entry);
var checkedStream = IEntryExtensions.WrapWithChecksumValidation(
reader.Entry,
entryStream,
options
);
var sourceStream = WrapWithProgress(checkedStream, reader.Entry);
await sourceStream
.CopyToAsync(writableStream, bufferSize ?? Constants.BufferSize, cancellationToken)
.CopyToAsync(writableStream, options.BufferSize, cancellationToken)
.ConfigureAwait(false);
}

View File

@@ -51,26 +51,35 @@ public static class IReaderExtensions
/// <summary>
/// Extract to specific file
/// </summary>
public void WriteEntryToFile(
string destinationFileName,
ExtractionOptions? options = null
) =>
public void WriteEntryToFile(string destinationFileName, ExtractionOptions? options = null)
{
options ??= new ExtractionOptions();
reader.Entry.WriteEntryToFile(
destinationFileName,
options,
(x, fm) =>
{
using var fs = File.Open(x, fm);
CopyEntryTo(reader, fs, options?.BufferSize ?? Constants.BufferSize);
CopyEntryTo(reader, fs, options ?? new ExtractionOptions());
}
);
}
}
private static void CopyEntryTo(IReader reader, Stream writableStream, int bufferSize)
private static void CopyEntryTo(
IReader reader,
Stream writableStream,
ExtractionOptions options
)
{
using var entryStream = reader.OpenEntryStream();
var sourceStream = WrapWithProgress(entryStream, reader.Entry);
sourceStream.CopyTo(writableStream, bufferSize);
var checkedStream = IEntryExtensions.WrapWithChecksumValidation(
reader.Entry,
entryStream,
options
);
var sourceStream = WrapWithProgress(checkedStream, reader.Entry);
sourceStream.CopyTo(writableStream, options.BufferSize);
}
private static Stream WrapWithProgress(Stream source, IEntry entry)