// Copyright (c) 2026 Timofey Solomko // Licensed under MIT License // // See LICENSE for license information import BitByteData struct ZipCentralDirectoryEntry { let versionMadeBy: UInt16 let versionNeeded: UInt16 let generalPurposeBitFlags: UInt16 let compressionMethod: UInt16 let lastModFileTime: UInt16 let lastModFileDate: UInt16 let crc32: UInt32 private(set) var compSize: UInt64 private(set) var uncompSize: UInt64 let fileName: String let fileComment: String private(set) var diskNumberStart: UInt32 let internalFileAttributes: UInt16 let externalFileAttributes: UInt32 private(set) var localHeaderOffset: UInt64 // 0x5455 extra field. private(set) var extendedTimestampExtraField: ExtendedTimestampExtraField? /// 0x000a extra field. private(set) var ntfsExtraField: NtfsExtraField? // 0x7855 extra field doesn't have any information in Central Directory. /// 0x7875 extra field. private(set) var infoZipNewUnixExtraField: InfoZipNewUnixExtraField? let customExtraFields: [ZipExtraField] let nextEntryOffset: Int init(_ byteReader: LittleEndianByteReader) throws { // Check signature. guard byteReader.uint32() == 0x02014b50 else { throw ZipError.wrongSignature } self.versionMadeBy = byteReader.uint16() self.versionNeeded = byteReader.uint16() self.generalPurposeBitFlags = byteReader.uint16() let useUtf8 = generalPurposeBitFlags & 0x800 != 0 self.compressionMethod = byteReader.uint16() self.lastModFileTime = byteReader.uint16() self.lastModFileDate = byteReader.uint16() self.crc32 = byteReader.uint32() self.compSize = byteReader.uint64(fromBytes: 4) self.uncompSize = byteReader.uint64(fromBytes: 4) let fileNameLength = byteReader.int(fromBytes: 2) let extraFieldLength = byteReader.int(fromBytes: 2) let fileCommentLength = byteReader.int(fromBytes: 2) self.diskNumberStart = byteReader.uint32(fromBytes: 2) self.internalFileAttributes = byteReader.uint16() self.externalFileAttributes = byteReader.uint32() self.localHeaderOffset = byteReader.uint64(fromBytes: 4) guard let fileName = byteReader.zipString(fileNameLength, useUtf8) else { throw ZipError.wrongTextField } self.fileName = fileName let extraFieldStart = byteReader.offset var customExtraFields = [ZipExtraField]() while byteReader.offset - extraFieldStart < extraFieldLength { // There are a lot of possible extra fields. let headerID = byteReader.uint16() let size = byteReader.int(fromBytes: 2) switch headerID { case 0x0001: // Zip64 // Zip64 extra field is a special case, because it requires knowledge about central directory fields. if self.uncompSize == 0xFFFFFFFF { self.uncompSize = byteReader.uint64() } if self.compSize == 0xFFFFFFFF { self.compSize = byteReader.uint64() } if self.localHeaderOffset == 0xFFFFFFFF { self.localHeaderOffset = byteReader.uint64() } if self.diskNumberStart == 0xFFFF { self.diskNumberStart = byteReader.uint32() } case 0x5455: // Extended Timestamp self.extendedTimestampExtraField = ExtendedTimestampExtraField(byteReader, size, location: .centralDirectory) case 0x000a: // NTFS Extra Fields self.ntfsExtraField = NtfsExtraField(byteReader, size, location: .centralDirectory) case 0x7855: // Info-ZIP Unix Extra Field // If there is any data for Info-ZIP Unix extra field in central directory (`size != 0`), skip it. // However, according to definition of this extra field it shouldn't have any data in CD. byteReader.offset += size case 0x7875: // Info-ZIP New Unix Extra Field self.infoZipNewUnixExtraField = InfoZipNewUnixExtraField(byteReader, size, location: .centralDirectory) default: let customFieldOffset = byteReader.offset if let customExtraFieldType = ZipContainer.customExtraFields[headerID], customExtraFieldType.id == headerID, let customExtraField = customExtraFieldType.init(byteReader, size, location: .centralDirectory), customExtraField.id == headerID { precondition(customExtraField.location == .centralDirectory, "Custom field in Central Directory with ID=\(headerID) of type=\(customExtraFieldType)" + " changed location.") precondition(customExtraField.size == size, "Custom field in Central Directory with ID=\(headerID) of type=\(customExtraFieldType)" + " changed size.") guard byteReader.offset == customFieldOffset + size else { fatalError("Custom field in Central Directory with ID=\(headerID) of" + "type=\(customExtraFieldType) failed to read exactly \(size) bytes.") } customExtraFields.append(customExtraField) } else { byteReader.offset = customFieldOffset + size } } } self.customExtraFields = customExtraFields guard let fileComment = byteReader.zipString(fileCommentLength, useUtf8) else { throw ZipError.wrongTextField } self.fileComment = fileComment self.nextEntryOffset = byteReader.offset } }