Validate fragmented GZip trailers asynchronously using inflater read-ahead

This commit is contained in:
Adam Hathcock
2026-10-10 15:40:39 +01:00
parent b919aaab1f
commit b07e6bc727
5 changed files with 186 additions and 15 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));