mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
399 lines
19 KiB
Swift
399 lines
19 KiB
Swift
//
|
|
// Archive.swift
|
|
// ZIPFoundation
|
|
//
|
|
// Copyright © 2017-2020 Thomas Zoechling, https://www.peakstep.com and the ZIP Foundation project authors.
|
|
// Released under the MIT License.
|
|
//
|
|
// See https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE for license information.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// The default chunk size when reading entry data from an archive.
|
|
public let defaultReadChunkSize = UInt32(16*1024)
|
|
/// The default chunk size when writing entry data to an archive.
|
|
public let defaultWriteChunkSize = defaultReadChunkSize
|
|
/// The default permissions for newly added entries.
|
|
public let defaultFilePermissions = UInt16(0o644)
|
|
public let defaultDirectoryPermissions = UInt16(0o755)
|
|
let defaultPOSIXBufferSize = defaultReadChunkSize
|
|
let defaultDirectoryUnitCount = Int64(1)
|
|
let minDirectoryEndOffset = 22
|
|
let maxDirectoryEndOffset = 66000
|
|
let endOfCentralDirectoryStructSignature = 0x06054b50
|
|
let localFileHeaderStructSignature = 0x04034b50
|
|
let dataDescriptorStructSignature = 0x08074b50
|
|
let centralDirectoryStructSignature = 0x02014b50
|
|
|
|
/// The compression method of an `Entry` in a ZIP `Archive`.
|
|
public enum CompressionMethod: UInt16 {
|
|
/// Indicates that an `Entry` has no compression applied to its contents.
|
|
case none = 0
|
|
/// Indicates that contents of an `Entry` have been compressed with a zlib compatible Deflate algorithm.
|
|
case deflate = 8
|
|
}
|
|
|
|
/// A sequence of uncompressed or compressed ZIP entries.
|
|
///
|
|
/// You use an `Archive` to create, read or update ZIP files.
|
|
/// To read an existing ZIP file, you have to pass in an existing file `URL` and `AccessMode.read`:
|
|
///
|
|
/// var archiveURL = URL(fileURLWithPath: "/path/file.zip")
|
|
/// var archive = Archive(url: archiveURL, accessMode: .read)
|
|
///
|
|
/// An `Archive` is a sequence of entries. You can
|
|
/// iterate over an archive using a `for`-`in` loop to get access to individual `Entry` objects:
|
|
///
|
|
/// for entry in archive {
|
|
/// print(entry.path)
|
|
/// }
|
|
///
|
|
/// Each `Entry` in an `Archive` is represented by its `path`. You can
|
|
/// use `path` to retrieve the corresponding `Entry` from an `Archive` via subscripting:
|
|
///
|
|
/// let entry = archive['/path/file.txt']
|
|
///
|
|
/// To create a new `Archive`, pass in a non-existing file URL and `AccessMode.create`. To modify an
|
|
/// existing `Archive` use `AccessMode.update`:
|
|
///
|
|
/// var archiveURL = URL(fileURLWithPath: "/path/file.zip")
|
|
/// var archive = Archive(url: archiveURL, accessMode: .update)
|
|
/// try archive?.addEntry("test.txt", relativeTo: baseURL, compressionMethod: .deflate)
|
|
public final class Archive: Sequence {
|
|
typealias LocalFileHeader = Entry.LocalFileHeader
|
|
typealias DataDescriptor = Entry.DataDescriptor
|
|
typealias CentralDirectoryStructure = Entry.CentralDirectoryStructure
|
|
|
|
/// An error that occurs during reading, creating or updating a ZIP file.
|
|
public enum ArchiveError: Error {
|
|
/// Thrown when an archive file is either damaged or inaccessible.
|
|
case unreadableArchive
|
|
/// Thrown when an archive is either opened with AccessMode.read or the destination file is unwritable.
|
|
case unwritableArchive
|
|
/// Thrown when the path of an `Entry` cannot be stored in an archive.
|
|
case invalidEntryPath
|
|
/// Thrown when an `Entry` can't be stored in the archive with the proposed compression method.
|
|
case invalidCompressionMethod
|
|
/// Thrown when the start of the central directory exceeds `UINT32_MAX`
|
|
case invalidStartOfCentralDirectoryOffset
|
|
/// Thrown when an archive does not contain the required End of Central Directory Record.
|
|
case missingEndOfCentralDirectoryRecord
|
|
/// Thrown when an extract, add or remove operation was canceled.
|
|
case cancelledOperation
|
|
}
|
|
|
|
/// The access mode for an `Archive`.
|
|
public enum AccessMode: UInt {
|
|
/// Indicates that a newly instantiated `Archive` should create its backing file.
|
|
case create
|
|
/// Indicates that a newly instantiated `Archive` should read from an existing backing file.
|
|
case read
|
|
/// Indicates that a newly instantiated `Archive` should update an existing backing file.
|
|
case update
|
|
}
|
|
|
|
struct EndOfCentralDirectoryRecord: DataSerializable {
|
|
let endOfCentralDirectorySignature = UInt32(endOfCentralDirectoryStructSignature)
|
|
let numberOfDisk: UInt16
|
|
let numberOfDiskStart: UInt16
|
|
let totalNumberOfEntriesOnDisk: UInt16
|
|
let totalNumberOfEntriesInCentralDirectory: UInt16
|
|
let sizeOfCentralDirectory: UInt32
|
|
let offsetToStartOfCentralDirectory: UInt32
|
|
let zipFileCommentLength: UInt16
|
|
let zipFileCommentData: Data
|
|
static let size = 22
|
|
}
|
|
|
|
private var preferredEncoding: String.Encoding?
|
|
/// URL of an Archive's backing file.
|
|
public let url: URL
|
|
/// Access mode for an archive file.
|
|
public let accessMode: AccessMode
|
|
var archiveFile: UnsafeMutablePointer<FILE>
|
|
var endOfCentralDirectoryRecord: EndOfCentralDirectoryRecord
|
|
|
|
/// Initializes a new ZIP `Archive`.
|
|
///
|
|
/// You can use this initalizer to create new archive files or to read and update existing ones.
|
|
/// The `mode` parameter indicates the intended usage of the archive: `.read`, `.create` or `.update`.
|
|
/// - Parameters:
|
|
/// - url: File URL to the receivers backing file.
|
|
/// - mode: Access mode of the receiver.
|
|
/// - preferredEncoding: Encoding for entry paths. Overrides the encoding specified in the archive.
|
|
/// This encoding is only used when _decoding_ paths from the receiver.
|
|
/// Paths of entries added with `addEntry` are always UTF-8 encoded.
|
|
/// - Returns: An archive initialized with a backing file at the passed in file URL and the given access mode
|
|
/// or `nil` if the following criteria are not met:
|
|
/// - Note:
|
|
/// - The file URL _must_ point to an existing file for `AccessMode.read`.
|
|
/// - The file URL _must_ point to a non-existing file for `AccessMode.create`.
|
|
/// - The file URL _must_ point to an existing file for `AccessMode.update`.
|
|
public init?(url: URL, accessMode mode: AccessMode, preferredEncoding: String.Encoding? = nil) {
|
|
self.url = url
|
|
self.accessMode = mode
|
|
self.preferredEncoding = preferredEncoding
|
|
guard let (archiveFile, endOfCentralDirectoryRecord) = Archive.configureFileBacking(for: url, mode: mode) else {
|
|
return nil
|
|
}
|
|
self.archiveFile = archiveFile
|
|
self.endOfCentralDirectoryRecord = endOfCentralDirectoryRecord
|
|
setvbuf(self.archiveFile, nil, _IOFBF, Int(defaultPOSIXBufferSize))
|
|
}
|
|
|
|
#if swift(>=5.0)
|
|
var memoryFile: MemoryFile?
|
|
|
|
/// Initializes a new in-memory ZIP `Archive`.
|
|
///
|
|
/// You can use this initalizer to create new in-memory archive files or to read and update existing ones.
|
|
///
|
|
/// - Parameters:
|
|
/// - data: `Data` object used as backing for in-memory archives.
|
|
/// - mode: Access mode of the receiver.
|
|
/// - preferredEncoding: Encoding for entry paths. Overrides the encoding specified in the archive.
|
|
/// This encoding is only used when _decoding_ paths from the receiver.
|
|
/// Paths of entries added with `addEntry` are always UTF-8 encoded.
|
|
/// - Returns: An in-memory archive initialized with passed in backing data.
|
|
/// - Note:
|
|
/// - The backing `data` _must_ contain a valid ZIP archive for `AccessMode.read` and `AccessMode.update`.
|
|
/// - The backing `data` _must_ be empty (or omitted) for `AccessMode.create`.
|
|
public init?(data: Data = Data(), accessMode mode: AccessMode, preferredEncoding: String.Encoding? = nil) {
|
|
guard let url = URL(string: "memory:"),
|
|
let (archiveFile, memoryFile) = Archive.configureMemoryBacking(for: data, mode: mode) else {
|
|
return nil
|
|
}
|
|
|
|
self.url = url
|
|
self.accessMode = mode
|
|
self.preferredEncoding = preferredEncoding
|
|
self.archiveFile = archiveFile
|
|
self.memoryFile = memoryFile
|
|
guard let endOfCentralDirectoryRecord = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile)
|
|
else {
|
|
fclose(self.archiveFile)
|
|
return nil
|
|
}
|
|
self.endOfCentralDirectoryRecord = endOfCentralDirectoryRecord
|
|
}
|
|
#endif
|
|
|
|
deinit {
|
|
fclose(self.archiveFile)
|
|
}
|
|
|
|
public func makeIterator() -> AnyIterator<Entry> {
|
|
let endOfCentralDirectoryRecord = self.endOfCentralDirectoryRecord
|
|
var directoryIndex = Int(endOfCentralDirectoryRecord.offsetToStartOfCentralDirectory)
|
|
var index = 0
|
|
return AnyIterator {
|
|
guard index < Int(endOfCentralDirectoryRecord.totalNumberOfEntriesInCentralDirectory) else { return nil }
|
|
guard let centralDirStruct: CentralDirectoryStructure = Data.readStruct(from: self.archiveFile,
|
|
at: directoryIndex) else {
|
|
return nil
|
|
}
|
|
let offset = Int(centralDirStruct.relativeOffsetOfLocalHeader)
|
|
guard let localFileHeader: LocalFileHeader = Data.readStruct(from: self.archiveFile,
|
|
at: offset) else { return nil }
|
|
var dataDescriptor: DataDescriptor?
|
|
if centralDirStruct.usesDataDescriptor {
|
|
let additionalSize = Int(localFileHeader.fileNameLength) + Int(localFileHeader.extraFieldLength)
|
|
let isCompressed = centralDirStruct.compressionMethod != CompressionMethod.none.rawValue
|
|
let dataSize = isCompressed ? centralDirStruct.compressedSize : centralDirStruct.uncompressedSize
|
|
let descriptorPosition = offset + LocalFileHeader.size + additionalSize + Int(dataSize)
|
|
dataDescriptor = Data.readStruct(from: self.archiveFile, at: descriptorPosition)
|
|
}
|
|
defer {
|
|
directoryIndex += CentralDirectoryStructure.size
|
|
directoryIndex += Int(centralDirStruct.fileNameLength)
|
|
directoryIndex += Int(centralDirStruct.extraFieldLength)
|
|
directoryIndex += Int(centralDirStruct.fileCommentLength)
|
|
index += 1
|
|
}
|
|
return Entry(centralDirectoryStructure: centralDirStruct,
|
|
localFileHeader: localFileHeader, dataDescriptor: dataDescriptor)
|
|
}
|
|
}
|
|
|
|
/// Retrieve the ZIP `Entry` with the given `path` from the receiver.
|
|
///
|
|
/// - Note: The ZIP file format specification does not enforce unique paths for entries.
|
|
/// Therefore an archive can contain multiple entries with the same path. This method
|
|
/// always returns the first `Entry` with the given `path`.
|
|
///
|
|
/// - Parameter path: A relative file path identifiying the corresponding `Entry`.
|
|
/// - Returns: An `Entry` with the given `path`. Otherwise, `nil`.
|
|
public subscript(path: String) -> Entry? {
|
|
if let encoding = preferredEncoding {
|
|
return self.first { $0.path(using: encoding) == path }
|
|
}
|
|
return self.first { $0.path == path }
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private static func configureFileBacking(for url: URL, mode: AccessMode)
|
|
-> (UnsafeMutablePointer<FILE>, EndOfCentralDirectoryRecord)? {
|
|
let fileManager = FileManager()
|
|
switch mode {
|
|
case .read:
|
|
let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
|
guard let archiveFile = fopen(fileSystemRepresentation, "rb"),
|
|
let endOfCentralDirectoryRecord = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else {
|
|
return nil
|
|
}
|
|
return (archiveFile, endOfCentralDirectoryRecord)
|
|
case .create:
|
|
let endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord(numberOfDisk: 0, numberOfDiskStart: 0,
|
|
totalNumberOfEntriesOnDisk: 0,
|
|
totalNumberOfEntriesInCentralDirectory: 0,
|
|
sizeOfCentralDirectory: 0,
|
|
offsetToStartOfCentralDirectory: 0,
|
|
zipFileCommentLength: 0,
|
|
zipFileCommentData: Data())
|
|
do {
|
|
try endOfCentralDirectoryRecord.data.write(to: url, options: .withoutOverwriting)
|
|
} catch { return nil }
|
|
fallthrough
|
|
case .update:
|
|
let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
|
guard let archiveFile = fopen(fileSystemRepresentation, "rb+"),
|
|
let endOfCentralDirectoryRecord = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else {
|
|
return nil
|
|
}
|
|
fseek(archiveFile, 0, SEEK_SET)
|
|
return (archiveFile, endOfCentralDirectoryRecord)
|
|
}
|
|
}
|
|
|
|
private static func scanForEndOfCentralDirectoryRecord(in file: UnsafeMutablePointer<FILE>)
|
|
-> EndOfCentralDirectoryRecord? {
|
|
var directoryEnd = 0
|
|
var index = minDirectoryEndOffset
|
|
fseek(file, 0, SEEK_END)
|
|
let archiveLength = ftell(file)
|
|
while directoryEnd == 0 && index < maxDirectoryEndOffset && index <= archiveLength {
|
|
fseek(file, archiveLength - index, SEEK_SET)
|
|
var potentialDirectoryEndTag: UInt32 = UInt32()
|
|
fread(&potentialDirectoryEndTag, 1, MemoryLayout<UInt32>.size, file)
|
|
if potentialDirectoryEndTag == UInt32(endOfCentralDirectoryStructSignature) {
|
|
directoryEnd = archiveLength - index
|
|
return Data.readStruct(from: file, at: directoryEnd)
|
|
}
|
|
index += 1
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
extension Archive {
|
|
/// The number of the work units that have to be performed when
|
|
/// removing `entry` from the receiver.
|
|
///
|
|
/// - Parameter entry: The entry that will be removed.
|
|
/// - Returns: The number of the work units.
|
|
public func totalUnitCountForRemoving(_ entry: Entry) -> Int64 {
|
|
return Int64(self.endOfCentralDirectoryRecord.offsetToStartOfCentralDirectory
|
|
- UInt32(entry.localSize))
|
|
}
|
|
|
|
func makeProgressForRemoving(_ entry: Entry) -> Progress {
|
|
return Progress(totalUnitCount: self.totalUnitCountForRemoving(entry))
|
|
}
|
|
|
|
/// The number of the work units that have to be performed when
|
|
/// reading `entry` from the receiver.
|
|
///
|
|
/// - Parameter entry: The entry that will be read.
|
|
/// - Returns: The number of the work units.
|
|
public func totalUnitCountForReading(_ entry: Entry) -> Int64 {
|
|
switch entry.type {
|
|
case .file, .symlink:
|
|
return Int64(entry.uncompressedSize)
|
|
case .directory:
|
|
return defaultDirectoryUnitCount
|
|
}
|
|
}
|
|
|
|
func makeProgressForReading(_ entry: Entry) -> Progress {
|
|
return Progress(totalUnitCount: self.totalUnitCountForReading(entry))
|
|
}
|
|
|
|
/// The number of the work units that have to be performed when
|
|
/// adding the file at `url` to the receiver.
|
|
/// - Parameter entry: The entry that will be removed.
|
|
/// - Returns: The number of the work units.
|
|
public func totalUnitCountForAddingItem(at url: URL) -> Int64 {
|
|
var count = Int64(0)
|
|
do {
|
|
let type = try FileManager.typeForItem(at: url)
|
|
switch type {
|
|
case .file, .symlink:
|
|
count = Int64(try FileManager.fileSizeForItem(at: url))
|
|
case .directory:
|
|
count = defaultDirectoryUnitCount
|
|
}
|
|
} catch { count = -1 }
|
|
return count
|
|
}
|
|
|
|
func makeProgressForAddingItem(at url: URL) -> Progress {
|
|
return Progress(totalUnitCount: self.totalUnitCountForAddingItem(at: url))
|
|
}
|
|
}
|
|
|
|
extension Archive.EndOfCentralDirectoryRecord {
|
|
var data: Data {
|
|
var endOfCDSignature = self.endOfCentralDirectorySignature
|
|
var numberOfDisk = self.numberOfDisk
|
|
var numberOfDiskStart = self.numberOfDiskStart
|
|
var totalNumberOfEntriesOnDisk = self.totalNumberOfEntriesOnDisk
|
|
var totalNumberOfEntriesInCD = self.totalNumberOfEntriesInCentralDirectory
|
|
var sizeOfCentralDirectory = self.sizeOfCentralDirectory
|
|
var offsetToStartOfCD = self.offsetToStartOfCentralDirectory
|
|
var zipFileCommentLength = self.zipFileCommentLength
|
|
var data = Data()
|
|
withUnsafePointer(to: &endOfCDSignature, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
|
withUnsafePointer(to: &numberOfDisk, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
|
withUnsafePointer(to: &numberOfDiskStart, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
|
withUnsafePointer(to: &totalNumberOfEntriesOnDisk, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
|
withUnsafePointer(to: &totalNumberOfEntriesInCD, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
|
withUnsafePointer(to: &sizeOfCentralDirectory, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
|
withUnsafePointer(to: &offsetToStartOfCD, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
|
withUnsafePointer(to: &zipFileCommentLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
|
data.append(self.zipFileCommentData)
|
|
return data
|
|
}
|
|
|
|
init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) {
|
|
guard data.count == Archive.EndOfCentralDirectoryRecord.size else { return nil }
|
|
guard data.scanValue(start: 0) == endOfCentralDirectorySignature else { return nil }
|
|
self.numberOfDisk = data.scanValue(start: 4)
|
|
self.numberOfDiskStart = data.scanValue(start: 6)
|
|
self.totalNumberOfEntriesOnDisk = data.scanValue(start: 8)
|
|
self.totalNumberOfEntriesInCentralDirectory = data.scanValue(start: 10)
|
|
self.sizeOfCentralDirectory = data.scanValue(start: 12)
|
|
self.offsetToStartOfCentralDirectory = data.scanValue(start: 16)
|
|
self.zipFileCommentLength = data.scanValue(start: 20)
|
|
guard let commentData = try? provider(Int(self.zipFileCommentLength)) else { return nil }
|
|
guard commentData.count == Int(self.zipFileCommentLength) else { return nil }
|
|
self.zipFileCommentData = commentData
|
|
}
|
|
|
|
init(record: Archive.EndOfCentralDirectoryRecord,
|
|
numberOfEntriesOnDisk: UInt16,
|
|
numberOfEntriesInCentralDirectory: UInt16,
|
|
updatedSizeOfCentralDirectory: UInt32,
|
|
startOfCentralDirectory: UInt32) {
|
|
numberOfDisk = record.numberOfDisk
|
|
numberOfDiskStart = record.numberOfDiskStart
|
|
totalNumberOfEntriesOnDisk = numberOfEntriesOnDisk
|
|
totalNumberOfEntriesInCentralDirectory = numberOfEntriesInCentralDirectory
|
|
sizeOfCentralDirectory = updatedSizeOfCentralDirectory
|
|
offsetToStartOfCentralDirectory = startOfCentralDirectory
|
|
zipFileCommentLength = record.zipFileCommentLength
|
|
zipFileCommentData = record.zipFileCommentData
|
|
}
|
|
}
|