mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Validate fragmented GZip trailers asynchronously using inflater read-ahead
This commit is contained in:
@@ -18,13 +18,15 @@ internal sealed class GZipChecksumValidationStream : Stream
|
||||
private uint _seed = Crc32Stream.DEFAULT_SEED;
|
||||
private uint _size;
|
||||
private bool _validated;
|
||||
private readonly Func<Stream?>? _bufferedInput;
|
||||
|
||||
internal GZipChecksumValidationStream(
|
||||
Stream source,
|
||||
Stream rawStream,
|
||||
string? entryName,
|
||||
uint? expectedCrc,
|
||||
uint? expectedSize
|
||||
uint? expectedSize,
|
||||
Func<Stream?>? bufferedInput = null
|
||||
)
|
||||
{
|
||||
_source = source;
|
||||
@@ -32,6 +34,7 @@ internal sealed class GZipChecksumValidationStream : Stream
|
||||
_entryName = string.IsNullOrEmpty(entryName) ? "Entry" : entryName!;
|
||||
_expectedCrc = expectedCrc;
|
||||
_expectedSize = expectedSize;
|
||||
_bufferedInput = bufferedInput;
|
||||
_crc32Table = Crc32Stream.InitializeTable(Crc32Stream.DEFAULT_POLYNOMIAL);
|
||||
}
|
||||
|
||||
@@ -101,7 +104,14 @@ internal sealed class GZipChecksumValidationStream : Stream
|
||||
.ConfigureAwait(false);
|
||||
if (count != 0)
|
||||
{
|
||||
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
|
||||
if (read == 0)
|
||||
{
|
||||
await ValidateAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
|
||||
}
|
||||
}
|
||||
return read;
|
||||
}
|
||||
@@ -115,7 +125,14 @@ internal sealed class GZipChecksumValidationStream : Stream
|
||||
var read = await _source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
if (!buffer.IsEmpty)
|
||||
{
|
||||
UpdateAndValidateAtEof(buffer.Span[..read], read);
|
||||
if (read == 0)
|
||||
{
|
||||
await ValidateAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateAndValidateAtEof(buffer.Span[..read], read);
|
||||
}
|
||||
}
|
||||
return read;
|
||||
}
|
||||
@@ -147,31 +164,77 @@ internal sealed class GZipChecksumValidationStream : Stream
|
||||
return;
|
||||
}
|
||||
|
||||
_validated = true;
|
||||
|
||||
var expectedCrc = _expectedCrc;
|
||||
var expectedSize = _expectedSize;
|
||||
if (!expectedCrc.HasValue || !expectedSize.HasValue)
|
||||
{
|
||||
Span<byte> trailer = stackalloc byte[8];
|
||||
_rawStream.ReadFully(trailer);
|
||||
var bufferedCount = ReadBufferedTrailer(trailer);
|
||||
if (!_rawStream.ReadFully(trailer.Slice(bufferedCount)))
|
||||
{
|
||||
throw new IncompleteArchiveException("Unexpected end of GZIP trailer.");
|
||||
}
|
||||
expectedCrc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
|
||||
expectedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer[4..]);
|
||||
}
|
||||
|
||||
ValidateValues(expectedCrc.Value, expectedSize.Value);
|
||||
}
|
||||
|
||||
private async ValueTask ValidateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_validated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var expectedCrc = _expectedCrc;
|
||||
var expectedSize = _expectedSize;
|
||||
if (!expectedCrc.HasValue || !expectedSize.HasValue)
|
||||
{
|
||||
var trailer = new byte[8];
|
||||
var bufferedCount = ReadBufferedTrailer(trailer);
|
||||
if (
|
||||
!await _rawStream
|
||||
.ReadFullyAsync(
|
||||
trailer,
|
||||
bufferedCount,
|
||||
trailer.Length - bufferedCount,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false)
|
||||
)
|
||||
{
|
||||
throw new IncompleteArchiveException("Unexpected end of GZIP trailer.");
|
||||
}
|
||||
expectedCrc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
|
||||
expectedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.AsSpan(4));
|
||||
}
|
||||
ValidateValues(expectedCrc.Value, expectedSize.Value);
|
||||
}
|
||||
|
||||
private int ReadBufferedTrailer(Span<byte> trailer)
|
||||
{
|
||||
// The inflater may have read ahead beyond the final compressed bit.
|
||||
using var buffered = _bufferedInput?.Invoke();
|
||||
return buffered?.Read(trailer) ?? 0;
|
||||
}
|
||||
|
||||
private void ValidateValues(uint expectedCrc, uint expectedSize)
|
||||
{
|
||||
var actualCrc = ~_seed;
|
||||
if (actualCrc != expectedCrc.Value)
|
||||
if (actualCrc != expectedCrc)
|
||||
{
|
||||
throw new InvalidFormatException(
|
||||
$"CRC mismatch for entry '{_entryName}'. Expected 0x{expectedCrc.Value:X8}, actual 0x{actualCrc:X8}."
|
||||
$"CRC mismatch for entry '{_entryName}'. Expected 0x{expectedCrc:X8}, actual 0x{actualCrc:X8}."
|
||||
);
|
||||
}
|
||||
|
||||
if (_size != expectedSize.Value)
|
||||
if (_size != expectedSize)
|
||||
{
|
||||
throw new InvalidFormatException(
|
||||
$"Size mismatch for entry '{_entryName}'. Expected {expectedSize.Value}, actual {_size}."
|
||||
$"Size mismatch for entry '{_entryName}'. Expected {expectedSize}, actual {_size}."
|
||||
);
|
||||
}
|
||||
_validated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,10 @@ internal sealed partial class GZipFilePart
|
||||
{
|
||||
// Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32
|
||||
var trailer = new byte[8];
|
||||
_ = await _stream.ReadFullyAsync(trailer, 0, 8, cancellationToken).ConfigureAwait(false);
|
||||
if (!await _stream.ReadFullyAsync(trailer, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
throw new ZlibException("Unexpected end-of-file reading GZIP trailer.");
|
||||
}
|
||||
|
||||
Crc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
|
||||
UncompressedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.AsSpan().Slice(4));
|
||||
@@ -150,8 +153,9 @@ internal sealed partial class GZipFilePart
|
||||
)
|
||||
{
|
||||
// GZip uses Deflate compression
|
||||
return await _compressionProviders
|
||||
_decompressor = await _compressionProviders
|
||||
.CreateDecompressStreamAsync(CompressionType.Deflate, _stream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return _decompressor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ internal sealed partial class GZipFilePart : FilePart
|
||||
private string? _name;
|
||||
private readonly Stream _stream;
|
||||
private readonly CompressionProviderRegistry _compressionProviders;
|
||||
private Stream? _decompressor;
|
||||
|
||||
internal static GZipFilePart Create(
|
||||
Stream stream,
|
||||
@@ -64,11 +65,22 @@ internal sealed partial class GZipFilePart : FilePart
|
||||
internal override Stream GetCompressedStream()
|
||||
{
|
||||
//GZip uses Deflate compression, at this point we need a deflate stream
|
||||
return _compressionProviders.CreateDecompressStream(CompressionType.Deflate, _stream);
|
||||
_decompressor = _compressionProviders.CreateDecompressStream(
|
||||
CompressionType.Deflate,
|
||||
_stream
|
||||
);
|
||||
return _decompressor;
|
||||
}
|
||||
|
||||
internal Stream WrapWithChecksumValidation(Stream source, string? entryName) =>
|
||||
new GZipChecksumValidationStream(source, _stream, entryName, Crc, UncompressedSize);
|
||||
new GZipChecksumValidationStream(
|
||||
source,
|
||||
_stream,
|
||||
entryName,
|
||||
Crc,
|
||||
UncompressedSize,
|
||||
() => (_decompressor as DeflateStream)?.InputBuffer
|
||||
);
|
||||
|
||||
internal override Stream GetRawStream() => _stream;
|
||||
|
||||
@@ -76,7 +88,10 @@ internal sealed partial class GZipFilePart : FilePart
|
||||
{
|
||||
// Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32
|
||||
Span<byte> trailer = stackalloc byte[8];
|
||||
_stream.ReadFully(trailer);
|
||||
if (!_stream.ReadFully(trailer))
|
||||
{
|
||||
throw new ZlibException("Unexpected end-of-file reading GZIP trailer.");
|
||||
}
|
||||
|
||||
Crc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
|
||||
UncompressedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.Slice(4));
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.GZip;
|
||||
@@ -15,6 +16,49 @@ namespace SharpCompress.Test.GZip;
|
||||
|
||||
public class GZipCrcExtractionTests : TestBase
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task GZipReader_Async_Only_NonSeekable_Validates_Fragmented_Trailer(bool corrupt)
|
||||
{
|
||||
var bytes = corrupt
|
||||
? ReadCorruptedGZipTrailer(true)
|
||||
: File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
|
||||
using var fragmented = new ShortReadStream(
|
||||
new MemoryStream(bytes),
|
||||
maxReadSize: 7,
|
||||
seekable: false
|
||||
);
|
||||
using var input = new AsyncOnlyStream(fragmented, disposeStream: false);
|
||||
await using var reader = await GZipReader.OpenAsyncReader(
|
||||
input,
|
||||
new ReaderOptions { LeaveStreamOpen = true },
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(await reader.MoveToNextEntryAsync(CancellationToken.None));
|
||||
var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
|
||||
if (corrupt)
|
||||
{
|
||||
await Assert.ThrowsAsync<InvalidFormatException>(async () =>
|
||||
await reader.WriteEntryToFileAsync(
|
||||
destination,
|
||||
cancellationToken: CancellationToken.None
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
await reader.WriteEntryToFileAsync(
|
||||
destination,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
Assert.Equal(
|
||||
File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")),
|
||||
File.ReadAllBytes(destination)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GZipArchive_WriteToFile_Throws_On_Crc_Mismatch()
|
||||
{
|
||||
|
||||
45
tests/SharpCompress.Test/GZip/GZipTrailerReadTests.cs
Normal file
45
tests/SharpCompress.Test/GZip/GZipTrailerReadTests.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.GZip;
|
||||
using SharpCompress.Crypto;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.GZip;
|
||||
|
||||
public class GZipTrailerReadTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(8)]
|
||||
[InlineData(7)]
|
||||
public async Task GZip_Async_Validation_Reads_Fragmented_Trailer(int length)
|
||||
{
|
||||
byte[] payload = [1, 2, 3];
|
||||
var trailer = new byte[8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(trailer, Crc32Stream.Compute(payload));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(trailer.AsSpan(4), 3);
|
||||
using var input = new MemoryStream(payload);
|
||||
using var fragmented = new ShortReadStream(
|
||||
new MemoryStream(trailer.AsSpan(0, length).ToArray()),
|
||||
seekable: false
|
||||
);
|
||||
using var raw = new AsyncOnlyStream(fragmented, disposeStream: false);
|
||||
using var stream = new GZipChecksumValidationStream(input, raw, "test", null, null);
|
||||
using var output = new MemoryStream();
|
||||
if (length == 8)
|
||||
{
|
||||
await stream.CopyToAsync(output, 81920, CancellationToken.None);
|
||||
Assert.Equal(payload, output.ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
|
||||
await stream.CopyToAsync(output, 81920, CancellationToken.None)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user