Zip: fix Zip64 streaming reader corrupting entry size/CRC and failing on non-seekable streams

The Zip64 branch of the streaming header reader assumed a data descriptor always follows a >=4GB entry. When the entry instead has back-patched sizes (no descriptor), the following header was parsed as descriptor fields, overwriting the entry's correct size/CRC with central-directory 0xFFFFFFFF sentinels and, on non-seekable async streams, leaving the reader misaligned so the next entry threw.

Now it detects a header signature after the entry data and leaves the already-correct metadata untouched, parsing that header normally. The fix has been applied to both the sync and async readers.
This commit is contained in:
Pat Hartl
2026-06-29 21:35:55 -05:00
parent 0e20a10961
commit c59aa78928
3 changed files with 244 additions and 85 deletions

View File

@@ -189,64 +189,77 @@ internal sealed partial class StreamingZipHeaderFactory
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false);
_ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // version
_ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // flags
_ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // compressionMethod
_ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedDate
_ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedTime
var crc = await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false);
if (crc == POST_DATA_DESCRIPTOR)
// A Zip64 entry that does not use a post-data descriptor stores its real CRC
// and sizes in the local header's Zip64 extra field, so a header signature
// (the next local entry, or the central directory) follows the data directly.
// In that case the entry's metadata is already correct and must not be
// overwritten with bytes read from the following header. We have only consumed
// the 4-byte signature, so fall through and parse this header normally. Because
// no seek-back is required here, this also works for non-seekable streams.
if (headerBytes == 0x04034b50 || headerBytes == 0x02014b50)
{
crc = await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false);
}
lastEntryHeader.Crc = crc;
lastEntryHeader.IsCrcAvailable = true;
// The DataDescriptor can be either 64bit or 32bit
var compressedSize = await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false);
var uncompressedSize = await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false);
// Check if we have header or 64bit DataDescriptor
var testHeader = !(headerBytes == 0x04034b50 || headerBytes == 0x02014b50);
var test64Bit = ((long)uncompressedSize << 32) | compressedSize;
if (test64Bit == lastEntryHeader.CompressedSize && testHeader)
{
lastEntryHeader.UncompressedSize =
(
(long)
await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false) << 32
) | headerBytes;
headerBytes = await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false);
if (pos.HasValue)
{
lastEntryHeader.DataStartPosition =
pos - lastEntryHeader.CompressedSize;
}
}
else
{
lastEntryHeader.UncompressedSize = uncompressedSize;
}
// A data descriptor follows. Recover the CRC and sizes from it; the
// descriptor can carry either 32-bit or 64-bit sizes.
_ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // version
_ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // flags
_ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // compressionMethod
_ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedDate
_ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedTime
if (pos.HasValue)
{
lastEntryHeader.DataStartPosition = pos - lastEntryHeader.CompressedSize;
var crc = await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false);
// For SeekableSharpCompressStream, seek back to just after the local header signature.
// Plain SharpCompressStream cannot seek to arbitrary positions, so we skip this.
// 4 = First 4 bytes of the entry header (i.e. 50 4B 03 04)
if (_sharpCompressStream is SeekableSharpCompressStream)
if (crc == POST_DATA_DESCRIPTOR)
{
crc = await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false);
}
lastEntryHeader.Crc = crc;
lastEntryHeader.IsCrcAvailable = true;
// The DataDescriptor can be either 64bit or 32bit
var compressedSize = await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false);
var uncompressedSize = await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false);
var test64Bit = ((long)uncompressedSize << 32) | compressedSize;
if (test64Bit == lastEntryHeader.CompressedSize)
{
lastEntryHeader.UncompressedSize =
(
(long)
await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false) << 32
) | headerBytes;
headerBytes = await _reader
.ReadUInt32Async(_cancellationToken)
.ConfigureAwait(false);
}
else
{
lastEntryHeader.UncompressedSize = uncompressedSize;
}
if (pos.HasValue)
{
lastEntryHeader.DataStartPosition =
pos - lastEntryHeader.CompressedSize;
// 4 = First 4 bytes of the entry header (i.e. 50 4B 03 04)
_sharpCompressStream.Position = pos.Value + 4;
}
}

