| | | 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 | | |
| | | 4 | | using System.Buffers; |
| | | 5 | | using System.Buffers.Binary; |
| | | 6 | | using System.Collections.Generic; |
| | | 7 | | using System.Diagnostics; |
| | | 8 | | using System.Diagnostics.CodeAnalysis; |
| | | 9 | | using System.Runtime.InteropServices; |
| | | 10 | | using System.Threading; |
| | | 11 | | using System.Threading.Tasks; |
| | | 12 | | |
| | | 13 | | namespace 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 | | |
| | 2312 | 25 | | public ushort Tag => _tag; |
| | | 26 | | // returns size of data, not of the entire block |
| | 2924 | 27 | | public ushort Size => _size; |
| | 310 | 28 | | public byte[] Data => _data ??= []; |
| | | 29 | | |
| | | 30 | | public unsafe void WriteBlock(Stream stream) |
| | 0 | 31 | | { |
| | 0 | 32 | | Span<byte> extraFieldHeader = stackalloc byte[SizeOfHeader]; |
| | 0 | 33 | | WriteBlockCore(extraFieldHeader); |
| | 0 | 34 | | stream.Write(extraFieldHeader); |
| | 0 | 35 | | stream.Write(Data); |
| | 0 | 36 | | } |
| | | 37 | | |
| | | 38 | | private void WriteBlockCore(Span<byte> extraFieldHeader) |
| | 0 | 39 | | { |
| | 0 | 40 | | BinaryPrimitives.WriteUInt16LittleEndian(extraFieldHeader[FieldLocations.Tag..], _tag); |
| | 0 | 41 | | BinaryPrimitives.WriteUInt16LittleEndian(extraFieldHeader[FieldLocations.Size..], _size); |
| | 0 | 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) |
| | 31884 | 46 | | { |
| | 31884 | 47 | | field = new(); |
| | 31884 | 48 | | bytesConsumed = 0; |
| | | 49 | | |
| | | 50 | | // not enough bytes to read tag + size |
| | 31884 | 51 | | if (bytes.Length < SizeOfHeader) |
| | 29152 | 52 | | { |
| | 29152 | 53 | | return false; |
| | | 54 | | } |
| | | 55 | | |
| | 2732 | 56 | | field._tag = BinaryPrimitives.ReadUInt16LittleEndian(bytes[FieldLocations.Tag..]); |
| | 2732 | 57 | | field._size = BinaryPrimitives.ReadUInt16LittleEndian(bytes[FieldLocations.Size..]); |
| | | 58 | | |
| | | 59 | | // not enough byte to read the data |
| | 2732 | 60 | | if ((bytes.Length - SizeOfHeader) < field._size) |
| | 420 | 61 | | { |
| | 420 | 62 | | return false; |
| | | 63 | | } |
| | | 64 | | |
| | 2312 | 65 | | field._data = bytes.Slice(FieldLocations.DynamicData, field._size).ToArray(); |
| | 2312 | 66 | | bytesConsumed = field.Size + SizeOfHeader; |
| | 2312 | 67 | | return true; |
| | 31884 | 68 | | } |
| | | 69 | | |
| | | 70 | | public static List<ZipGenericExtraField> ParseExtraField(ReadOnlySpan<byte> extraFieldData, out ReadOnlySpan<byt |
| | 0 | 71 | | { |
| | 0 | 72 | | List<ZipGenericExtraField> extraFields = new List<ZipGenericExtraField>(); |
| | 0 | 73 | | int totalBytesConsumed = 0; |
| | | 74 | | |
| | 0 | 75 | | while (TryReadBlock(extraFieldData[totalBytesConsumed..], out int currBytesConsumed, out ZipGenericExtraFiel |
| | 0 | 76 | | { |
| | 0 | 77 | | totalBytesConsumed += currBytesConsumed; |
| | 0 | 78 | | extraFields.Add(field); |
| | 0 | 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. |
| | 0 | 85 | | trailingExtraFieldData = extraFieldData[totalBytesConsumed..]; |
| | | 86 | | |
| | 0 | 87 | | return extraFields; |
| | 0 | 88 | | } |
| | | 89 | | |
| | | 90 | | public static int TotalSize(List<ZipGenericExtraField>? fields, int trailingDataLength) |
| | 0 | 91 | | { |
| | 0 | 92 | | int size = trailingDataLength; |
| | | 93 | | |
| | 0 | 94 | | if (fields != null) |
| | 0 | 95 | | { |
| | 0 | 96 | | foreach (ZipGenericExtraField field in fields) |
| | 0 | 97 | | { |
| | 0 | 98 | | size += field.Size + SizeOfHeader; //size is only size of data |
| | 0 | 99 | | } |
| | 0 | 100 | | } |
| | 0 | 101 | | return size; |
| | 0 | 102 | | } |
| | | 103 | | |
| | | 104 | | public static void WriteAllBlocks(List<ZipGenericExtraField>? fields, ReadOnlySpan<byte> trailingExtraFieldData, |
| | 0 | 105 | | { |
| | 0 | 106 | | if (fields != null) |
| | 0 | 107 | | { |
| | 0 | 108 | | foreach (ZipGenericExtraField field in fields) |
| | 0 | 109 | | { |
| | 0 | 110 | | field.WriteBlock(stream); |
| | 0 | 111 | | } |
| | 0 | 112 | | } |
| | | 113 | | |
| | 0 | 114 | | if (!trailingExtraFieldData.IsEmpty) |
| | 0 | 115 | | { |
| | 0 | 116 | | stream.Write(trailingExtraFieldData); |
| | 0 | 117 | | } |
| | 0 | 118 | | } |
| | | 119 | | |
| | | 120 | | public static void WriteAllBlocksExcludingTag(List<ZipGenericExtraField>? fields, ReadOnlySpan<byte> trailingExt |
| | 0 | 121 | | { |
| | 0 | 122 | | if (fields != null) |
| | 0 | 123 | | { |
| | 0 | 124 | | foreach (ZipGenericExtraField field in fields) |
| | 0 | 125 | | { |
| | 0 | 126 | | if (field.Tag != excludeTag) |
| | 0 | 127 | | { |
| | 0 | 128 | | field.WriteBlock(stream); |
| | 0 | 129 | | } |
| | 0 | 130 | | } |
| | 0 | 131 | | } |
| | | 132 | | |
| | 0 | 133 | | if (!trailingExtraFieldData.IsEmpty) |
| | 0 | 134 | | { |
| | 0 | 135 | | stream.Write(trailingExtraFieldData); |
| | 0 | 136 | | } |
| | 0 | 137 | | } |
| | | 138 | | |
| | | 139 | | public static int TotalSizeExcludingTag(List<ZipGenericExtraField>? fields, int trailingDataLength, ushort exclu |
| | 0 | 140 | | { |
| | 0 | 141 | | int size = trailingDataLength; |
| | | 142 | | |
| | 0 | 143 | | if (fields != null) |
| | 0 | 144 | | { |
| | 0 | 145 | | foreach (ZipGenericExtraField field in fields) |
| | 0 | 146 | | { |
| | 0 | 147 | | if (field.Tag != excludeTag) |
| | 0 | 148 | | { |
| | 0 | 149 | | size += field.Size + ZipGenericExtraField.FieldLengths.Tag + ZipGenericExtraField.FieldLengths.S |
| | 0 | 150 | | } |
| | 0 | 151 | | } |
| | 0 | 152 | | } |
| | | 153 | | |
| | 0 | 154 | | return size; |
| | 0 | 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. |
| | | 606 | | public static ReadOnlySpan<byte> DataDescriptorSignatureConstantBytes => [0x50, 0x4B, 0x07, 0x08]; |
| | | 607 | | 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 |
| | | 611 | | { |
| | | 612 | | relativeFilenameLengthLocation = FieldLocations.FilenameLength - FieldLocations.FilenameLength; |
| | | 613 | | relativeExtraFieldLengthLocation = FieldLocations.ExtraFieldLength - FieldLocations.FilenameLength; |
| | | 614 | | stream.Seek(FieldLocations.FilenameLength, SeekOrigin.Current); |
| | | 615 | | } |
| | | 616 | | |
| | | 617 | | private static void GetExtraFieldsCore(Span<byte> fixedHeaderBuffer, int relativeFilenameLengthLocation, int rel |
| | | 618 | | { |
| | | 619 | | filenameLength = BinaryPrimitives.ReadUInt16LittleEndian(fixedHeaderBuffer[relativeFilenameLengthLocation..] |
| | | 620 | | extraFieldLength = BinaryPrimitives.ReadUInt16LittleEndian(fixedHeaderBuffer[relativeExtraFieldLengthLocatio |
| | | 621 | | } |
| | | 622 | | |
| | | 623 | | private static List<ZipGenericExtraField> GetExtraFieldPostReadWork(Span<byte> extraFieldBuffer, out byte[] trai |
| | | 624 | | { |
| | | 625 | | List<ZipGenericExtraField> list = ZipGenericExtraField.ParseExtraField(extraFieldBuffer, out ReadOnlySpan<by |
| | | 626 | | Zip64ExtraField.RemoveZip64Blocks(list); |
| | | 627 | | trailingData = trailingDataSpan.ToArray(); |
| | | 628 | | return list; |
| | | 629 | | } |
| | | 630 | | |
| | | 631 | | public static unsafe List<ZipGenericExtraField> GetExtraFields(Stream stream, out byte[] trailingData) |
| | | 632 | | { |
| | | 633 | | // assumes that TrySkipBlock has already been called, so we don't have to validate twice |
| | | 634 | | |
| | | 635 | | Span<byte> fixedHeaderBuffer = stackalloc byte[FieldLengths.FilenameLength + FieldLengths.ExtraFieldLength]; |
| | | 636 | | GetExtraFieldsInitialize(stream, out int relativeFilenameLengthLocation, out int relativeExtraFieldLengthLoc |
| | | 637 | | stream.ReadExactly(fixedHeaderBuffer); |
| | | 638 | | |
| | | 639 | | GetExtraFieldsCore(fixedHeaderBuffer, relativeFilenameLengthLocation, relativeExtraFieldLengthLocation, out |
| | | 640 | | |
| | | 641 | | const int StackAllocationThreshold = 512; |
| | | 642 | | |
| | | 643 | | byte[]? arrayPoolBuffer = extraFieldLength > StackAllocationThreshold ? ArrayPool<byte>.Shared.Rent(extraFie |
| | | 644 | | Span<byte> extraFieldBuffer = extraFieldLength <= StackAllocationThreshold ? stackalloc byte[StackAllocation |
| | | 645 | | try |
| | | 646 | | { |
| | | 647 | | stream.Seek(filenameLength, SeekOrigin.Current); |
| | | 648 | | stream.ReadExactly(extraFieldBuffer); |
| | | 649 | | |
| | | 650 | | return GetExtraFieldPostReadWork(extraFieldBuffer, out trailingData); |
| | | 651 | | } |
| | | 652 | | finally |
| | | 653 | | { |
| | | 654 | | if (arrayPoolBuffer != null) |
| | | 655 | | { |
| | | 656 | | ArrayPool<byte>.Shared.Return(arrayPoolBuffer); |
| | | 657 | | } |
| | | 658 | | } |
| | | 659 | | } |
| | | 660 | | |
| | | 661 | | private static bool TrySkipBlockCore(Stream stream, Span<byte> blockBytes, int bytesRead) |
| | | 662 | | { |
| | | 663 | | if (bytesRead != FieldLengths.Signature || !blockBytes.SequenceEqual(SignatureConstantBytes)) |
| | | 664 | | { |
| | | 665 | | return false; |
| | | 666 | | } |
| | | 667 | | |
| | | 668 | | // Already read the signature, so make the filename length field location relative to that |
| | | 669 | | 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. |
| | | 673 | | Debug.Assert(blockBytes.Length == FieldLengths.FilenameLength + FieldLengths.ExtraFieldLength); |
| | | 674 | | |
| | | 675 | | return true; |
| | | 676 | | } |
| | | 677 | | |
| | | 678 | | private static bool TrySkipBlockFinalize(Stream stream, Span<byte> blockBytes, int bytesRead) |
| | | 679 | | { |
| | | 680 | | if (bytesRead != FieldLengths.FilenameLength + FieldLengths.ExtraFieldLength) |
| | | 681 | | { |
| | | 682 | | return false; |
| | | 683 | | } |
| | | 684 | | |
| | | 685 | | int relativeFilenameLengthLocation = FieldLocations.FilenameLength - FieldLocations.FilenameLength; |
| | | 686 | | int relativeExtraFieldLengthLocation = FieldLocations.ExtraFieldLength - FieldLocations.FilenameLength; |
| | | 687 | | ushort filenameLength = BinaryPrimitives.ReadUInt16LittleEndian(blockBytes[relativeFilenameLengthLocation..] |
| | | 688 | | ushort extraFieldLength = BinaryPrimitives.ReadUInt16LittleEndian(blockBytes[relativeExtraFieldLengthLocatio |
| | | 689 | | |
| | | 690 | | if (stream.Length < stream.Position + filenameLength + extraFieldLength) |
| | | 691 | | { |
| | | 692 | | 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 |
| | | 698 | | long dataStart = stream.Position + filenameLength + extraFieldLength; |
| | | 699 | | stream.Seek(dataStart, SeekOrigin.Begin); |
| | | 700 | | |
| | | 701 | | return true; |
| | | 702 | | } |
| | | 703 | | |
| | | 704 | | // will not throw end of stream exception |
| | | 705 | | public static unsafe bool TrySkipBlock(Stream stream) |
| | | 706 | | { |
| | | 707 | | Span<byte> blockBytes = stackalloc byte[FieldLengths.Signature]; |
| | | 708 | | int bytesRead = stream.ReadAtLeast(blockBytes, blockBytes.Length, throwOnEndOfStream: false); |
| | | 709 | | if (!TrySkipBlockCore(stream, blockBytes, bytesRead)) |
| | | 710 | | { |
| | | 711 | | return false; |
| | | 712 | | } |
| | | 713 | | bytesRead = stream.ReadAtLeast(blockBytes, blockBytes.Length, throwOnEndOfStream: false); |
| | | 714 | | return TrySkipBlockFinalize(stream, blockBytes, bytesRead); |
| | | 715 | | } |
| | | 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 | | } |