mirror of
https://github.com/tsolomko/SWCompression.git
synced 2026-06-23 14:56:41 +00:00
346 lines
14 KiB
Swift
346 lines
14 KiB
Swift
//
|
|
// ZipContainer.swift
|
|
// SWCompression
|
|
//
|
|
// Created by Timofey Solomko on 14.01.17.
|
|
// Copyright © 2017 Timofey Solomko. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public enum ZipError: Error {
|
|
case NotFoundCentralDirectoryEnd
|
|
case WrongCentralDirectoryDisk
|
|
case WrongZip64LocatorSignature
|
|
case WrongZip64EndCentralDirectorySignature
|
|
case WrongVersion
|
|
case WrongCentralDirectoryHeaderSignature
|
|
case EncryptionNotSupported
|
|
case PatchingNotSupported
|
|
case CompressionNotSupported
|
|
case WrongLocalHeaderSignature
|
|
case WrongLocalHeader
|
|
case DataDescriptorNotSupported
|
|
case WrongCRC32
|
|
}
|
|
|
|
public class ZipContainer {
|
|
|
|
public static func open(containerData data: Data) throws -> [String : Data] {
|
|
/// Object with input data which supports convenient work with bit shifts.
|
|
var pointerData = DataWithPointer(data: data, bitOrder: .reversed)
|
|
|
|
// Looking for the end of central directory (CD) record.
|
|
var zip64RecordExists = false
|
|
|
|
pointerData.index = pointerData.size - 22 // 22 is a minimum amount which could take end of CD record.
|
|
while true {
|
|
// Check signature.
|
|
if pointerData.uint64FromAlignedBytes(count: 4) == 0x06054b50 {
|
|
// We found it!
|
|
break
|
|
}
|
|
if pointerData.index == 0 {
|
|
throw ZipError.NotFoundCentralDirectoryEnd
|
|
}
|
|
pointerData.index -= 5
|
|
}
|
|
|
|
/// Number of current disk.
|
|
var currentDiskNumber = pointerData.uint64FromAlignedBytes(count: 2)
|
|
/// Number of the disk with the start of CD.
|
|
var cdDiskNumber = pointerData.uint64FromAlignedBytes(count: 2)
|
|
if currentDiskNumber == 0xFFFF || cdDiskNumber == 0xFFFF {
|
|
zip64RecordExists = true
|
|
}
|
|
guard currentDiskNumber == cdDiskNumber
|
|
else { throw ZipError.WrongCentralDirectoryDisk }
|
|
|
|
/// Number of CD entries on the current disk.
|
|
var cdEntriesCurrentDisk = pointerData.uint64FromAlignedBytes(count: 2)
|
|
/// Total number of CD entries.
|
|
var cdEntries = pointerData.uint64FromAlignedBytes(count: 2)
|
|
if cdEntriesCurrentDisk == 0xFFFF || cdEntries == 0xFFFF {
|
|
zip64RecordExists = true
|
|
}
|
|
guard cdEntries == cdEntriesCurrentDisk
|
|
else { throw ZipError.WrongCentralDirectoryDisk } // TODO: Probably it should be another error.
|
|
|
|
/// Size of Central Directory.
|
|
var cdSize = pointerData.uint64FromAlignedBytes(count: 4)
|
|
/// Offset to the start of Central Directory.
|
|
var cdOffset = pointerData.uint64FromAlignedBytes(count: 4)
|
|
let zipCommentLength = pointerData.intFromAlignedBytes(count: 2)
|
|
if cdSize == 0xFFFFFFFF || cdOffset == 0xFFFFFFFF {
|
|
zip64RecordExists = true
|
|
}
|
|
|
|
// There is also a .ZIP file comment, but we don't need it.
|
|
// Here's how it can be processed:
|
|
// let zipComment = String(data: Data(bytes: pointerData.alignedBytes(count: zipCommentLength)),
|
|
// encoding: .utf8)
|
|
|
|
if zip64RecordExists { // We need to find Zip64 end of CD locator.
|
|
// Back to start of end of CD record.
|
|
pointerData.index -= zipCommentLength + 22
|
|
// Zip64 locator takes exactly 20 bytes.
|
|
pointerData.index -= 20
|
|
|
|
// Check signature.
|
|
guard pointerData.uint64FromAlignedBytes(count: 4) == 0x07064b50
|
|
else { throw ZipError.WrongZip64LocatorSignature }
|
|
|
|
let zip64CDStartDisk = pointerData.uint64FromAlignedBytes(count: 4)
|
|
guard currentDiskNumber == zip64CDStartDisk
|
|
else { throw ZipError.WrongCentralDirectoryDisk } // TODO: Probably it should be another error.
|
|
|
|
let zip64CDEndOffset = pointerData.uint64FromAlignedBytes(count: 8)
|
|
let totalDisks = pointerData.uint64FromAlignedBytes(count: 1)
|
|
guard totalDisks == 1
|
|
else { throw ZipError.WrongCentralDirectoryDisk } // TODO: Probably it should be another error.
|
|
|
|
// Now we need to move to Zip64 End of CD.
|
|
pointerData.index = Int(UInt(truncatingBitPattern: zip64CDEndOffset))
|
|
|
|
// Check signature.
|
|
guard pointerData.uint64FromAlignedBytes(count: 4) == 0x06064b50
|
|
else { throw ZipError.WrongZip64EndCentralDirectorySignature }
|
|
|
|
let zip64EndCDSize = pointerData.uint64FromAlignedBytes(count: 8)
|
|
|
|
let versionMadeBy = pointerData.uint64FromAlignedBytes(count: 2)
|
|
let versionNeeded = pointerData.uint64FromAlignedBytes(count: 2)
|
|
guard versionNeeded <= 45 // TODO: This value should probably be adjusted according to really supported features.
|
|
else { throw ZipError.WrongVersion }
|
|
|
|
// Update values read from basic End of CD to the one from Zip64 End of CD.
|
|
currentDiskNumber = pointerData.uint64FromAlignedBytes(count: 4)
|
|
cdDiskNumber = pointerData.uint64FromAlignedBytes(count: 4)
|
|
guard currentDiskNumber == cdDiskNumber
|
|
else { throw ZipError.WrongCentralDirectoryDisk }
|
|
|
|
cdEntriesCurrentDisk = pointerData.uint64FromAlignedBytes(count: 8)
|
|
cdEntries = pointerData.uint64FromAlignedBytes(count: 8)
|
|
guard cdEntries == cdEntriesCurrentDisk
|
|
else { throw ZipError.WrongCentralDirectoryDisk } // TODO: Probably it should be another error.
|
|
|
|
cdSize = pointerData.uint64FromAlignedBytes(count: 8)
|
|
cdOffset = pointerData.uint64FromAlignedBytes(count: 8)
|
|
|
|
// Then, there might be 'zip64 extensible data sector' with 'special purpose data'.
|
|
// But we don't need them currently, so let's skip them.
|
|
|
|
// To find the size of these data:
|
|
// let specialPurposeDataSize = zip64EndCDSize - 56
|
|
}
|
|
|
|
// OK, now we are ready to read Central Directory itself.
|
|
pointerData.index = Int(UInt(truncatingBitPattern: cdOffset))
|
|
|
|
var result: [String : Data] = [:]
|
|
for _ in 0..<cdEntries {
|
|
// Check signature.
|
|
guard pointerData.uint64FromAlignedBytes(count: 4) == 0x02014b50
|
|
else { throw ZipError.WrongCentralDirectoryHeaderSignature }
|
|
|
|
let cdEntry = CentralDirectoryEntry(&pointerData)
|
|
// Let's check entry's values for consistency.
|
|
guard cdEntry.versionNeeded <= 45 // TODO: This value should probably be adjusted according to really supported features.
|
|
else { throw ZipError.WrongVersion }
|
|
guard cdEntry.diskNumberStart == currentDiskNumber
|
|
else { throw ZipError.WrongCentralDirectoryDisk }
|
|
guard cdEntry.generalPurposeBitFlags & 0x2000 == 0 ||
|
|
cdEntry.generalPurposeBitFlags & 0x40 == 0 ||
|
|
cdEntry.generalPurposeBitFlags & 0x01 == 0
|
|
else { throw ZipError.EncryptionNotSupported }
|
|
guard cdEntry.generalPurposeBitFlags & 0x20 == 0
|
|
else { throw ZipError.PatchingNotSupported }
|
|
guard cdEntry.compressionMethod == 8 || cdEntry.compressionMethod == 0
|
|
else { throw ZipError.CompressionNotSupported }
|
|
|
|
let currentCDOffset = pointerData.index
|
|
|
|
// Now, let's move to the location of local header.
|
|
pointerData.index = Int(UInt32(truncatingBitPattern: cdEntry.offset))
|
|
|
|
// Check signature.
|
|
guard pointerData.uint64FromAlignedBytes(count: 4) == 0x04034b50
|
|
else { throw ZipError.WrongLocalHeaderSignature }
|
|
|
|
let localHeader = LocalHeader(&pointerData)
|
|
|
|
// Check local header for consistency.
|
|
guard localHeader.versionNeeded <= 45 &&
|
|
localHeader.generalPurposeBitFlags == cdEntry.generalPurposeBitFlags &&
|
|
localHeader.compressionMethod == cdEntry.compressionMethod &&
|
|
localHeader.lastModFileTime == cdEntry.lastModFileTime &&
|
|
localHeader.lastModFileDate == cdEntry.lastModFileDate
|
|
else { throw ZipError.WrongLocalHeader }
|
|
guard localHeader.generalPurposeBitFlags & 0x08 == 0
|
|
else { throw ZipError.DataDescriptorNotSupported }
|
|
|
|
let fileBytes: [UInt8]
|
|
switch localHeader.compressionMethod {
|
|
case 0:
|
|
fileBytes = pointerData.alignedBytes(count: Int(UInt32(truncatingBitPattern: localHeader.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()
|
|
default:
|
|
throw ZipError.CompressionNotSupported
|
|
}
|
|
|
|
guard localHeader.crc32 == UInt32(CheckSums.crc32(fileBytes))
|
|
else { throw ZipError.WrongCRC32 }
|
|
|
|
result[localHeader.fileName!] = Data(bytes: fileBytes)
|
|
|
|
pointerData.index = currentCDOffset
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
}
|
|
|
|
struct LocalHeader {
|
|
|
|
let versionNeeded: Int
|
|
let generalPurposeBitFlags: Int
|
|
let compressionMethod: Int
|
|
let lastModFileTime: Int
|
|
let lastModFileDate: Int
|
|
|
|
let crc32: UInt32
|
|
private(set) var compSize: UInt64
|
|
private(set) var uncompSize: UInt64
|
|
|
|
let fileName: String?
|
|
|
|
init(_ pointerData: inout DataWithPointer) {
|
|
self.versionNeeded = pointerData.intFromAlignedBytes(count: 2)
|
|
|
|
self.generalPurposeBitFlags = pointerData.intFromAlignedBytes(count: 2)
|
|
|
|
self.compressionMethod = pointerData.intFromAlignedBytes(count: 2)
|
|
|
|
self.lastModFileTime = pointerData.intFromAlignedBytes(count: 2)
|
|
self.lastModFileDate = pointerData.intFromAlignedBytes(count: 2)
|
|
|
|
self.crc32 = UInt32(truncatingBitPattern: pointerData.uint64FromAlignedBytes(count: 4))
|
|
|
|
self.compSize = pointerData.uint64FromAlignedBytes(count: 4)
|
|
self.uncompSize = pointerData.uint64FromAlignedBytes(count: 4)
|
|
|
|
let fileNameLength = pointerData.intFromAlignedBytes(count: 2)
|
|
let extraFieldLength = pointerData.intFromAlignedBytes(count: 2)
|
|
|
|
self.fileName = String(data: Data(bytes: pointerData.alignedBytes(count: fileNameLength)),
|
|
encoding: .utf8)
|
|
|
|
let extraFieldStart = pointerData.index
|
|
while pointerData.index - extraFieldStart < extraFieldLength {
|
|
// There are a lot of possible extra fields.
|
|
// But we are (currently) only interested in Zip64 related fields (with headerID = 0x0001),
|
|
// because they directly impact further extraction process.
|
|
let headerID = pointerData.intFromAlignedBytes(count: 2)
|
|
let size = pointerData.intFromAlignedBytes(count: 2)
|
|
switch headerID {
|
|
case 0x0001:
|
|
// In local header both uncompressed size and compressed size fields are required.
|
|
self.uncompSize = pointerData.uint64FromAlignedBytes(count: 8)
|
|
self.compSize = pointerData.uint64FromAlignedBytes(count: 8)
|
|
default:
|
|
pointerData.index += size
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
struct CentralDirectoryEntry {
|
|
|
|
let versionMadeBy: Int
|
|
let versionNeeded: Int
|
|
let generalPurposeBitFlags: Int
|
|
let compressionMethod: Int
|
|
let lastModFileTime: Int
|
|
let lastModFileDate: Int
|
|
let crc32: UInt32
|
|
private(set) var compSize: UInt64
|
|
private(set) var uncompSize: UInt64
|
|
|
|
let fileName: String?
|
|
let fileComment: String?
|
|
|
|
private(set) var diskNumberStart: UInt64
|
|
|
|
let internalFileAttributes: Int
|
|
let externalFileAttributes: UInt32
|
|
|
|
private(set) var offset: UInt64
|
|
|
|
init(_ pointerData: inout DataWithPointer) {
|
|
self.versionMadeBy = pointerData.intFromAlignedBytes(count: 2)
|
|
self.versionNeeded = pointerData.intFromAlignedBytes(count: 2)
|
|
|
|
self.generalPurposeBitFlags = pointerData.intFromAlignedBytes(count: 2)
|
|
|
|
self.compressionMethod = pointerData.intFromAlignedBytes(count: 2)
|
|
|
|
self.lastModFileTime = pointerData.intFromAlignedBytes(count: 2)
|
|
self.lastModFileDate = pointerData.intFromAlignedBytes(count: 2)
|
|
|
|
self.crc32 = UInt32(truncatingBitPattern: pointerData.uint64FromAlignedBytes(count: 4))
|
|
|
|
self.compSize = pointerData.uint64FromAlignedBytes(count: 4)
|
|
self.uncompSize = pointerData.uint64FromAlignedBytes(count: 4)
|
|
|
|
let fileNameLength = pointerData.intFromAlignedBytes(count: 2)
|
|
let extraFieldLength = pointerData.intFromAlignedBytes(count: 2)
|
|
let fileCommentLength = pointerData.intFromAlignedBytes(count: 2)
|
|
|
|
self.diskNumberStart = pointerData.uint64FromAlignedBytes(count: 2)
|
|
|
|
self.internalFileAttributes = pointerData.intFromAlignedBytes(count: 2)
|
|
self.externalFileAttributes = UInt32(truncatingBitPattern: pointerData.uint64FromAlignedBytes(count: 4))
|
|
|
|
self.offset = pointerData.uint64FromAlignedBytes(count: 4)
|
|
|
|
self.fileName = String(data: Data(bytes: pointerData.alignedBytes(count: fileNameLength)),
|
|
encoding: .utf8)
|
|
|
|
let extraFieldStart = pointerData.index
|
|
while pointerData.index - extraFieldStart < extraFieldLength {
|
|
// There are a lot of possible extra fields.
|
|
// But we are (currently) only interested in Zip64 related fields (with headerID = 0x0001),
|
|
// because they directly impact further extraction process.
|
|
let headerID = pointerData.intFromAlignedBytes(count: 2)
|
|
let size = pointerData.intFromAlignedBytes(count: 2)
|
|
switch headerID {
|
|
case 0x0001:
|
|
if self.uncompSize == 0xFFFFFFFF {
|
|
self.uncompSize = pointerData.uint64FromAlignedBytes(count: 8)
|
|
}
|
|
if self.compSize == 0xFFFFFFFF {
|
|
self.compSize = pointerData.uint64FromAlignedBytes(count: 8)
|
|
}
|
|
if self.offset == 0xFFFFFFFF {
|
|
self.offset = pointerData.uint64FromAlignedBytes(count: 8)
|
|
}
|
|
if self.diskNumberStart == 0xFFFF {
|
|
self.diskNumberStart = pointerData.uint64FromAlignedBytes(count: 4)
|
|
}
|
|
default:
|
|
pointerData.index += size
|
|
}
|
|
}
|
|
|
|
self.fileComment = String(data: Data(bytes: pointerData.alignedBytes(count: fileCommentLength)),
|
|
encoding: .utf8)
|
|
}
|
|
|
|
}
|