diff --git a/docs/API.md b/docs/API.md
index 19edd6ed..79745918 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -320,7 +320,11 @@ var flatOptions = ExtractionOptions.FlatExtract; // No directory structure
var metadataOptions = ExtractionOptions.PreserveMetadata; // Keep timestamps and attributes
// Tune extraction copy buffering
-var extractionOptions = new ExtractionOptions { BufferSize = 131072 };
+var extractionOptions = new ExtractionOptions
+{
+ BufferSize = 131072,
+ CheckCrc = true, // Default: validate entry checksums when archive metadata provides one
+};
// Factory defaults:
// - file path / FileInfo overloads use LeaveStreamOpen = false
@@ -429,7 +433,8 @@ var options = new ExtractionOptions
{
ExtractFullPath = true, // Recreate directory structure
Overwrite = true, // Overwrite existing files
- PreserveFileTime = true // Keep original timestamps
+ PreserveFileTime = true, // Keep original timestamps
+ CheckCrc = true // Validate payload checksums when available
};
using (var archive = ZipArchive.OpenArchive("file.zip"))
@@ -438,6 +443,8 @@ using (var archive = ZipArchive.OpenArchive("file.zip"))
}
```
+`CheckCrc` validates archive-level payload checksums when the format stores reliable metadata, such as ZIP CRC32 values. Formats without payload checksums skip this validation. Decompressor integrity checks that are required to decode a stream may still fail even when `CheckCrc` is disabled.
+
### Options matrix
```text
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 72356614..9666d43f 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -105,6 +105,7 @@ using (var archive = RarArchive.OpenArchive("Test.rar", ReaderOptions.ForFilePat
ExtractFullPath = true,
Overwrite = true,
BufferSize = 131072,
+ CheckCrc = true, // Default: validate payload checksums when available
}
);
}
@@ -141,7 +142,7 @@ using (var archive = RarArchive.OpenArchive("archive.rar",
{
archive.WriteToDirectory(
@"D:\output",
- new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
+ new ExtractionOptions { ExtractFullPath = true, Overwrite = true, CheckCrc = true }
);
}
```
diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
index 0de4b21c..a9c3a054 100644
--- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
+++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
@@ -18,11 +18,12 @@ public static class IArchiveEntryExtensions
/// The stream to write the entry content to.
/// Optional progress reporter for tracking extraction progress.
public void WriteTo(Stream streamToWriteTo, IProgress? progress = null) =>
- archiveEntry.WriteTo(streamToWriteTo, null, progress);
+ archiveEntry.WriteTo(streamToWriteTo, null, progress: progress);
private void WriteTo(
Stream streamToWriteTo,
int? bufferSize,
+ ExtractionOptions? options = null,
IProgress? 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? 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
///
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);
+ }
}
}
diff --git a/src/SharpCompress/Common/ChecksumDescriptor.cs b/src/SharpCompress/Common/ChecksumDescriptor.cs
new file mode 100644
index 00000000..2dbb6a45
--- /dev/null
+++ b/src/SharpCompress/Common/ChecksumDescriptor.cs
@@ -0,0 +1,12 @@
+namespace SharpCompress.Common;
+
+internal enum ChecksumKind
+{
+ Crc32,
+}
+
+internal readonly record struct ChecksumDescriptor(
+ ChecksumKind Kind,
+ long ExpectedValue,
+ bool IsAvailable
+);
diff --git a/src/SharpCompress/Common/ChecksumValidationStream.cs b/src/SharpCompress/Common/ChecksumValidationStream.cs
new file mode 100644
index 00000000..fdf4ba42
--- /dev/null
+++ b/src/SharpCompress/Common/ChecksumValidationStream.cs
@@ -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 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 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 ReadAsync(
+ Memory 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 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}."
+ );
+ }
+ }
+}
diff --git a/src/SharpCompress/Common/Entry.cs b/src/SharpCompress/Common/Entry.cs
index 1942ba46..6c724c32 100644
--- a/src/SharpCompress/Common/Entry.cs
+++ b/src/SharpCompress/Common/Entry.cs
@@ -84,6 +84,8 @@ public abstract class Entry : IEntry
internal virtual void Close() { }
+ internal virtual ChecksumDescriptor Checksum => default;
+
///
/// Entry file attribute.
///
diff --git a/src/SharpCompress/Common/ExtractionOptions.cs b/src/SharpCompress/Common/ExtractionOptions.cs
index b99658f8..04b33416 100644
--- a/src/SharpCompress/Common/ExtractionOptions.cs
+++ b/src/SharpCompress/Common/ExtractionOptions.cs
@@ -43,6 +43,15 @@ public sealed record ExtractionOptions : IExtractionOptions
///
public int BufferSize { get; set; } = Constants.BufferSize;
+ ///
+ /// Validate archive entry checksums during extraction when checksum metadata is available.
+ ///
+ ///
+ /// 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.
+ ///
+ public bool CheckCrc { get; set; } = true;
+
///
/// Delegate for writing symbolic links to disk.
/// The first parameter is the source path (where the symlink is created).
diff --git a/src/SharpCompress/Common/IEntryExtensions.cs b/src/SharpCompress/Common/IEntryExtensions.cs
index d3158f03..84645910 100644
--- a/src/SharpCompress/Common/IEntryExtensions.cs
+++ b/src/SharpCompress/Common/IEntryExtensions.cs
@@ -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)
{
///
diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs
index c09cac91..af978f64 100644
--- a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs
+++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs
@@ -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);
diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs
index e4e0f331..f41c6047 100644
--- a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs
+++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs
@@ -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();
diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs
index 950494df..2e96c942 100644
--- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs
+++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs
@@ -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);
diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs
index d9490137..9d4512b1 100644
--- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs
+++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs
@@ -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();
diff --git a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs
index b62b6a67..e7f0b042 100644
--- a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs
+++ b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs
@@ -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; )
diff --git a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs
index 22304b9d..7a88a480 100644
--- a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs
+++ b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs
@@ -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;
}
diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs
index 80ff16b4..4e3bcc3a 100644
--- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs
+++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs
@@ -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
diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs
index 7dcc4c54..c955afb0 100644
--- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs
+++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs
@@ -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
diff --git a/src/SharpCompress/Common/Zip/ZipEntry.cs b/src/SharpCompress/Common/Zip/ZipEntry.cs
index 753c6dc7..bfe1a50d 100644
--- a/src/SharpCompress/Common/Zip/ZipEntry.cs
+++ b/src/SharpCompress/Common/Zip/ZipEntry.cs
@@ -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;
diff --git a/src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs b/src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs
index c31e24a6..bc5944b4 100644
--- a/src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs
+++ b/src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs
@@ -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);
diff --git a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs
index 8d0ffdb4..e1ebb8c1 100644
--- a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs
+++ b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs
@@ -66,6 +66,7 @@ internal partial class ZipHeaderFactory
)
{
_lastEntryHeader.Crc = reader.ReadUInt32();
+ _lastEntryHeader.IsCrcAvailable = true;
_lastEntryHeader.CompressedSize = zip64
? (long)reader.ReadUInt64()
: reader.ReadUInt32();
diff --git a/src/SharpCompress/Crypto/Crc32Stream.cs b/src/SharpCompress/Crypto/Crc32Stream.cs
index b3294831..67dcb913 100644
--- a/src/SharpCompress/Crypto/Crc32Stream.cs
+++ b/src/SharpCompress/Crypto/Crc32Stream.cs
@@ -119,7 +119,7 @@ public sealed class Crc32Stream : Stream
public static uint Compute(uint polynomial, uint seed, ReadOnlySpan 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 buffer)
+ internal static uint CalculateCrc(uint[] table, uint crc, ReadOnlySpan 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];
}
diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs
index a982982c..7515b257 100644
--- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs
+++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs
@@ -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);
+ }
///
/// 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);
}
diff --git a/src/SharpCompress/Readers/IReaderExtensions.cs b/src/SharpCompress/Readers/IReaderExtensions.cs
index 93c391d2..8eb97193 100644
--- a/src/SharpCompress/Readers/IReaderExtensions.cs
+++ b/src/SharpCompress/Readers/IReaderExtensions.cs
@@ -51,26 +51,35 @@ public static class IReaderExtensions
///
/// Extract to specific file
///
- 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)
diff --git a/tests/SharpCompress.Test/OptionsUsabilityTests.cs b/tests/SharpCompress.Test/OptionsUsabilityTests.cs
index 8b08b265..1902e4ab 100644
--- a/tests/SharpCompress.Test/OptionsUsabilityTests.cs
+++ b/tests/SharpCompress.Test/OptionsUsabilityTests.cs
@@ -306,6 +306,7 @@ public class OptionsUsabilityTests : TestBase
Assert.True(preserveMetadata.PreserveAttributes);
Assert.Equal(Constants.BufferSize, new ExtractionOptions().BufferSize);
+ Assert.True(new ExtractionOptions().CheckCrc);
}
[Fact]
diff --git a/tests/SharpCompress.Test/Zip/ZipCrcExtractionTests.cs b/tests/SharpCompress.Test/Zip/ZipCrcExtractionTests.cs
new file mode 100644
index 00000000..14a55fc6
--- /dev/null
+++ b/tests/SharpCompress.Test/Zip/ZipCrcExtractionTests.cs
@@ -0,0 +1,196 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using SharpCompress.Archives;
+using SharpCompress.Archives.Zip;
+using SharpCompress.Common;
+using SharpCompress.Readers;
+using SharpCompress.Writers;
+using SharpCompress.Writers.Zip;
+using Xunit;
+
+namespace SharpCompress.Test.Zip;
+
+public class ZipCrcExtractionTests : ArchiveTests
+{
+ private const string EntryName = "crc.txt";
+ private static readonly byte[] EntryData = Encoding.UTF8.GetBytes("crc validation payload");
+
+ [Fact]
+ public void Zip_Archive_WriteToFile_Throws_On_Crc_Mismatch()
+ {
+ using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false);
+ using var archive = ZipArchive.OpenArchive(zipStream);
+ var entry = archive.Entries.Single(e => !e.IsDirectory);
+ var destination = Path.Combine(SCRATCH_FILES_PATH, "zip-crc-mismatch.txt");
+
+ var exception = Assert.Throws(() => entry.WriteToFile(destination));
+
+ Assert.Contains(EntryName, exception.Message);
+ }
+
+ [Fact]
+ public void Zip_Archive_WriteToFile_Skips_Crc_Mismatch_When_Disabled()
+ {
+ using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false);
+ using var archive = ZipArchive.OpenArchive(zipStream);
+ var entry = archive.Entries.Single(e => !e.IsDirectory);
+ var destination = Path.Combine(SCRATCH_FILES_PATH, "zip-crc-disabled.txt");
+
+ entry.WriteToFile(destination, new ExtractionOptions { CheckCrc = false });
+
+ Assert.Equal(EntryData, File.ReadAllBytes(destination));
+ }
+
+ [Fact]
+ public void Zip_Reader_WriteEntryToFile_Throws_On_Crc_Mismatch()
+ {
+ using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false);
+ using var reader = ReaderFactory.OpenReader(zipStream);
+ var destination = Path.Combine(SCRATCH_FILES_PATH, "zip-reader-crc-mismatch.txt");
+
+ Assert.True(reader.MoveToNextEntry());
+ var exception = Assert.Throws(() =>
+ reader.WriteEntryToFile(destination)
+ );
+
+ Assert.Contains(EntryName, exception.Message);
+ }
+
+ [Fact]
+ public void Zip_Reader_WriteEntryToFile_Skips_Crc_Mismatch_When_Disabled()
+ {
+ using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false);
+ using var reader = ReaderFactory.OpenReader(zipStream);
+ var destination = Path.Combine(SCRATCH_FILES_PATH, "zip-reader-crc-disabled.txt");
+
+ Assert.True(reader.MoveToNextEntry());
+ reader.WriteEntryToFile(destination, new ExtractionOptions { CheckCrc = false });
+
+ Assert.Equal(EntryData, File.ReadAllBytes(destination));
+ }
+
+ [Fact]
+ public async Task Zip_Archive_WriteToFileAsync_Throws_On_Crc_Mismatch()
+ {
+ using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false);
+ using var archive = ZipArchive.OpenArchive(zipStream);
+ var entry = archive.Entries.Single(e => !e.IsDirectory);
+ var destination = Path.Combine(SCRATCH_FILES_PATH, "zip-crc-mismatch-async.txt");
+
+ var exception = await Assert.ThrowsAsync(async () =>
+ await entry.WriteToFileAsync(destination)
+ );
+
+ Assert.Contains(EntryName, exception.Message);
+ }
+
+ [Fact]
+ public async Task Zip_Reader_WriteEntryToFileAsync_Throws_On_Crc_Mismatch()
+ {
+ using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false);
+ await using var reader = await ReaderFactory.OpenAsyncReader(zipStream);
+ var destination = Path.Combine(SCRATCH_FILES_PATH, "zip-reader-crc-mismatch-async.txt");
+
+ Assert.True(await reader.MoveToNextEntryAsync());
+ var exception = await Assert.ThrowsAsync(async () =>
+ await reader.WriteEntryToFileAsync(destination)
+ );
+
+ Assert.Contains(EntryName, exception.Message);
+ }
+
+ [Fact]
+ public void Zip_Archive_WriteToFile_Throws_On_DataDescriptor_Crc_Mismatch()
+ {
+ using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: true);
+ using var archive = ZipArchive.OpenArchive(zipStream);
+ var entry = archive.Entries.Single(e => !e.IsDirectory);
+ var destination = Path.Combine(SCRATCH_FILES_PATH, "zip-dd-crc-mismatch.txt");
+
+ var exception = Assert.Throws(() => entry.WriteToFile(destination));
+
+ Assert.Contains(EntryName, exception.Message);
+ }
+
+ private static MemoryStream CreateZipWithInvalidCrc(bool useDataDescriptor)
+ {
+ var zipStream = new MemoryStream();
+ Stream writerStream = useDataDescriptor ? new NonSeekableWriteStream(zipStream) : zipStream;
+ using (
+ var writer = WriterFactory.OpenWriter(
+ writerStream,
+ ArchiveType.Zip,
+ new ZipWriterOptions(CompressionType.Deflate) { LeaveStreamOpen = true }
+ )
+ )
+ {
+ writer.Write(EntryName, new MemoryStream(EntryData));
+ }
+
+ var bytes = zipStream.ToArray();
+ CorruptCrc(bytes, ZipHeaderFactoryEntrySignature, 14);
+ CorruptCrc(bytes, ZipHeaderFactoryDirectorySignature, 16);
+ return new MemoryStream(bytes);
+ }
+
+ private const uint ZipHeaderFactoryEntrySignature = 0x04034b50;
+ private const uint ZipHeaderFactoryDirectorySignature = 0x02014b50;
+
+ private static void CorruptCrc(byte[] bytes, uint signature, int crcOffset)
+ {
+ var offset = FindSignature(bytes, signature);
+ var crcIndex = offset + crcOffset;
+ bytes[crcIndex] ^= 0xFF;
+ }
+
+ private static int FindSignature(byte[] bytes, uint signature)
+ {
+ var signatureBytes = BitConverter.GetBytes(signature);
+ for (var i = 0; i <= bytes.Length - signatureBytes.Length; i++)
+ {
+ if (bytes.AsSpan(i, signatureBytes.Length).SequenceEqual(signatureBytes))
+ {
+ return i;
+ }
+ }
+
+ throw new InvalidOperationException($"ZIP signature 0x{signature:X8} was not found.");
+ }
+
+ private sealed class NonSeekableWriteStream(Stream stream) : Stream
+ {
+ public override bool CanRead => false;
+ public override bool CanSeek => false;
+ public override bool CanWrite => true;
+ public override long Length => throw new NotSupportedException();
+ public override long Position
+ {
+ get => throw new NotSupportedException();
+ 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) =>
+ throw new NotSupportedException();
+
+ 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) =>
+ stream.Write(buffer, offset, count);
+
+#if !LEGACY_DOTNET
+ public override void Write(ReadOnlySpan buffer) => stream.Write(buffer);
+#endif
+ }
+}