// Copyright (c) 2026 Timofey Solomko // Licensed under MIT License // // See LICENSE for license information import Foundation import BitByteData /// Represents the header of a GZip archive. public struct GzipHeader: Sendable { struct Flags: OptionSet { let rawValue: UInt8 init(rawValue: UInt8) { self.rawValue = rawValue } static let ftext = Flags(rawValue: 0x01) static let fhcrc = Flags(rawValue: 0x02) static let fextra = Flags(rawValue: 0x04) static let fname = Flags(rawValue: 0x08) static let fcomment = Flags(rawValue: 0x10) } /// Compression method of archive. Always `.deflate` for GZip archives. public var compressionMethod: CompressionMethod /** The most recent modification time of the original file. If corresponding archive's field is set to 0, which means that no time was specified, then this property is `nil`. */ public var modificationTime: Date? /// Type of file system on which archivation took place. public var osType: FileSystemType /// Name of the original file. If archive doesn't contain file's name, then `nil`. public var fileName: String? /// Comment stored in archive. If archive doesn't contain any comment, then `nil`. public var comment: String? /// True, if file is likely to be text file or ASCII-file. public var isTextFile: Bool /** Extra fields present in the header. - Note: This feature of the GZip format is extremely rarely used, so in vast majority of cases this property contains an empty array. */ public var extraFields: [ExtraField] /** Initializes the structure with the values from the first 'member' of GZip `archive`. - Parameter archive: Data archived with GZip. - Throws: `GzipError`. It may indicate that either archive is damaged or it might not be archived with GZip at all. */ public init(archive data: Data) throws { let reader = LsbBitReader(data: data) try self.init(reader) } init(_ reader: LsbBitReader) throws { // Valid GZip header must contain at least 10 bytes of data. guard reader.bytesLeft >= 10 else { throw GzipError.wrongMagic } // First two bytes should be correct 'magic' bytes let magic = reader.uint16() guard magic == 0x8b1f else { throw GzipError.wrongMagic } var headerBytes: [UInt8] = [0x1f, 0x8b] // Third byte is a method of compression. Only type 8 (DEFLATE) compression is supported for GZip archives. let method = reader.byte() guard method == 8 else { throw GzipError.wrongCompressionMethod } headerBytes.append(method) self.compressionMethod = .deflate let rawFlags = reader.byte() guard rawFlags & 0xE0 == 0 else { throw GzipError.wrongFlags } let flags = Flags(rawValue: rawFlags) headerBytes.append(rawFlags) var mtime = 0 for i in 0..<4 { let byte = reader.byte() mtime |= byte.toInt() << (8 * i) headerBytes.append(byte) } self.modificationTime = mtime == 0 ? nil : Date(timeIntervalSince1970: TimeInterval(mtime)) let extraFlags = reader.byte() headerBytes.append(extraFlags) let rawOsType = reader.byte() self.osType = FileSystemType(rawOsType) headerBytes.append(rawOsType) self.isTextFile = flags.contains(.ftext) // Some archives may contain extra fields. self.extraFields = [ExtraField]() if flags.contains(.fextra) { guard reader.bytesLeft >= 2 else { throw GzipError.wrongMagic } var xlen = 0 for i in 0..<2 { let byte = reader.byte() xlen |= byte.toInt() << (8 * i) headerBytes.append(byte) } // There must be least four bytes of extra fields for SI1, SI2, and 2 bytes of the length parameter // filled with zeros (minimal variant). guard reader.bytesLeft >= xlen && xlen >= 4 else { throw GzipError.wrongMagic } while xlen > 0 { let si1 = reader.byte() headerBytes.append(si1) let si2 = reader.byte() // IDs with zero in the second byte are reserved. guard si2 != 0 else { throw GzipError.wrongFlags } headerBytes.append(si2) var len = 0 for i in 0..<2 { let byte = reader.byte() len |= byte.toInt() << (8 * i) headerBytes.append(byte) } xlen -= 4 // Total remaining extra fields length must be larger than the length of the binary content of the // current extra field. guard xlen >= len else { throw GzipError.wrongMagic } var extraFieldBytes = [UInt8]() for _ in 0..= 2 else { throw GzipError.wrongMagic } let crc16 = reader.uint16() guard CheckSums.crc32(headerBytes) & 0xFFFF == crc16 else { throw GzipError.wrongHeaderCRC } } } }