// // ZipContainer.swift // SWCompression // // Created by Timofey Solomko on 14.01.17. // Copyright © 2017 Timofey Solomko. All rights reserved. // import Foundation /** Error happened during processing ZIP archive (container). It may indicate that either the data is damaged or it might not be ZIP archive (container) at all. - `notFoundCentralDirectoryEnd`: end of Central Directoty record wasn't found. - `wrongSignature`: unsupported signature of one of ZIP container's structures. - `wrongSize`: incorrect compressed or uncompressed size of a ZIP container's entry's data. - `wrongVersion`: unsupported number of version needed to extract ZIP container (unsupported features are required to open this file). - `multiVolumesNotSupported`: unsupported feature: multi-volumed or spanned container. - `encryptionNotSupported`: unsupported feature: encryption. - `patchingNotSupported`: unsupported feature: patched data. - `compressionNotSupported`: unsupported feature: specified compression method. - `wrongLocalHeader`: local header of an entry wasn't consistent with Central Directory record. - `wrongCRC32`: computed checksum of entry's data wasn't the same as the one stored in container. */ public enum ZipError: Error { /// End of Central Directoty record was not found. case notFoundCentralDirectoryEnd /// Wrong signature of one of ZIP container's structures. case wrongSignature /// Wrong either compressed or uncompressed size of a ZIP container's entry. case wrongSize /// Wrong number of version needed to extract ZIP container. case wrongVersion /// Archive either spanned or consists of several volumes. This feature is not supported. case multiVolumesNotSupported /// Entry or record is encrypted. This feature is not supported. case encryptionNotSupported /// Entry contains patched data. This feature is not supported. case patchingNotSupported /// Wrong compression method of an entry. case compressionNotSupported /// Wrong local header of an entry. case wrongLocalHeader /** Computed CRC32 of entry's data didn't match the value stored in the container. Associated value contains extracted data. */ case wrongCRC32(Data) case wrongTextField } /// Represents either a file or directory entry inside ZIP archive. public class ZipEntry: ContainerEntry { private let cdEntry: CentralDirectoryEntry private var pointerData: DataWithPointer /// Name of the file or directory. public var name: String? { return self.cdEntry.fileName } /// Comment associated with the entry. public var comment: String? { return self.cdEntry.fileComment } /// File or directory attributes related to the file system of archive's creator. public var attributes: UInt32 { return self.cdEntry.externalFileAttributes } /** Returns data associated with this entry. - Note: Returned `Data` object with the size of 0 can either indicate that the entry is an empty file or it is a directory. - Throws: `ZipError` or any other error associated with compression type, depending on the type of inconsistency in data. An error can indicate that the container is damaged. */ public func data() throws -> Data { // Now, let's move to the location of local header. pointerData.index = Int(UInt32(truncatingBitPattern: self.cdEntry.offset)) let localHeader = try LocalHeader(&pointerData) // Check local header for consistency with Central Directory entry. guard localHeader.versionNeeded <= 45 && localHeader.generalPurposeBitFlags == cdEntry.generalPurposeBitFlags && localHeader.compressionMethod == cdEntry.compressionMethod && localHeader.lastModFileTime == cdEntry.lastModFileTime && localHeader.lastModFileDate == cdEntry.lastModFileDate else { throw ZipError.wrongLocalHeader } let hasDataDescriptor = localHeader.generalPurposeBitFlags & 0x08 != 0 // If file has data descriptor, then some values in local header are absent. // So we need to use values from CD entry. var uncompSize = hasDataDescriptor ? Int(UInt32(truncatingBitPattern: cdEntry.uncompSize)) : Int(UInt32(truncatingBitPattern: localHeader.uncompSize)) var compSize = hasDataDescriptor ? Int(UInt32(truncatingBitPattern: cdEntry.compSize)) : Int(UInt32(truncatingBitPattern: localHeader.compSize)) var crc32 = hasDataDescriptor ? cdEntry.crc32 : localHeader.crc32 let fileBytes: [UInt8] let fileDataStart = pointerData.index switch localHeader.compressionMethod { case 0: fileBytes = pointerData.alignedBytes(count: uncompSize) case 8: fileBytes = try Deflate.decompress(&pointerData) // Sometimes pointerData stays in not-aligned state after deflate decompression. // Following line ensures that this is not the case. pointerData.skipUntilNextByte() case 12: #if (!SWCOMP_ZIP_POD_BUILD) || (SWCOMP_ZIP_POD_BUILD && SWCOMP_ZIP_POD_BZ2) fileBytes = try BZip2.decompress(&pointerData) #else throw ZipError.compressionNotSupported #endif case 14: #if (!SWCOMP_ZIP_POD_BUILD) || (SWCOMP_ZIP_POD_BUILD && SWCOMP_ZIP_POD_LZMA) fileBytes = try LZMA.decompress(&pointerData) #else throw ZipError.compressionNotSupported #endif default: throw ZipError.compressionNotSupported } let realCompSize = pointerData.index - fileDataStart if hasDataDescriptor { // Now we need to parse data descriptor itself. // First, it might or might not have signature. let ddSignature = pointerData.uint32FromAlignedBytes(count: 4) if ddSignature != 0x08074b50 { pointerData.index -= 4 } // Now, let's update from CD with values from data descriptor. crc32 = pointerData.uint32FromAlignedBytes(count: 4) compSize = Int(pointerData.uint32FromAlignedBytes(count: 4)) uncompSize = Int(pointerData.uint32FromAlignedBytes(count: 4)) } guard compSize == realCompSize && uncompSize == fileBytes.count else { throw ZipError.wrongSize } guard crc32 == UInt32(CheckSums.crc32(fileBytes)) else { throw ZipError.wrongCRC32(Data(bytes: fileBytes)) } return Data(bytes: fileBytes) } init(_ cdEntry: CentralDirectoryEntry, _ pointerData: inout DataWithPointer) { self.cdEntry = cdEntry self.pointerData = pointerData } } /// Provides function which opens ZIP archives (containers). public class ZipContainer: Container { /** Processes ZIP archive (container) and returns an array of `ContainerEntries` (which are actually `ZipEntries`). First member of a tuple is entry's name, second member is entry's data. - Important: The order of entries is defined by ZIP archive and, particularly, creator of given ZIP container. It is likely that directories will be encountered earlier than files stored in those directories, but one SHOULD NOT assume that this is the case. - Note: Currently, there is no universal (platform and file system independent) method to determine if entry is a directory. One can check this by looking at the size of entry's data (it should be 0 for directory) AND the last character of entry's name (it should be '/'). If all of these is true then entry is likely to be a directory. - Parameter containerData: Data of ZIP container. - Throws: `ZipError` or any other error associated with compression type, depending on the type of inconsistency in data. It may indicate that either the container is damaged or it might not be ZIP container at all. - Returns: Array of pairs `ZipEntries` as an array of `ContainerEntries`. */ public static func open(containerData: Data) throws -> [ContainerEntry] { /// Object with input data which supports convenient work with bit shifts. var pointerData = DataWithPointer(data: containerData, bitOrder: .reversed) var entries = [ZipEntry]() pointerData.index = pointerData.size - 22 // 22 is a minimum amount which could take end of CD record. while true { // Check signature. if pointerData.uint32FromAlignedBytes(count: 4) == 0x06054b50 { // We found it! break } if pointerData.index == 0 { throw ZipError.notFoundCentralDirectoryEnd } pointerData.index -= 5 } let endOfCD = try EndOfCentralDirectory(&pointerData) let cdEntries = endOfCD.cdEntries // OK, now we are ready to read Central Directory itself. pointerData.index = Int(UInt(truncatingBitPattern: endOfCD.cdOffset)) for _ in 0..