diff --git a/src/SharpCompress/Common/Entry.cs b/src/SharpCompress/Common/Entry.cs
index 6c724c32..43095b71 100644
--- a/src/SharpCompress/Common/Entry.cs
+++ b/src/SharpCompress/Common/Entry.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using SharpCompress.Common.Options;
@@ -86,6 +87,17 @@ public abstract class Entry : IEntry
internal virtual ChecksumDescriptor Checksum => default;
+ internal virtual Stream WrapWithChecksumValidation(Stream source, ExtractionOptions options)
+ {
+ var checksum = Checksum;
+ if (!checksum.IsAvailable)
+ {
+ return source;
+ }
+
+ return new ChecksumValidationStream(source, checksum, Key);
+ }
+
///
/// Entry file attribute.
///
diff --git a/src/SharpCompress/Common/GZip/GZipChecksumValidationStream.cs b/src/SharpCompress/Common/GZip/GZipChecksumValidationStream.cs
new file mode 100644
index 00000000..512d7cc2
--- /dev/null
+++ b/src/SharpCompress/Common/GZip/GZipChecksumValidationStream.cs
@@ -0,0 +1,165 @@
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using SharpCompress.Crypto;
+
+namespace SharpCompress.Common.GZip;
+
+internal sealed class GZipChecksumValidationStream : Stream
+{
+ private readonly Stream _source;
+ private readonly Stream _rawStream;
+ private readonly string _entryName;
+ private readonly uint? _expectedCrc;
+ private readonly uint? _expectedSize;
+ private readonly uint[] _crc32Table;
+ private uint _seed = Crc32Stream.DEFAULT_SEED;
+ private uint _size;
+ private bool _validated;
+
+ internal GZipChecksumValidationStream(
+ Stream source,
+ Stream rawStream,
+ string? entryName,
+ uint? expectedCrc,
+ uint? expectedSize
+ )
+ {
+ _source = source;
+ _rawStream = rawStream;
+ _entryName = string.IsNullOrEmpty(entryName) ? "Entry" : entryName!;
+ _expectedCrc = expectedCrc;
+ _expectedSize = expectedSize;
+ _crc32Table = Crc32Stream.InitializeTable(Crc32Stream.DEFAULT_POLYNOMIAL);
+ }
+
+ public override bool CanRead => _source.CanRead;
+ public override bool CanSeek => false;
+ public override bool CanWrite => false;
+ public override long Length => _source.Length;
+
+ public override long Position
+ {
+ get => _source.Position;
+ set => throw new NotSupportedException();
+ }
+
+ public override void Flush() => _source.Flush();
+
+ public override Task FlushAsync(CancellationToken cancellationToken) =>
+ _source.FlushAsync(cancellationToken);
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ var read = _source.Read(buffer, offset, count);
+ UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
+ return read;
+ }
+
+#if !LEGACY_DOTNET
+ public override int Read(Span buffer)
+ {
+ var read = _source.Read(buffer);
+ UpdateAndValidateAtEof(buffer[..read], read);
+ return read;
+ }
+#endif
+
+ public override int ReadByte()
+ {
+ var value = _source.ReadByte();
+ if (value == -1)
+ {
+ Validate();
+ }
+ else
+ {
+ _seed = Crc32Stream.CalculateCrc(_crc32Table, _seed, (byte)value);
+ _size++;
+ }
+
+ return value;
+ }
+
+ public override async Task ReadAsync(
+ byte[] buffer,
+ int offset,
+ int count,
+ CancellationToken cancellationToken
+ )
+ {
+ var read = await _source
+ .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 _source.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);
+ _size += unchecked((uint)read);
+ return;
+ }
+
+ Validate();
+ }
+
+ private void Validate()
+ {
+ if (_validated)
+ {
+ return;
+ }
+
+ _validated = true;
+
+ var expectedCrc = _expectedCrc;
+ var expectedSize = _expectedSize;
+ if (!expectedCrc.HasValue || !expectedSize.HasValue)
+ {
+ Span trailer = stackalloc byte[8];
+ _rawStream.ReadFully(trailer);
+ expectedCrc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
+ expectedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer[4..]);
+ }
+
+ var actualCrc = ~_seed;
+ if (actualCrc != expectedCrc.Value)
+ {
+ throw new InvalidFormatException(
+ $"CRC mismatch for entry '{_entryName}'. Expected 0x{expectedCrc.Value:X8}, actual 0x{actualCrc:X8}."
+ );
+ }
+
+ if (_size != expectedSize.Value)
+ {
+ throw new InvalidFormatException(
+ $"Size mismatch for entry '{_entryName}'. Expected {expectedSize.Value}, actual {_size}."
+ );
+ }
+ }
+}
diff --git a/src/SharpCompress/Common/GZip/GZipEntry.cs b/src/SharpCompress/Common/GZip/GZipEntry.cs
index 635fc4cd..48f08d46 100644
--- a/src/SharpCompress/Common/GZip/GZipEntry.cs
+++ b/src/SharpCompress/Common/GZip/GZipEntry.cs
@@ -20,6 +20,9 @@ public partial class GZipEntry : Entry
public override long Crc => _filePart?.Crc ?? 0;
+ internal override Stream WrapWithChecksumValidation(Stream source, ExtractionOptions options) =>
+ _filePart?.WrapWithChecksumValidation(source, Key) ?? source;
+
public override string? Key => _filePart?.FilePartName;
public override string? LinkTarget => null;
diff --git a/src/SharpCompress/Common/GZip/GZipFilePart.cs b/src/SharpCompress/Common/GZip/GZipFilePart.cs
index e8d89a5d..13a1c968 100644
--- a/src/SharpCompress/Common/GZip/GZipFilePart.cs
+++ b/src/SharpCompress/Common/GZip/GZipFilePart.cs
@@ -5,6 +5,7 @@ using System.IO;
using SharpCompress.Common.Tar.Headers;
using SharpCompress.Compressors;
using SharpCompress.Compressors.Deflate;
+using SharpCompress.IO;
using SharpCompress.Providers;
namespace SharpCompress.Common.GZip;
@@ -48,7 +49,7 @@ internal sealed partial class GZipFilePart : FilePart
)
: base(archiveEncoding)
{
- _stream = stream;
+ _stream = SharpCompressStream.Create(stream);
_compressionProviders = compressionProviders;
}
@@ -66,6 +67,9 @@ internal sealed partial class GZipFilePart : FilePart
return _compressionProviders.CreateDecompressStream(CompressionType.Deflate, _stream);
}
+ internal Stream WrapWithChecksumValidation(Stream source, string? entryName) =>
+ new GZipChecksumValidationStream(source, _stream, entryName, Crc, UncompressedSize);
+
internal override Stream GetRawStream() => _stream;
private void ReadTrailer()
diff --git a/src/SharpCompress/Common/IEntryExtensions.cs b/src/SharpCompress/Common/IEntryExtensions.cs
index 84645910..492ad85d 100644
--- a/src/SharpCompress/Common/IEntryExtensions.cs
+++ b/src/SharpCompress/Common/IEntryExtensions.cs
@@ -11,13 +11,10 @@ internal static partial class IEntryExtensions
ExtractionOptions? options
)
{
- if (options?.CheckCrc != false && entry is Entry typedEntry)
+ options ??= new ExtractionOptions();
+ if (options.CheckCrc && entry is Entry typedEntry)
{
- var checksum = typedEntry.Checksum;
- if (checksum.IsAvailable)
- {
- return new ChecksumValidationStream(source, checksum, entry.Key);
- }
+ return typedEntry.WrapWithChecksumValidation(source, options);
}
return source;
diff --git a/tests/SharpCompress.Test/BZip2/BZip2StreamTests.cs b/tests/SharpCompress.Test/BZip2/BZip2StreamTests.cs
new file mode 100644
index 00000000..9bd7f44a
--- /dev/null
+++ b/tests/SharpCompress.Test/BZip2/BZip2StreamTests.cs
@@ -0,0 +1,45 @@
+using System.IO;
+using System.Text;
+using SharpCompress.Common;
+using SharpCompress.Compressors.BZip2;
+using Xunit;
+
+namespace SharpCompress.Test.BZip2;
+
+public class BZip2StreamTests
+{
+ [Fact]
+ public void BZip2Stream_Throws_On_Corrupt_Checksum()
+ {
+ var compressed = Compress("BZip2 checksum validation test data.");
+ compressed[^5] ^= 1;
+
+ using var stream = BZip2Stream.Create(
+ new MemoryStream(compressed),
+ SharpCompress.Compressors.CompressionMode.Decompress,
+ false
+ );
+ using var output = new MemoryStream();
+
+ Assert.Throws(() => stream.CopyTo(output));
+ }
+
+ private static byte[] Compress(string value)
+ {
+ using var memoryStream = new MemoryStream();
+ using (
+ var bzip2Stream = BZip2Stream.Create(
+ memoryStream,
+ SharpCompress.Compressors.CompressionMode.Compress,
+ false,
+ leaveOpen: true
+ )
+ )
+ {
+ var bytes = Encoding.ASCII.GetBytes(value);
+ bzip2Stream.Write(bytes, 0, bytes.Length);
+ }
+
+ return memoryStream.ToArray();
+ }
+}
diff --git a/tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs b/tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs
new file mode 100644
index 00000000..4f908b3c
--- /dev/null
+++ b/tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using SharpCompress.Archives;
+using SharpCompress.Archives.GZip;
+using SharpCompress.Common;
+using SharpCompress.Readers;
+using SharpCompress.Readers.GZip;
+using SharpCompress.Test.Mocks;
+using Xunit;
+
+namespace SharpCompress.Test.GZip;
+
+public class GZipCrcExtractionTests : TestBase
+{
+ [Fact]
+ public void GZipArchive_WriteToFile_Throws_On_Crc_Mismatch()
+ {
+ using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true));
+ using var archive = GZipArchive.OpenArchive(stream);
+ var entry = archive.Entries.Single();
+ var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
+
+ Assert.Throws(() => entry.WriteToFile(destination));
+ }
+
+ [Fact]
+ public void GZipArchive_WriteToFile_Throws_On_Size_Mismatch()
+ {
+ using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: false));
+ using var archive = GZipArchive.OpenArchive(stream);
+ var entry = archive.Entries.Single();
+ var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
+
+ Assert.Throws(() => entry.WriteToFile(destination));
+ }
+
+ [Fact]
+ public void GZipReader_WriteEntryToFile_Throws_On_NonSeekable_Crc_Mismatch()
+ {
+ using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true));
+ using var nonSeekableStream = new ForwardOnlyStream(stream);
+ using var reader = GZipReader.OpenReader(nonSeekableStream);
+ var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
+
+ Assert.True(reader.MoveToNextEntry());
+ Assert.Throws(() => reader.WriteEntryToFile(destination));
+ }
+
+ [Fact]
+ public void GZipArchive_WriteToFile_Skips_Trailer_Validation_When_CheckCrc_Is_False()
+ {
+ using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true));
+ using var archive = GZipArchive.OpenArchive(stream);
+ var entry = archive.Entries.Single();
+ var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
+
+ entry.WriteToFile(destination, new ExtractionOptions { CheckCrc = false });
+
+ Assert.Equal(
+ new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")).Length,
+ new FileInfo(destination).Length
+ );
+ }
+
+ [Fact]
+ public async Task GZipArchive_WriteToFileAsync_Throws_On_Crc_Mismatch()
+ {
+ await using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true));
+ await using var archive = await GZipArchive.OpenAsyncArchive(stream);
+ var entry = await archive.EntriesAsync.SingleAsync();
+ var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
+
+ await Assert.ThrowsAsync(async () =>
+ await entry.WriteToFileAsync(destination)
+ );
+ }
+
+ private static byte[] ReadCorruptedGZipTrailer(bool corruptCrc)
+ {
+ var bytes = File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
+ var trailer = bytes.AsSpan(bytes.Length - 8);
+ var offset = corruptCrc ? 0 : 4;
+ var value = BinaryPrimitives.ReadUInt32LittleEndian(trailer[offset..]);
+ BinaryPrimitives.WriteUInt32LittleEndian(trailer[offset..], value + 1);
+ return bytes;
+ }
+}
diff --git a/tests/SharpCompress.Test/Xz/XZStreamTests.cs b/tests/SharpCompress.Test/Xz/XZStreamTests.cs
index 80ad5dc3..00cd2f27 100644
--- a/tests/SharpCompress.Test/Xz/XZStreamTests.cs
+++ b/tests/SharpCompress.Test/Xz/XZStreamTests.cs
@@ -1,4 +1,5 @@
using System.IO;
+using SharpCompress.Common;
using SharpCompress.Compressors.Xz;
using SharpCompress.IO;
using SharpCompress.Test.Mocks;
@@ -54,4 +55,15 @@ public class XzStreamTests : XzTestsBase
var uncompressed = sr.ReadToEnd();
Assert.Equal(OriginalEmpty, uncompressed);
}
+
+ [Fact]
+ public void Throws_On_Corrupt_Block_Check()
+ {
+ var compressed = (byte[])Compressed.Clone();
+ compressed[compressed.Length - 29] ^= 1;
+ using var xz = new XZStream(new MemoryStream(compressed));
+ using var output = new MemoryStream();
+
+ Assert.Throws(() => xz.CopyTo(output));
+ }
}