View File

@@ -109,46 +109,63 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory
headerBytes = reader.ReadUInt32();
var version = reader.ReadUInt16();
var flags = (HeaderFlags)reader.ReadUInt16();
var compressionMethod = (ZipCompressionMethod)reader.ReadUInt16();
var lastModifiedDate = reader.ReadUInt16();
var lastModifiedTime = reader.ReadUInt16();
var crc = reader.ReadUInt32();
if (crc == POST_DATA_DESCRIPTOR)
// A Zip64 entry that does not use a post-data descriptor stores its real CRC
// and sizes in the local header's Zip64 extra field, so a header signature
// (the next local entry, or the central directory) follows the data directly.
// In that case the entry's metadata is already correct and must not be
// overwritten with bytes read from the following header. We have only consumed
// the 4-byte signature, so fall through and parse this header normally.
if (headerBytes == 0x04034b50 || headerBytes == 0x02014b50)
{
crc = reader.ReadUInt32();
}
_lastEntryHeader.Crc = crc;
_lastEntryHeader.IsCrcAvailable = true;
// The DataDescriptor can be either 64bit or 32bit
var compressed_size = reader.ReadUInt32();
var uncompressed_size = reader.ReadUInt32();
// Check if we have header or 64bit DataDescriptor
var test_header = !(headerBytes == 0x04034b50 || headerBytes == 0x02014b50);
var test_64bit = ((long)uncompressed_size << 32) | compressed_size;
if (test_64bit == _lastEntryHeader.CompressedSize && test_header)
{
_lastEntryHeader.UncompressedSize =
((long)reader.ReadUInt32() << 32) | headerBytes;
headerBytes = reader.ReadUInt32();
if (pos.HasValue)
{
_lastEntryHeader.DataStartPosition =
pos - _lastEntryHeader.CompressedSize;
}
}
else
{
_lastEntryHeader.UncompressedSize = uncompressed_size;
}
// A data descriptor follows. Recover the CRC and sizes from it; the
// descriptor can carry either 32-bit or 64-bit sizes.
_ = reader.ReadUInt16(); // version
_ = reader.ReadUInt16(); // flags
_ = reader.ReadUInt16(); // compressionMethod
_ = reader.ReadUInt16(); // lastModifiedDate
_ = reader.ReadUInt16(); // lastModifiedTime
if (pos.HasValue)
{
_lastEntryHeader.DataStartPosition = pos - _lastEntryHeader.CompressedSize;
var crc = reader.ReadUInt32();
// 4 = First 4 bytes of the entry header (i.e. 50 4B 03 04)
sharpCompressStream.Position = pos.Value + 4;
if (crc == POST_DATA_DESCRIPTOR)
{
crc = reader.ReadUInt32();
}
_lastEntryHeader.Crc = crc;
_lastEntryHeader.IsCrcAvailable = true;
// The DataDescriptor can be either 64bit or 32bit
var compressed_size = reader.ReadUInt32();
var uncompressed_size = reader.ReadUInt32();
var test_64bit = ((long)uncompressed_size << 32) | compressed_size;
if (test_64bit == _lastEntryHeader.CompressedSize)
{
_lastEntryHeader.UncompressedSize =
((long)reader.ReadUInt32() << 32) | headerBytes;
headerBytes = reader.ReadUInt32();
}
else
{
_lastEntryHeader.UncompressedSize = uncompressed_size;
}
if (pos.HasValue)
{
_lastEntryHeader.DataStartPosition =
pos - _lastEntryHeader.CompressedSize;
// 4 = First 4 bytes of the entry header (i.e. 50 4B 03 04)
sharpCompressStream.Position = pos.Value + 4;
}
}
}
else