< Summary

Line coverage
42%
Covered lines: 41
Uncovered lines: 56
Coverable lines: 97
Total lines: 1454
Line coverage: 42.2%
Branch coverage
55%
Covered branches: 11
Total branches: 20
Branch coverage: 55%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

D:\runner\runtime\src\libraries\System.IO.Compression\src\System\IO\Compression\ZipBlocks.Async.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers;
 5using System.Collections.Generic;
 6using System.Diagnostics;
 7using System.Threading;
 8using System.Threading.Tasks;
 9
 10namespace System.IO.Compression;
 11
 12internal sealed partial class ZipGenericExtraField
 13{
 14    public async Task WriteBlockAsync(Stream stream, CancellationToken cancellationToken)
 15    {
 16        cancellationToken.ThrowIfCancellationRequested();
 17
 18        byte[] extraFieldHeader = new byte[SizeOfHeader];
 19        WriteBlockCore(extraFieldHeader);
 20        await stream.WriteAsync(extraFieldHeader, cancellationToken).ConfigureAwait(false);
 21        await stream.WriteAsync(Data, cancellationToken).ConfigureAwait(false);
 22    }
 23
 24    public static async Task WriteAllBlocksAsync(List<ZipGenericExtraField>? fields, ReadOnlyMemory<byte> trailingExtraF
 25    {
 26        cancellationToken.ThrowIfCancellationRequested();
 27
 28        if (fields != null)
 29        {
 30            foreach (ZipGenericExtraField field in fields)
 31            {
 32                await field.WriteBlockAsync(stream, cancellationToken).ConfigureAwait(false);
 33            }
 34        }
 35
 36        if (!trailingExtraFieldData.IsEmpty)
 37        {
 38            await stream.WriteAsync(trailingExtraFieldData, cancellationToken).ConfigureAwait(false);
 39        }
 40    }
 41
 42    public static async Task WriteAllBlocksExcludingTagAsync(List<ZipGenericExtraField>? fields, ReadOnlyMemory<byte> tr
 43    {
 44        cancellationToken.ThrowIfCancellationRequested();
 45
 46        if (fields != null)
 47        {
 48            foreach (ZipGenericExtraField field in fields)
 49            {
 50                if (field.Tag != excludeTag)
 51                {
 52                    await field.WriteBlockAsync(stream, cancellationToken).ConfigureAwait(false);
 53                }
 54            }
 55        }
 56
 57        if (!trailingExtraFieldData.IsEmpty)
 58        {
 59            await stream.WriteAsync(trailingExtraFieldData, cancellationToken).ConfigureAwait(false);
 60        }
 61    }
 62}
 63
 64internal sealed partial class Zip64ExtraField
 65{
 66    public ValueTask WriteBlockAsync(Stream stream, CancellationToken cancellationToken)
 67    {
 68        cancellationToken.ThrowIfCancellationRequested();
 69
 70        byte[] extraFieldData = new byte[TotalSize];
 71        WriteBlockCore(extraFieldData);
 72        return stream.WriteAsync(extraFieldData, cancellationToken);
 73    }
 74
 75}
 76
 77internal sealed partial class Zip64EndOfCentralDirectoryLocator
 78{
 79    public static async Task<Zip64EndOfCentralDirectoryLocator> TryReadBlockAsync(Stream stream, CancellationToken cance
 80    {
 81        cancellationToken.ThrowIfCancellationRequested();
 82
 83        byte[] blockContents = new byte[TotalSize];
 84        int bytesRead = await stream.ReadAtLeastAsync(blockContents, blockContents.Length, throwOnEndOfStream: false, ca
 85        bool zip64eocdLocatorProper = TryReadBlockCore(blockContents, bytesRead, out Zip64EndOfCentralDirectoryLocator? 
 86
 87        Debug.Assert(zip64eocdLocatorProper && zip64EOCDLocator != null); // we just found this using the signature find
 88
 89        return zip64EOCDLocator;
 90    }
 91
 92    public static ValueTask WriteBlockAsync(Stream stream, long zip64EOCDRecordStart, CancellationToken cancellationToke
 93    {
 94        cancellationToken.ThrowIfCancellationRequested();
 95
 96        byte[] blockContents = new byte[TotalSize];
 97        WriteBlockCore(blockContents, zip64EOCDRecordStart);
 98        return stream.WriteAsync(blockContents, cancellationToken);
 99    }
 100}
 101
 102internal sealed partial class Zip64EndOfCentralDirectoryRecord
 103{
 104    public static async Task<Zip64EndOfCentralDirectoryRecord> TryReadBlockAsync(Stream stream, CancellationToken cancel
 105    {
 106        cancellationToken.ThrowIfCancellationRequested();
 107
 108        byte[] blockContents = new byte[BlockConstantSectionSize];
 109        int bytesRead = await stream.ReadAtLeastAsync(blockContents, blockContents.Length, throwOnEndOfStream: false, ca
 110
 111        if (!TryReadBlockCore(blockContents, bytesRead, out Zip64EndOfCentralDirectoryRecord? zip64EOCDRecord))
 112        {
 113            throw new InvalidDataException(SR.Zip64EOCDNotWhereExpected);
 114        }
 115
 116        return zip64EOCDRecord;
 117    }
 118
 119    public static ValueTask WriteBlockAsync(Stream stream, long numberOfEntries, long startOfCentralDirectory, long size
 120    {
 121        cancellationToken.ThrowIfCancellationRequested();
 122
 123        byte[] blockContents = new byte[BlockConstantSectionSize];
 124        WriteBlockCore(blockContents, numberOfEntries, startOfCentralDirectory, sizeOfCentralDirectory);
 125        // write Zip 64 EOCD record
 126        return stream.WriteAsync(blockContents, cancellationToken);
 127    }
 128}
 129
 130internal readonly partial struct ZipLocalFileHeader
 131{
 132    public static async Task<(List<ZipGenericExtraField>, byte[] trailingData)> GetExtraFieldsAsync(Stream stream, Cance
 0133    {
 0134        cancellationToken.ThrowIfCancellationRequested();
 135
 136        // assumes that TrySkipBlock has already been called, so we don't have to validate twice
 137
 0138        byte[] fixedHeaderBuffer = new byte[FieldLengths.FilenameLength + FieldLengths.ExtraFieldLength];
 0139        GetExtraFieldsInitialize(stream, out int relativeFilenameLengthLocation, out int relativeExtraFieldLengthLocatio
 0140        await stream.ReadExactlyAsync(fixedHeaderBuffer, cancellationToken).ConfigureAwait(false);
 141
 0142        GetExtraFieldsCore(fixedHeaderBuffer, relativeFilenameLengthLocation, relativeExtraFieldLengthLocation, out usho
 143
 0144        byte[] arrayPoolBuffer = ArrayPool<byte>.Shared.Rent(extraFieldLength);
 0145        Memory<byte> extraFieldBuffer = arrayPoolBuffer.AsMemory(0, extraFieldLength);
 146
 147        try
 0148        {
 0149            stream.Seek(filenameLength, SeekOrigin.Current);
 0150            await stream.ReadExactlyAsync(extraFieldBuffer, cancellationToken).ConfigureAwait(false);
 151
 0152            List<ZipGenericExtraField> list = GetExtraFieldPostReadWork(extraFieldBuffer.Span, out byte[] trailingData);
 153
 0154            return (list, trailingData);
 155        }
 156        finally
 0157        {
 0158            if (arrayPoolBuffer != null)
 0159            {
 0160                ArrayPool<byte>.Shared.Return(arrayPoolBuffer);
 0161            }
 0162        }
 0163    }
 164
 165    // will not throw end of stream exception
 166    public static async Task<bool> TrySkipBlockAsync(Stream stream, CancellationToken cancellationToken)
 403167    {
 403168        cancellationToken.ThrowIfCancellationRequested();
 169
 403170        byte[] blockBytes = new byte[FieldLengths.Signature];
 403171        int bytesRead = await stream.ReadAtLeastAsync(blockBytes, blockBytes.Length, throwOnEndOfStream: false, cancella
 403172        if (!TrySkipBlockCore(stream, blockBytes, bytesRead))
 371173        {
 371174            return false;
 175        }
 32176        bytesRead = await stream.ReadAtLeastAsync(blockBytes, blockBytes.Length, throwOnEndOfStream: false, cancellation
 32177        return TrySkipBlockFinalize(stream, blockBytes, bytesRead);
 403178    }
 179}
 180
 181internal sealed partial class ZipCentralDirectoryFileHeader
 182{
 183    // if saveExtraFieldsAndComments is false, FileComment and ExtraFields will be null
 184    // in either case, the zip64 extra field info will be incorporated into other fields
 185    public static async Task<(bool, int, ZipCentralDirectoryFileHeader?)> TryReadBlockAsync(ReadOnlyMemory<byte> buffer,
 186    {
 187        cancellationToken.ThrowIfCancellationRequested();
 188
 189        ZipCentralDirectoryFileHeader? header;
 190        if (!TryReadBlockInitialize(buffer.Span, out header, out int bytesRead, out uint compressedSizeSmall, out uint u
 191        {
 192            return (false, 0, null);
 193        }
 194
 195        byte[]? arrayPoolBuffer = null;
 196        try
 197        {
 198            // Assemble the dynamic header in a separate buffer. We can't guarantee that it's all in the input buffer,
 199            // some additional data might need to come from the stream.
 200            int dynamicHeaderSize = header.FilenameLength + header.ExtraFieldLength + header.FileCommentLength;
 201            int remainingBufferLength = buffer.Length - FieldLocations.DynamicData;
 202            int bytesToRead = dynamicHeaderSize - remainingBufferLength;
 203            scoped ReadOnlySpan<byte> dynamicHeader;
 204
 205            // No need to read extra data from the stream, no need to allocate a new buffer.
 206            if (bytesToRead <= 0)
 207            {
 208                dynamicHeader = buffer.Span[FieldLocations.DynamicData..];
 209            }
 210            // Data needs to come from two sources, and we must thus copy data into a single address space.
 211            else
 212            {
 213                arrayPoolBuffer = ArrayPool<byte>.Shared.Rent(dynamicHeaderSize);
 214                Memory<byte> collatedHeader = arrayPoolBuffer.AsMemory(0, dynamicHeaderSize);
 215
 216                buffer[FieldLocations.DynamicData..].CopyTo(collatedHeader);
 217
 218                Debug.Assert(bytesToRead == collatedHeader.Length - remainingBufferLength);
 219                int realBytesRead = await furtherReads.ReadAtLeastAsync(collatedHeader.Slice(remainingBufferLength), byt
 220
 221                if (realBytesRead != bytesToRead)
 222                {
 223                    return (false, bytesRead, null);
 224                }
 225
 226                dynamicHeader = collatedHeader.Span;
 227            }
 228
 229            TryReadBlockFinalize(header, dynamicHeader, dynamicHeaderSize, uncompressedSizeSmall, compressedSizeSmall, d
 230        }
 231        finally
 232        {
 233            if (arrayPoolBuffer != null)
 234            {
 235                ArrayPool<byte>.Shared.Return(arrayPoolBuffer);
 236            }
 237        }
 238
 239        return (true, bytesRead, header);
 240    }
 241}
 242
 243internal sealed partial class ZipEndOfCentralDirectoryBlock
 244{
 245    public static async Task WriteBlockAsync(Stream stream, long numberOfEntries, long startOfCentralDirectory, long siz
 246    {
 247        cancellationToken.ThrowIfCancellationRequested();
 248
 249        byte[] blockContents = new byte[TotalSize];
 250
 251        WriteBlockInitialize(blockContents, numberOfEntries, startOfCentralDirectory, sizeOfCentralDirectory, archiveCom
 252
 253        await stream.WriteAsync(blockContents, cancellationToken).ConfigureAwait(false);
 254        if (archiveComment.Length > 0)
 255        {
 256            await stream.WriteAsync(archiveComment, cancellationToken).ConfigureAwait(false);
 257        }
 258    }
 259
 260    public static async Task<ZipEndOfCentralDirectoryBlock> ReadBlockAsync(Stream stream, CancellationToken cancellation
 261    {
 262        cancellationToken.ThrowIfCancellationRequested();
 263
 264        byte[] blockContents = new byte[TotalSize];
 265        int bytesRead = await stream.ReadAtLeastAsync(blockContents, blockContents.Length, throwOnEndOfStream: false, ca
 266
 267        if (!TryReadBlockInitialize(stream, blockContents, bytesRead, out ZipEndOfCentralDirectoryBlock? eocdBlock, out 
 268        {
 269            // // We shouldn't get here becasue we found the eocd block using the signature finder
 270            throw new InvalidDataException(SR.EOCDNotFound);
 271        }
 272        else if (readComment)
 273        {
 274            await stream.ReadExactlyAsync(eocdBlock._archiveComment, cancellationToken).ConfigureAwait(false);
 275        }
 276        return eocdBlock;
 277    }
 278}

D:\runner\runtime\src\libraries\System.IO.Compression\src\System\IO\Compression\ZipBlocks.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers;
 5using System.Buffers.Binary;
 6using System.Collections.Generic;
 7using System.Diagnostics;
 8using System.Diagnostics.CodeAnalysis;
 9using System.Runtime.InteropServices;
 10using System.Threading;
 11using System.Threading.Tasks;
 12
 13namespace System.IO.Compression
 14{
 15    // All blocks.TryReadBlock do a check to see if signature is correct. Generic extra field is slightly different
 16    // all of the TryReadBlocks will throw if there are not enough bytes in the stream
 17    internal sealed partial class ZipGenericExtraField
 18    {
 19        private const int SizeOfHeader = FieldLengths.Tag + FieldLengths.Size;
 20
 21        private ushort _tag;
 22        private ushort _size;
 23        private byte[]? _data;
 24
 25        public ushort Tag => _tag;
 26        // returns size of data, not of the entire block
 27        public ushort Size => _size;
 28        public byte[] Data => _data ??= [];
 29
 30        public unsafe void WriteBlock(Stream stream)
 31        {
 32            Span<byte> extraFieldHeader = stackalloc byte[SizeOfHeader];
 33            WriteBlockCore(extraFieldHeader);
 34            stream.Write(extraFieldHeader);
 35            stream.Write(Data);
 36        }
 37
 38        private void WriteBlockCore(Span<byte> extraFieldHeader)
 39        {
 40            BinaryPrimitives.WriteUInt16LittleEndian(extraFieldHeader[FieldLocations.Tag..], _tag);
 41            BinaryPrimitives.WriteUInt16LittleEndian(extraFieldHeader[FieldLocations.Size..], _size);
 42        }
 43
 44        // assumes that bytes starts at the beginning of an extra field subfield
 45        public static bool TryReadBlock(ReadOnlySpan<byte> bytes, out int bytesConsumed, out ZipGenericExtraField field)
 46        {
 47            field = new();
 48            bytesConsumed = 0;
 49
 50            // not enough bytes to read tag + size
 51            if (bytes.Length < SizeOfHeader)
 52            {
 53                return false;
 54            }
 55
 56            field._tag = BinaryPrimitives.ReadUInt16LittleEndian(bytes[FieldLocations.Tag..]);
 57            field._size = BinaryPrimitives.ReadUInt16LittleEndian(bytes[FieldLocations.Size..]);
 58
 59            // not enough byte to read the data
 60            if ((bytes.Length - SizeOfHeader) < field._size)
 61            {
 62                return false;
 63            }
 64
 65            field._data = bytes.Slice(FieldLocations.DynamicData, field._size).ToArray();
 66            bytesConsumed = field.Size + SizeOfHeader;
 67            return true;
 68        }
 69
 70        public static List<ZipGenericExtraField> ParseExtraField(ReadOnlySpan<byte> extraFieldData, out ReadOnlySpan<byt
 71        {
 72            List<ZipGenericExtraField> extraFields = new List<ZipGenericExtraField>();
 73            int totalBytesConsumed = 0;
 74
 75            while (TryReadBlock(extraFieldData[totalBytesConsumed..], out int currBytesConsumed, out ZipGenericExtraFiel
 76            {
 77                totalBytesConsumed += currBytesConsumed;
 78                extraFields.Add(field);
 79            }
 80            // It's possible for some ZIP files to contain extra data which isn't a well-formed extra field. zipalign do
 81            // field with zeroes to align the start of the file data to an X-byte boundary. We need to preserve this dat
 82            // fields in the central directory and local file headers are valid.
 83            // We need to account for this because zipalign is part of the build process for Android applications, and f
 84            // .NET for Android generating corrupted .apk files and failing to build.
 85            trailingExtraFieldData = extraFieldData[totalBytesConsumed..];
 86
 87            return extraFields;
 88        }
 89
 90        public static int TotalSize(List<ZipGenericExtraField>? fields, int trailingDataLength)
 91        {
 92            int size = trailingDataLength;
 93
 94            if (fields != null)
 95            {
 96                foreach (ZipGenericExtraField field in fields)
 97                {
 98                    size += field.Size + SizeOfHeader; //size is only size of data
 99                }
 100            }
 101            return size;
 102        }
 103
 104        public static void WriteAllBlocks(List<ZipGenericExtraField>? fields, ReadOnlySpan<byte> trailingExtraFieldData,
 105        {
 106            if (fields != null)
 107            {
 108                foreach (ZipGenericExtraField field in fields)
 109                {
 110                    field.WriteBlock(stream);
 111                }
 112            }
 113
 114            if (!trailingExtraFieldData.IsEmpty)
 115            {
 116                stream.Write(trailingExtraFieldData);
 117            }
 118        }
 119
 120        public static void WriteAllBlocksExcludingTag(List<ZipGenericExtraField>? fields, ReadOnlySpan<byte> trailingExt
 121        {
 122            if (fields != null)
 123            {
 124                foreach (ZipGenericExtraField field in fields)
 125                {
 126                    if (field.Tag != excludeTag)
 127                    {
 128                        field.WriteBlock(stream);
 129                    }
 130                }
 131            }
 132
 133            if (!trailingExtraFieldData.IsEmpty)
 134            {
 135                stream.Write(trailingExtraFieldData);
 136            }
 137        }
 138
 139        public static int TotalSizeExcludingTag(List<ZipGenericExtraField>? fields, int trailingDataLength, ushort exclu
 140        {
 141            int size = trailingDataLength;
 142
 143            if (fields != null)
 144            {
 145                foreach (ZipGenericExtraField field in fields)
 146                {
 147                    if (field.Tag != excludeTag)
 148                    {
 149                        size += field.Size + ZipGenericExtraField.FieldLengths.Tag + ZipGenericExtraField.FieldLengths.S
 150                    }
 151                }
 152            }
 153
 154            return size;
 155        }
 156    }
 157
 158    internal sealed partial class Zip64ExtraField
 159    {
 160        // Size is size of the record not including the tag or size fields
 161        // If the extra field is going in the local header, it cannot include only
 162        // one of uncompressed/compressed size
 163
 164        public const int OffsetToFirstField = ZipGenericExtraField.FieldLocations.DynamicData;
 165        private const ushort TagConstant = 1;
 166
 167        private ushort _size;
 168        private long? _uncompressedSize;
 169        private long? _compressedSize;
 170        private long? _localHeaderOffset;
 171        private uint? _startDiskNumber;
 172
 173        public ushort TotalSize => (ushort)(_size + 4);
 174
 175        public long? UncompressedSize
 176        {
 177            get { return _uncompressedSize; }
 178            set { _uncompressedSize = value; UpdateSize(); }
 179        }
 180        public long? CompressedSize
 181        {
 182            get { return _compressedSize; }
 183            set { _compressedSize = value; UpdateSize(); }
 184        }
 185        public long? LocalHeaderOffset
 186        {
 187            get { return _localHeaderOffset; }
 188            set { _localHeaderOffset = value; UpdateSize(); }
 189        }
 190        public uint? StartDiskNumber => _startDiskNumber;
 191
 192        private void UpdateSize()
 193        {
 194            _size = 0;
 195            if (_uncompressedSize != null)
 196            {
 197                _size += FieldLengths.UncompressedSize;
 198            }
 199            if (_compressedSize != null)
 200            {
 201                _size += FieldLengths.CompressedSize;
 202            }
 203            if (_localHeaderOffset != null)
 204            {
 205                _size += FieldLengths.LocalHeaderOffset;
 206            }
 207            if (_startDiskNumber != null)
 208            {
 209                _size += FieldLengths.StartDiskNumber;
 210            }
 211        }
 212
 213        // There is a small chance that something very weird could happen here. The code calling into this function
 214        // will ask for a value from the extra field if the field was masked with FF's. It's theoretically possible
 215        // that a field was FF's legitimately, and the writer didn't decide to write the corresponding extra field.
 216        // Also, at the same time, other fields were masked with FF's to indicate looking in the zip64 record.
 217        // Then, the search for the zip64 record will fail because the expected size is wrong,
 218        // and a nulled out Zip64ExtraField will be returned. Thus, even though there was Zip64 data,
 219        // it will not be used. It is questionable whether this situation is possible to detect
 220        // unlike the other functions that have try-pattern semantics, these functions always return a
 221        // Zip64ExtraField. If a Zip64 extra field actually doesn't exist, all of the fields in the
 222        // returned struct will be null
 223        //
 224        // If there are more than one Zip64 extra fields, we take the first one that has the expected size
 225        //
 226        public static Zip64ExtraField GetJustZip64Block(ReadOnlySpan<byte> extraFieldData,
 227            bool readUncompressedSize, bool readCompressedSize,
 228            bool readLocalHeaderOffset, bool readStartDiskNumber, bool isInLocalHeader)
 229        {
 230            Zip64ExtraField zip64Field;
 231            int totalBytesConsumed = 0;
 232
 233            while (ZipGenericExtraField.TryReadBlock(extraFieldData.Slice(totalBytesConsumed), out int currBytesConsumed
 234            {
 235                totalBytesConsumed += currBytesConsumed;
 236
 237                if (TryGetZip64BlockFromGenericExtraField(currentExtraField, readUncompressedSize,
 238                    readCompressedSize, readLocalHeaderOffset, readStartDiskNumber, isInLocalHeader, out zip64Field))
 239                {
 240                    return zip64Field;
 241                }
 242            }
 243
 244            zip64Field = new()
 245            {
 246                _compressedSize = null,
 247                _uncompressedSize = null,
 248                _localHeaderOffset = null,
 249                _startDiskNumber = null,
 250            };
 251
 252            return zip64Field;
 253        }
 254
 255        private static bool TryGetZip64BlockFromGenericExtraField(ZipGenericExtraField extraField,
 256            bool readUncompressedSize, bool readCompressedSize,
 257            bool readLocalHeaderOffset, bool readStartDiskNumber,
 258            bool isInLocalHeader, out Zip64ExtraField zip64Block)
 259        {
 260            const int MaximumExtraFieldLength = FieldLengths.UncompressedSize + FieldLengths.CompressedSize + FieldLengt
 261            zip64Block = new()
 262            {
 263                _compressedSize = null,
 264                _uncompressedSize = null,
 265                _localHeaderOffset = null,
 266                _startDiskNumber = null,
 267            };
 268
 269            if (extraField.Tag != TagConstant)
 270            {
 271                return false;
 272            }
 273
 274            zip64Block._size = extraField.Size;
 275
 276            ReadOnlySpan<byte> data = extraField.Data;
 277
 278            // The spec section 4.5.3:
 279            //      The order of the fields in the zip64 extended
 280            //      information record is fixed, but the fields MUST
 281            //      only appear if the corresponding Local or Central
 282            //      directory record field is set to 0xFFFF or 0xFFFFFFFF.
 283            // However tools commonly write the fields anyway; the prevailing convention
 284            // is to respect the size, but only actually use the values if their 32 bit
 285            // values were all 0xFF.
 286
 287            if (data.Length < FieldLengths.UncompressedSize)
 288            {
 289                // The spec section 4.5.3 later:
 290                //      This entry in the Local header MUST include BOTH original
 291                //      and compressed file size fields.
 292
 293                return !isInLocalHeader;
 294            }
 295
 296            // Advancing the stream (by reading from it) is possible only when:
 297            // 1. There is an explicit ask to do that (valid files, corresponding boolean flag(s) set to true).
 298            // 2. Field is mandated to be present by spec (see "section 4.5.3 later" comment above)
 299            // 3. When the size indicates that all the information is available ("slightly invalid files").
 300            bool readAllFields = extraField.Size >= MaximumExtraFieldLength;
 301
 302            // The original values are unsigned 64-bit, so a negative signed value means the
 303            // value does not fit in Int64 and cannot be represented by the rest of the API
 304            // (which uses long). Validate each field as it is read so that short extra fields
 305            // (which exit early below) cannot bypass the check.
 306
 307            if (readUncompressedSize)
 308            {
 309                zip64Block._uncompressedSize = BinaryPrimitives.ReadInt64LittleEndian(data);
 310                if (zip64Block._uncompressedSize < 0)
 311                {
 312                    throw new InvalidDataException(SR.FieldTooBigUncompressedSize);
 313                }
 314                data = data.Slice(FieldLengths.UncompressedSize);
 315            }
 316            else if (readAllFields || isInLocalHeader)
 317            {
 318                data = data.Slice(FieldLengths.UncompressedSize);
 319            }
 320
 321            if (data.Length < FieldLengths.CompressedSize)
 322            {
 323                return !isInLocalHeader;
 324            }
 325
 326            if (readCompressedSize)
 327            {
 328                zip64Block._compressedSize = BinaryPrimitives.ReadInt64LittleEndian(data);
 329                if (zip64Block._compressedSize < 0)
 330                {
 331                    throw new InvalidDataException(SR.FieldTooBigCompressedSize);
 332                }
 333                data = data.Slice(FieldLengths.CompressedSize);
 334            }
 335            else if (readAllFields || isInLocalHeader)
 336            {
 337                data = data.Slice(FieldLengths.CompressedSize);
 338            }
 339
 340            if (data.Length < FieldLengths.LocalHeaderOffset)
 341            {
 342                return true;
 343            }
 344
 345            if (readLocalHeaderOffset)
 346            {
 347                zip64Block._localHeaderOffset = BinaryPrimitives.ReadInt64LittleEndian(data);
 348                if (zip64Block._localHeaderOffset < 0)
 349                {
 350                    throw new InvalidDataException(SR.FieldTooBigLocalHeaderOffset);
 351                }
 352                data = data.Slice(FieldLengths.LocalHeaderOffset);
 353            }
 354            else if (readAllFields)
 355            {
 356                data = data.Slice(FieldLengths.LocalHeaderOffset);
 357            }
 358
 359            if (data.Length < FieldLengths.StartDiskNumber)
 360            {
 361                return true;
 362            }
 363
 364            if (readStartDiskNumber)
 365            {
 366                zip64Block._startDiskNumber = BinaryPrimitives.ReadUInt32LittleEndian(data);
 367            }
 368
 369            return true;
 370        }
 371
 372        public static Zip64ExtraField GetAndRemoveZip64Block(List<ZipGenericExtraField> extraFields,
 373            bool readUncompressedSize, bool readCompressedSize,
 374            bool readLocalHeaderOffset, bool readStartDiskNumber, bool isInLocalHeader)
 375        {
 376            Zip64ExtraField zip64Field = new()
 377            {
 378                _compressedSize = null,
 379                _uncompressedSize = null,
 380                _localHeaderOffset = null,
 381                _startDiskNumber = null,
 382            };
 383
 384            bool zip64FieldFound = false;
 385
 386            extraFields.RemoveAll(ef =>
 387            {
 388                if (ef.Tag == TagConstant)
 389                {
 390                    if (!zip64FieldFound)
 391                    {
 392                        if (TryGetZip64BlockFromGenericExtraField(ef, readUncompressedSize, readCompressedSize,
 393                                    readLocalHeaderOffset, readStartDiskNumber, isInLocalHeader, out zip64Field))
 394                        {
 395                            zip64FieldFound = true;
 396                        }
 397                    }
 398                    return true;
 399                }
 400
 401                return false;
 402            });
 403
 404            return zip64Field;
 405        }
 406
 407        public static void RemoveZip64Blocks(List<ZipGenericExtraField> extraFields)
 408        {
 409            extraFields.RemoveAll(field => field.Tag == TagConstant);
 410        }
 411
 412        public void WriteBlockCore(Span<byte> extraFieldData)
 413        {
 414            int startOffset = ZipGenericExtraField.FieldLocations.DynamicData;
 415
 416            BinaryPrimitives.WriteUInt16LittleEndian(extraFieldData[FieldLocations.Tag..], TagConstant);
 417            BinaryPrimitives.WriteUInt16LittleEndian(extraFieldData[FieldLocations.Size..], _size);
 418
 419            if (_uncompressedSize != null)
 420            {
 421                BinaryPrimitives.WriteInt64LittleEndian(extraFieldData[startOffset..], _uncompressedSize.Value);
 422                startOffset += FieldLengths.UncompressedSize;
 423            }
 424
 425            if (_compressedSize != null)
 426            {
 427                BinaryPrimitives.WriteInt64LittleEndian(extraFieldData[startOffset..], _compressedSize.Value);
 428                startOffset += FieldLengths.CompressedSize;
 429            }
 430
 431            if (_localHeaderOffset != null)
 432            {
 433                BinaryPrimitives.WriteInt64LittleEndian(extraFieldData[startOffset..], _localHeaderOffset.Value);
 434                startOffset += FieldLengths.LocalHeaderOffset;
 435            }
 436
 437            if (_startDiskNumber != null)
 438            {
 439                BinaryPrimitives.WriteUInt32LittleEndian(extraFieldData[startOffset..], _startDiskNumber.Value);
 440            }
 441        }
 442
 443        public unsafe void WriteBlock(Stream stream)
 444        {
 445            Span<byte> extraFieldData = stackalloc byte[TotalSize];
 446            WriteBlockCore(extraFieldData);
 447            stream.Write(extraFieldData);
 448        }
 449    }
 450
 451    internal sealed partial class Zip64EndOfCentralDirectoryLocator
 452    {
 453        // The Zip File Format Specification references 0x07064B50, this is a big endian representation.
 454        // ZIP files store values in little endian, so this is reversed.
 455        public static readonly byte[] SignatureConstantBytes = [0x50, 0x4B, 0x06, 0x07];
 456
 457        public const int TotalSize = FieldLocations.TotalNumberOfDisks + FieldLengths.TotalNumberOfDisks;
 458        public const int SizeOfBlockWithoutSignature = TotalSize - FieldLengths.Signature;
 459
 460        public uint NumberOfDiskWithZip64EOCD;
 461        public ulong OffsetOfZip64EOCD;
 462        public uint TotalNumberOfDisks;
 463
 464        private static bool TryReadBlockCore(Span<byte> blockContents, int bytesRead, [NotNullWhen(returnValue: true)] o
 465        {
 466            zip64EOCDLocator = null;
 467            if (bytesRead < TotalSize || !blockContents.StartsWith(SignatureConstantBytes))
 468            {
 469                return false;
 470            }
 471
 472            zip64EOCDLocator = new()
 473            {
 474                NumberOfDiskWithZip64EOCD = BinaryPrimitives.ReadUInt32LittleEndian(blockContents[FieldLocations.NumberO
 475                OffsetOfZip64EOCD = BinaryPrimitives.ReadUInt64LittleEndian(blockContents[FieldLocations.OffsetOfZip64EO
 476                TotalNumberOfDisks = BinaryPrimitives.ReadUInt32LittleEndian(blockContents[FieldLocations.TotalNumberOfD
 477            };
 478
 479            return true;
 480        }
 481
 482        public static unsafe Zip64EndOfCentralDirectoryLocator TryReadBlock(Stream stream)
 483        {
 484            Span<byte> blockContents = stackalloc byte[TotalSize];
 485            int bytesRead = stream.ReadAtLeast(blockContents, blockContents.Length, throwOnEndOfStream: false);
 486            bool zip64eocdLocatorProper = TryReadBlockCore(blockContents, bytesRead, out Zip64EndOfCentralDirectoryLocat
 487
 488            Debug.Assert(zip64eocdLocatorProper && zip64EOCDLocator != null); // we just found this using the signature 
 489
 490            return zip64EOCDLocator;
 491        }
 492
 493        private static void WriteBlockCore(Span<byte> blockContents, long zip64EOCDRecordStart)
 494        {
 495            SignatureConstantBytes.CopyTo(blockContents[FieldLocations.Signature..]);
 496            // number of disk with start of zip64 eocd
 497            BinaryPrimitives.WriteUInt32LittleEndian(blockContents[FieldLocations.NumberOfDiskWithZip64EOCD..], 0);
 498            BinaryPrimitives.WriteInt64LittleEndian(blockContents[FieldLocations.OffsetOfZip64EOCD..], zip64EOCDRecordSt
 499            // total number of disks
 500            BinaryPrimitives.WriteUInt32LittleEndian(blockContents[FieldLocations.TotalNumberOfDisks..], 1);
 501
 502        }
 503
 504        public static unsafe void WriteBlock(Stream stream, long zip64EOCDRecordStart)
 505        {
 506            Span<byte> blockContents = stackalloc byte[TotalSize];
 507            WriteBlockCore(blockContents, zip64EOCDRecordStart);
 508            stream.Write(blockContents);
 509        }
 510    }
 511
 512    internal sealed partial class Zip64EndOfCentralDirectoryRecord
 513    {
 514        // The Zip File Format Specification references 0x06064B50, this is a big endian representation.
 515        // ZIP files store values in little endian, so this is reversed.
 516        public static ReadOnlySpan<byte> SignatureConstantBytes => [0x50, 0x4B, 0x06, 0x06];
 517
 518        private const int BlockConstantSectionSize = 56;
 519        private const ulong NormalSize = 0x2C; // the size of the data excluding the size/signature fields if no extra d
 520        public const long TotalSize = (long)NormalSize + 12;    // total size of the entire block
 521
 522        public ulong SizeOfThisRecord;
 523        public ushort VersionMadeBy;
 524        public ushort VersionNeededToExtract;
 525        public uint NumberOfThisDisk;
 526        public uint NumberOfDiskWithStartOfCD;
 527        public ulong NumberOfEntriesOnThisDisk;
 528        public ulong NumberOfEntriesTotal;
 529        public ulong SizeOfCentralDirectory;
 530        public ulong OffsetOfCentralDirectory;
 531
 532        private static bool TryReadBlockCore(Span<byte> blockContents, int bytesRead, [NotNullWhen(returnValue: true)] o
 533        {
 534            zip64EOCDRecord = null;
 535            if (bytesRead < BlockConstantSectionSize)
 536            {
 537                return false;
 538            }
 539
 540            if (!blockContents.StartsWith(SignatureConstantBytes))
 541            {
 542                return false;
 543            }
 544
 545            zip64EOCDRecord = new Zip64EndOfCentralDirectoryRecord()
 546            {
 547                SizeOfThisRecord = BinaryPrimitives.ReadUInt64LittleEndian(blockContents[FieldLocations.SizeOfThisRecord
 548                VersionMadeBy = BinaryPrimitives.ReadUInt16LittleEndian(blockContents[FieldLocations.VersionMadeBy..]),
 549                VersionNeededToExtract = BinaryPrimitives.ReadUInt16LittleEndian(blockContents[FieldLocations.VersionNee
 550                NumberOfThisDisk = BinaryPrimitives.ReadUInt32LittleEndian(blockContents[FieldLocations.NumberOfThisDisk
 551                NumberOfDiskWithStartOfCD = BinaryPrimitives.ReadUInt32LittleEndian(blockContents[FieldLocations.NumberO
 552                NumberOfEntriesOnThisDisk = BinaryPrimitives.ReadUInt64LittleEndian(blockContents[FieldLocations.NumberO
 553                NumberOfEntriesTotal = BinaryPrimitives.ReadUInt64LittleEndian(blockContents[FieldLocations.NumberOfEntr
 554                SizeOfCentralDirectory = BinaryPrimitives.ReadUInt64LittleEndian(blockContents[FieldLocations.SizeOfCent
 555                OffsetOfCentralDirectory = BinaryPrimitives.ReadUInt64LittleEndian(blockContents[FieldLocations.OffsetOf
 556            };
 557
 558            return true;
 559        }
 560
 561        public static unsafe Zip64EndOfCentralDirectoryRecord TryReadBlock(Stream stream)
 562        {
 563            Span<byte> blockContents = stackalloc byte[BlockConstantSectionSize];
 564            int bytesRead = stream.ReadAtLeast(blockContents, blockContents.Length, throwOnEndOfStream: false);
 565            if (!TryReadBlockCore(blockContents, bytesRead, out Zip64EndOfCentralDirectoryRecord? zip64EOCDRecord))
 566            {
 567                throw new InvalidDataException(SR.Zip64EOCDNotWhereExpected);
 568            }
 569
 570            return zip64EOCDRecord;
 571        }
 572
 573        private static void WriteBlockCore(Span<byte> blockContents, long numberOfEntries, long startOfCentralDirectory,
 574        {
 575            SignatureConstantBytes.CopyTo(blockContents[FieldLocations.Signature..]);
 576            BinaryPrimitives.WriteUInt64LittleEndian(blockContents[FieldLocations.SizeOfThisRecord..], NormalSize);
 577            // version made by: high byte is 0 for MS DOS, low byte is version needed
 578            BinaryPrimitives.WriteUInt16LittleEndian(blockContents[FieldLocations.VersionMadeBy..], (ushort)ZipVersionNe
 579            // version needed is 45 for zip 64 support
 580            BinaryPrimitives.WriteUInt16LittleEndian(blockContents[FieldLocations.VersionNeededToExtract..], (ushort)Zip
 581            // number of this disk is 0
 582            BinaryPrimitives.WriteUInt32LittleEndian(blockContents[FieldLocations.NumberOfThisDisk..], 0);
 583            // number of disk with start of central directory is 0
 584            BinaryPrimitives.WriteUInt32LittleEndian(blockContents[FieldLocations.NumberOfDiskWithStartOfCD..], 0);
 585            // number of entries on this disk
 586            BinaryPrimitives.WriteInt64LittleEndian(blockContents[FieldLocations.NumberOfEntriesOnThisDisk..], numberOfE
 587            // number of entries total
 588            BinaryPrimitives.WriteInt64LittleEndian(blockContents[FieldLocations.NumberOfEntriesTotal..], numberOfEntrie
 589            BinaryPrimitives.WriteInt64LittleEndian(blockContents[FieldLocations.SizeOfCentralDirectory..], sizeOfCentra
 590            BinaryPrimitives.WriteInt64LittleEndian(blockContents[FieldLocations.OffsetOfCentralDirectory..], startOfCen
 591        }
 592
 593        public static unsafe void WriteBlock(Stream stream, long numberOfEntries, long startOfCentralDirectory, long siz
 594        {
 595            Span<byte> blockContents = stackalloc byte[BlockConstantSectionSize];
 596            WriteBlockCore(blockContents, numberOfEntries, startOfCentralDirectory, sizeOfCentralDirectory);
 597            // write Zip 64 EOCD record
 598            stream.Write(blockContents);
 599        }
 600    }
 601
 602    internal readonly partial struct ZipLocalFileHeader
 603    {
 604        // The Zip File Format Specification references 0x08074B50 and 0x04034B50, these are big endian representations.
 605        // ZIP files store values in little endian, so these are reversed.
 0606        public static ReadOnlySpan<byte> DataDescriptorSignatureConstantBytes => [0x50, 0x4B, 0x07, 0x08];
 804607        public static ReadOnlySpan<byte> SignatureConstantBytes => [0x50, 0x4B, 0x03, 0x04];
 608        public const int SizeOfLocalHeader = 30;
 609
 610        private static void GetExtraFieldsInitialize(Stream stream, out int relativeFilenameLengthLocation, out int rela
 0611        {
 0612            relativeFilenameLengthLocation = FieldLocations.FilenameLength - FieldLocations.FilenameLength;
 0613            relativeExtraFieldLengthLocation = FieldLocations.ExtraFieldLength - FieldLocations.FilenameLength;
 0614            stream.Seek(FieldLocations.FilenameLength, SeekOrigin.Current);
 0615        }
 616
 617        private static void GetExtraFieldsCore(Span<byte> fixedHeaderBuffer, int relativeFilenameLengthLocation, int rel
 0618        {
 0619            filenameLength = BinaryPrimitives.ReadUInt16LittleEndian(fixedHeaderBuffer[relativeFilenameLengthLocation..]
 0620            extraFieldLength = BinaryPrimitives.ReadUInt16LittleEndian(fixedHeaderBuffer[relativeExtraFieldLengthLocatio
 0621        }
 622
 623        private static List<ZipGenericExtraField> GetExtraFieldPostReadWork(Span<byte> extraFieldBuffer, out byte[] trai
 0624        {
 0625            List<ZipGenericExtraField> list = ZipGenericExtraField.ParseExtraField(extraFieldBuffer, out ReadOnlySpan<by
 0626            Zip64ExtraField.RemoveZip64Blocks(list);
 0627            trailingData = trailingDataSpan.ToArray();
 0628            return list;
 0629        }
 630
 631        public static unsafe List<ZipGenericExtraField> GetExtraFields(Stream stream, out byte[] trailingData)
 0632        {
 633            // assumes that TrySkipBlock has already been called, so we don't have to validate twice
 634
 0635            Span<byte> fixedHeaderBuffer = stackalloc byte[FieldLengths.FilenameLength + FieldLengths.ExtraFieldLength];
 0636            GetExtraFieldsInitialize(stream, out int relativeFilenameLengthLocation, out int relativeExtraFieldLengthLoc
 0637            stream.ReadExactly(fixedHeaderBuffer);
 638
 0639            GetExtraFieldsCore(fixedHeaderBuffer, relativeFilenameLengthLocation, relativeExtraFieldLengthLocation, out 
 640
 641            const int StackAllocationThreshold = 512;
 642
 0643            byte[]? arrayPoolBuffer = extraFieldLength > StackAllocationThreshold ? ArrayPool<byte>.Shared.Rent(extraFie
 0644            Span<byte> extraFieldBuffer = extraFieldLength <= StackAllocationThreshold ? stackalloc byte[StackAllocation
 645            try
 0646            {
 0647                stream.Seek(filenameLength, SeekOrigin.Current);
 0648                stream.ReadExactly(extraFieldBuffer);
 649
 0650                return GetExtraFieldPostReadWork(extraFieldBuffer, out trailingData);
 651            }
 652            finally
 0653            {
 0654                if (arrayPoolBuffer != null)
 0655                {
 0656                    ArrayPool<byte>.Shared.Return(arrayPoolBuffer);
 0657                }
 0658            }
 0659        }
 660
 661        private static bool TrySkipBlockCore(Stream stream, Span<byte> blockBytes, int bytesRead)
 806662        {
 806663            if (bytesRead != FieldLengths.Signature || !blockBytes.SequenceEqual(SignatureConstantBytes))
 742664            {
 742665                return false;
 666            }
 667
 668            // Already read the signature, so make the filename length field location relative to that
 64669            stream.Seek(FieldLocations.FilenameLength - FieldLengths.Signature, SeekOrigin.Current);
 670
 671            // Reuse blockBytes to read the filename length and the extra field length - these two consecutive
 672            // fields fit inside blockBytes.
 64673            Debug.Assert(blockBytes.Length == FieldLengths.FilenameLength + FieldLengths.ExtraFieldLength);
 674
 64675            return true;
 806676        }
 677
 678        private static bool TrySkipBlockFinalize(Stream stream, Span<byte> blockBytes, int bytesRead)
 64679        {
 64680            if (bytesRead != FieldLengths.FilenameLength + FieldLengths.ExtraFieldLength)
 0681            {
 0682                return false;
 683            }
 684
 64685            int relativeFilenameLengthLocation = FieldLocations.FilenameLength - FieldLocations.FilenameLength;
 64686            int relativeExtraFieldLengthLocation = FieldLocations.ExtraFieldLength - FieldLocations.FilenameLength;
 64687            ushort filenameLength = BinaryPrimitives.ReadUInt16LittleEndian(blockBytes[relativeFilenameLengthLocation..]
 64688            ushort extraFieldLength = BinaryPrimitives.ReadUInt16LittleEndian(blockBytes[relativeExtraFieldLengthLocatio
 689
 64690            if (stream.Length < stream.Position + filenameLength + extraFieldLength)
 24691            {
 24692                return false;
 693            }
 694
 695            // Calculate absolute position of compressed data and seek there
 696            // Using SeekOrigin.Begin ensures we end up at the correct position
 697            // regardless of any edge cases during header parsing
 40698            long dataStart = stream.Position + filenameLength + extraFieldLength;
 40699            stream.Seek(dataStart, SeekOrigin.Begin);
 700
 40701            return true;
 64702        }
 703
 704        // will not throw end of stream exception
 705        public static unsafe bool TrySkipBlock(Stream stream)
 403706        {
 403707            Span<byte> blockBytes = stackalloc byte[FieldLengths.Signature];
 403708            int bytesRead = stream.ReadAtLeast(blockBytes, blockBytes.Length, throwOnEndOfStream: false);
 403709            if (!TrySkipBlockCore(stream, blockBytes, bytesRead))
 371710            {
 371711                return false;
 712            }
 32713            bytesRead = stream.ReadAtLeast(blockBytes, blockBytes.Length, throwOnEndOfStream: false);
 32714            return TrySkipBlockFinalize(stream, blockBytes, bytesRead);
 403715        }
 716    }
 717
 718    internal struct WinZipAesExtraField
 719    {
 720        public const ushort HeaderId = 0x9901;
 721        private const int DataSize = 7; // Vendor version (2) + Vendor ID (2) + AES strength (1) + Compression method (2
 722        private const byte VendorIdByte0 = (byte)'A';
 723        private const byte VendorIdByte1 = (byte)'E';
 724
 725        public WinZipAesExtraField(ushort vendorVersion, byte aesStrength, ushort compressionMethod)
 726        {
 727            VendorVersion = vendorVersion;
 728            AesStrength = aesStrength;
 729            CompressionMethod = compressionMethod;
 730        }
 731
 732        public ushort VendorVersion { get; set; } = 2;
 733        public byte AesStrength { get; set; } // 1=128bit, 2=192bit, 3=256bit
 734        public ushort CompressionMethod { get; set; } // Original compression method
 735
 736        public const int TotalSize = 11; // 2 (header) + 2 (size) + 7 (data)
 737
 738        /// <summary>
 739        /// Tries to find and parse the WinZip AES extra field (0x9901) from a list of generic extra fields.
 740        /// </summary>
 741        /// <param name="extraFields">The list of extra fields to search.</param>
 742        /// <param name="aesExtraField">When this method returns true, contains the parsed AES extra field.</param>
 743        /// <returns>true if the AES extra field was found and parsed; otherwise, false.</returns>
 744        public static bool TryGetFromExtraFields(List<ZipGenericExtraField>? extraFields, out WinZipAesExtraField aesExt
 745        {
 746            aesExtraField = default;
 747
 748            if (extraFields == null)
 749            {
 750                return false;
 751            }
 752
 753            foreach (ZipGenericExtraField field in extraFields)
 754            {
 755                if (field.Tag == HeaderId && field.Size >= DataSize &&
 756                    TryParseData(field.Data.AsSpan(0, field.Size), out aesExtraField))
 757                {
 758                    return true;
 759                }
 760            }
 761
 762            return false;
 763        }
 764
 765        /// <summary>
 766        /// Tries to find and parse the WinZip AES extra field (0x9901) from raw extra field data bytes.
 767        /// This is used when ExtraFields are not saved (Read mode) but we still need to parse the AES field.
 768        /// </summary>
 769        /// <param name="extraFieldData">The raw extra field data bytes.</param>
 770        /// <param name="aesExtraField">When this method returns true, contains the parsed AES extra field.</param>
 771        /// <returns>true if the AES extra field was found and parsed; otherwise, false.</returns>
 772        public static bool TryGetFromRawExtraFieldData(ReadOnlySpan<byte> extraFieldData, out WinZipAesExtraField aesExt
 773        {
 774            aesExtraField = default;
 775
 776            while (extraFieldData.Length >= 4) // Need at least 4 bytes for header ID and size
 777            {
 778                ushort headerId = BinaryPrimitives.ReadUInt16LittleEndian(extraFieldData);
 779                ushort fieldSize = BinaryPrimitives.ReadUInt16LittleEndian(extraFieldData.Slice(2));
 780                extraFieldData = extraFieldData.Slice(4);
 781
 782                if (fieldSize > extraFieldData.Length)
 783                {
 784                    break; // Not enough data for this field
 785                }
 786
 787                if (headerId == HeaderId && fieldSize >= DataSize &&
 788                    TryParseData(extraFieldData.Slice(0, fieldSize), out aesExtraField))
 789                {
 790                    return true;
 791                }
 792
 793                extraFieldData = extraFieldData.Slice(fieldSize);
 794            }
 795
 796            return false;
 797        }
 798
 799        /// <summary>
 800        /// Parses and validates the data payload of a candidate WinZip AES extra field.
 801        /// Validates the vendor ID ("AE"), vendor version (1 or 2), and AES strength (1–3).
 802        /// </summary>
 803        /// <param name="data">The raw field data bytes (must be at least <see cref="DataSize"/> bytes).</param>
 804        /// <param name="aesExtraField">When this method returns true, contains the parsed AES extra field.</param>
 805        /// <returns>true if the data represents a valid WinZip AES extra field; otherwise, false.</returns>
 806        private static bool TryParseData(ReadOnlySpan<byte> data, out WinZipAesExtraField aesExtraField)
 807        {
 808            aesExtraField = default;
 809
 810            ushort vendorVersion = BinaryPrimitives.ReadUInt16LittleEndian(data);
 811            byte aesStrength = data[4];
 812
 813            // Validate vendor ID must be "AE", vendor version must be 1 or 2,
 814            // and AES strength must be 1 (128-bit), 2 (192-bit), or 3 (256-bit).
 815            if (data[2] != VendorIdByte0 || data[3] != VendorIdByte1 ||
 816                vendorVersion is < 1 or > 2 ||
 817                aesStrength is < 1 or > 3)
 818            {
 819                return false;
 820            }
 821
 822            aesExtraField = new WinZipAesExtraField(
 823                vendorVersion: vendorVersion,
 824                aesStrength: aesStrength,
 825                compressionMethod: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(5, 2))
 826            );
 827            return true;
 828        }
 829
 830        public unsafe void WriteBlock(Stream stream)
 831        {
 832            Span<byte> buffer = stackalloc byte[TotalSize];
 833            WriteBlockCore(buffer);
 834            stream.Write(buffer);
 835        }
 836
 837        public async Task WriteBlockAsync(Stream stream, CancellationToken cancellationToken = default)
 838        {
 839            byte[] buffer = new byte[TotalSize];
 840            WriteBlockCore(buffer);
 841            await stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
 842        }
 843
 844        private void WriteBlockCore(Span<byte> buffer)
 845        {
 846            BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(0), HeaderId);
 847            BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(2), DataSize);
 848            BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(4), VendorVersion);
 849            buffer[6] = (byte)'A';
 850            buffer[7] = (byte)'E';
 851            buffer[8] = AesStrength;
 852            BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(9), CompressionMethod);
 853        }
 854    }
 855
 856    internal sealed partial class ZipCentralDirectoryFileHeader
 857    {
 858        // The Zip File Format Specification references 0x02014B50, this is a big endian representation.
 859        // ZIP files store values in little endian, so this is reversed.
 860        public static ReadOnlySpan<byte> SignatureConstantBytes => [0x50, 0x4B, 0x01, 0x02];
 861
 862        private const int StackAllocationThreshold = 512;
 863
 864        // These are the minimum possible size, assuming the zip file comments variable section is empty
 865        public const int BlockConstantSectionSize = 46;
 866
 867        public byte VersionMadeByCompatibility;
 868        public byte VersionMadeBySpecification;
 869        public ushort VersionNeededToExtract;
 870        public ushort GeneralPurposeBitFlag;
 871        public ushort CompressionMethod;
 872        public uint LastModified; // convert this on the fly
 873        public uint Crc32;
 874        public long CompressedSize;
 875        public long UncompressedSize;
 876        public ushort FilenameLength;
 877        public ushort ExtraFieldLength;
 878        public ushort FileCommentLength;
 879        public uint DiskNumberStart;
 880        public ushort InternalFileAttributes;
 881        public uint ExternalFileAttributes;
 882        public long RelativeOffsetOfLocalHeader;
 883
 884        public byte[] Filename = [];
 885        public byte[] FileComment = [];
 886        public List<ZipGenericExtraField>? ExtraFields;
 887        public byte[]? TrailingExtraFieldData;
 888
 889        /// <summary>
 890        /// The WinZip AES extra field (0x9901) if present in the central directory.
 891        /// This is always parsed (regardless of saveExtraFieldsAndComments) so that
 892        /// ZipArchiveEntry can determine the real compression method for AES-encrypted entries
 893        /// without needing to read the local file header.
 894        /// </summary>
 895        public WinZipAesExtraField? AesExtraField;
 896
 897        private static bool TryReadBlockInitialize(ReadOnlySpan<byte> buffer, [NotNullWhen(returnValue: true)] out ZipCe
 898        {
 899            // the buffer will always be large enough for at least the constant section to be verified
 900            Debug.Assert(buffer.Length >= BlockConstantSectionSize);
 901
 902            header = null;
 903            bytesRead = 0;
 904            compressedSizeSmall = 0;
 905            uncompressedSizeSmall = 0;
 906            diskNumberStartSmall = 0;
 907            relativeOffsetOfLocalHeaderSmall = 0;
 908            if (!buffer.StartsWith(SignatureConstantBytes))
 909            {
 910                return false;
 911            }
 912
 913            header = new()
 914            {
 915                VersionMadeBySpecification = buffer[FieldLocations.VersionMadeBySpecification],
 916                VersionMadeByCompatibility = buffer[FieldLocations.VersionMadeByCompatibility],
 917                VersionNeededToExtract = BinaryPrimitives.ReadUInt16LittleEndian(buffer[FieldLocations.VersionNeededToEx
 918                GeneralPurposeBitFlag = BinaryPrimitives.ReadUInt16LittleEndian(buffer[FieldLocations.GeneralPurposeBitF
 919                CompressionMethod = BinaryPrimitives.ReadUInt16LittleEndian(buffer[FieldLocations.CompressionMethod..]),
 920                LastModified = BinaryPrimitives.ReadUInt32LittleEndian(buffer[FieldLocations.LastModified..]),
 921                Crc32 = BinaryPrimitives.ReadUInt32LittleEndian(buffer[FieldLocations.Crc32..]),
 922                FilenameLength = BinaryPrimitives.ReadUInt16LittleEndian(buffer[FieldLocations.FilenameLength..]),
 923                ExtraFieldLength = BinaryPrimitives.ReadUInt16LittleEndian(buffer[FieldLocations.ExtraFieldLength..]),
 924                FileCommentLength = BinaryPrimitives.ReadUInt16LittleEndian(buffer[FieldLocations.FileCommentLength..]),
 925                InternalFileAttributes = BinaryPrimitives.ReadUInt16LittleEndian(buffer[FieldLocations.InternalFileAttri
 926                ExternalFileAttributes = BinaryPrimitives.ReadUInt32LittleEndian(buffer[FieldLocations.ExternalFileAttri
 927            };
 928
 929            compressedSizeSmall = BinaryPrimitives.ReadUInt32LittleEndian(buffer[FieldLocations.CompressedSize..]);
 930            uncompressedSizeSmall = BinaryPrimitives.ReadUInt32LittleEndian(buffer[FieldLocations.UncompressedSize..]);
 931            diskNumberStartSmall = BinaryPrimitives.ReadUInt16LittleEndian(buffer[FieldLocations.DiskNumberStart..]);
 932            relativeOffsetOfLocalHeaderSmall = BinaryPrimitives.ReadUInt32LittleEndian(buffer[FieldLocations.RelativeOff
 933
 934            return true;
 935        }
 936
 937        private static void TryReadBlockFinalize(ZipCentralDirectoryFileHeader header, ReadOnlySpan<byte> dynamicHeader,
 938        {
 939            header.Filename = dynamicHeader[..header.FilenameLength].ToArray();
 940
 941            bool uncompressedSizeInZip64 = uncompressedSizeSmall == ZipHelper.Mask32Bit;
 942            bool compressedSizeInZip64 = compressedSizeSmall == ZipHelper.Mask32Bit;
 943            bool relativeOffsetInZip64 = relativeOffsetOfLocalHeaderSmall == ZipHelper.Mask32Bit;
 944            bool diskNumberStartInZip64 = diskNumberStartSmall == ZipHelper.Mask16Bit;
 945
 946            ReadOnlySpan<byte> zipExtraFields = dynamicHeader.Slice(header.FilenameLength, header.ExtraFieldLength);
 947
 948            // Always parse AES extra field (0x9901) from the central directory if present.
 949            // This is needed so ZipArchiveEntry can determine the real compression method
 950            // for AES-encrypted entries without requiring Open() to be called.
 951            if (WinZipAesExtraField.TryGetFromRawExtraFieldData(zipExtraFields, out WinZipAesExtraField aesField))
 952            {
 953                header.AesExtraField = aesField;
 954            }
 955
 956            if (saveExtraFieldsAndComments)
 957            {
 958                header.ExtraFields = ZipGenericExtraField.ParseExtraField(zipExtraFields, out ReadOnlySpan<byte> trailin
 959                zip64 = Zip64ExtraField.GetAndRemoveZip64Block(header.ExtraFields,
 960                            uncompressedSizeInZip64, compressedSizeInZip64,
 961                            relativeOffsetInZip64, diskNumberStartInZip64,
 962                            isInLocalHeader: false);
 963                header.TrailingExtraFieldData = trailingDataSpan.ToArray();
 964            }
 965            else
 966            {
 967                header.ExtraFields = null;
 968                header.TrailingExtraFieldData = null;
 969                zip64 = Zip64ExtraField.GetJustZip64Block(zipExtraFields,
 970                            uncompressedSizeInZip64, compressedSizeInZip64,
 971                            relativeOffsetInZip64, diskNumberStartInZip64,
 972                            isInLocalHeader: false);
 973            }
 974
 975            header.FileComment = dynamicHeader.Slice(header.FilenameLength + header.ExtraFieldLength, header.FileComment
 976
 977            bytesRead = FieldLocations.DynamicData + dynamicHeaderSize;
 978
 979            header.UncompressedSize = zip64.UncompressedSize ?? uncompressedSizeSmall;
 980            header.CompressedSize = zip64.CompressedSize ?? compressedSizeSmall;
 981            header.RelativeOffsetOfLocalHeader = zip64.LocalHeaderOffset ?? relativeOffsetOfLocalHeaderSmall;
 982            header.DiskNumberStart = zip64.StartDiskNumber ?? diskNumberStartSmall;
 983        }
 984
 985        // if saveExtraFieldsAndComments is false, FileComment and ExtraFields will be null
 986        // in either case, the zip64 extra field info will be incorporated into other fields
 987        public static bool TryReadBlock(ReadOnlySpan<byte> buffer, Stream furtherReads, bool saveExtraFieldsAndComments,
 988        {
 989            if (!TryReadBlockInitialize(buffer, out header, out bytesRead, out uint compressedSizeSmall, out uint uncomp
 990            {
 991                return false;
 992            }
 993
 994            byte[]? arrayPoolBuffer = null;
 995            try
 996            {
 997                // Assemble the dynamic header in a separate buffer. We can't guarantee that it's all in the input buffe
 998                // some additional data might need to come from the stream.
 999                int dynamicHeaderSize = header.FilenameLength + header.ExtraFieldLength + header.FileCommentLength;
 1000                int remainingBufferLength = buffer.Length - FieldLocations.DynamicData;
 1001                int bytesToRead = dynamicHeaderSize - remainingBufferLength;
 1002                scoped ReadOnlySpan<byte> dynamicHeader;
 1003
 1004                // No need to read extra data from the stream, no need to allocate a new buffer.
 1005                if (bytesToRead <= 0)
 1006                {
 1007                    dynamicHeader = buffer[FieldLocations.DynamicData..];
 1008                }
 1009                // Data needs to come from two sources, and we must thus copy data into a single address space.
 1010                else
 1011                {
 1012                    if (dynamicHeaderSize > StackAllocationThreshold)
 1013                    {
 1014                        arrayPoolBuffer = ArrayPool<byte>.Shared.Rent(dynamicHeaderSize);
 1015                    }
 1016
 1017                    Span<byte> collatedHeader = dynamicHeaderSize <= StackAllocationThreshold ? stackalloc byte[StackAll
 1018
 1019                    buffer[FieldLocations.DynamicData..].CopyTo(collatedHeader);
 1020
 1021                    Debug.Assert(bytesToRead == collatedHeader[remainingBufferLength..].Length);
 1022                    int realBytesRead = furtherReads.ReadAtLeast(collatedHeader[remainingBufferLength..], bytesToRead, t
 1023
 1024                    if (realBytesRead != bytesToRead)
 1025                    {
 1026                        return false;
 1027                    }
 1028                    dynamicHeader = collatedHeader;
 1029                }
 1030
 1031                TryReadBlockFinalize(header, dynamicHeader, dynamicHeaderSize, uncompressedSizeSmall, compressedSizeSmal
 1032            }
 1033            finally
 1034            {
 1035                if (arrayPoolBuffer != null)
 1036                {
 1037                    ArrayPool<byte>.Shared.Return(arrayPoolBuffer);
 1038                }
 1039            }
 1040
 1041            return true;
 1042        }
 1043    }
 1044
 1045    internal sealed partial class ZipEndOfCentralDirectoryBlock
 1046    {
 1047        // The Zip File Format Specification references 0x06054B50, this is a big endian representation.
 1048        // ZIP files store values in little endian, so this is reversed.
 1049        public static readonly byte[] SignatureConstantBytes = [0x50, 0x4B, 0x05, 0x06];
 1050
 1051        // This also assumes a zero-length comment.
 1052        public const int TotalSize = FieldLocations.ArchiveCommentLength + FieldLengths.ArchiveCommentLength;
 1053        // These are the minimum possible size, assuming the zip file comments variable section is empty
 1054        public const int SizeOfBlockWithoutSignature = TotalSize - FieldLengths.Signature;
 1055
 1056        // The end of central directory can have a variable size zip file comment at the end, but its max length can be 
 1057        // The Zip File Format Specification does not explicitly mention a max size for this field, but we are assuming 
 1058        // max size because that is the maximum value an ushort can hold.
 1059        public const int ZipFileCommentMaxLength = ushort.MaxValue;
 1060
 1061        public uint Signature;
 1062        public ushort NumberOfThisDisk;
 1063        public ushort NumberOfTheDiskWithTheStartOfTheCentralDirectory;
 1064        public ushort NumberOfEntriesInTheCentralDirectoryOnThisDisk;
 1065        public ushort NumberOfEntriesInTheCentralDirectory;
 1066        public uint SizeOfCentralDirectory;
 1067        public uint OffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber;
 1068        private byte[]? _archiveComment;
 1069        public byte[] ArchiveComment => _archiveComment ??= [];
 1070
 1071        private static void WriteBlockInitialize(Span<byte> blockContents, long numberOfEntries, long startOfCentralDire
 1072        {
 1073            ushort numberOfEntriesTruncated = numberOfEntries > ushort.MaxValue ?
 1074                                                        ZipHelper.Mask16Bit : (ushort)numberOfEntries;
 1075            uint startOfCentralDirectoryTruncated = startOfCentralDirectory > uint.MaxValue ?
 1076                                                        ZipHelper.Mask32Bit : (uint)startOfCentralDirectory;
 1077            uint sizeOfCentralDirectoryTruncated = sizeOfCentralDirectory > uint.MaxValue ?
 1078                                                        ZipHelper.Mask32Bit : (uint)sizeOfCentralDirectory;
 1079
 1080            SignatureConstantBytes.CopyTo(blockContents[FieldLocations.Signature..]);
 1081            // number of this disk
 1082            BinaryPrimitives.WriteUInt16LittleEndian(blockContents[FieldLocations.NumberOfThisDisk..], 0);
 1083            // number of disk with start of CD
 1084            BinaryPrimitives.WriteUInt16LittleEndian(blockContents[FieldLocations.NumberOfTheDiskWithTheStartOfTheCentra
 1085            // number of entries on this disk's cd
 1086            BinaryPrimitives.WriteUInt16LittleEndian(blockContents[FieldLocations.NumberOfEntriesInTheCentralDirectoryOn
 1087            // number of entries in entire cd
 1088            BinaryPrimitives.WriteUInt16LittleEndian(blockContents[FieldLocations.NumberOfEntriesInTheCentralDirectory..
 1089            BinaryPrimitives.WriteUInt32LittleEndian(blockContents[FieldLocations.SizeOfCentralDirectory..], sizeOfCentr
 1090            BinaryPrimitives.WriteUInt32LittleEndian(blockContents[FieldLocations.OffsetOfStartOfCentralDirectoryWithRes
 1091
 1092            // Should be valid because of how we read archiveComment in TryReadBlock:
 1093            Debug.Assert(archiveComment.Length <= ZipFileCommentMaxLength);
 1094
 1095            // zip file comment length
 1096            BinaryPrimitives.WriteUInt16LittleEndian(blockContents[FieldLocations.ArchiveCommentLength..], (ushort)archi
 1097        }
 1098
 1099        public static unsafe void WriteBlock(Stream stream, long numberOfEntries, long startOfCentralDirectory, long siz
 1100        {
 1101            Span<byte> blockContents = stackalloc byte[TotalSize];
 1102
 1103            WriteBlockInitialize(blockContents, numberOfEntries, startOfCentralDirectory, sizeOfCentralDirectory, archiv
 1104
 1105            stream.Write(blockContents);
 1106            if (archiveComment.Length > 0)
 1107            {
 1108                stream.Write(archiveComment);
 1109            }
 1110        }
 1111
 1112        private static bool TryReadBlockInitialize(Stream stream, Span<byte> blockContents, int bytesRead, [NotNullWhen(
 1113        {
 1114            readComment = false;
 1115
 1116            eocdBlock = null;
 1117            if (bytesRead < TotalSize)
 1118            {
 1119                return false;
 1120            }
 1121
 1122            if (!blockContents.StartsWith(SignatureConstantBytes))
 1123            {
 1124                return false;
 1125            }
 1126            eocdBlock = new()
 1127            {
 1128                Signature = BinaryPrimitives.ReadUInt32LittleEndian(blockContents[FieldLocations.Signature..]),
 1129                NumberOfThisDisk = BinaryPrimitives.ReadUInt16LittleEndian(blockContents[FieldLocations.NumberOfThisDisk
 1130                NumberOfTheDiskWithTheStartOfTheCentralDirectory = BinaryPrimitives.ReadUInt16LittleEndian(blockContents
 1131                NumberOfEntriesInTheCentralDirectoryOnThisDisk = BinaryPrimitives.ReadUInt16LittleEndian(blockContents[F
 1132                NumberOfEntriesInTheCentralDirectory = BinaryPrimitives.ReadUInt16LittleEndian(blockContents[FieldLocati
 1133                SizeOfCentralDirectory = BinaryPrimitives.ReadUInt32LittleEndian(blockContents[FieldLocations.SizeOfCent
 1134                OffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber =
 1135                    BinaryPrimitives.ReadUInt32LittleEndian(blockContents[FieldLocations.OffsetOfStartOfCentralDirectory
 1136            };
 1137
 1138            ushort commentLength = BinaryPrimitives.ReadUInt16LittleEndian(blockContents[FieldLocations.ArchiveCommentLe
 1139
 1140            if (stream.Position + commentLength > stream.Length)
 1141            {
 1142                return false;
 1143            }
 1144
 1145            if (commentLength == 0)
 1146            {
 1147                eocdBlock._archiveComment = [];
 1148            }
 1149            else
 1150            {
 1151                eocdBlock._archiveComment = new byte[commentLength];
 1152                readComment = true;
 1153            }
 1154
 1155            return true;
 1156        }
 1157
 1158        public static unsafe ZipEndOfCentralDirectoryBlock ReadBlock(Stream stream)
 1159        {
 1160            Span<byte> blockContents = stackalloc byte[TotalSize];
 1161            int bytesRead = stream.ReadAtLeast(blockContents, blockContents.Length, throwOnEndOfStream: false);
 1162
 1163            if (!TryReadBlockInitialize(stream, blockContents, bytesRead, out ZipEndOfCentralDirectoryBlock? eocdBlock, 
 1164            {
 1165                // // We shouldn't get here becasue we found the eocd block using the signature finder
 1166                throw new InvalidDataException(SR.EOCDNotFound);
 1167            }
 1168            else if (readComment)
 1169            {
 1170                stream.ReadExactly(eocdBlock._archiveComment);
 1171            }
 1172
 1173            return eocdBlock;
 1174        }
 1175    }
 1176}