Epub阅读器0.0.1
This commit is contained in:
Generated
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2025 Thomas Zoechling (https://www.peakstep.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
<img src="https://user-images.githubusercontent.com/1577319/27564151-1d99e3a0-5ad6-11e7-8ab6-417c5b5a3ff2.png" width="489"/>
|
||||
|
||||
[](https://github.com/apple/swift-package-manager)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://cocoapods.org/pods/ZIPFoundation)
|
||||
[](https://github.com/weichsel/ZIPFoundation)
|
||||
[](http://twitter.com/weichsel)
|
||||
|
||||
ZIP Foundation is a library to create, read and modify ZIP archive files.
|
||||
It is written in Swift and based on [Apple's libcompression](https://developer.apple.com/documentation/compression) for high performance and energy efficiency.
|
||||
To learn more about the performance characteristics of the framework, you can read [this blog post](https://thomas.zoechling.me/journal/2017/07/ZIPFoundation.html).
|
||||
|
||||
- [Features](#features)
|
||||
- [Requirements](#requirements)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Zipping Files and Directories](#zipping-files-and-directories)
|
||||
- [Unzipping Archives](#unzipping-archives)
|
||||
- [Advanced Usage](#advanced-usage)
|
||||
- [Accessing individual Entries](#accessing-individual-entries)
|
||||
- [Creating Archives](#creating-archives)
|
||||
- [Adding and Removing Entries](#adding-and-removing-entries)
|
||||
- [Closure based Reading and Writing](#closure-based-reading-and-writing)
|
||||
- [In-Memory Archives](#in-memory-archives)
|
||||
- [Progress Tracking and Cancellation](#progress-tracking-and-cancellation)
|
||||
- [Credits](#credits)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Modern Swift API
|
||||
- [x] High Performance Compression and Decompression
|
||||
- [x] Large File Support
|
||||
- [x] In-Memory Archives
|
||||
- [x] Deterministic Memory Consumption
|
||||
- [x] Linux compatibility
|
||||
- [x] No 3rd party dependencies (on Apple platforms, zlib on Linux)
|
||||
- [x] Comprehensive Unit and Performance Test Coverage
|
||||
- [x] Complete Documentation
|
||||
|
||||
## Requirements
|
||||
|
||||
- iOS 12.0+ / macOS 10.11+ / tvOS 12.0+ / watchOS 2.0+ / visionOS 1.0+
|
||||
- Or Linux with zlib development package
|
||||
- Xcode 11.0
|
||||
- Swift 4.0
|
||||
|
||||
## Installation
|
||||
|
||||
### Swift Package Manager
|
||||
The Swift Package Manager is a dependency manager integrated with the Swift build system. To learn how to use the Swift Package Manager for your project, please read the [official documentation](https://github.com/apple/swift-package-manager/blob/master/Documentation/Usage.md).
|
||||
To add ZIP Foundation as a dependency, you have to add it to the `dependencies` of your `Package.swift` file and refer to that dependency in your `target`.
|
||||
|
||||
```swift
|
||||
// swift-tools-version:5.0
|
||||
import PackageDescription
|
||||
let package = Package(
|
||||
name: "<Your Product Name>",
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0"))
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "<Your Target Name>",
|
||||
dependencies: ["ZIPFoundation"]),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
After adding the dependency, you can fetch the library with:
|
||||
|
||||
```bash
|
||||
$ swift package resolve
|
||||
```
|
||||
|
||||
### Carthage
|
||||
|
||||
[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager.
|
||||
Installation instructions can be found in the project's [README file](https://github.com/Carthage/Carthage#installing-carthage).
|
||||
|
||||
To integrate ZIPFoundation into your Xcode project using Carthage, you have to add it to your `Cartfile`:
|
||||
|
||||
```ogdl
|
||||
github "weichsel/ZIPFoundation" ~> 0.9
|
||||
```
|
||||
|
||||
After adding ZIPFoundation to the `Cartfile`, you have to fetch the sources by running:
|
||||
|
||||
```bash
|
||||
carthage update --no-build
|
||||
```
|
||||
|
||||
The fetched project has to be integrated into your workspace by dragging `ZIPFoundation.xcodeproj` to Xcode's Project Navigator. (See [official Carhage docs](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).)
|
||||
|
||||
### CocoaPods
|
||||
|
||||
CocoaPods is a dependency manager for Objective-C and Swift.
|
||||
To learn more about setting up your project for CocoaPods, please refer to the [official documentation](https://cocoapods.org/#install).
|
||||
To integrate ZIP Foundation into your Xcode project using CocoaPods, you have to add it to your project's `Podfile`:
|
||||
|
||||
```ruby
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
platform :ios, '10.0'
|
||||
use_frameworks!
|
||||
target '<Your Target Name>' do
|
||||
pod 'ZIPFoundation', '~> 0.9'
|
||||
end
|
||||
```
|
||||
|
||||
Afterwards, run the following command:
|
||||
|
||||
```bash
|
||||
$ pod install
|
||||
```
|
||||
|
||||
## Usage
|
||||
ZIP Foundation provides two high level methods to zip and unzip items. Both are implemented as extension of `FileManager`.
|
||||
The functionality of those methods is modeled after the behavior of the Archive Utility in macOS.
|
||||
|
||||
### Zipping Files and Directories
|
||||
To zip a single file you simply pass a file URL representing the item you want to zip and a destination URL to `FileManager.zipItem(at sourceURL: URL, to destinationURL: URL)`:
|
||||
|
||||
```swift
|
||||
let fileManager = FileManager()
|
||||
let currentWorkingPath = fileManager.currentDirectoryPath
|
||||
var sourceURL = URL(fileURLWithPath: currentWorkingPath)
|
||||
sourceURL.appendPathComponent("file.txt")
|
||||
var destinationURL = URL(fileURLWithPath: currentWorkingPath)
|
||||
destinationURL.appendPathComponent("archive.zip")
|
||||
do {
|
||||
try fileManager.zipItem(at: sourceURL, to: destinationURL)
|
||||
} catch {
|
||||
print("Creation of ZIP archive failed with error:\(error)")
|
||||
}
|
||||
```
|
||||
|
||||
By default, archives are created without any compression. To create compressed ZIP archives, the optional `compressionMethod` parameter has to be set to `.deflate`.
|
||||
The same method also accepts URLs that represent directory items. In that case, `zipItem` adds the directory content of `sourceURL` to the archive.
|
||||
By default, a root directory entry named after the `lastPathComponent` of the `sourceURL` is added to the destination archive. If you don't want to preserve the parent directory of the source in your archive, you can pass `shouldKeepParent: false`.
|
||||
|
||||
### Unzipping Archives
|
||||
To unzip existing archives, you can use `FileManager.unzipItem(at sourceURL: URL, to destinationURL: URL)`.
|
||||
This recursively extracts all entries within the archive to the destination URL:
|
||||
|
||||
```swift
|
||||
let fileManager = FileManager()
|
||||
let currentWorkingPath = fileManager.currentDirectoryPath
|
||||
var sourceURL = URL(fileURLWithPath: currentWorkingPath)
|
||||
sourceURL.appendPathComponent("archive.zip")
|
||||
var destinationURL = URL(fileURLWithPath: currentWorkingPath)
|
||||
destinationURL.appendPathComponent("directory")
|
||||
do {
|
||||
try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
|
||||
try fileManager.unzipItem(at: sourceURL, to: destinationURL)
|
||||
} catch {
|
||||
print("Extraction of ZIP archive failed with error:\(error)")
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
ZIP Foundation also allows you to individually access specific entries without the need to extract the whole archive. Additionally it comes with the ability to incrementally update archive contents.
|
||||
|
||||
### Accessing individual Entries
|
||||
To gain access to specific ZIP entries, you have to initialize an `Archive` object with a file URL that represents an existing archive. After doing that, entries can be retrieved via their relative path. `Archive` conforms to `Sequence` and therefore supports subscripting:
|
||||
|
||||
```swift
|
||||
let fileManager = FileManager()
|
||||
let currentWorkingPath = fileManager.currentDirectoryPath
|
||||
var archiveURL = URL(fileURLWithPath: currentWorkingPath)
|
||||
archiveURL.appendPathComponent("archive.zip")
|
||||
let archive = try Archive(url: archiveURL, accessMode: .read)
|
||||
guard let entry = archive["file.txt"] else {
|
||||
return
|
||||
}
|
||||
var destinationURL = URL(fileURLWithPath: currentWorkingPath)
|
||||
destinationURL.appendPathComponent("out.txt")
|
||||
do {
|
||||
try archive.extract(entry, to: destinationURL)
|
||||
} catch {
|
||||
print("Extracting entry from archive failed with error:\(error)")
|
||||
}
|
||||
```
|
||||
|
||||
The `extract` method accepts optional parameters that allow you to control compression and memory consumption.
|
||||
You can find detailed information about that parameters in the method's documentation.
|
||||
|
||||
### Creating Archives
|
||||
To create a new `Archive`, pass in a non-existing file URL and `AccessMode.create`.
|
||||
|
||||
```swift
|
||||
let fileManager = FileManager()
|
||||
let currentWorkingPath = fileManager.currentDirectoryPath
|
||||
var archiveURL = URL(fileURLWithPath: currentWorkingPath)
|
||||
archiveURL.appendPathComponent("newArchive.zip")
|
||||
let archive = try Archive(url: archiveURL, accessMode: .create)
|
||||
```
|
||||
|
||||
### Adding and Removing Entries
|
||||
You can add or remove entries to/from archives that have been opened with `.create` or `.update` `AccessMode`.
|
||||
To add an entry from an existing file, you can pass a relative path and a base URL to `addEntry`. The relative path identifies the
|
||||
entry within the ZIP archive. The relative path and the base URL must form an absolute file URL that points to the file you want to add to
|
||||
the archive:
|
||||
|
||||
```swift
|
||||
let fileManager = FileManager()
|
||||
let currentWorkingPath = fileManager.currentDirectoryPath
|
||||
var archiveURL = URL(fileURLWithPath: currentWorkingPath)
|
||||
archiveURL.appendPathComponent("archive.zip")
|
||||
let archive = try Archive(url: archiveURL, accessMode: .update)
|
||||
var fileURL = URL(fileURLWithPath: currentWorkingPath)
|
||||
fileURL.appendPathComponent("file.txt")
|
||||
do {
|
||||
try archive.addEntry(with: fileURL.lastPathComponent, relativeTo: fileURL.deletingLastPathComponent())
|
||||
} catch {
|
||||
print("Adding entry to ZIP archive failed with error:\(error)")
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, the `addEntry(with path: String, fileURL: URL)` method can be used to add files that are _not_ sharing a common base directory.
|
||||
The `fileURL` parameter must contain an absolute file URL that points to a file, symlink or directory on an arbitrary file system location.
|
||||
|
||||
The `addEntry` method accepts several optional parameters that allow you to control compression, memory consumption and file attributes.
|
||||
You can find detailed information about that parameters in the method's documentation.
|
||||
|
||||
To remove an entry, you need a reference to an entry within an archive that you can pass to `removeEntry`:
|
||||
|
||||
```swift
|
||||
guard let entry = archive["file.txt"] else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try archive.remove(entry)
|
||||
} catch {
|
||||
print("Removing entry from ZIP archive failed with error:\(error)")
|
||||
}
|
||||
```
|
||||
|
||||
### Closure based Reading and Writing
|
||||
ZIP Foundation also allows you to consume ZIP entry contents without writing them to the file system.
|
||||
The `extract` method accepts a closure of type `Consumer`. This closure is called during extraction until the contents of an entry are exhausted:
|
||||
|
||||
```swift
|
||||
try archive.extract(entry, consumer: { (data) in
|
||||
print(data.count)
|
||||
})
|
||||
```
|
||||
The `data` passed into the closure contains chunks of the current entry. You can control the chunk size of the entry by providing the optional `bufferSize` parameter.
|
||||
|
||||
You can also add entries from an in-memory data source. To do this you have to provide a closure of type `Provider` to the `addEntry` method:
|
||||
|
||||
```swift
|
||||
let string = "abcdefghijkl"
|
||||
guard let data = string.data(using: .utf8) else { return }
|
||||
try? archive.addEntry(with: "fromMemory.txt", type: .file, uncompressedSize: Int64(data.count), bufferSize: 4, provider: { (position, size) -> Data in
|
||||
// This will be called until `data` is exhausted (3x in this case).
|
||||
return data.subdata(in: Data.Index(position)..<Int(position)+size)
|
||||
})
|
||||
```
|
||||
The closure is called until enough data has been provided to create an entry of `uncompressedSize`. The closure receives `position` and `size` arguments
|
||||
so that you can manage the state of your data source.
|
||||
|
||||
### In-Memory Archives
|
||||
Besides closure based reading and writing of file based archives, ZIP Foundation also provides capabilities to process in-memory archives.
|
||||
This allows creation or extraction of archives that only reside in RAM. One use case for this functionality is dynamic creation of ZIP archives that are later sent to a client - without performing any disk IO.
|
||||
|
||||
To work with in-memory archives the `init(data: Data, accessMode: AccessMode)` initializer must be used.
|
||||
To _read_ or _update_ an in-memory archive, the passed-in `data` must contain a representation of a valid ZIP archive.
|
||||
To _create_ an in-memory archive, the `data` parameter can be omitted:
|
||||
|
||||
```swift
|
||||
let string = "Some string!"
|
||||
let archive = try Archive(accessMode: .create)
|
||||
guard let data = string.data(using: .utf8) else { return }
|
||||
try archive.addEntry(with: "inMemory.txt", type: .file, uncompressedSize: Int64(data.count), bufferSize: 4, provider: { (position, size) -> Data in
|
||||
return data.subdata(in: Data.Index(position)..<Int(position)+size)
|
||||
})
|
||||
let archiveData = archive.data
|
||||
```
|
||||
|
||||
### Progress Tracking and Cancellation
|
||||
All `Archive` operations take an optional `progress` parameter. By passing in an instance of [Progress](https://developer.apple.com/documentation/foundation/progress), you indicate that
|
||||
you want to track the progress of the current ZIP operation. ZIP Foundation automatically configures the `totalUnitCount` of the `progress` object and continuously updates its `completedUnitCount`.
|
||||
To get notifications about the completed work of the current operation, you can attach a Key-Value Observer to the `fractionCompleted` property of your `progress` object.
|
||||
The ZIP Foundation `FileManager` extension methods also accept optional `progress` parameters. `zipItem` and `unzipItem` both automatically create a hierarchy of progress objects that reflect the progress of all items contained in a directory or an archive that contains multiple items.
|
||||
|
||||
The [cancel()](https://developer.apple.com/documentation/foundation/progress/1413832-cancel) method of `Progress` can be used to terminate an unfinished ZIP operation. In case of cancelation, the current operation throws an `ArchiveError.cancelledOperation` exception.
|
||||
|
||||
## Credits
|
||||
|
||||
ZIP Foundation is written and maintained by [Thomas Zoechling](http://thomas.zoechling.me).
|
||||
Twitter: [@weichsel](https://twitter.com/weichsel).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
ZIP Foundation is released under the MIT License.
|
||||
See [LICENSE](https://github.com/weichsel/ZIPFoundation/blob/master/LICENSE) for details.
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// Archive+BackingConfiguration.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
extension Archive {
|
||||
|
||||
struct BackingConfiguration {
|
||||
let file: FILEPointer
|
||||
let endOfCentralDirectoryRecord: EndOfCentralDirectoryRecord
|
||||
let zip64EndOfCentralDirectory: ZIP64EndOfCentralDirectory?
|
||||
#if swift(>=5.0)
|
||||
let memoryFile: MemoryFile?
|
||||
|
||||
init(file: FILEPointer,
|
||||
endOfCentralDirectoryRecord: EndOfCentralDirectoryRecord,
|
||||
zip64EndOfCentralDirectory: ZIP64EndOfCentralDirectory? = nil,
|
||||
memoryFile: MemoryFile? = nil) {
|
||||
self.file = file
|
||||
self.endOfCentralDirectoryRecord = endOfCentralDirectoryRecord
|
||||
self.zip64EndOfCentralDirectory = zip64EndOfCentralDirectory
|
||||
self.memoryFile = memoryFile
|
||||
}
|
||||
#else
|
||||
|
||||
init(file: FILEPointer,
|
||||
endOfCentralDirectoryRecord: EndOfCentralDirectoryRecord,
|
||||
zip64EndOfCentralDirectory: ZIP64EndOfCentralDirectory?) {
|
||||
self.file = file
|
||||
self.endOfCentralDirectoryRecord = endOfCentralDirectoryRecord
|
||||
self.zip64EndOfCentralDirectory = zip64EndOfCentralDirectory
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static func makeBackingConfiguration(for url: URL, mode: AccessMode) throws
|
||||
-> BackingConfiguration {
|
||||
let fileManager = FileManager()
|
||||
switch mode {
|
||||
case .read:
|
||||
let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
guard let archiveFile = fopen(fileSystemRepresentation, "rb") else {
|
||||
throw POSIXError(errno, path: url.path)
|
||||
}
|
||||
guard let (eocdRecord, zip64EOCD) = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else {
|
||||
fclose(archiveFile)
|
||||
throw ArchiveError.missingEndOfCentralDirectoryRecord
|
||||
}
|
||||
return BackingConfiguration(file: archiveFile,
|
||||
endOfCentralDirectoryRecord: eocdRecord,
|
||||
zip64EndOfCentralDirectory: zip64EOCD)
|
||||
case .create:
|
||||
let endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord(numberOfDisk: 0, numberOfDiskStart: 0,
|
||||
totalNumberOfEntriesOnDisk: 0,
|
||||
totalNumberOfEntriesInCentralDirectory: 0,
|
||||
sizeOfCentralDirectory: 0,
|
||||
offsetToStartOfCentralDirectory: 0,
|
||||
zipFileCommentLength: 0,
|
||||
zipFileCommentData: Data())
|
||||
try endOfCentralDirectoryRecord.data.write(to: url, options: .withoutOverwriting)
|
||||
fallthrough
|
||||
case .update:
|
||||
let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
guard let archiveFile = fopen(fileSystemRepresentation, "rb+") else {
|
||||
throw POSIXError(errno, path: url.path)
|
||||
}
|
||||
guard let (eocdRecord, zip64EOCD) = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else {
|
||||
fclose(archiveFile)
|
||||
throw ArchiveError.missingEndOfCentralDirectoryRecord
|
||||
}
|
||||
fseeko(archiveFile, 0, SEEK_SET)
|
||||
return BackingConfiguration(file: archiveFile,
|
||||
endOfCentralDirectoryRecord: eocdRecord,
|
||||
zip64EndOfCentralDirectory: zip64EOCD)
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.0)
|
||||
static func makeBackingConfiguration(for data: Data, mode: AccessMode) throws
|
||||
-> BackingConfiguration {
|
||||
let memoryFile = MemoryFile(data: data)
|
||||
let archiveFile = try memoryFile.open(mode: mode)
|
||||
switch mode {
|
||||
case .read:
|
||||
guard let (eocdRecord, zip64EOCD) = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else {
|
||||
throw ArchiveError.missingEndOfCentralDirectoryRecord
|
||||
}
|
||||
|
||||
return BackingConfiguration(file: archiveFile,
|
||||
endOfCentralDirectoryRecord: eocdRecord,
|
||||
zip64EndOfCentralDirectory: zip64EOCD,
|
||||
memoryFile: memoryFile)
|
||||
case .create:
|
||||
let endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord(numberOfDisk: 0, numberOfDiskStart: 0,
|
||||
totalNumberOfEntriesOnDisk: 0,
|
||||
totalNumberOfEntriesInCentralDirectory: 0,
|
||||
sizeOfCentralDirectory: 0,
|
||||
offsetToStartOfCentralDirectory: 0,
|
||||
zipFileCommentLength: 0,
|
||||
zipFileCommentData: Data())
|
||||
_ = endOfCentralDirectoryRecord.data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in
|
||||
fwrite(buffer.baseAddress, buffer.count, 1, archiveFile) // Errors handled during read
|
||||
}
|
||||
fallthrough
|
||||
case .update:
|
||||
guard let (eocdRecord, zip64EOCD) = Archive.scanForEndOfCentralDirectoryRecord(in: archiveFile) else {
|
||||
throw ArchiveError.missingEndOfCentralDirectoryRecord
|
||||
}
|
||||
|
||||
fseeko(archiveFile, 0, SEEK_SET)
|
||||
return BackingConfiguration(file: archiveFile,
|
||||
endOfCentralDirectoryRecord: eocdRecord,
|
||||
zip64EndOfCentralDirectory: zip64EOCD,
|
||||
memoryFile: memoryFile)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Archive+Deprecated.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Created by Thomas Zoechling on 06.02.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Archive {
|
||||
|
||||
@available(*, deprecated, message: "Please use the throwing initializer.")
|
||||
convenience init?(url: URL, accessMode mode: AccessMode, preferredEncoding: String.Encoding? = nil) {
|
||||
try? self.init(url: url, accessMode: mode, pathEncoding: preferredEncoding)
|
||||
}
|
||||
|
||||
#if swift(>=5.0)
|
||||
@available(*, deprecated, message: "Please use the throwing initializer.")
|
||||
convenience init?(data: Data = Data(), accessMode mode: AccessMode, preferredEncoding: String.Encoding? = nil) {
|
||||
try? self.init(data: data, accessMode: mode, pathEncoding: preferredEncoding)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
//
|
||||
// Archive+Helpers.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
extension Archive {
|
||||
|
||||
// MARK: - Reading
|
||||
|
||||
func readUncompressed(entry: Entry, bufferSize: Int, skipCRC32: Bool,
|
||||
progress: Progress? = nil, with consumer: Consumer) throws -> CRC32 {
|
||||
let size = entry.centralDirectoryStructure.effectiveUncompressedSize
|
||||
guard size <= .max else { throw ArchiveError.invalidEntrySize }
|
||||
return try Data.consumePart(of: Int64(size), chunkSize: bufferSize, skipCRC32: skipCRC32,
|
||||
provider: { (_, chunkSize) -> Data in
|
||||
return try Data.readChunk(of: chunkSize, from: self.archiveFile)
|
||||
}, consumer: { (data) in
|
||||
if progress?.isCancelled == true { throw ArchiveError.cancelledOperation }
|
||||
try consumer(data)
|
||||
progress?.completedUnitCount += Int64(data.count)
|
||||
})
|
||||
}
|
||||
|
||||
func readCompressed(entry: Entry, bufferSize: Int, skipCRC32: Bool,
|
||||
progress: Progress? = nil, with consumer: Consumer) throws -> CRC32 {
|
||||
let size = entry.centralDirectoryStructure.effectiveCompressedSize
|
||||
guard size <= .max else { throw ArchiveError.invalidEntrySize }
|
||||
return try Data.decompress(size: Int64(size), bufferSize: bufferSize, skipCRC32: skipCRC32,
|
||||
provider: { (_, chunkSize) -> Data in
|
||||
return try Data.readChunk(of: chunkSize, from: self.archiveFile)
|
||||
}, consumer: { (data) in
|
||||
if progress?.isCancelled == true { throw ArchiveError.cancelledOperation }
|
||||
try consumer(data)
|
||||
progress?.completedUnitCount += Int64(data.count)
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - Writing
|
||||
|
||||
func writeEntry(uncompressedSize: Int64, type: Entry.EntryType,
|
||||
compressionMethod: CompressionMethod, bufferSize: Int, progress: Progress? = nil,
|
||||
provider: Provider) throws -> (sizeWritten: Int64, crc32: CRC32) {
|
||||
var checksum = CRC32(0)
|
||||
var sizeWritten = Int64(0)
|
||||
switch type {
|
||||
case .file:
|
||||
switch compressionMethod {
|
||||
case .none:
|
||||
(sizeWritten, checksum) = try self.writeUncompressed(size: uncompressedSize,
|
||||
bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
case .deflate:
|
||||
(sizeWritten, checksum) = try self.writeCompressed(size: uncompressedSize,
|
||||
bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
}
|
||||
case .directory:
|
||||
_ = try provider(0, 0)
|
||||
if let progress = progress { progress.completedUnitCount = progress.totalUnitCount }
|
||||
case .symlink:
|
||||
let (linkSizeWritten, linkChecksum) = try self.writeSymbolicLink(size: Int(uncompressedSize),
|
||||
provider: provider)
|
||||
(sizeWritten, checksum) = (Int64(linkSizeWritten), linkChecksum)
|
||||
if let progress = progress { progress.completedUnitCount = progress.totalUnitCount }
|
||||
}
|
||||
return (sizeWritten, checksum)
|
||||
}
|
||||
|
||||
func writeLocalFileHeader(path: String, compressionMethod: CompressionMethod,
|
||||
size: (uncompressed: UInt64, compressed: UInt64), checksum: CRC32,
|
||||
modificationDateTime: (UInt16, UInt16)) throws -> LocalFileHeader {
|
||||
// We always set Bit 11 in generalPurposeBitFlag, which indicates an UTF-8 encoded path.
|
||||
let fileNameData = Data(path.utf8)
|
||||
var uncompressedSizeOfLFH = UInt32(0)
|
||||
var compressedSizeOfLFH = UInt32(0)
|
||||
var extraFieldLength = UInt16(0)
|
||||
var zip64ExtendedInformation: Entry.ZIP64ExtendedInformation?
|
||||
var versionNeededToExtract = Version.v20.rawValue
|
||||
// ZIP64 Extended Information in the Local header MUST include BOTH original and compressed file size fields.
|
||||
if size.uncompressed >= maxUncompressedSize || size.compressed >= maxCompressedSize {
|
||||
uncompressedSizeOfLFH = .max
|
||||
compressedSizeOfLFH = .max
|
||||
extraFieldLength = UInt16(20) // 2 + 2 + 8 + 8
|
||||
versionNeededToExtract = Version.v45.rawValue
|
||||
zip64ExtendedInformation = Entry.ZIP64ExtendedInformation(dataSize: extraFieldLength - 4,
|
||||
uncompressedSize: size.uncompressed,
|
||||
compressedSize: size.compressed,
|
||||
relativeOffsetOfLocalHeader: 0,
|
||||
diskNumberStart: 0)
|
||||
} else {
|
||||
uncompressedSizeOfLFH = UInt32(size.uncompressed)
|
||||
compressedSizeOfLFH = UInt32(size.compressed)
|
||||
}
|
||||
|
||||
let localFileHeader = LocalFileHeader(versionNeededToExtract: versionNeededToExtract,
|
||||
generalPurposeBitFlag: UInt16(2048),
|
||||
compressionMethod: compressionMethod.rawValue,
|
||||
lastModFileTime: modificationDateTime.1,
|
||||
lastModFileDate: modificationDateTime.0, crc32: checksum,
|
||||
compressedSize: compressedSizeOfLFH,
|
||||
uncompressedSize: uncompressedSizeOfLFH,
|
||||
fileNameLength: UInt16(fileNameData.count),
|
||||
extraFieldLength: extraFieldLength, fileNameData: fileNameData,
|
||||
extraFieldData: zip64ExtendedInformation?.data ?? Data())
|
||||
_ = try Data.write(chunk: localFileHeader.data, to: self.archiveFile)
|
||||
return localFileHeader
|
||||
}
|
||||
|
||||
func writeCentralDirectoryStructure(localFileHeader: LocalFileHeader, relativeOffset: UInt64,
|
||||
externalFileAttributes: UInt32) throws -> CentralDirectoryStructure {
|
||||
var extraUncompressedSize: UInt64?
|
||||
var extraCompressedSize: UInt64?
|
||||
var extraOffset: UInt64?
|
||||
var relativeOffsetOfCD = UInt32(0)
|
||||
var extraFieldLength = UInt16(0)
|
||||
var zip64ExtendedInformation: Entry.ZIP64ExtendedInformation?
|
||||
if localFileHeader.uncompressedSize == .max || localFileHeader.compressedSize == .max {
|
||||
let zip64Field = Entry.ZIP64ExtendedInformation
|
||||
.scanForZIP64Field(in: localFileHeader.extraFieldData, fields: [.uncompressedSize, .compressedSize])
|
||||
extraUncompressedSize = zip64Field?.uncompressedSize
|
||||
extraCompressedSize = zip64Field?.compressedSize
|
||||
}
|
||||
if relativeOffset >= maxOffsetOfLocalFileHeader {
|
||||
extraOffset = relativeOffset
|
||||
relativeOffsetOfCD = .max
|
||||
} else {
|
||||
relativeOffsetOfCD = UInt32(relativeOffset)
|
||||
}
|
||||
extraFieldLength = [extraUncompressedSize, extraCompressedSize, extraOffset]
|
||||
.compactMap { $0 }
|
||||
.reduce(UInt16(0), { $0 + UInt16(MemoryLayout.size(ofValue: $1)) })
|
||||
if extraFieldLength > 0 {
|
||||
// Size of extra fields, shouldn't include the leading 4 bytes
|
||||
zip64ExtendedInformation = Entry.ZIP64ExtendedInformation(dataSize: extraFieldLength,
|
||||
uncompressedSize: extraUncompressedSize ?? 0,
|
||||
compressedSize: extraCompressedSize ?? 0,
|
||||
relativeOffsetOfLocalHeader: extraOffset ?? 0,
|
||||
diskNumberStart: 0)
|
||||
extraFieldLength += Entry.ZIP64ExtendedInformation.headerSize
|
||||
}
|
||||
let centralDirectory = CentralDirectoryStructure(localFileHeader: localFileHeader,
|
||||
fileAttributes: externalFileAttributes,
|
||||
relativeOffset: relativeOffsetOfCD,
|
||||
extraField: (extraFieldLength,
|
||||
zip64ExtendedInformation?.data ?? Data()))
|
||||
_ = try Data.write(chunk: centralDirectory.data, to: self.archiveFile)
|
||||
return centralDirectory
|
||||
}
|
||||
|
||||
func writeEndOfCentralDirectory(centralDirectoryStructure: CentralDirectoryStructure,
|
||||
startOfCentralDirectory: UInt64,
|
||||
startOfEndOfCentralDirectory: UInt64,
|
||||
operation: ModifyOperation) throws -> EndOfCentralDirectoryStructure {
|
||||
var record = self.endOfCentralDirectoryRecord
|
||||
let sizeOfCD = self.sizeOfCentralDirectory
|
||||
let numberOfTotalEntries = self.totalNumberOfEntriesInCentralDirectory
|
||||
let countChange = operation.rawValue
|
||||
var dataLength = centralDirectoryStructure.extraFieldLength
|
||||
dataLength += centralDirectoryStructure.fileNameLength
|
||||
dataLength += centralDirectoryStructure.fileCommentLength
|
||||
let cdDataLengthChange = countChange * (Int(dataLength) + CentralDirectoryStructure.size)
|
||||
let (updatedSizeOfCD, updatedNumberOfEntries): (UInt64, UInt64) = try {
|
||||
switch operation {
|
||||
case .add:
|
||||
guard .max - sizeOfCD >= cdDataLengthChange else {
|
||||
throw ArchiveError.invalidCentralDirectorySize
|
||||
}
|
||||
guard .max - numberOfTotalEntries >= countChange else {
|
||||
throw ArchiveError.invalidCentralDirectoryEntryCount
|
||||
}
|
||||
return (sizeOfCD + UInt64(cdDataLengthChange), numberOfTotalEntries + UInt64(countChange))
|
||||
case .remove:
|
||||
return (sizeOfCD - UInt64(-cdDataLengthChange), numberOfTotalEntries - UInt64(-countChange))
|
||||
}
|
||||
}()
|
||||
let sizeOfCDForEOCD = updatedSizeOfCD >= maxSizeOfCentralDirectory
|
||||
? UInt32.max
|
||||
: UInt32(updatedSizeOfCD)
|
||||
let numberOfTotalEntriesForEOCD = updatedNumberOfEntries >= maxTotalNumberOfEntries
|
||||
? UInt16.max
|
||||
: UInt16(updatedNumberOfEntries)
|
||||
let offsetOfCDForEOCD = startOfCentralDirectory >= maxOffsetOfCentralDirectory
|
||||
? UInt32.max
|
||||
: UInt32(startOfCentralDirectory)
|
||||
// ZIP64 End of Central Directory
|
||||
var zip64EOCD: ZIP64EndOfCentralDirectory?
|
||||
if numberOfTotalEntriesForEOCD == .max || offsetOfCDForEOCD == .max || sizeOfCDForEOCD == .max {
|
||||
zip64EOCD = try self.writeZIP64EOCD(totalNumberOfEntries: updatedNumberOfEntries,
|
||||
sizeOfCentralDirectory: updatedSizeOfCD,
|
||||
offsetOfCentralDirectory: startOfCentralDirectory,
|
||||
offsetOfEndOfCentralDirectory: startOfEndOfCentralDirectory)
|
||||
}
|
||||
record = EndOfCentralDirectoryRecord(record: record, numberOfEntriesOnDisk: numberOfTotalEntriesForEOCD,
|
||||
numberOfEntriesInCentralDirectory: numberOfTotalEntriesForEOCD,
|
||||
updatedSizeOfCentralDirectory: sizeOfCDForEOCD,
|
||||
startOfCentralDirectory: offsetOfCDForEOCD)
|
||||
_ = try Data.write(chunk: record.data, to: self.archiveFile)
|
||||
return (record, zip64EOCD)
|
||||
}
|
||||
|
||||
func writeUncompressed(size: Int64, bufferSize: Int, progress: Progress? = nil,
|
||||
provider: Provider) throws -> (sizeWritten: Int64, checksum: CRC32) {
|
||||
var position: Int64 = 0
|
||||
var sizeWritten: Int64 = 0
|
||||
var checksum = CRC32(0)
|
||||
while position < size {
|
||||
if progress?.isCancelled == true { throw ArchiveError.cancelledOperation }
|
||||
let readSize = (size - position) >= bufferSize ? bufferSize : Int(size - position)
|
||||
let entryChunk = try provider(position, readSize)
|
||||
checksum = entryChunk.crc32(checksum: checksum)
|
||||
sizeWritten += Int64(try Data.write(chunk: entryChunk, to: self.archiveFile))
|
||||
position += Int64(bufferSize)
|
||||
progress?.completedUnitCount = sizeWritten
|
||||
}
|
||||
return (sizeWritten, checksum)
|
||||
}
|
||||
|
||||
func writeCompressed(size: Int64, bufferSize: Int, progress: Progress? = nil,
|
||||
provider: Provider) throws -> (sizeWritten: Int64, checksum: CRC32) {
|
||||
var sizeWritten: Int64 = 0
|
||||
let consumer: Consumer = { data in sizeWritten += Int64(try Data.write(chunk: data, to: self.archiveFile)) }
|
||||
let checksum = try Data.compress(size: size, bufferSize: bufferSize,
|
||||
provider: { (position, size) -> Data in
|
||||
if progress?.isCancelled == true { throw ArchiveError.cancelledOperation }
|
||||
let data = try provider(position, size)
|
||||
progress?.completedUnitCount += Int64(data.count)
|
||||
return data
|
||||
}, consumer: consumer)
|
||||
return(sizeWritten, checksum)
|
||||
}
|
||||
|
||||
func writeSymbolicLink(size: Int, provider: Provider) throws -> (sizeWritten: Int, checksum: CRC32) {
|
||||
// The reported size of a symlink is the number of characters in the path it points to.
|
||||
let linkData = try provider(0, size)
|
||||
let checksum = linkData.crc32(checksum: 0)
|
||||
let sizeWritten = try Data.write(chunk: linkData, to: self.archiveFile)
|
||||
return (sizeWritten, checksum)
|
||||
}
|
||||
|
||||
func writeZIP64EOCD(totalNumberOfEntries: UInt64,
|
||||
sizeOfCentralDirectory: UInt64,
|
||||
offsetOfCentralDirectory: UInt64,
|
||||
offsetOfEndOfCentralDirectory: UInt64) throws -> ZIP64EndOfCentralDirectory {
|
||||
var zip64EOCD: ZIP64EndOfCentralDirectory = self.zip64EndOfCentralDirectory ?? {
|
||||
// Shouldn't include the leading 12 bytes: (size - 12 = 44)
|
||||
let record = ZIP64EndOfCentralDirectoryRecord(sizeOfZIP64EndOfCentralDirectoryRecord: UInt64(44),
|
||||
versionMadeBy: UInt16(789),
|
||||
versionNeededToExtract: Version.v45.rawValue,
|
||||
numberOfDisk: 0, numberOfDiskStart: 0,
|
||||
totalNumberOfEntriesOnDisk: 0,
|
||||
totalNumberOfEntriesInCentralDirectory: 0,
|
||||
sizeOfCentralDirectory: 0,
|
||||
offsetToStartOfCentralDirectory: 0,
|
||||
zip64ExtensibleDataSector: Data())
|
||||
let locator = ZIP64EndOfCentralDirectoryLocator(numberOfDiskWithZIP64EOCDRecordStart: 0,
|
||||
relativeOffsetOfZIP64EOCDRecord: 0,
|
||||
totalNumberOfDisk: 1)
|
||||
return ZIP64EndOfCentralDirectory(record: record, locator: locator)
|
||||
}()
|
||||
|
||||
let updatedRecord = ZIP64EndOfCentralDirectoryRecord(record: zip64EOCD.record,
|
||||
numberOfEntriesOnDisk: totalNumberOfEntries,
|
||||
numberOfEntriesInCD: totalNumberOfEntries,
|
||||
sizeOfCentralDirectory: sizeOfCentralDirectory,
|
||||
offsetToStartOfCD: offsetOfCentralDirectory)
|
||||
let updatedLocator = ZIP64EndOfCentralDirectoryLocator(locator: zip64EOCD.locator,
|
||||
offsetOfZIP64EOCDRecord: offsetOfEndOfCentralDirectory)
|
||||
zip64EOCD = ZIP64EndOfCentralDirectory(record: updatedRecord, locator: updatedLocator)
|
||||
_ = try Data.write(chunk: zip64EOCD.data, to: self.archiveFile)
|
||||
return zip64EOCD
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
//
|
||||
// Archive+MemoryFile.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
extension Archive {
|
||||
var isMemoryArchive: Bool { return self.url.scheme == memoryURLScheme }
|
||||
}
|
||||
|
||||
#if swift(>=5.0)
|
||||
|
||||
extension Archive {
|
||||
|
||||
class MemoryFile {
|
||||
|
||||
private(set) var data: Data
|
||||
private var offset = 0
|
||||
|
||||
init(data: Data = Data()) {
|
||||
self.data = data
|
||||
}
|
||||
|
||||
func open(mode: AccessMode) throws -> FILEPointer {
|
||||
let cookie = Unmanaged.passRetained(self)
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS) || os(Android)
|
||||
guard let result = mode.isWritable
|
||||
? funopen(cookie.toOpaque(), readStub, writeStub, seekStub, closeStub)
|
||||
: funopen(cookie.toOpaque(), readStub, nil, seekStub, closeStub)
|
||||
else { throw MemoryFileError.invalidMemoryFile }
|
||||
#else
|
||||
let stubs = cookie_io_functions_t(read: readStub, write: writeStub, seek: seekStub, close: closeStub)
|
||||
guard let result = fopencookie(cookie.toOpaque(), mode.posixMode, stubs)
|
||||
else { throw MemoryFileError.invalidMemoryFile }
|
||||
#endif
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `Data` object containing a representation of the receiver.
|
||||
public var data: Data? { return self.memoryFile?.data }
|
||||
}
|
||||
|
||||
public enum MemoryFileError: Error {
|
||||
case invalidMemoryFile
|
||||
}
|
||||
|
||||
private extension Archive.MemoryFile {
|
||||
|
||||
func readData(buffer: UnsafeMutableRawBufferPointer) -> Int {
|
||||
let size = min(buffer.count, data.count-offset)
|
||||
let start = data.startIndex
|
||||
self.data.copyBytes(to: buffer.bindMemory(to: UInt8.self), from: start+offset..<start+offset+size)
|
||||
self.offset += size
|
||||
return size
|
||||
}
|
||||
|
||||
func writeData(buffer: UnsafeRawBufferPointer) -> Int {
|
||||
let start = self.data.startIndex
|
||||
if self.offset < self.data.count && self.offset+buffer.count > self.data.count {
|
||||
self.data.removeSubrange(start+self.offset..<start+self.data.count)
|
||||
} else if offset > data.count {
|
||||
self.data.append(Data(count: self.offset-self.data.count))
|
||||
}
|
||||
if self.offset == self.data.count {
|
||||
self.data.append(buffer.bindMemory(to: UInt8.self))
|
||||
} else {
|
||||
let start = self.data.startIndex // May have changed in earlier mutation
|
||||
self.data.replaceSubrange(
|
||||
start+self.offset..<start+self.offset+buffer.count,
|
||||
with: buffer.bindMemory(to: UInt8.self)
|
||||
)
|
||||
}
|
||||
self.offset += buffer.count
|
||||
return buffer.count
|
||||
}
|
||||
|
||||
func seek(offset: Int, whence: Int32) -> Int {
|
||||
var result = -1
|
||||
if whence == SEEK_SET {
|
||||
result = offset
|
||||
} else if whence == SEEK_CUR {
|
||||
result = self.offset + offset
|
||||
} else if whence == SEEK_END {
|
||||
result = data.count + offset
|
||||
}
|
||||
self.offset = result
|
||||
return self.offset
|
||||
}
|
||||
}
|
||||
|
||||
private func fileFromCookie(cookie: UnsafeRawPointer) -> Archive.MemoryFile {
|
||||
return Unmanaged<Archive.MemoryFile>.fromOpaque(cookie).takeUnretainedValue()
|
||||
}
|
||||
|
||||
private func closeStub(_ cookie: UnsafeMutableRawPointer?) -> Int32 {
|
||||
if let cookie = cookie {
|
||||
Unmanaged<Archive.MemoryFile>.fromOpaque(cookie).release()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS) || os(Android)
|
||||
|
||||
private func readStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ bytePtr: UnsafeMutablePointer<Int8>?,
|
||||
_ count: Int32) -> Int32 {
|
||||
guard let cookie = cookie, let bytePtr = bytePtr else { return 0 }
|
||||
return Int32(fileFromCookie(cookie: cookie).readData(
|
||||
buffer: UnsafeMutableRawBufferPointer(start: bytePtr, count: Int(count))))
|
||||
}
|
||||
|
||||
private func writeStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ bytePtr: UnsafePointer<Int8>?,
|
||||
_ count: Int32) -> Int32 {
|
||||
guard let cookie = cookie, let bytePtr = bytePtr else { return 0 }
|
||||
return Int32(fileFromCookie(cookie: cookie).writeData(
|
||||
buffer: UnsafeRawBufferPointer(start: bytePtr, count: Int(count))))
|
||||
}
|
||||
|
||||
private func seekStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ offset: fpos_t,
|
||||
_ whence: Int32) -> fpos_t {
|
||||
guard let cookie = cookie else { return 0 }
|
||||
return fpos_t(fileFromCookie(cookie: cookie).seek(offset: Int(offset), whence: whence))
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
extension Archive.AccessMode {
|
||||
|
||||
var posixMode: String {
|
||||
switch self {
|
||||
case .read: return "rb"
|
||||
case .create: return "wb+"
|
||||
case .update: return "rb+"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func readStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ bytePtr: UnsafeMutablePointer<Int8>?,
|
||||
_ count: Int) -> Int {
|
||||
guard let cookie = cookie, let bytePtr = bytePtr else { return 0 }
|
||||
return fileFromCookie(cookie: cookie).readData(
|
||||
buffer: UnsafeMutableRawBufferPointer(start: bytePtr, count: count))
|
||||
}
|
||||
|
||||
private func writeStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ bytePtr: UnsafePointer<Int8>?,
|
||||
_ count: Int) -> Int {
|
||||
guard let cookie = cookie, let bytePtr = bytePtr else { return 0 }
|
||||
return fileFromCookie(cookie: cookie).writeData(
|
||||
buffer: UnsafeRawBufferPointer(start: bytePtr, count: count))
|
||||
}
|
||||
|
||||
private func seekStub(_ cookie: UnsafeMutableRawPointer?,
|
||||
_ offset: UnsafeMutablePointer<Int>?,
|
||||
_ whence: Int32) -> Int32 {
|
||||
guard let cookie = cookie, let offset = offset else { return 0 }
|
||||
let result = fileFromCookie(cookie: cookie).seek(offset: Int(offset.pointee), whence: whence)
|
||||
if result >= 0 {
|
||||
offset.pointee = result
|
||||
return 0
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// Archive+Progress.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
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.offsetToStartOfCentralDirectory - 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))
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// Archive+Reading.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
extension Archive {
|
||||
/// Read a ZIP `Entry` from the receiver and write it to `url`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - entry: The ZIP `Entry` to read.
|
||||
/// - url: The destination file URL.
|
||||
/// - bufferSize: The maximum size of the read buffer and the decompression buffer (if needed).
|
||||
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
|
||||
/// - allowUncontainedSymlinks: Optional flag to allow symlinks that point to paths outside the destination.
|
||||
/// - progress: A progress object that can be used to track or cancel the extract operation.
|
||||
/// - Returns: The checksum of the processed content or 0 if the `skipCRC32` flag was set to `true`.
|
||||
/// - Throws: An error if the destination file cannot be written or the entry contains malformed content.
|
||||
public func extract(_ entry: Entry, to url: URL, bufferSize: Int = defaultReadChunkSize,
|
||||
skipCRC32: Bool = false, allowUncontainedSymlinks: Bool = false,
|
||||
progress: Progress? = nil) throws -> CRC32 {
|
||||
guard bufferSize > 0 else {
|
||||
throw ArchiveError.invalidBufferSize
|
||||
}
|
||||
let fileManager = FileManager()
|
||||
var checksum = CRC32(0)
|
||||
switch entry.type {
|
||||
case .file:
|
||||
guard fileManager.itemExists(at: url) == false else {
|
||||
throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
try fileManager.createParentDirectoryStructure(for: url)
|
||||
let destinationRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
guard let destinationFile: FILEPointer = fopen(destinationRepresentation, "wb+") else {
|
||||
throw POSIXError(errno, path: url.path)
|
||||
}
|
||||
defer { fclose(destinationFile) }
|
||||
let consumer = { _ = try Data.write(chunk: $0, to: destinationFile) }
|
||||
checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32,
|
||||
progress: progress, consumer: consumer)
|
||||
case .directory:
|
||||
let consumer = { (_: Data) in
|
||||
try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32,
|
||||
progress: progress, consumer: consumer)
|
||||
case .symlink:
|
||||
guard fileManager.itemExists(at: url) == false else {
|
||||
throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
let consumer = { (data: Data) in
|
||||
guard let linkPath = String(data: data, encoding: .utf8) else { throw ArchiveError.invalidEntryPath }
|
||||
|
||||
let parentURL = url.deletingLastPathComponent()
|
||||
let isAbsolutePath = (linkPath as NSString).isAbsolutePath
|
||||
let linkURL = URL(fileURLWithPath: linkPath, relativeTo: isAbsolutePath ? nil : parentURL)
|
||||
let isContained = allowUncontainedSymlinks || linkURL.isContained(in: parentURL)
|
||||
guard isContained else { throw ArchiveError.uncontainedSymlink }
|
||||
|
||||
try fileManager.createParentDirectoryStructure(for: url)
|
||||
try fileManager.createSymbolicLink(atPath: url.path, withDestinationPath: linkPath)
|
||||
}
|
||||
checksum = try self.extract(entry, bufferSize: bufferSize, skipCRC32: skipCRC32,
|
||||
progress: progress, consumer: consumer)
|
||||
}
|
||||
try fileManager.transferAttributes(from: entry, toItemAtURL: url)
|
||||
return checksum
|
||||
}
|
||||
|
||||
/// Read a ZIP `Entry` from the receiver and forward its contents to a `Consumer` closure.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - entry: The ZIP `Entry` to read.
|
||||
/// - bufferSize: The maximum size of the read buffer and the decompression buffer (if needed).
|
||||
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
|
||||
/// - progress: A progress object that can be used to track or cancel the extract operation.
|
||||
/// - consumer: A closure that consumes contents of `Entry` as `Data` chunks.
|
||||
/// - Returns: The checksum of the processed content or 0 if the `skipCRC32` flag was set to `true`..
|
||||
/// - Throws: An error if the destination file cannot be written or the entry contains malformed content.
|
||||
public func extract(_ entry: Entry, bufferSize: Int = defaultReadChunkSize, skipCRC32: Bool = false,
|
||||
progress: Progress? = nil, consumer: Consumer) throws -> CRC32 {
|
||||
guard bufferSize > 0 else {
|
||||
throw ArchiveError.invalidBufferSize
|
||||
}
|
||||
var checksum = CRC32(0)
|
||||
let localFileHeader = entry.localFileHeader
|
||||
guard entry.dataOffset <= .max else { throw ArchiveError.invalidLocalHeaderDataOffset }
|
||||
fseeko(self.archiveFile, off_t(entry.dataOffset), SEEK_SET)
|
||||
progress?.totalUnitCount = self.totalUnitCountForReading(entry)
|
||||
switch entry.type {
|
||||
case .file:
|
||||
guard let compressionMethod = CompressionMethod(rawValue: localFileHeader.compressionMethod) else {
|
||||
throw ArchiveError.invalidCompressionMethod
|
||||
}
|
||||
switch compressionMethod {
|
||||
case .none: checksum = try self.readUncompressed(entry: entry, bufferSize: bufferSize,
|
||||
skipCRC32: skipCRC32, progress: progress, with: consumer)
|
||||
case .deflate: checksum = try self.readCompressed(entry: entry, bufferSize: bufferSize,
|
||||
skipCRC32: skipCRC32, progress: progress, with: consumer)
|
||||
}
|
||||
case .directory:
|
||||
try consumer(Data())
|
||||
progress?.completedUnitCount = self.totalUnitCountForReading(entry)
|
||||
case .symlink:
|
||||
let localFileHeader = entry.localFileHeader
|
||||
let size = Int(localFileHeader.compressedSize)
|
||||
let data = try Data.readChunk(of: size, from: self.archiveFile)
|
||||
checksum = data.crc32(checksum: 0)
|
||||
try consumer(data)
|
||||
progress?.completedUnitCount = self.totalUnitCountForReading(entry)
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// Archive+ReadingDeprecated.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
public extension Archive {
|
||||
|
||||
@available(*, deprecated,
|
||||
message: "Please use `Int` for `bufferSize`.")
|
||||
func extract(_ entry: Entry, to url: URL, bufferSize: UInt32, skipCRC32: Bool = false,
|
||||
progress: Progress? = nil) throws -> CRC32 {
|
||||
try self.extract(entry, to: url, bufferSize: Int(bufferSize), skipCRC32: skipCRC32, progress: progress)
|
||||
}
|
||||
|
||||
@available(*, deprecated,
|
||||
message: "Please use `Int` for `bufferSize`.")
|
||||
func extract(_ entry: Entry, bufferSize: UInt32, skipCRC32: Bool = false,
|
||||
progress: Progress? = nil, consumer: Consumer) throws -> CRC32 {
|
||||
try self.extract(entry, bufferSize: Int(bufferSize), skipCRC32: skipCRC32,
|
||||
progress: progress, consumer: consumer)
|
||||
}
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
//
|
||||
// Archive+Writing.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
extension Archive {
|
||||
enum ModifyOperation: Int {
|
||||
case remove = -1
|
||||
case add = 1
|
||||
}
|
||||
|
||||
typealias EndOfCentralDirectoryStructure = (EndOfCentralDirectoryRecord, ZIP64EndOfCentralDirectory?)
|
||||
|
||||
/// Write files, directories or symlinks to the receiver.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The path that is used to identify an `Entry` within the `Archive` file.
|
||||
/// - baseURL: The base URL of the resource to add.
|
||||
/// The `baseURL` combined with `path` must form a fully qualified file URL.
|
||||
/// - compressionMethod: Indicates the `CompressionMethod` that should be applied to `Entry`.
|
||||
/// By default, no compression will be applied.
|
||||
/// - bufferSize: The maximum size of the write buffer and the compression buffer (if needed).
|
||||
/// - progress: A progress object that can be used to track or cancel the add operation.
|
||||
/// - Throws: An error if the source file cannot be read or the receiver is not writable.
|
||||
public func addEntry(with path: String, relativeTo baseURL: URL,
|
||||
compressionMethod: CompressionMethod = .none,
|
||||
bufferSize: Int = defaultWriteChunkSize, progress: Progress? = nil) throws {
|
||||
let fileURL = baseURL.appendingPathComponent(path)
|
||||
|
||||
try self.addEntry(with: path, fileURL: fileURL, compressionMethod: compressionMethod,
|
||||
bufferSize: bufferSize, progress: progress)
|
||||
}
|
||||
|
||||
/// Write files, directories or symlinks to the receiver.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The path that is used to identify an `Entry` within the `Archive` file.
|
||||
/// - fileURL: An absolute file URL referring to the resource to add.
|
||||
/// - compressionMethod: Indicates the `CompressionMethod` that should be applied to `Entry`.
|
||||
/// By default, no compression will be applied.
|
||||
/// - bufferSize: The maximum size of the write buffer and the compression buffer (if needed).
|
||||
/// - progress: A progress object that can be used to track or cancel the add operation.
|
||||
/// - Throws: An error if the source file cannot be read or the receiver is not writable.
|
||||
public func addEntry(with path: String, fileURL: URL, compressionMethod: CompressionMethod = .none,
|
||||
bufferSize: Int = defaultWriteChunkSize, progress: Progress? = nil) throws {
|
||||
let fileManager = FileManager()
|
||||
guard fileManager.itemExists(at: fileURL) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: fileURL.path])
|
||||
}
|
||||
let type = try FileManager.typeForItem(at: fileURL)
|
||||
// symlinks do not need to be readable
|
||||
guard type == .symlink || fileManager.isReadableFile(atPath: fileURL.path) else {
|
||||
throw CocoaError(.fileReadNoPermission, userInfo: [NSFilePathErrorKey: fileURL.path])
|
||||
}
|
||||
let modDate = try FileManager.fileModificationDateTimeForItem(at: fileURL)
|
||||
let uncompressedSize = type == .directory ? 0 : try FileManager.fileSizeForItem(at: fileURL)
|
||||
let permissions = try FileManager.permissionsForItem(at: fileURL)
|
||||
var provider: Provider
|
||||
switch type {
|
||||
case .file:
|
||||
let entryFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: fileURL.path)
|
||||
guard let entryFile: FILEPointer = fopen(entryFileSystemRepresentation, "rb") else {
|
||||
throw POSIXError(errno, path: url.path)
|
||||
}
|
||||
defer { fclose(entryFile) }
|
||||
provider = { _, _ in return try Data.readChunk(of: bufferSize, from: entryFile) }
|
||||
try self.addEntry(with: path, type: type, uncompressedSize: uncompressedSize,
|
||||
modificationDate: modDate, permissions: permissions,
|
||||
compressionMethod: compressionMethod, bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
case .directory:
|
||||
provider = { _, _ in return Data() }
|
||||
try self.addEntry(with: path.hasSuffix("/") ? path : path + "/",
|
||||
type: type, uncompressedSize: uncompressedSize,
|
||||
modificationDate: modDate, permissions: permissions,
|
||||
compressionMethod: compressionMethod, bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
case .symlink:
|
||||
provider = { _, _ -> Data in
|
||||
let linkDestination = try fileManager.destinationOfSymbolicLink(atPath: fileURL.path)
|
||||
let linkFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: linkDestination)
|
||||
let linkLength = Int(strlen(linkFileSystemRepresentation))
|
||||
let linkBuffer = UnsafeBufferPointer(start: linkFileSystemRepresentation, count: linkLength)
|
||||
return Data(buffer: linkBuffer)
|
||||
}
|
||||
try self.addEntry(with: path, type: type, uncompressedSize: uncompressedSize,
|
||||
modificationDate: modDate, permissions: permissions,
|
||||
compressionMethod: compressionMethod, bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write files, directories or symlinks to the receiver.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The path that is used to identify an `Entry` within the `Archive` file.
|
||||
/// - type: Indicates the `Entry.EntryType` of the added content.
|
||||
/// - uncompressedSize: The uncompressed size of the data that is going to be added with `provider`.
|
||||
/// - modificationDate: A `Date` describing the file modification date of the `Entry`.
|
||||
/// Default is the current `Date`.
|
||||
/// - permissions: POSIX file permissions for the `Entry`.
|
||||
/// Default is `0`o`644` for files and symlinks and `0`o`755` for directories.
|
||||
/// - compressionMethod: Indicates the `CompressionMethod` that should be applied to `Entry`.
|
||||
/// By default, no compression will be applied.
|
||||
/// - bufferSize: The maximum size of the write buffer and the compression buffer (if needed).
|
||||
/// - progress: A progress object that can be used to track or cancel the add operation.
|
||||
/// - provider: A closure that accepts a position and a chunk size. Returns a `Data` chunk.
|
||||
/// - Throws: An error if the source data is invalid or the receiver is not writable.
|
||||
public func addEntry(with path: String, type: Entry.EntryType, uncompressedSize: Int64,
|
||||
modificationDate: Date = Date(), permissions: UInt16? = nil,
|
||||
compressionMethod: CompressionMethod = .none, bufferSize: Int = defaultWriteChunkSize,
|
||||
progress: Progress? = nil, provider: Provider) throws {
|
||||
guard self.accessMode != .read else { throw ArchiveError.unwritableArchive }
|
||||
// Directories and symlinks cannot be compressed
|
||||
let compressionMethod = type == .file ? compressionMethod : .none
|
||||
progress?.totalUnitCount = type == .directory ? defaultDirectoryUnitCount : uncompressedSize
|
||||
let (eocdRecord, zip64EOCD) = (self.endOfCentralDirectoryRecord, self.zip64EndOfCentralDirectory)
|
||||
guard self.offsetToStartOfCentralDirectory <= .max else { throw ArchiveError.invalidCentralDirectoryOffset }
|
||||
var startOfCD = Int64(self.offsetToStartOfCentralDirectory)
|
||||
fseeko(self.archiveFile, off_t(startOfCD), SEEK_SET)
|
||||
let existingSize = self.sizeOfCentralDirectory
|
||||
let existingData = try Data.readChunk(of: Int(existingSize), from: self.archiveFile)
|
||||
fseeko(self.archiveFile, off_t(startOfCD), SEEK_SET)
|
||||
let fileHeaderStart = Int64(ftello(self.archiveFile))
|
||||
guard fileHeaderStart >= 0 else { throw ArchiveError.unwritableArchive }
|
||||
|
||||
let modDateTime = modificationDate.fileModificationDateTime
|
||||
defer { fflush(self.archiveFile) }
|
||||
do {
|
||||
// Local File Header
|
||||
var localFileHeader = try self.writeLocalFileHeader(path: path, compressionMethod: compressionMethod,
|
||||
size: (UInt64(uncompressedSize), 0), checksum: 0,
|
||||
modificationDateTime: modDateTime)
|
||||
// File Data
|
||||
let (written, checksum) = try self.writeEntry(uncompressedSize: uncompressedSize, type: type,
|
||||
compressionMethod: compressionMethod, bufferSize: bufferSize,
|
||||
progress: progress, provider: provider)
|
||||
startOfCD = Int64(ftello(self.archiveFile))
|
||||
guard startOfCD >= 0 else { throw ArchiveError.unwritableArchive }
|
||||
|
||||
// Write the local file header a second time. Now with compressedSize (if applicable) and a valid checksum.
|
||||
fseeko(self.archiveFile, off_t(fileHeaderStart), SEEK_SET)
|
||||
localFileHeader = try self.writeLocalFileHeader(path: path, compressionMethod: compressionMethod,
|
||||
size: (UInt64(uncompressedSize), UInt64(written)),
|
||||
checksum: checksum, modificationDateTime: modDateTime)
|
||||
// Central Directory
|
||||
fseeko(self.archiveFile, off_t(startOfCD), SEEK_SET)
|
||||
_ = try Data.writeLargeChunk(existingData, size: existingSize, bufferSize: bufferSize, to: archiveFile)
|
||||
let permissions = permissions ?? (type == .directory ? defaultDirectoryPermissions : defaultFilePermissions)
|
||||
let externalAttributes = FileManager.externalFileAttributesForEntry(of: type, permissions: permissions)
|
||||
let centralDir = try self.writeCentralDirectoryStructure(localFileHeader: localFileHeader,
|
||||
relativeOffset: UInt64(fileHeaderStart),
|
||||
externalFileAttributes: externalAttributes)
|
||||
// End of Central Directory Record (including ZIP64 End of Central Directory Record/Locator)
|
||||
let startOfEOCD = Int64(ftello(self.archiveFile))
|
||||
guard startOfEOCD >= 0 else { throw ArchiveError.unwritableArchive }
|
||||
|
||||
let eocd = try
|
||||
self.writeEndOfCentralDirectory(centralDirectoryStructure: centralDir,
|
||||
startOfCentralDirectory: UInt64(startOfCD),
|
||||
startOfEndOfCentralDirectory: UInt64(startOfEOCD), operation: .add)
|
||||
(self.endOfCentralDirectoryRecord, self.zip64EndOfCentralDirectory) = eocd
|
||||
} catch ArchiveError.cancelledOperation {
|
||||
try rollback(UInt64(fileHeaderStart), (existingData, existingSize), bufferSize, eocdRecord, zip64EOCD)
|
||||
throw ArchiveError.cancelledOperation
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a ZIP `Entry` from the receiver.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - entry: The `Entry` to remove.
|
||||
/// - bufferSize: The maximum size for the read and write buffers used during removal.
|
||||
/// - progress: A progress object that can be used to track or cancel the remove operation.
|
||||
/// - Throws: An error if the `Entry` is malformed or the receiver is not writable.
|
||||
public func remove(_ entry: Entry, bufferSize: Int = defaultReadChunkSize, progress: Progress? = nil) throws {
|
||||
guard self.accessMode != .read else { throw ArchiveError.unwritableArchive }
|
||||
let (tempArchive, tempDir) = try self.makeTempArchive()
|
||||
defer { tempDir.map { try? FileManager().removeItem(at: $0) } }
|
||||
progress?.totalUnitCount = self.totalUnitCountForRemoving(entry)
|
||||
var centralDirectoryData = Data()
|
||||
var offset: UInt64 = 0
|
||||
for currentEntry in self {
|
||||
let cds = currentEntry.centralDirectoryStructure
|
||||
if currentEntry != entry {
|
||||
let entryStart = cds.effectiveRelativeOffsetOfLocalHeader
|
||||
fseeko(self.archiveFile, off_t(entryStart), SEEK_SET)
|
||||
let provider: Provider = { (_, chunkSize) -> Data in
|
||||
return try Data.readChunk(of: chunkSize, from: self.archiveFile)
|
||||
}
|
||||
let consumer: Consumer = {
|
||||
if progress?.isCancelled == true { throw ArchiveError.cancelledOperation }
|
||||
_ = try Data.write(chunk: $0, to: tempArchive.archiveFile)
|
||||
progress?.completedUnitCount += Int64($0.count)
|
||||
}
|
||||
guard currentEntry.localSize <= .max else { throw ArchiveError.invalidLocalHeaderSize }
|
||||
_ = try Data.consumePart(of: Int64(currentEntry.localSize), chunkSize: bufferSize,
|
||||
provider: provider, consumer: consumer)
|
||||
let updatedCentralDirectory = updateOffsetInCentralDirectory(centralDirectoryStructure: cds,
|
||||
updatedOffset: entryStart - offset)
|
||||
centralDirectoryData.append(updatedCentralDirectory.data)
|
||||
} else { offset = currentEntry.localSize }
|
||||
}
|
||||
let startOfCentralDirectory = Int64(ftello(tempArchive.archiveFile))
|
||||
guard startOfCentralDirectory >= 0 else { throw ArchiveError.unwritableArchive }
|
||||
|
||||
_ = try Data.write(chunk: centralDirectoryData, to: tempArchive.archiveFile)
|
||||
let startOfEndOfCentralDirectory = Int64(ftello(tempArchive.archiveFile))
|
||||
guard startOfEndOfCentralDirectory >= 0 else { throw ArchiveError.unwritableArchive }
|
||||
|
||||
tempArchive.endOfCentralDirectoryRecord = self.endOfCentralDirectoryRecord
|
||||
tempArchive.zip64EndOfCentralDirectory = self.zip64EndOfCentralDirectory
|
||||
let ecodStructure = try
|
||||
tempArchive.writeEndOfCentralDirectory(centralDirectoryStructure: entry.centralDirectoryStructure,
|
||||
startOfCentralDirectory: UInt64(startOfCentralDirectory),
|
||||
startOfEndOfCentralDirectory: UInt64(startOfEndOfCentralDirectory),
|
||||
operation: .remove)
|
||||
(tempArchive.endOfCentralDirectoryRecord, tempArchive.zip64EndOfCentralDirectory) = ecodStructure
|
||||
(self.endOfCentralDirectoryRecord, self.zip64EndOfCentralDirectory) = ecodStructure
|
||||
fflush(tempArchive.archiveFile)
|
||||
try self.replaceCurrentArchive(with: tempArchive)
|
||||
}
|
||||
|
||||
func replaceCurrentArchive(with archive: Archive) throws {
|
||||
if self.isMemoryArchive {
|
||||
#if swift(>=5.0)
|
||||
guard let data = archive.data else {
|
||||
throw ArchiveError.unwritableArchive
|
||||
}
|
||||
|
||||
let config = try Archive.makeBackingConfiguration(for: data, mode: .update)
|
||||
self.archiveFile = config.file
|
||||
self.memoryFile = config.memoryFile
|
||||
self.endOfCentralDirectoryRecord = config.endOfCentralDirectoryRecord
|
||||
self.zip64EndOfCentralDirectory = config.zip64EndOfCentralDirectory
|
||||
#endif
|
||||
} else {
|
||||
let fileManager = FileManager()
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
|
||||
do {
|
||||
_ = try fileManager.replaceItemAt(self.url, withItemAt: archive.url)
|
||||
} catch {
|
||||
_ = try fileManager.removeItem(at: self.url)
|
||||
_ = try fileManager.moveItem(at: archive.url, to: self.url)
|
||||
}
|
||||
#else
|
||||
_ = try fileManager.removeItem(at: self.url)
|
||||
_ = try fileManager.moveItem(at: archive.url, to: self.url)
|
||||
#endif
|
||||
let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: self.url.path)
|
||||
guard let file = fopen(fileSystemRepresentation, "rb+") else { throw ArchiveError.unreadableArchive }
|
||||
|
||||
self.archiveFile = file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension Archive {
|
||||
|
||||
func updateOffsetInCentralDirectory(centralDirectoryStructure: CentralDirectoryStructure,
|
||||
updatedOffset: UInt64) -> CentralDirectoryStructure {
|
||||
let zip64ExtendedInformation = Entry.ZIP64ExtendedInformation(
|
||||
zip64ExtendedInformation: centralDirectoryStructure.zip64ExtendedInformation, offset: updatedOffset)
|
||||
let offsetInCD = updatedOffset < maxOffsetOfLocalFileHeader ? UInt32(updatedOffset) : UInt32.max
|
||||
return CentralDirectoryStructure(centralDirectoryStructure: centralDirectoryStructure,
|
||||
zip64ExtendedInformation: zip64ExtendedInformation,
|
||||
relativeOffset: offsetInCD)
|
||||
}
|
||||
|
||||
func rollback(_ localFileHeaderStart: UInt64, _ existingCentralDirectory: (data: Data, size: UInt64),
|
||||
_ bufferSize: Int, _ endOfCentralDirRecord: EndOfCentralDirectoryRecord,
|
||||
_ zip64EndOfCentralDirectory: ZIP64EndOfCentralDirectory?) throws {
|
||||
fflush(self.archiveFile)
|
||||
ftruncate(fileno(self.archiveFile), off_t(localFileHeaderStart))
|
||||
fseeko(self.archiveFile, off_t(localFileHeaderStart), SEEK_SET)
|
||||
_ = try Data.writeLargeChunk(existingCentralDirectory.data, size: existingCentralDirectory.size,
|
||||
bufferSize: bufferSize, to: archiveFile)
|
||||
_ = try Data.write(chunk: existingCentralDirectory.data, to: self.archiveFile)
|
||||
if let zip64EOCD = zip64EndOfCentralDirectory {
|
||||
_ = try Data.write(chunk: zip64EOCD.data, to: self.archiveFile)
|
||||
}
|
||||
_ = try Data.write(chunk: endOfCentralDirRecord.data, to: self.archiveFile)
|
||||
}
|
||||
|
||||
func makeTempArchive() throws -> (Archive, URL?) {
|
||||
var archive: Archive
|
||||
var url: URL?
|
||||
if self.isMemoryArchive {
|
||||
#if swift(>=5.0)
|
||||
archive = try Archive(data: Data(), accessMode: .create,
|
||||
pathEncoding: self.pathEncoding)
|
||||
#else
|
||||
fatalError("Memory archives are unsupported.")
|
||||
#endif
|
||||
} else {
|
||||
let manager = FileManager()
|
||||
let tempDir = URL.temporaryReplacementDirectoryURL(for: self)
|
||||
let uniqueString = ProcessInfo.processInfo.globallyUniqueString
|
||||
let tempArchiveURL = tempDir.appendingPathComponent(uniqueString)
|
||||
try manager.createParentDirectoryStructure(for: tempArchiveURL)
|
||||
let tempArchive = try Archive(url: tempArchiveURL, accessMode: .create)
|
||||
archive = tempArchive
|
||||
url = tempDir
|
||||
}
|
||||
return (archive, url)
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Archive+WritingDeprecated.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
public extension Archive {
|
||||
|
||||
@available(*, deprecated,
|
||||
message: "Please use `Int` for `bufferSize`.")
|
||||
func addEntry(with path: String, relativeTo baseURL: URL,
|
||||
compressionMethod: CompressionMethod = .none,
|
||||
bufferSize: UInt32, progress: Progress? = nil) throws {
|
||||
try self.addEntry(with: path, relativeTo: baseURL, compressionMethod: compressionMethod,
|
||||
bufferSize: Int(bufferSize), progress: progress)
|
||||
}
|
||||
|
||||
@available(*, deprecated,
|
||||
message: "Please use `Int` for `bufferSize`.")
|
||||
func addEntry(with path: String, fileURL: URL, compressionMethod: CompressionMethod = .none,
|
||||
bufferSize: UInt32, progress: Progress? = nil) throws {
|
||||
try self.addEntry(with: path, fileURL: fileURL, compressionMethod: compressionMethod,
|
||||
bufferSize: Int(bufferSize), progress: progress)
|
||||
}
|
||||
|
||||
@available(*, deprecated,
|
||||
message: "Please use `Int64` for `uncompressedSize` and provider `position`. `Int` for `bufferSize`.")
|
||||
func addEntry(with path: String, type: Entry.EntryType, uncompressedSize: UInt32,
|
||||
modificationDate: Date = Date(), permissions: UInt16? = nil,
|
||||
compressionMethod: CompressionMethod = .none, bufferSize: Int = defaultWriteChunkSize,
|
||||
progress: Progress? = nil, provider: (_ position: Int, _ size: Int) throws -> Data) throws {
|
||||
let newProvider: Provider = { try provider(Int($0), $1) }
|
||||
try self.addEntry(with: path, type: type, uncompressedSize: Int64(uncompressedSize),
|
||||
modificationDate: modificationDate, permissions: permissions,
|
||||
compressionMethod: compressionMethod, bufferSize: bufferSize,
|
||||
progress: progress, provider: newProvider)
|
||||
}
|
||||
|
||||
@available(*, deprecated,
|
||||
message: "Please use `Int` for `bufferSize`.")
|
||||
func remove(_ entry: Entry, bufferSize: UInt32, progress: Progress? = nil) throws {
|
||||
try self.remove(entry, bufferSize: Int(bufferSize), progress: progress)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//
|
||||
// Archive+ZIP64.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
let zip64EOCDRecordStructSignature = 0x06064b50
|
||||
let zip64EOCDLocatorStructSignature = 0x07064b50
|
||||
|
||||
enum ExtraFieldHeaderID: UInt16 {
|
||||
case zip64ExtendedInformation = 0x0001
|
||||
}
|
||||
|
||||
extension Archive {
|
||||
struct ZIP64EndOfCentralDirectory {
|
||||
let record: ZIP64EndOfCentralDirectoryRecord
|
||||
let locator: ZIP64EndOfCentralDirectoryLocator
|
||||
}
|
||||
|
||||
struct ZIP64EndOfCentralDirectoryRecord: DataSerializable {
|
||||
let zip64EOCDRecordSignature = UInt32(zip64EOCDRecordStructSignature)
|
||||
let sizeOfZIP64EndOfCentralDirectoryRecord: UInt64
|
||||
let versionMadeBy: UInt16
|
||||
let versionNeededToExtract: UInt16
|
||||
let numberOfDisk: UInt32
|
||||
let numberOfDiskStart: UInt32
|
||||
let totalNumberOfEntriesOnDisk: UInt64
|
||||
let totalNumberOfEntriesInCentralDirectory: UInt64
|
||||
let sizeOfCentralDirectory: UInt64
|
||||
let offsetToStartOfCentralDirectory: UInt64
|
||||
let zip64ExtensibleDataSector: Data
|
||||
static let size = 56
|
||||
}
|
||||
|
||||
struct ZIP64EndOfCentralDirectoryLocator: DataSerializable {
|
||||
let zip64EOCDLocatorSignature = UInt32(zip64EOCDLocatorStructSignature)
|
||||
let numberOfDiskWithZIP64EOCDRecordStart: UInt32
|
||||
let relativeOffsetOfZIP64EOCDRecord: UInt64
|
||||
let totalNumberOfDisk: UInt32
|
||||
static let size = 20
|
||||
}
|
||||
}
|
||||
|
||||
extension Archive.ZIP64EndOfCentralDirectoryRecord {
|
||||
var data: Data {
|
||||
var zip64EOCDRecordSignature = self.zip64EOCDRecordSignature
|
||||
var sizeOfZIP64EOCDRecord = self.sizeOfZIP64EndOfCentralDirectoryRecord
|
||||
var versionMadeBy = self.versionMadeBy
|
||||
var versionNeededToExtract = self.versionNeededToExtract
|
||||
var numberOfDisk = self.numberOfDisk
|
||||
var numberOfDiskStart = self.numberOfDiskStart
|
||||
var totalNumberOfEntriesOnDisk = self.totalNumberOfEntriesOnDisk
|
||||
var totalNumberOfEntriesInCD = self.totalNumberOfEntriesInCentralDirectory
|
||||
var sizeOfCD = self.sizeOfCentralDirectory
|
||||
var offsetToStartOfCD = self.offsetToStartOfCentralDirectory
|
||||
var data = Data()
|
||||
withUnsafePointer(to: &zip64EOCDRecordSignature, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &sizeOfZIP64EOCDRecord, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &versionMadeBy, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &versionNeededToExtract, { 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: &sizeOfCD, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &offsetToStartOfCD, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
data.append(self.zip64ExtensibleDataSector)
|
||||
return data
|
||||
}
|
||||
|
||||
init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) {
|
||||
guard data.count == Archive.ZIP64EndOfCentralDirectoryRecord.size else { return nil }
|
||||
guard data.scanValue(start: 0) == zip64EOCDRecordSignature else { return nil }
|
||||
self.sizeOfZIP64EndOfCentralDirectoryRecord = data.scanValue(start: 4)
|
||||
self.versionMadeBy = data.scanValue(start: 12)
|
||||
self.versionNeededToExtract = data.scanValue(start: 14)
|
||||
// Version Needed to Extract: 4.5 - File uses ZIP64 format extensions
|
||||
guard self.versionNeededToExtract >= Archive.Version.v45.rawValue else { return nil }
|
||||
self.numberOfDisk = data.scanValue(start: 16)
|
||||
self.numberOfDiskStart = data.scanValue(start: 20)
|
||||
self.totalNumberOfEntriesOnDisk = data.scanValue(start: 24)
|
||||
self.totalNumberOfEntriesInCentralDirectory = data.scanValue(start: 32)
|
||||
self.sizeOfCentralDirectory = data.scanValue(start: 40)
|
||||
self.offsetToStartOfCentralDirectory = data.scanValue(start: 48)
|
||||
self.zip64ExtensibleDataSector = Data()
|
||||
}
|
||||
|
||||
init(record: Archive.ZIP64EndOfCentralDirectoryRecord,
|
||||
numberOfEntriesOnDisk: UInt64,
|
||||
numberOfEntriesInCD: UInt64,
|
||||
sizeOfCentralDirectory: UInt64,
|
||||
offsetToStartOfCD: UInt64) {
|
||||
self.sizeOfZIP64EndOfCentralDirectoryRecord = record.sizeOfZIP64EndOfCentralDirectoryRecord
|
||||
self.versionMadeBy = record.versionMadeBy
|
||||
self.versionNeededToExtract = record.versionNeededToExtract
|
||||
self.numberOfDisk = record.numberOfDisk
|
||||
self.numberOfDiskStart = record.numberOfDiskStart
|
||||
self.totalNumberOfEntriesOnDisk = numberOfEntriesOnDisk
|
||||
self.totalNumberOfEntriesInCentralDirectory = numberOfEntriesInCD
|
||||
self.sizeOfCentralDirectory = sizeOfCentralDirectory
|
||||
self.offsetToStartOfCentralDirectory = offsetToStartOfCD
|
||||
self.zip64ExtensibleDataSector = record.zip64ExtensibleDataSector
|
||||
}
|
||||
}
|
||||
|
||||
extension Archive.ZIP64EndOfCentralDirectoryLocator {
|
||||
var data: Data {
|
||||
var zip64EOCDLocatorSignature = self.zip64EOCDLocatorSignature
|
||||
var numberOfDiskWithZIP64EOCD = self.numberOfDiskWithZIP64EOCDRecordStart
|
||||
var offsetOfZIP64EOCDRecord = self.relativeOffsetOfZIP64EOCDRecord
|
||||
var totalNumberOfDisk = self.totalNumberOfDisk
|
||||
var data = Data()
|
||||
withUnsafePointer(to: &zip64EOCDLocatorSignature, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &numberOfDiskWithZIP64EOCD, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &offsetOfZIP64EOCDRecord, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &totalNumberOfDisk, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
return data
|
||||
}
|
||||
|
||||
init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) {
|
||||
guard data.count == Archive.ZIP64EndOfCentralDirectoryLocator.size else { return nil }
|
||||
guard data.scanValue(start: 0) == zip64EOCDLocatorSignature else { return nil }
|
||||
self.numberOfDiskWithZIP64EOCDRecordStart = data.scanValue(start: 4)
|
||||
self.relativeOffsetOfZIP64EOCDRecord = data.scanValue(start: 8)
|
||||
self.totalNumberOfDisk = data.scanValue(start: 16)
|
||||
}
|
||||
|
||||
init(locator: Archive.ZIP64EndOfCentralDirectoryLocator, offsetOfZIP64EOCDRecord: UInt64) {
|
||||
self.numberOfDiskWithZIP64EOCDRecordStart = locator.numberOfDiskWithZIP64EOCDRecordStart
|
||||
self.relativeOffsetOfZIP64EOCDRecord = offsetOfZIP64EOCDRecord
|
||||
self.totalNumberOfDisk = locator.totalNumberOfDisk
|
||||
}
|
||||
}
|
||||
|
||||
extension Archive.ZIP64EndOfCentralDirectory {
|
||||
var data: Data { record.data + locator.data }
|
||||
}
|
||||
|
||||
/// Properties that represent the maximum value of each field
|
||||
var maxUInt32 = UInt32.max
|
||||
var maxUInt16 = UInt16.max
|
||||
|
||||
var maxCompressedSize: UInt32 { maxUInt32 }
|
||||
var maxUncompressedSize: UInt32 { maxUInt32 }
|
||||
var maxOffsetOfLocalFileHeader: UInt32 { maxUInt32 }
|
||||
var maxOffsetOfCentralDirectory: UInt32 { maxUInt32 }
|
||||
var maxSizeOfCentralDirectory: UInt32 { maxUInt32 }
|
||||
var maxTotalNumberOfEntries: UInt16 { maxUInt16 }
|
||||
@@ -0,0 +1,369 @@
|
||||
//
|
||||
// Archive.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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 = Int(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)
|
||||
/// The default permissions for newly added directories.
|
||||
public let defaultDirectoryPermissions = UInt16(0o755)
|
||||
let defaultPOSIXBufferSize = defaultReadChunkSize
|
||||
let defaultDirectoryUnitCount = Int64(1)
|
||||
let minEndOfCentralDirectoryOffset = Int64(22)
|
||||
let endOfCentralDirectoryStructSignature = 0x06054b50
|
||||
let localFileHeaderStructSignature = 0x04034b50
|
||||
let dataDescriptorStructSignature = 0x08074b50
|
||||
let centralDirectoryStructSignature = 0x02014b50
|
||||
let memoryURLScheme = "memory"
|
||||
|
||||
/// 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.DefaultDataDescriptor
|
||||
typealias ZIP64DataDescriptor = Entry.ZIP64DataDescriptor
|
||||
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 stored checksum of an `Entry` doesn't match the checksum during reading.
|
||||
case invalidCRC32
|
||||
/// Thrown when an extract, add or remove operation was canceled.
|
||||
case cancelledOperation
|
||||
/// Thrown when an extract operation was called with zero or negative `bufferSize` parameter.
|
||||
case invalidBufferSize
|
||||
/// Thrown when uncompressedSize/compressedSize exceeds `Int64.max` (Imposed by file API).
|
||||
case invalidEntrySize
|
||||
/// Thrown when the offset of local header data exceeds `Int64.max` (Imposed by file API).
|
||||
case invalidLocalHeaderDataOffset
|
||||
/// Thrown when the size of local header exceeds `Int64.max` (Imposed by file API).
|
||||
case invalidLocalHeaderSize
|
||||
/// Thrown when the offset of central directory exceeds `Int64.max` (Imposed by file API).
|
||||
case invalidCentralDirectoryOffset
|
||||
/// Thrown when the size of central directory exceeds `UInt64.max` (Imposed by ZIP specification).
|
||||
case invalidCentralDirectorySize
|
||||
/// Thrown when number of entries in central directory exceeds `UInt64.max` (Imposed by ZIP specification).
|
||||
case invalidCentralDirectoryEntryCount
|
||||
/// Thrown when an archive does not contain the required End of Central Directory Record.
|
||||
case missingEndOfCentralDirectoryRecord
|
||||
/// Thrown when an entry contains a symlink pointing to a path outside the destination directory.
|
||||
case uncontainedSymlink
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
/// Indicates that the archive can be written to.
|
||||
var isWritable: Bool { self != .read }
|
||||
}
|
||||
|
||||
/// The version of an `Archive`
|
||||
enum Version: UInt16 {
|
||||
/// The minimum version for deflate compressed archives
|
||||
case v20 = 20
|
||||
/// The minimum version for archives making use of ZIP64 extensions
|
||||
case v45 = 45
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// URL of an Archive's backing file.
|
||||
public let url: URL
|
||||
/// Access mode for an archive file.
|
||||
public let accessMode: AccessMode
|
||||
var archiveFile: FILEPointer {
|
||||
willSet {
|
||||
fclose(self.archiveFile)
|
||||
}
|
||||
}
|
||||
var endOfCentralDirectoryRecord: EndOfCentralDirectoryRecord
|
||||
var zip64EndOfCentralDirectory: ZIP64EndOfCentralDirectory?
|
||||
var pathEncoding: String.Encoding?
|
||||
|
||||
var totalNumberOfEntriesInCentralDirectory: UInt64 {
|
||||
zip64EndOfCentralDirectory?.record.totalNumberOfEntriesInCentralDirectory
|
||||
?? UInt64(endOfCentralDirectoryRecord.totalNumberOfEntriesInCentralDirectory)
|
||||
}
|
||||
var sizeOfCentralDirectory: UInt64 {
|
||||
zip64EndOfCentralDirectory?.record.sizeOfCentralDirectory
|
||||
?? UInt64(endOfCentralDirectoryRecord.sizeOfCentralDirectory)
|
||||
}
|
||||
var offsetToStartOfCentralDirectory: UInt64 {
|
||||
zip64EndOfCentralDirectory?.record.offsetToStartOfCentralDirectory
|
||||
?? UInt64(endOfCentralDirectoryRecord.offsetToStartOfCentralDirectory)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// - pathEncoding: 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, pathEncoding: String.Encoding? = nil) throws {
|
||||
self.url = url
|
||||
self.accessMode = mode
|
||||
self.pathEncoding = pathEncoding
|
||||
let config = try Archive.makeBackingConfiguration(for: url, mode: mode)
|
||||
self.archiveFile = config.file
|
||||
self.endOfCentralDirectoryRecord = config.endOfCentralDirectoryRecord
|
||||
self.zip64EndOfCentralDirectory = config.zip64EndOfCentralDirectory
|
||||
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.
|
||||
/// - pathEncoding: 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, pathEncoding: String.Encoding? = nil) throws {
|
||||
let url = URL(string: "\(memoryURLScheme)://")!
|
||||
self.url = url
|
||||
self.accessMode = mode
|
||||
self.pathEncoding = pathEncoding
|
||||
let config = try Archive.makeBackingConfiguration(for: data, mode: mode)
|
||||
self.archiveFile = config.file
|
||||
self.memoryFile = config.memoryFile
|
||||
self.endOfCentralDirectoryRecord = config.endOfCentralDirectoryRecord
|
||||
self.zip64EndOfCentralDirectory = config.zip64EndOfCentralDirectory
|
||||
}
|
||||
#endif
|
||||
|
||||
deinit {
|
||||
fclose(self.archiveFile)
|
||||
}
|
||||
|
||||
public func makeIterator() -> AnyIterator<Entry> {
|
||||
let totalNumberOfEntriesInCD = self.totalNumberOfEntriesInCentralDirectory
|
||||
var directoryIndex = self.offsetToStartOfCentralDirectory
|
||||
var index = 0
|
||||
return AnyIterator {
|
||||
guard index < totalNumberOfEntriesInCD else { return nil }
|
||||
guard let centralDirStruct: CentralDirectoryStructure = Data.readStruct(from: self.archiveFile,
|
||||
at: directoryIndex) else {
|
||||
return nil
|
||||
}
|
||||
let offset = UInt64(centralDirStruct.effectiveRelativeOffsetOfLocalHeader)
|
||||
guard let localFileHeader: LocalFileHeader = Data.readStruct(from: self.archiveFile,
|
||||
at: offset) else { return nil }
|
||||
var dataDescriptor: DataDescriptor?
|
||||
var zip64DataDescriptor: ZIP64DataDescriptor?
|
||||
if centralDirStruct.usesDataDescriptor {
|
||||
let additionalSize = UInt64(localFileHeader.fileNameLength) + UInt64(localFileHeader.extraFieldLength)
|
||||
let isCompressed = centralDirStruct.compressionMethod != CompressionMethod.none.rawValue
|
||||
let dataSize = isCompressed
|
||||
? centralDirStruct.effectiveCompressedSize
|
||||
: centralDirStruct.effectiveUncompressedSize
|
||||
let descriptorPosition = offset + UInt64(LocalFileHeader.size) + additionalSize + dataSize
|
||||
if centralDirStruct.isZIP64 {
|
||||
zip64DataDescriptor = Data.readStruct(from: self.archiveFile, at: descriptorPosition)
|
||||
} else {
|
||||
dataDescriptor = Data.readStruct(from: self.archiveFile, at: descriptorPosition)
|
||||
}
|
||||
}
|
||||
defer {
|
||||
directoryIndex += UInt64(CentralDirectoryStructure.size)
|
||||
directoryIndex += UInt64(centralDirStruct.fileNameLength)
|
||||
directoryIndex += UInt64(centralDirStruct.extraFieldLength)
|
||||
directoryIndex += UInt64(centralDirStruct.fileCommentLength)
|
||||
index += 1
|
||||
}
|
||||
return Entry(centralDirectoryStructure: centralDirStruct, localFileHeader: localFileHeader,
|
||||
dataDescriptor: dataDescriptor, zip64DataDescriptor: zip64DataDescriptor)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 identifying the corresponding `Entry`.
|
||||
/// - Returns: An `Entry` with the given `path`. Otherwise, `nil`.
|
||||
public subscript(path: String) -> Entry? {
|
||||
if let encoding = self.pathEncoding {
|
||||
return self.first { $0.path(using: encoding) == path }
|
||||
}
|
||||
return self.first { $0.path == path }
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
static func scanForEndOfCentralDirectoryRecord(in file: FILEPointer)
|
||||
-> EndOfCentralDirectoryStructure? {
|
||||
var eocdOffset: UInt64 = 0
|
||||
var index = minEndOfCentralDirectoryOffset
|
||||
fseeko(file, 0, SEEK_END)
|
||||
let archiveLength = Int64(ftello(file))
|
||||
guard archiveLength >= 0 else { return nil }
|
||||
|
||||
while eocdOffset == 0 && index <= archiveLength {
|
||||
fseeko(file, off_t(archiveLength - index), SEEK_SET)
|
||||
var potentialDirectoryEndTag: UInt32 = UInt32()
|
||||
fread(&potentialDirectoryEndTag, 1, MemoryLayout<UInt32>.size, file)
|
||||
if potentialDirectoryEndTag == UInt32(endOfCentralDirectoryStructSignature) {
|
||||
eocdOffset = UInt64(archiveLength - index)
|
||||
guard let eocd: EndOfCentralDirectoryRecord = Data.readStruct(from: file, at: eocdOffset) else {
|
||||
return nil
|
||||
}
|
||||
let zip64EOCD = scanForZIP64EndOfCentralDirectory(in: file, eocdOffset: eocdOffset)
|
||||
return (eocd, zip64EOCD)
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func scanForZIP64EndOfCentralDirectory(in file: FILEPointer, eocdOffset: UInt64)
|
||||
-> ZIP64EndOfCentralDirectory? {
|
||||
guard UInt64(ZIP64EndOfCentralDirectoryLocator.size) < eocdOffset else { return nil }
|
||||
|
||||
let locatorOffset = eocdOffset - UInt64(ZIP64EndOfCentralDirectoryLocator.size)
|
||||
guard UInt64(ZIP64EndOfCentralDirectoryRecord.size) < locatorOffset else { return nil }
|
||||
|
||||
let recordOffset = locatorOffset - UInt64(ZIP64EndOfCentralDirectoryRecord.size)
|
||||
guard let locator: ZIP64EndOfCentralDirectoryLocator = Data.readStruct(from: file, at: locatorOffset),
|
||||
let record: ZIP64EndOfCentralDirectoryRecord = Data.readStruct(from: file, at: recordOffset) else {
|
||||
return nil
|
||||
}
|
||||
return ZIP64EndOfCentralDirectory(record: record, locator: locator)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
self.numberOfDisk = record.numberOfDisk
|
||||
self.numberOfDiskStart = record.numberOfDiskStart
|
||||
self.totalNumberOfEntriesOnDisk = numberOfEntriesOnDisk
|
||||
self.totalNumberOfEntriesInCentralDirectory = numberOfEntriesInCentralDirectory
|
||||
self.sizeOfCentralDirectory = updatedSizeOfCentralDirectory
|
||||
self.offsetToStartOfCentralDirectory = startOfCentralDirectory
|
||||
self.zipFileCommentLength = record.zipFileCommentLength
|
||||
self.zipFileCommentData = record.zipFileCommentData
|
||||
}
|
||||
}
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
//
|
||||
// Data+Compression.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
#if canImport(zlib)
|
||||
import zlib
|
||||
#endif
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// An unsigned 32-Bit Integer representing a checksum.
|
||||
public typealias CRC32 = UInt32
|
||||
/// A custom handler that consumes a `Data` object containing partial entry data.
|
||||
/// - Parameters:
|
||||
/// - data: A chunk of `Data` to consume.
|
||||
/// - Throws: Can throw to indicate errors during data consumption.
|
||||
public typealias Consumer = (_ data: Data) throws -> Void
|
||||
/// A custom handler that receives a position and a size that can be used to provide data from an arbitrary source.
|
||||
/// - Parameters:
|
||||
/// - position: The current read position.
|
||||
/// - size: The size of the chunk to provide.
|
||||
/// - Returns: A chunk of `Data`.
|
||||
/// - Throws: Can throw to indicate errors in the data source.
|
||||
public typealias Provider = (_ position: Int64, _ size: Int) throws -> Data
|
||||
|
||||
extension Data {
|
||||
enum CompressionError: Error {
|
||||
case invalidStream
|
||||
case corruptedData
|
||||
}
|
||||
|
||||
/// Calculate the `CRC32` checksum of the receiver.
|
||||
///
|
||||
/// - Parameter checksum: The starting seed.
|
||||
/// - Returns: The checksum calculated from the bytes of the receiver and the starting seed.
|
||||
public func crc32(checksum: CRC32) -> CRC32 {
|
||||
#if canImport(zlib)
|
||||
return withUnsafeBytes { bufferPointer in
|
||||
let length = UInt32(count)
|
||||
return CRC32(zlib.crc32(UInt(checksum), bufferPointer.bindMemory(to: UInt8.self).baseAddress, length))
|
||||
}
|
||||
#else
|
||||
return self.builtInCRC32(checksum: checksum)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Compress the output of `provider` and pass it to `consumer`.
|
||||
/// - Parameters:
|
||||
/// - size: The uncompressed size of the data to be compressed.
|
||||
/// - bufferSize: The maximum size of the compression buffer.
|
||||
/// - provider: A closure that accepts a position and a chunk size. Returns a `Data` chunk.
|
||||
/// - consumer: A closure that processes the result of the compress operation.
|
||||
/// - Returns: The checksum of the processed content.
|
||||
public static func compress(size: Int64, bufferSize: Int, provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
|
||||
return try self.process(operation: COMPRESSION_STREAM_ENCODE, size: size, bufferSize: bufferSize,
|
||||
provider: provider, consumer: consumer)
|
||||
#else
|
||||
return try self.encode(size: size, bufferSize: bufferSize, provider: provider, consumer: consumer)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Decompress the output of `provider` and pass it to `consumer`.
|
||||
/// - Parameters:
|
||||
/// - size: The compressed size of the data to be decompressed.
|
||||
/// - bufferSize: The maximum size of the decompression buffer.
|
||||
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
|
||||
/// - provider: A closure that accepts a position and a chunk size. Returns a `Data` chunk.
|
||||
/// - consumer: A closure that processes the result of the decompress operation.
|
||||
/// - Returns: The checksum of the processed content.
|
||||
public static func decompress(size: Int64, bufferSize: Int, skipCRC32: Bool,
|
||||
provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
|
||||
return try self.process(operation: COMPRESSION_STREAM_DECODE, size: size, bufferSize: bufferSize,
|
||||
skipCRC32: skipCRC32, provider: provider, consumer: consumer)
|
||||
#else
|
||||
return try self.decode(bufferSize: bufferSize, skipCRC32: skipCRC32, provider: provider, consumer: consumer)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Apple Platforms
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
|
||||
import Compression
|
||||
|
||||
extension Data {
|
||||
|
||||
static func process(operation: compression_stream_operation, size: Int64, bufferSize: Int, skipCRC32: Bool = false,
|
||||
provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
var crc32 = CRC32(0)
|
||||
let destPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
||||
defer { destPointer.deallocate() }
|
||||
let streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
|
||||
defer { streamPointer.deallocate() }
|
||||
var stream = streamPointer.pointee
|
||||
var status = compression_stream_init(&stream, operation, COMPRESSION_ZLIB)
|
||||
guard status != COMPRESSION_STATUS_ERROR else { throw CompressionError.invalidStream }
|
||||
defer { compression_stream_destroy(&stream) }
|
||||
stream.src_size = 0
|
||||
stream.dst_ptr = destPointer
|
||||
stream.dst_size = bufferSize
|
||||
var position: Int64 = 0
|
||||
var sourceData: Data?
|
||||
repeat {
|
||||
let isExhausted = stream.src_size == 0
|
||||
if isExhausted {
|
||||
do {
|
||||
sourceData = try provider(position, Int(Swift.min((size - position), Int64(bufferSize))))
|
||||
position += Int64(stream.prepare(for: sourceData))
|
||||
} catch { throw error }
|
||||
}
|
||||
if let sourceData = sourceData {
|
||||
sourceData.withUnsafeBytes { rawBufferPointer in
|
||||
if let baseAddress = rawBufferPointer.baseAddress {
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
stream.src_ptr = pointer.advanced(by: sourceData.count - stream.src_size)
|
||||
let flags = sourceData.count < bufferSize ? Int32(COMPRESSION_STREAM_FINALIZE.rawValue) : 0
|
||||
status = compression_stream_process(&stream, flags)
|
||||
}
|
||||
}
|
||||
if operation == COMPRESSION_STREAM_ENCODE &&
|
||||
isExhausted && skipCRC32 == false { crc32 = sourceData.crc32(checksum: crc32) }
|
||||
}
|
||||
switch status {
|
||||
case COMPRESSION_STATUS_OK, COMPRESSION_STATUS_END:
|
||||
let outputData = Data(bytesNoCopy: destPointer, count: bufferSize - stream.dst_size, deallocator: .none)
|
||||
try consumer(outputData)
|
||||
if operation == COMPRESSION_STREAM_DECODE && !skipCRC32 { crc32 = outputData.crc32(checksum: crc32) }
|
||||
stream.dst_ptr = destPointer
|
||||
stream.dst_size = bufferSize
|
||||
default: throw CompressionError.corruptedData
|
||||
}
|
||||
} while status == COMPRESSION_STATUS_OK
|
||||
return crc32
|
||||
}
|
||||
}
|
||||
|
||||
private extension compression_stream {
|
||||
|
||||
mutating func prepare(for sourceData: Data?) -> Int {
|
||||
guard let sourceData = sourceData else { return 0 }
|
||||
|
||||
self.src_size = sourceData.count
|
||||
return sourceData.count
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Linux
|
||||
|
||||
#else
|
||||
import CZlib
|
||||
|
||||
extension Data {
|
||||
static func encode(size: Int64, bufferSize: Int, provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
var stream = z_stream()
|
||||
let streamSize = Int32(MemoryLayout<z_stream>.size)
|
||||
var result = deflateInit2_(&stream, Z_DEFAULT_COMPRESSION,
|
||||
Z_DEFLATED, -MAX_WBITS, 9, Z_DEFAULT_STRATEGY, ZLIB_VERSION, streamSize)
|
||||
defer { deflateEnd(&stream) }
|
||||
guard result == Z_OK else { throw CompressionError.invalidStream }
|
||||
var flush = Z_NO_FLUSH
|
||||
var position: Int64 = 0
|
||||
var zipCRC32 = CRC32(0)
|
||||
repeat {
|
||||
let readSize = Int(Swift.min((size - position), Int64(bufferSize)))
|
||||
var inputChunk = try provider(position, readSize)
|
||||
zipCRC32 = inputChunk.crc32(checksum: zipCRC32)
|
||||
stream.avail_in = UInt32(inputChunk.count)
|
||||
try inputChunk.withUnsafeMutableBytes { (rawBufferPointer) in
|
||||
if let baseAddress = rawBufferPointer.baseAddress {
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
stream.next_in = pointer
|
||||
flush = position + Int64(bufferSize) >= size ? Z_FINISH : Z_NO_FLUSH
|
||||
} else if rawBufferPointer.count > 0 {
|
||||
throw CompressionError.corruptedData
|
||||
} else {
|
||||
stream.next_in = nil
|
||||
flush = Z_FINISH
|
||||
}
|
||||
var outputChunk = Data(count: bufferSize)
|
||||
repeat {
|
||||
stream.avail_out = UInt32(bufferSize)
|
||||
try outputChunk.withUnsafeMutableBytes { (rawBufferPointer) in
|
||||
guard let baseAddress = rawBufferPointer.baseAddress, rawBufferPointer.count > 0 else {
|
||||
throw CompressionError.corruptedData
|
||||
}
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
stream.next_out = pointer
|
||||
result = deflate(&stream, flush)
|
||||
}
|
||||
guard result >= Z_OK else { throw CompressionError.corruptedData }
|
||||
|
||||
outputChunk.count = bufferSize - Int(stream.avail_out)
|
||||
try consumer(outputChunk)
|
||||
} while stream.avail_out == 0
|
||||
}
|
||||
position += Int64(readSize)
|
||||
} while flush != Z_FINISH
|
||||
return zipCRC32
|
||||
}
|
||||
|
||||
static func decode(bufferSize: Int, skipCRC32: Bool, provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
var stream = z_stream()
|
||||
let streamSize = Int32(MemoryLayout<z_stream>.size)
|
||||
var result = inflateInit2_(&stream, -MAX_WBITS, ZLIB_VERSION, streamSize)
|
||||
defer { inflateEnd(&stream) }
|
||||
guard result == Z_OK else { throw CompressionError.invalidStream }
|
||||
var unzipCRC32 = CRC32(0)
|
||||
var position: Int64 = 0
|
||||
repeat {
|
||||
stream.avail_in = UInt32(bufferSize)
|
||||
var chunk = try provider(position, bufferSize)
|
||||
position += Int64(chunk.count)
|
||||
try chunk.withUnsafeMutableBytes { (rawBufferPointer) in
|
||||
if let baseAddress = rawBufferPointer.baseAddress, rawBufferPointer.count > 0 {
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
stream.next_in = pointer
|
||||
repeat {
|
||||
var outputData = Data(count: bufferSize)
|
||||
stream.avail_out = UInt32(bufferSize)
|
||||
try outputData.withUnsafeMutableBytes { (rawBufferPointer) in
|
||||
if let baseAddress = rawBufferPointer.baseAddress, rawBufferPointer.count > 0 {
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
stream.next_out = pointer
|
||||
} else {
|
||||
throw CompressionError.corruptedData
|
||||
}
|
||||
result = inflate(&stream, Z_NO_FLUSH)
|
||||
guard result != Z_NEED_DICT &&
|
||||
result != Z_DATA_ERROR &&
|
||||
result != Z_MEM_ERROR else {
|
||||
throw CompressionError.corruptedData
|
||||
}
|
||||
}
|
||||
let remainingLength = UInt32(bufferSize) - stream.avail_out
|
||||
outputData.count = Int(remainingLength)
|
||||
try consumer(outputData)
|
||||
if !skipCRC32 { unzipCRC32 = outputData.crc32(checksum: unzipCRC32) }
|
||||
} while stream.avail_out == 0
|
||||
}
|
||||
}
|
||||
} while result != Z_STREAM_END
|
||||
return unzipCRC32
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// The lookup table used to calculate `CRC32` checksums when using the built-in
|
||||
/// CRC32 implementation.
|
||||
private let crcTable: [CRC32] = [
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832,
|
||||
0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
|
||||
0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a,
|
||||
0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
|
||||
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3,
|
||||
0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
|
||||
0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab,
|
||||
0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4,
|
||||
0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
|
||||
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074,
|
||||
0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
|
||||
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525,
|
||||
0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
|
||||
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615,
|
||||
0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76,
|
||||
0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
|
||||
0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6,
|
||||
0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
|
||||
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7,
|
||||
0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
|
||||
0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7,
|
||||
0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278,
|
||||
0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
|
||||
0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330,
|
||||
0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
||||
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d]
|
||||
|
||||
extension Data {
|
||||
|
||||
/// Lookup table-based CRC32 implenetation that is used
|
||||
/// if `zlib` isn't available.
|
||||
/// - Parameter checksum: Running checksum or `0` for the initial run.
|
||||
/// - Returns: The calculated checksum of the receiver.
|
||||
func builtInCRC32(checksum: CRC32) -> CRC32 {
|
||||
// The typecast is necessary on 32-bit platforms because of
|
||||
// https://bugs.swift.org/browse/SR-1774
|
||||
let mask = 0xffffffff as CRC32
|
||||
var result = checksum ^ mask
|
||||
#if swift(>=5.0)
|
||||
crcTable.withUnsafeBufferPointer { crcTablePointer in
|
||||
self.withUnsafeBytes { bufferPointer in
|
||||
var bufferIndex = 0
|
||||
while bufferIndex < self.count {
|
||||
let byte = bufferPointer[bufferIndex]
|
||||
let index = Int((result ^ CRC32(byte)) & 0xff)
|
||||
result = (result >> 8) ^ crcTablePointer[index]
|
||||
bufferIndex += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
self.withUnsafeBytes { (bytes) in
|
||||
let bins = stride(from: 0, to: self.count, by: 256)
|
||||
for bin in bins {
|
||||
for binIndex in 0..<256 {
|
||||
let byteIndex = bin + binIndex
|
||||
guard byteIndex < self.count else { break }
|
||||
|
||||
let byte = bytes[byteIndex]
|
||||
let index = Int((result ^ CRC32(byte)) & 0xff)
|
||||
result = (result >> 8) ^ crcTable[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return result ^ mask
|
||||
}
|
||||
}
|
||||
|
||||
#if !swift(>=5.0)
|
||||
|
||||
// Since Swift 5.0, `Data.withUnsafeBytes()` passes an `UnsafeRawBufferPointer` instead of an `UnsafePointer<UInt8>`
|
||||
// into `body`.
|
||||
// We provide a compatible method for targets that use Swift 4.x so that we can use the new version
|
||||
// across all language versions.
|
||||
|
||||
extension Data {
|
||||
func withUnsafeBytes<T>(_ body: (UnsafeRawBufferPointer) throws -> T) rethrows -> T {
|
||||
let count = self.count
|
||||
return try withUnsafeBytes { (pointer: UnsafePointer<UInt8>) throws -> T in
|
||||
try body(UnsafeRawBufferPointer(start: pointer, count: count))
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
|
||||
#else
|
||||
mutating func withUnsafeMutableBytes<T>(_ body: (UnsafeMutableRawBufferPointer) throws -> T) rethrows -> T {
|
||||
let count = self.count
|
||||
guard count > 0 else {
|
||||
return try body(UnsafeMutableRawBufferPointer(start: nil, count: count))
|
||||
}
|
||||
return try withUnsafeMutableBytes { (pointer: UnsafeMutablePointer<UInt8>) throws -> T in
|
||||
try body(UnsafeMutableRawBufferPointer(start: pointer, count: count))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// Data+CompressionDeprecated.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
public extension Data {
|
||||
|
||||
@available(*, deprecated, message: "Please use `Int64` for `size` and provider `position`.")
|
||||
static func compress(size: Int, bufferSize: Int,
|
||||
provider: (_ position: Int, _ size: Int) throws -> Data,
|
||||
consumer: Consumer) throws -> CRC32 {
|
||||
let newProvider: Provider = { try provider(Int($0), $1) }
|
||||
return try self.compress(size: Int64(size), bufferSize: bufferSize, provider: newProvider, consumer: consumer)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Please use `Int64` for `size` and provider `position`.")
|
||||
static func decompress(size: Int, bufferSize: Int, skipCRC32: Bool,
|
||||
provider: (_ position: Int, _ size: Int) throws -> Data,
|
||||
consumer: Consumer) throws -> CRC32 {
|
||||
let newProvider: Provider = { try provider(Int($0), $1) }
|
||||
return try self.decompress(size: Int64(size), bufferSize: bufferSize, skipCRC32: skipCRC32,
|
||||
provider: newProvider, consumer: consumer)
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// Data+Serialization.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
#if os(Android)
|
||||
public typealias FILEPointer = OpaquePointer
|
||||
#else
|
||||
public typealias FILEPointer = UnsafeMutablePointer<FILE>
|
||||
#endif
|
||||
|
||||
protocol DataSerializable {
|
||||
static var size: Int { get }
|
||||
init?(data: Data, additionalDataProvider: (Int) throws -> Data)
|
||||
var data: Data { get }
|
||||
}
|
||||
|
||||
extension Data {
|
||||
public enum DataError: Error {
|
||||
case unreadableFile
|
||||
case unwritableFile
|
||||
}
|
||||
|
||||
func scanValue<T>(start: Int) -> T {
|
||||
return self.withUnsafeBytes {
|
||||
$0.loadUnaligned(fromByteOffset: start, as: T.self)
|
||||
}
|
||||
}
|
||||
|
||||
static func readStruct<T>(from file: FILEPointer, at offset: UInt64)
|
||||
-> T? where T: DataSerializable {
|
||||
guard offset <= .max else { return nil }
|
||||
fseeko(file, off_t(offset), SEEK_SET)
|
||||
guard let data = try? self.readChunk(of: T.size, from: file) else {
|
||||
return nil
|
||||
}
|
||||
let structure = T(data: data, additionalDataProvider: { (additionalDataSize) -> Data in
|
||||
return try self.readChunk(of: additionalDataSize, from: file)
|
||||
})
|
||||
return structure
|
||||
}
|
||||
|
||||
static func consumePart(of size: Int64, chunkSize: Int, skipCRC32: Bool = false,
|
||||
provider: Provider, consumer: Consumer) throws -> CRC32 {
|
||||
var checksum = CRC32(0)
|
||||
guard size > 0 else {
|
||||
try consumer(Data())
|
||||
return checksum
|
||||
}
|
||||
|
||||
let readInOneChunk = (size < chunkSize)
|
||||
var chunkSize = readInOneChunk ? Int(size) : chunkSize
|
||||
var bytesRead: Int64 = 0
|
||||
while bytesRead < size {
|
||||
let remainingSize = size - bytesRead
|
||||
chunkSize = remainingSize < chunkSize ? Int(remainingSize) : chunkSize
|
||||
let data = try provider(bytesRead, chunkSize)
|
||||
try consumer(data)
|
||||
if !skipCRC32 {
|
||||
checksum = data.crc32(checksum: checksum)
|
||||
}
|
||||
bytesRead += Int64(chunkSize)
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
|
||||
static func readChunk(of size: Int, from file: FILEPointer) throws -> Data {
|
||||
let alignment = MemoryLayout<UInt>.alignment
|
||||
#if swift(>=4.1)
|
||||
let bytes = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: alignment)
|
||||
#else
|
||||
let bytes = UnsafeMutableRawPointer.allocate(bytes: size, alignedTo: alignment)
|
||||
#endif
|
||||
let bytesRead = fread(bytes, 1, size, file)
|
||||
let error = ferror(file)
|
||||
if error > 0 {
|
||||
bytes.deallocate()
|
||||
throw DataError.unreadableFile
|
||||
}
|
||||
#if swift(>=4.1)
|
||||
return Data(bytesNoCopy: bytes, count: bytesRead, deallocator: .custom({ buf, _ in buf.deallocate() }))
|
||||
#else
|
||||
let deallocator = Deallocator.custom({ buf, _ in buf.deallocate(bytes: size, alignedTo: 1) })
|
||||
return Data(bytesNoCopy: bytes, count: bytesRead, deallocator: deallocator)
|
||||
#endif
|
||||
}
|
||||
|
||||
static func write(chunk: Data, to file: FILEPointer) throws -> Int {
|
||||
var sizeWritten: Int = 0
|
||||
chunk.withUnsafeBytes { (rawBufferPointer) in
|
||||
if let baseAddress = rawBufferPointer.baseAddress, rawBufferPointer.count > 0 {
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
sizeWritten = fwrite(pointer, 1, chunk.count, file)
|
||||
}
|
||||
}
|
||||
let error = ferror(file)
|
||||
if error > 0 {
|
||||
throw DataError.unwritableFile
|
||||
}
|
||||
return sizeWritten
|
||||
}
|
||||
|
||||
static func writeLargeChunk(_ chunk: Data, size: UInt64, bufferSize: Int,
|
||||
to file: FILEPointer) throws -> UInt64 {
|
||||
var sizeWritten: UInt64 = 0
|
||||
chunk.withUnsafeBytes { (rawBufferPointer) in
|
||||
if let baseAddress = rawBufferPointer.baseAddress, rawBufferPointer.count > 0 {
|
||||
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
while sizeWritten < size {
|
||||
let remainingSize = size - sizeWritten
|
||||
let chunkSize = Swift.min(Int(remainingSize), bufferSize)
|
||||
let curPointer = pointer.advanced(by: Int(sizeWritten))
|
||||
fwrite(curPointer, 1, chunkSize, file)
|
||||
sizeWritten += UInt64(chunkSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
let error = ferror(file)
|
||||
if error > 0 {
|
||||
throw DataError.unwritableFile
|
||||
}
|
||||
return sizeWritten
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// Date+ZIP.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Created by Thomas Zoechling on 20.12.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Date {
|
||||
|
||||
var fileModificationDateTime: (UInt16, UInt16) {
|
||||
return (self.fileModificationDate, self.fileModificationTime)
|
||||
}
|
||||
|
||||
var fileModificationDate: UInt16 {
|
||||
var time = time_t(self.timeIntervalSince1970)
|
||||
guard let unixTime = gmtime(&time) else {
|
||||
return 0
|
||||
}
|
||||
var year = unixTime.pointee.tm_year + 1900 // UNIX time structs count in "years since 1900".
|
||||
// ZIP uses the MSDOS date format which has a valid range of 1980 - 2099.
|
||||
year = year >= 1980 ? year : 1980
|
||||
year = year <= 2099 ? year : 2099
|
||||
let month = unixTime.pointee.tm_mon + 1 // UNIX time struct month entries are zero based.
|
||||
let day = unixTime.pointee.tm_mday
|
||||
return (UInt16)(day + ((month) * 32) + ((year - 1980) * 512))
|
||||
}
|
||||
|
||||
var fileModificationTime: UInt16 {
|
||||
var time = time_t(self.timeIntervalSince1970)
|
||||
guard let unixTime = gmtime(&time) else {
|
||||
return 0
|
||||
}
|
||||
let hour = unixTime.pointee.tm_hour
|
||||
let minute = unixTime.pointee.tm_min
|
||||
let second = unixTime.pointee.tm_sec
|
||||
return (UInt16)((second/2) + (minute * 32) + (hour * 2048))
|
||||
}
|
||||
|
||||
init(dateTime: (UInt16, UInt16)) {
|
||||
var msdosDateTime = Int(dateTime.0)
|
||||
msdosDateTime <<= 16
|
||||
msdosDateTime |= Int(dateTime.1)
|
||||
var unixTime = tm()
|
||||
unixTime.tm_sec = Int32((msdosDateTime&31)*2)
|
||||
unixTime.tm_min = Int32((msdosDateTime>>5)&63)
|
||||
unixTime.tm_hour = Int32((Int(dateTime.1)>>11)&31)
|
||||
unixTime.tm_mday = Int32((msdosDateTime>>16)&31)
|
||||
unixTime.tm_mon = Int32((msdosDateTime>>21)&15)
|
||||
unixTime.tm_mon -= 1 // UNIX time struct month entries are zero based.
|
||||
unixTime.tm_year = Int32(1980+(msdosDateTime>>25))
|
||||
unixTime.tm_year -= 1900 // UNIX time structs count in "years since 1900".
|
||||
let time = timegm(&unixTime)
|
||||
self = Date(timeIntervalSince1970: TimeInterval(time))
|
||||
}
|
||||
|
||||
init(timespec: timespec) {
|
||||
let seconds = 1.0e-9 * Double(timespec.tv_nsec)
|
||||
let timeIntervalSince1970 = TimeInterval(timespec.tv_sec)
|
||||
let absoluteTimeIntervalSince1970 = Constants.absoluteTimeIntervalSince1970
|
||||
self.init(timeIntervalSinceReferenceDate: (timeIntervalSince1970 - absoluteTimeIntervalSince1970) + seconds)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Date {
|
||||
|
||||
enum Constants {
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
|
||||
static let absoluteTimeIntervalSince1970 = kCFAbsoluteTimeIntervalSince1970
|
||||
#else
|
||||
static let absoluteTimeIntervalSince1970: Double = 978307200.0
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension stat {
|
||||
|
||||
var lastAccessDate: Date {
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
|
||||
return Date(timespec: st_atimespec)
|
||||
#else
|
||||
return Date(timespec: st_atim)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension timeval {
|
||||
|
||||
init(timeIntervalSince1970: TimeInterval) {
|
||||
let (integral, fractional) = modf(timeIntervalSince1970)
|
||||
self.init(tv_sec: time_t(integral), tv_usec: suseconds_t(1.0e6 * fractional))
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// Entry+Serialization.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
extension Entry.LocalFileHeader {
|
||||
var data: Data {
|
||||
var localFileHeaderSignature = self.localFileHeaderSignature
|
||||
var versionNeededToExtract = self.versionNeededToExtract
|
||||
var generalPurposeBitFlag = self.generalPurposeBitFlag
|
||||
var compressionMethod = self.compressionMethod
|
||||
var lastModFileTime = self.lastModFileTime
|
||||
var lastModFileDate = self.lastModFileDate
|
||||
var crc32 = self.crc32
|
||||
var compressedSize = self.compressedSize
|
||||
var uncompressedSize = self.uncompressedSize
|
||||
var fileNameLength = self.fileNameLength
|
||||
var extraFieldLength = self.extraFieldLength
|
||||
var data = Data()
|
||||
withUnsafePointer(to: &localFileHeaderSignature, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &versionNeededToExtract, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &generalPurposeBitFlag, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &compressionMethod, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &lastModFileTime, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &lastModFileDate, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &crc32, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &compressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &uncompressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &fileNameLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &extraFieldLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
data.append(self.fileNameData)
|
||||
data.append(self.extraFieldData)
|
||||
return data
|
||||
}
|
||||
|
||||
init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) {
|
||||
guard data.count == Entry.LocalFileHeader.size else { return nil }
|
||||
guard data.scanValue(start: 0) == localFileHeaderSignature else { return nil }
|
||||
self.versionNeededToExtract = data.scanValue(start: 4)
|
||||
self.generalPurposeBitFlag = data.scanValue(start: 6)
|
||||
self.compressionMethod = data.scanValue(start: 8)
|
||||
self.lastModFileTime = data.scanValue(start: 10)
|
||||
self.lastModFileDate = data.scanValue(start: 12)
|
||||
self.crc32 = data.scanValue(start: 14)
|
||||
self.compressedSize = data.scanValue(start: 18)
|
||||
self.uncompressedSize = data.scanValue(start: 22)
|
||||
self.fileNameLength = data.scanValue(start: 26)
|
||||
self.extraFieldLength = data.scanValue(start: 28)
|
||||
let additionalDataLength = Int(self.fileNameLength) + Int(self.extraFieldLength)
|
||||
guard let additionalData = try? provider(additionalDataLength) else { return nil }
|
||||
guard additionalData.count == additionalDataLength else { return nil }
|
||||
var subRangeStart = 0
|
||||
var subRangeEnd = Int(self.fileNameLength)
|
||||
self.fileNameData = additionalData.subdata(in: subRangeStart..<subRangeEnd)
|
||||
subRangeStart += Int(self.fileNameLength)
|
||||
subRangeEnd = subRangeStart + Int(self.extraFieldLength)
|
||||
self.extraFieldData = additionalData.subdata(in: subRangeStart..<subRangeEnd)
|
||||
if let zip64ExtendedInformation = Entry.ZIP64ExtendedInformation.scanForZIP64Field(in: self.extraFieldData,
|
||||
fields: self.validFields) {
|
||||
self.extraFields = [zip64ExtendedInformation]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Entry.CentralDirectoryStructure {
|
||||
var data: Data {
|
||||
var centralDirectorySignature = self.centralDirectorySignature
|
||||
var versionMadeBy = self.versionMadeBy
|
||||
var versionNeededToExtract = self.versionNeededToExtract
|
||||
var generalPurposeBitFlag = self.generalPurposeBitFlag
|
||||
var compressionMethod = self.compressionMethod
|
||||
var lastModFileTime = self.lastModFileTime
|
||||
var lastModFileDate = self.lastModFileDate
|
||||
var crc32 = self.crc32
|
||||
var compressedSize = self.compressedSize
|
||||
var uncompressedSize = self.uncompressedSize
|
||||
var fileNameLength = self.fileNameLength
|
||||
var extraFieldLength = self.extraFieldLength
|
||||
var fileCommentLength = self.fileCommentLength
|
||||
var diskNumberStart = self.diskNumberStart
|
||||
var internalFileAttributes = self.internalFileAttributes
|
||||
var externalFileAttributes = self.externalFileAttributes
|
||||
var relativeOffsetOfLocalHeader = self.relativeOffsetOfLocalHeader
|
||||
var data = Data()
|
||||
withUnsafePointer(to: ¢ralDirectorySignature, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &versionMadeBy, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &versionNeededToExtract, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &generalPurposeBitFlag, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &compressionMethod, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &lastModFileTime, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &lastModFileDate, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &crc32, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &compressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &uncompressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &fileNameLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &extraFieldLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &fileCommentLength, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &diskNumberStart, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &internalFileAttributes, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &externalFileAttributes, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &relativeOffsetOfLocalHeader, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
data.append(self.fileNameData)
|
||||
data.append(self.extraFieldData)
|
||||
data.append(self.fileCommentData)
|
||||
return data
|
||||
}
|
||||
|
||||
init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) {
|
||||
guard data.count == Entry.CentralDirectoryStructure.size else { return nil }
|
||||
guard data.scanValue(start: 0) == centralDirectorySignature else { return nil }
|
||||
self.versionMadeBy = data.scanValue(start: 4)
|
||||
self.versionNeededToExtract = data.scanValue(start: 6)
|
||||
self.generalPurposeBitFlag = data.scanValue(start: 8)
|
||||
self.compressionMethod = data.scanValue(start: 10)
|
||||
self.lastModFileTime = data.scanValue(start: 12)
|
||||
self.lastModFileDate = data.scanValue(start: 14)
|
||||
self.crc32 = data.scanValue(start: 16)
|
||||
self.compressedSize = data.scanValue(start: 20)
|
||||
self.uncompressedSize = data.scanValue(start: 24)
|
||||
self.fileNameLength = data.scanValue(start: 28)
|
||||
self.extraFieldLength = data.scanValue(start: 30)
|
||||
self.fileCommentLength = data.scanValue(start: 32)
|
||||
self.diskNumberStart = data.scanValue(start: 34)
|
||||
self.internalFileAttributes = data.scanValue(start: 36)
|
||||
self.externalFileAttributes = data.scanValue(start: 38)
|
||||
self.relativeOffsetOfLocalHeader = data.scanValue(start: 42)
|
||||
let additionalDataLength = Int(self.fileNameLength) + Int(self.extraFieldLength) + Int(self.fileCommentLength)
|
||||
guard let additionalData = try? provider(additionalDataLength) else { return nil }
|
||||
guard additionalData.count == additionalDataLength else { return nil }
|
||||
var subRangeStart = 0
|
||||
var subRangeEnd = Int(self.fileNameLength)
|
||||
self.fileNameData = additionalData.subdata(in: subRangeStart..<subRangeEnd)
|
||||
subRangeStart += Int(self.fileNameLength)
|
||||
subRangeEnd = subRangeStart + Int(self.extraFieldLength)
|
||||
self.extraFieldData = additionalData.subdata(in: subRangeStart..<subRangeEnd)
|
||||
subRangeStart += Int(self.extraFieldLength)
|
||||
subRangeEnd = subRangeStart + Int(self.fileCommentLength)
|
||||
self.fileCommentData = additionalData.subdata(in: subRangeStart..<subRangeEnd)
|
||||
if let zip64ExtendedInformation = Entry.ZIP64ExtendedInformation.scanForZIP64Field(in: self.extraFieldData,
|
||||
fields: self.validFields) {
|
||||
self.extraFields = [zip64ExtendedInformation]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Entry.DataDescriptor {
|
||||
init?(data: Data, additionalDataProvider provider: (Int) throws -> Data) {
|
||||
guard data.count == Self.size else { return nil }
|
||||
let signature: UInt32 = data.scanValue(start: 0)
|
||||
// The DataDescriptor signature is not mandatory so we have to re-arrange the input data if it is missing.
|
||||
var readOffset = 0
|
||||
if signature == self.dataDescriptorSignature { readOffset = 4 }
|
||||
self.crc32 = data.scanValue(start: readOffset)
|
||||
readOffset += MemoryLayout<UInt32>.size
|
||||
self.compressedSize = data.scanValue(start: readOffset)
|
||||
readOffset += Self.memoryLengthOfSize
|
||||
self.uncompressedSize = data.scanValue(start: readOffset)
|
||||
// Our add(_ entry:) methods always maintain compressed & uncompressed
|
||||
// sizes and so we don't need a data descriptor for newly added entries.
|
||||
// Data descriptors of already existing entries are manually preserved
|
||||
// when copying those entries to the tempArchive during remove(_ entry:).
|
||||
self.data = Data()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
//
|
||||
// Entry+ZIP64.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
protocol ExtensibleDataField {
|
||||
var headerID: UInt16 { get }
|
||||
var dataSize: UInt16 { get }
|
||||
}
|
||||
|
||||
extension Entry {
|
||||
|
||||
enum EntryError: Error {
|
||||
case missingPermissionsAttributeError
|
||||
case missingModificationDateAttributeError
|
||||
}
|
||||
|
||||
struct ZIP64ExtendedInformation: ExtensibleDataField {
|
||||
let headerID: UInt16 = ExtraFieldHeaderID.zip64ExtendedInformation.rawValue
|
||||
let dataSize: UInt16
|
||||
static let headerSize: UInt16 = 4
|
||||
let uncompressedSize: UInt64
|
||||
let compressedSize: UInt64
|
||||
let relativeOffsetOfLocalHeader: UInt64
|
||||
let diskNumberStart: UInt32
|
||||
}
|
||||
|
||||
var zip64ExtendedInformation: ZIP64ExtendedInformation? {
|
||||
self.centralDirectoryStructure.zip64ExtendedInformation
|
||||
}
|
||||
}
|
||||
|
||||
typealias Field = Entry.ZIP64ExtendedInformation.Field
|
||||
|
||||
extension Entry.LocalFileHeader {
|
||||
var validFields: [Field] {
|
||||
var fields: [Field] = []
|
||||
if self.uncompressedSize == .max { fields.append(.uncompressedSize) }
|
||||
if self.compressedSize == .max { fields.append(.compressedSize) }
|
||||
return fields
|
||||
}
|
||||
}
|
||||
|
||||
extension Entry.CentralDirectoryStructure {
|
||||
var validFields: [Field] {
|
||||
var fields: [Field] = []
|
||||
if self.uncompressedSize == .max { fields.append(.uncompressedSize) }
|
||||
if self.compressedSize == .max { fields.append(.compressedSize) }
|
||||
if self.relativeOffsetOfLocalHeader == .max { fields.append(.relativeOffsetOfLocalHeader) }
|
||||
if self.diskNumberStart == .max { fields.append(.diskNumberStart) }
|
||||
return fields
|
||||
}
|
||||
var zip64ExtendedInformation: Entry.ZIP64ExtendedInformation? {
|
||||
self.extraFields?.compactMap { $0 as? Entry.ZIP64ExtendedInformation }.first
|
||||
}
|
||||
}
|
||||
|
||||
extension Entry.ZIP64ExtendedInformation {
|
||||
|
||||
enum Field {
|
||||
case uncompressedSize
|
||||
case compressedSize
|
||||
case relativeOffsetOfLocalHeader
|
||||
case diskNumberStart
|
||||
|
||||
var size: Int {
|
||||
switch self {
|
||||
case .uncompressedSize, .compressedSize, .relativeOffsetOfLocalHeader:
|
||||
return 8
|
||||
case .diskNumberStart:
|
||||
return 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
var headerID = self.headerID
|
||||
var dataSize = self.dataSize
|
||||
var uncompressedSize = self.uncompressedSize
|
||||
var compressedSize = self.compressedSize
|
||||
var relativeOffsetOfLFH = self.relativeOffsetOfLocalHeader
|
||||
var diskNumberStart = self.diskNumberStart
|
||||
var data = Data()
|
||||
withUnsafePointer(to: &headerID, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &dataSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
if uncompressedSize != 0 || compressedSize != 0 {
|
||||
withUnsafePointer(to: &uncompressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
withUnsafePointer(to: &compressedSize, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
}
|
||||
if relativeOffsetOfLocalHeader != 0 {
|
||||
withUnsafePointer(to: &relativeOffsetOfLFH, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
}
|
||||
if diskNumberStart != 0 {
|
||||
withUnsafePointer(to: &diskNumberStart, { data.append(UnsafeBufferPointer(start: $0, count: 1))})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
init?(data: Data, fields: [Field]) {
|
||||
let headerLength = 4
|
||||
guard fields.reduce(0, { $0 + $1.size }) + headerLength == data.count else { return nil }
|
||||
|
||||
var readOffset = headerLength
|
||||
func value<T>(of field: Field) -> T where T: BinaryInteger {
|
||||
if fields.contains(field), readOffset + field.size <= data.count {
|
||||
defer { readOffset += MemoryLayout<T>.size }
|
||||
|
||||
return data.scanValue(start: readOffset)
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
self.dataSize = data.scanValue(start: 2)
|
||||
self.uncompressedSize = value(of: .uncompressedSize)
|
||||
self.compressedSize = value(of: .compressedSize)
|
||||
self.relativeOffsetOfLocalHeader = value(of: .relativeOffsetOfLocalHeader)
|
||||
self.diskNumberStart = value(of: .diskNumberStart)
|
||||
}
|
||||
|
||||
init?(zip64ExtendedInformation: Entry.ZIP64ExtendedInformation?, offset: UInt64) {
|
||||
// Only used when removing entry, if no ZIP64 extended information exists,
|
||||
// then this information will not be newly added either
|
||||
guard let existingInfo = zip64ExtendedInformation else { return nil }
|
||||
relativeOffsetOfLocalHeader = offset >= maxOffsetOfLocalFileHeader ? offset : 0
|
||||
uncompressedSize = existingInfo.uncompressedSize
|
||||
compressedSize = existingInfo.compressedSize
|
||||
diskNumberStart = existingInfo.diskNumberStart
|
||||
let tempDataSize = [relativeOffsetOfLocalHeader, uncompressedSize, compressedSize]
|
||||
.filter { $0 != 0 }
|
||||
.reduce(UInt16(0), { $0 + UInt16(MemoryLayout.size(ofValue: $1))})
|
||||
dataSize = tempDataSize + (diskNumberStart > 0 ? UInt16(MemoryLayout.size(ofValue: diskNumberStart)) : 0)
|
||||
if dataSize == 0 { return nil }
|
||||
}
|
||||
|
||||
static func scanForZIP64Field(in data: Data, fields: [Field]) -> Entry.ZIP64ExtendedInformation? {
|
||||
guard data.isEmpty == false else { return nil }
|
||||
var offset = 0
|
||||
var headerID: UInt16
|
||||
var dataSize: UInt16
|
||||
let extraFieldLength = data.count
|
||||
let headerSize = Int(Entry.ZIP64ExtendedInformation.headerSize)
|
||||
while offset < extraFieldLength - headerSize {
|
||||
headerID = data.scanValue(start: offset)
|
||||
dataSize = data.scanValue(start: offset + 2)
|
||||
let nextOffset = offset + headerSize + Int(dataSize)
|
||||
guard nextOffset <= extraFieldLength else { return nil }
|
||||
if headerID == ExtraFieldHeaderID.zip64ExtendedInformation.rawValue {
|
||||
return Entry.ZIP64ExtendedInformation(data: data.subdata(in: offset..<nextOffset), fields: fields)
|
||||
}
|
||||
offset = nextOffset
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
//
|
||||
// Entry.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
import CoreFoundation
|
||||
|
||||
/// A value that represents a file, a directory or a symbolic link within a ZIP `Archive`.
|
||||
///
|
||||
/// You can retrieve instances of `Entry` from an `Archive` via subscripting or iteration.
|
||||
/// Entries are identified by their `path`.
|
||||
public struct Entry: Equatable {
|
||||
/// The type of an `Entry` in a ZIP `Archive`.
|
||||
public enum EntryType: Int {
|
||||
/// Indicates a regular file.
|
||||
case file
|
||||
/// Indicates a directory.
|
||||
case directory
|
||||
/// Indicates a symbolic link.
|
||||
case symlink
|
||||
|
||||
init(mode: mode_t) {
|
||||
switch mode & S_IFMT {
|
||||
case S_IFDIR:
|
||||
self = .directory
|
||||
case S_IFLNK:
|
||||
self = .symlink
|
||||
default:
|
||||
self = .file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum OSType: UInt {
|
||||
case msdos = 0
|
||||
case unix = 3
|
||||
case osx = 19
|
||||
case unused = 20
|
||||
}
|
||||
|
||||
struct LocalFileHeader: DataSerializable {
|
||||
let localFileHeaderSignature = UInt32(localFileHeaderStructSignature)
|
||||
let versionNeededToExtract: UInt16
|
||||
let generalPurposeBitFlag: UInt16
|
||||
let compressionMethod: UInt16
|
||||
let lastModFileTime: UInt16
|
||||
let lastModFileDate: UInt16
|
||||
let crc32: UInt32
|
||||
let compressedSize: UInt32
|
||||
let uncompressedSize: UInt32
|
||||
let fileNameLength: UInt16
|
||||
let extraFieldLength: UInt16
|
||||
static let size = 30
|
||||
let fileNameData: Data
|
||||
let extraFieldData: Data
|
||||
var extraFields: [ExtensibleDataField]?
|
||||
}
|
||||
|
||||
struct DataDescriptor<T: BinaryInteger>: DataSerializable {
|
||||
let data: Data
|
||||
let dataDescriptorSignature = UInt32(dataDescriptorStructSignature)
|
||||
let crc32: UInt32
|
||||
// For normal archives, the compressed and uncompressed sizes are 4 bytes each.
|
||||
// For ZIP64 format archives, the compressed and uncompressed sizes are 8 bytes each.
|
||||
let compressedSize: T
|
||||
let uncompressedSize: T
|
||||
static var memoryLengthOfSize: Int { MemoryLayout<T>.size }
|
||||
static var size: Int { memoryLengthOfSize * 2 + 8 }
|
||||
}
|
||||
|
||||
typealias DefaultDataDescriptor = DataDescriptor<UInt32>
|
||||
typealias ZIP64DataDescriptor = DataDescriptor<UInt64>
|
||||
|
||||
struct CentralDirectoryStructure: DataSerializable {
|
||||
let centralDirectorySignature = UInt32(centralDirectoryStructSignature)
|
||||
let versionMadeBy: UInt16
|
||||
let versionNeededToExtract: UInt16
|
||||
let generalPurposeBitFlag: UInt16
|
||||
let compressionMethod: UInt16
|
||||
let lastModFileTime: UInt16
|
||||
let lastModFileDate: UInt16
|
||||
let crc32: UInt32
|
||||
let compressedSize: UInt32
|
||||
let uncompressedSize: UInt32
|
||||
let fileNameLength: UInt16
|
||||
let extraFieldLength: UInt16
|
||||
let fileCommentLength: UInt16
|
||||
let diskNumberStart: UInt16
|
||||
let internalFileAttributes: UInt16
|
||||
let externalFileAttributes: UInt32
|
||||
let relativeOffsetOfLocalHeader: UInt32
|
||||
static let size = 46
|
||||
let fileNameData: Data
|
||||
let extraFieldData: Data
|
||||
let fileCommentData: Data
|
||||
|
||||
var extraFields: [ExtensibleDataField]?
|
||||
var usesDataDescriptor: Bool { return (self.generalPurposeBitFlag & (1 << 3 )) != 0 }
|
||||
var usesUTF8PathEncoding: Bool { return (self.generalPurposeBitFlag & (1 << 11 )) != 0 }
|
||||
var isEncrypted: Bool { return (self.generalPurposeBitFlag & (1 << 0)) != 0 }
|
||||
var isZIP64: Bool {
|
||||
// If ZIP64 extended information is existing, try to treat cd as ZIP64 format
|
||||
// even if the version needed to extract is lower than 4.5
|
||||
return UInt8(truncatingIfNeeded: self.versionNeededToExtract) >= 45 || zip64ExtendedInformation != nil
|
||||
}
|
||||
}
|
||||
/// Returns the `path` of the receiver within a ZIP `Archive` using a given encoding.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - encoding: `String.Encoding`
|
||||
public func path(using encoding: String.Encoding) -> String {
|
||||
return String(pathData: self.centralDirectoryStructure.fileNameData, encoding: encoding)
|
||||
}
|
||||
/// The `path` of the receiver within a ZIP `Archive`.
|
||||
public var path: String {
|
||||
let encoding = self.centralDirectoryStructure.usesUTF8PathEncoding ? String.Encoding.utf8 : .codepage437
|
||||
return self.path(using: encoding)
|
||||
}
|
||||
/// The file attributes of the receiver as key/value pairs.
|
||||
///
|
||||
/// Contains the modification date and file permissions.
|
||||
public var fileAttributes: [FileAttributeKey: Any] {
|
||||
return FileManager.attributes(from: self)
|
||||
}
|
||||
/// The `CRC32` checksum of the receiver.
|
||||
///
|
||||
/// - Note: Always returns `0` for entries of type `EntryType.directory`.
|
||||
public var checksum: CRC32 {
|
||||
if self.centralDirectoryStructure.usesDataDescriptor {
|
||||
return self.zip64DataDescriptor?.crc32 ?? self.dataDescriptor?.crc32 ?? 0
|
||||
}
|
||||
return self.centralDirectoryStructure.crc32
|
||||
}
|
||||
/// The `EntryType` of the receiver.
|
||||
public var type: EntryType {
|
||||
// OS Type is stored in the upper byte of versionMadeBy
|
||||
let osTypeRaw = self.centralDirectoryStructure.versionMadeBy >> 8
|
||||
let osType = OSType(rawValue: UInt(osTypeRaw)) ?? .unused
|
||||
var isDirectory = self.path.hasSuffix("/")
|
||||
switch osType {
|
||||
case .unix, .osx:
|
||||
// Use truncatingIfNeeded for safer conversion across platforms
|
||||
let modeValue = UInt16(truncatingIfNeeded: self.centralDirectoryStructure.externalFileAttributes >> 16)
|
||||
let mode = mode_t(modeValue) & S_IFMT
|
||||
switch mode {
|
||||
case S_IFREG:
|
||||
return .file
|
||||
case S_IFDIR:
|
||||
return .directory
|
||||
case S_IFLNK:
|
||||
return .symlink
|
||||
default:
|
||||
return isDirectory ? .directory : .file
|
||||
}
|
||||
case .msdos:
|
||||
isDirectory = isDirectory || ((centralDirectoryStructure.externalFileAttributes >> 4) == 0x01)
|
||||
fallthrough // For all other OSes we can only guess based on the directory suffix char
|
||||
default: return isDirectory ? .directory : .file
|
||||
}
|
||||
}
|
||||
/// Indicates whether or not the receiver is compressed.
|
||||
public var isCompressed: Bool {
|
||||
self.localFileHeader.compressionMethod != CompressionMethod.none.rawValue
|
||||
}
|
||||
/// The size of the receiver's compressed data.
|
||||
public var compressedSize: UInt64 {
|
||||
if centralDirectoryStructure.isZIP64 {
|
||||
return zip64DataDescriptor?.compressedSize ?? centralDirectoryStructure.effectiveCompressedSize
|
||||
}
|
||||
return UInt64(dataDescriptor?.compressedSize ?? centralDirectoryStructure.compressedSize)
|
||||
}
|
||||
/// The size of the receiver's uncompressed data.
|
||||
public var uncompressedSize: UInt64 {
|
||||
if centralDirectoryStructure.isZIP64 {
|
||||
return zip64DataDescriptor?.uncompressedSize ?? centralDirectoryStructure.effectiveUncompressedSize
|
||||
}
|
||||
return UInt64(dataDescriptor?.uncompressedSize ?? centralDirectoryStructure.uncompressedSize)
|
||||
}
|
||||
/// The combined size of the local header, the data and the optional data descriptor.
|
||||
var localSize: UInt64 {
|
||||
let localFileHeader = self.localFileHeader
|
||||
var extraDataLength = Int(localFileHeader.fileNameLength)
|
||||
extraDataLength += Int(localFileHeader.extraFieldLength)
|
||||
var size = UInt64(LocalFileHeader.size + extraDataLength)
|
||||
size += self.isCompressed ? self.compressedSize : self.uncompressedSize
|
||||
if centralDirectoryStructure.isZIP64 {
|
||||
size += self.zip64DataDescriptor != nil ? UInt64(ZIP64DataDescriptor.size) : 0
|
||||
} else {
|
||||
size += self.dataDescriptor != nil ? UInt64(DefaultDataDescriptor.size) : 0
|
||||
}
|
||||
return size
|
||||
}
|
||||
var dataOffset: UInt64 {
|
||||
var dataOffset = self.centralDirectoryStructure.effectiveRelativeOffsetOfLocalHeader
|
||||
dataOffset += UInt64(LocalFileHeader.size)
|
||||
dataOffset += UInt64(self.localFileHeader.fileNameLength)
|
||||
dataOffset += UInt64(self.localFileHeader.extraFieldLength)
|
||||
return dataOffset
|
||||
}
|
||||
let centralDirectoryStructure: CentralDirectoryStructure
|
||||
let localFileHeader: LocalFileHeader
|
||||
let dataDescriptor: DefaultDataDescriptor?
|
||||
let zip64DataDescriptor: ZIP64DataDescriptor?
|
||||
|
||||
public static func == (lhs: Entry, rhs: Entry) -> Bool {
|
||||
return lhs.path == rhs.path
|
||||
&& lhs.localFileHeader.crc32 == rhs.localFileHeader.crc32
|
||||
&& lhs.centralDirectoryStructure.effectiveRelativeOffsetOfLocalHeader
|
||||
== rhs.centralDirectoryStructure.effectiveRelativeOffsetOfLocalHeader
|
||||
}
|
||||
|
||||
init?(centralDirectoryStructure: CentralDirectoryStructure,
|
||||
localFileHeader: LocalFileHeader,
|
||||
dataDescriptor: DefaultDataDescriptor? = nil,
|
||||
zip64DataDescriptor: ZIP64DataDescriptor? = nil) {
|
||||
// We currently don't support encrypted archives
|
||||
guard !centralDirectoryStructure.isEncrypted else { return nil }
|
||||
self.centralDirectoryStructure = centralDirectoryStructure
|
||||
self.localFileHeader = localFileHeader
|
||||
self.dataDescriptor = dataDescriptor
|
||||
self.zip64DataDescriptor = zip64DataDescriptor
|
||||
}
|
||||
}
|
||||
|
||||
extension Entry.CentralDirectoryStructure {
|
||||
|
||||
init(localFileHeader: Entry.LocalFileHeader, fileAttributes: UInt32, relativeOffset: UInt32,
|
||||
extraField: (length: UInt16, data: Data)) {
|
||||
self.versionMadeBy = UInt16(789)
|
||||
self.versionNeededToExtract = localFileHeader.versionNeededToExtract
|
||||
self.generalPurposeBitFlag = localFileHeader.generalPurposeBitFlag
|
||||
self.compressionMethod = localFileHeader.compressionMethod
|
||||
self.lastModFileTime = localFileHeader.lastModFileTime
|
||||
self.lastModFileDate = localFileHeader.lastModFileDate
|
||||
self.crc32 = localFileHeader.crc32
|
||||
self.compressedSize = localFileHeader.compressedSize
|
||||
self.uncompressedSize = localFileHeader.uncompressedSize
|
||||
self.fileNameLength = localFileHeader.fileNameLength
|
||||
self.extraFieldLength = extraField.length
|
||||
self.fileCommentLength = UInt16(0)
|
||||
self.diskNumberStart = UInt16(0)
|
||||
self.internalFileAttributes = UInt16(0)
|
||||
self.externalFileAttributes = fileAttributes
|
||||
self.relativeOffsetOfLocalHeader = relativeOffset
|
||||
self.fileNameData = localFileHeader.fileNameData
|
||||
self.extraFieldData = extraField.data
|
||||
self.fileCommentData = Data()
|
||||
if let zip64ExtendedInformation = Entry.ZIP64ExtendedInformation.scanForZIP64Field(in: self.extraFieldData,
|
||||
fields: self.validFields) {
|
||||
self.extraFields = [zip64ExtendedInformation]
|
||||
}
|
||||
}
|
||||
|
||||
init(centralDirectoryStructure: Entry.CentralDirectoryStructure,
|
||||
zip64ExtendedInformation: Entry.ZIP64ExtendedInformation?, relativeOffset: UInt32) {
|
||||
if let existingInfo = zip64ExtendedInformation {
|
||||
self.extraFieldData = existingInfo.data
|
||||
self.versionNeededToExtract = max(centralDirectoryStructure.versionNeededToExtract,
|
||||
Archive.Version.v45.rawValue)
|
||||
} else {
|
||||
self.extraFieldData = centralDirectoryStructure.extraFieldData
|
||||
let existingVersion = centralDirectoryStructure.versionNeededToExtract
|
||||
self.versionNeededToExtract = existingVersion < Archive.Version.v45.rawValue
|
||||
? centralDirectoryStructure.versionNeededToExtract
|
||||
: Archive.Version.v20.rawValue
|
||||
}
|
||||
self.extraFieldLength = UInt16(extraFieldData.count)
|
||||
self.relativeOffsetOfLocalHeader = relativeOffset
|
||||
self.versionMadeBy = centralDirectoryStructure.versionMadeBy
|
||||
self.generalPurposeBitFlag = centralDirectoryStructure.generalPurposeBitFlag
|
||||
self.compressionMethod = centralDirectoryStructure.compressionMethod
|
||||
self.lastModFileTime = centralDirectoryStructure.lastModFileTime
|
||||
self.lastModFileDate = centralDirectoryStructure.lastModFileDate
|
||||
self.crc32 = centralDirectoryStructure.crc32
|
||||
self.compressedSize = centralDirectoryStructure.compressedSize
|
||||
self.uncompressedSize = centralDirectoryStructure.uncompressedSize
|
||||
self.fileNameLength = centralDirectoryStructure.fileNameLength
|
||||
self.fileCommentLength = centralDirectoryStructure.fileCommentLength
|
||||
self.diskNumberStart = centralDirectoryStructure.diskNumberStart
|
||||
self.internalFileAttributes = centralDirectoryStructure.internalFileAttributes
|
||||
self.externalFileAttributes = centralDirectoryStructure.externalFileAttributes
|
||||
self.fileNameData = centralDirectoryStructure.fileNameData
|
||||
self.fileCommentData = centralDirectoryStructure.fileCommentData
|
||||
if let zip64ExtendedInformation = Entry.ZIP64ExtendedInformation.scanForZIP64Field(in: self.extraFieldData,
|
||||
fields: self.validFields) {
|
||||
self.extraFields = [zip64ExtendedInformation]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Entry.CentralDirectoryStructure {
|
||||
|
||||
var effectiveCompressedSize: UInt64 {
|
||||
if self.isZIP64, let compressedSize = self.zip64ExtendedInformation?.compressedSize, compressedSize > 0 {
|
||||
return compressedSize
|
||||
}
|
||||
return UInt64(compressedSize)
|
||||
}
|
||||
var effectiveUncompressedSize: UInt64 {
|
||||
if self.isZIP64, let uncompressedSize = self.zip64ExtendedInformation?.uncompressedSize, uncompressedSize > 0 {
|
||||
return uncompressedSize
|
||||
}
|
||||
return UInt64(uncompressedSize)
|
||||
}
|
||||
var effectiveRelativeOffsetOfLocalHeader: UInt64 {
|
||||
if self.isZIP64, let offset = self.zip64ExtendedInformation?.relativeOffsetOfLocalHeader, offset > 0 {
|
||||
return offset
|
||||
}
|
||||
return UInt64(relativeOffsetOfLocalHeader)
|
||||
}
|
||||
}
|
||||
|
||||
extension String.Encoding {
|
||||
|
||||
static let codepage437: Self = {
|
||||
let dosLatinUS = 0x400
|
||||
let dosLatinUSEncoding = CFStringEncoding(dosLatinUS)
|
||||
let dosLatinUSStringEncoding = CFStringConvertEncodingToNSStringEncoding(dosLatinUSEncoding)
|
||||
return String.Encoding(rawValue: dosLatinUSStringEncoding)
|
||||
}()
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
init(pathData: Data, encoding: String.Encoding) {
|
||||
#if os(Linux)
|
||||
if encoding == .codepage437 {
|
||||
self.init()
|
||||
for byte in pathData {
|
||||
self.unicodeScalars.append(Self.cp437Lookup[Int(byte)])
|
||||
}
|
||||
return
|
||||
}
|
||||
#endif
|
||||
self = String(data: pathData, encoding: encoding) ?? ""
|
||||
}
|
||||
|
||||
#if os(Linux)
|
||||
// Source: https://en.wikipedia.org/wiki/Code_page_437#Character_set
|
||||
private static let cp437Lookup: [UnicodeScalar] = [
|
||||
0x0000, 0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022,
|
||||
0x25D8, 0x25CB, 0x25D9, 0x2642, 0x2640, 0x266A, 0x266B, 0x263C,
|
||||
0x25BA, 0x25C4, 0x2195, 0x203C, 0x00B6, 0x00A7, 0x25AC, 0x21A8,
|
||||
0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC,
|
||||
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
|
||||
0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
|
||||
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
|
||||
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
|
||||
0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
|
||||
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
|
||||
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
|
||||
0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x2302,
|
||||
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7,
|
||||
0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5,
|
||||
0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9,
|
||||
0x00FF, 0x00D6, 0x00DC, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192,
|
||||
0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA,
|
||||
0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB,
|
||||
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556,
|
||||
0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510,
|
||||
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567,
|
||||
0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B,
|
||||
0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580,
|
||||
0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4,
|
||||
0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229,
|
||||
0x2261, 0x00B1, 0x2265, 0x2264, 0x2329, 0x2321, 0x00F7, 0x2248,
|
||||
0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0
|
||||
].map { UnicodeScalar($0)! }
|
||||
#endif
|
||||
|
||||
}
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
//
|
||||
// FileManager+ZIP.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
extension FileManager {
|
||||
|
||||
typealias CentralDirectoryStructure = Entry.CentralDirectoryStructure
|
||||
|
||||
/// Zips the file or directory contents at the specified source URL to the destination URL.
|
||||
///
|
||||
/// If the item at the source URL is a directory, the directory itself will be
|
||||
/// represented within the ZIP `Archive`. Calling this method with a directory URL
|
||||
/// `file:///path/directory/` will create an archive with a `directory/` entry at the root level.
|
||||
/// You can override this behavior by passing `false` for `shouldKeepParent`. In that case, the contents
|
||||
/// of the source directory will be placed at the root of the archive.
|
||||
/// - Parameters:
|
||||
/// - sourceURL: The file URL pointing to an existing file or directory.
|
||||
/// - destinationURL: The file URL that identifies the destination of the zip operation.
|
||||
/// - shouldKeepParent: Indicates that the directory name of a source item should be used as root element
|
||||
/// within the archive. Default is `true`.
|
||||
/// - compressionMethod: Indicates the `CompressionMethod` that should be applied.
|
||||
/// By default, `zipItem` will create uncompressed archives.
|
||||
/// - progress: A progress object that can be used to track or cancel the zip operation.
|
||||
/// - Throws: Throws an error if the source item does not exist or the destination URL is not writable.
|
||||
public func zipItem(at sourceURL: URL, to destinationURL: URL,
|
||||
shouldKeepParent: Bool = true, compressionMethod: CompressionMethod = .none,
|
||||
progress: Progress? = nil) throws {
|
||||
let fileManager = FileManager()
|
||||
guard fileManager.itemExists(at: sourceURL) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: sourceURL.path])
|
||||
}
|
||||
guard !fileManager.itemExists(at: destinationURL) else {
|
||||
throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: destinationURL.path])
|
||||
}
|
||||
let archive = try Archive(url: destinationURL, accessMode: .create)
|
||||
let isDirectory = try FileManager.typeForItem(at: sourceURL) == .directory
|
||||
if isDirectory {
|
||||
var subPaths = try self.subpathsOfDirectory(atPath: sourceURL.path)
|
||||
// Enforce an entry for the root directory to preserve its file attributes
|
||||
if shouldKeepParent { subPaths.append("") }
|
||||
var totalUnitCount = Int64(0)
|
||||
if let progress = progress {
|
||||
totalUnitCount = subPaths.reduce(Int64(0), {
|
||||
let itemURL = sourceURL.appendingPathComponent($1)
|
||||
let itemSize = archive.totalUnitCountForAddingItem(at: itemURL)
|
||||
return $0 + itemSize
|
||||
})
|
||||
progress.totalUnitCount = totalUnitCount
|
||||
}
|
||||
|
||||
// If the caller wants to keep the parent directory, we use the lastPathComponent of the source URL
|
||||
// as common base for all entries (similar to macOS' Archive Utility.app)
|
||||
let directoryPrefix = sourceURL.lastPathComponent
|
||||
for entryPath in subPaths {
|
||||
let finalEntryPath = shouldKeepParent ? directoryPrefix + "/" + entryPath : entryPath
|
||||
let finalBaseURL = shouldKeepParent ? sourceURL.deletingLastPathComponent() : sourceURL
|
||||
if let progress = progress {
|
||||
let itemURL = sourceURL.appendingPathComponent(entryPath)
|
||||
let entryProgress = archive.makeProgressForAddingItem(at: itemURL)
|
||||
progress.addChild(entryProgress, withPendingUnitCount: entryProgress.totalUnitCount)
|
||||
try archive.addEntry(with: finalEntryPath, relativeTo: finalBaseURL,
|
||||
compressionMethod: compressionMethod, progress: entryProgress)
|
||||
} else {
|
||||
try archive.addEntry(with: finalEntryPath, relativeTo: finalBaseURL,
|
||||
compressionMethod: compressionMethod)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
progress?.totalUnitCount = archive.totalUnitCountForAddingItem(at: sourceURL)
|
||||
let baseURL = sourceURL.deletingLastPathComponent()
|
||||
try archive.addEntry(with: sourceURL.lastPathComponent, relativeTo: baseURL,
|
||||
compressionMethod: compressionMethod, progress: progress)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unzips the contents at the specified source URL to the destination URL.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - sourceURL: The file URL pointing to an existing ZIP file.
|
||||
/// - destinationURL: The file URL that identifies the destination directory of the unzip operation.
|
||||
/// - skipCRC32: Optional flag to skip calculation of the CRC32 checksum to improve performance.
|
||||
/// - allowUncontainedSymlinks: Optional flag to allow symlinks that point to paths outside the destination.
|
||||
/// - progress: A progress object that can be used to track or cancel the unzip operation.
|
||||
/// - pathEncoding: Encoding for entry paths. Overrides the encoding specified in the archive.
|
||||
/// - Throws: Throws an error if the source item does not exist or the destination URL is not writable.
|
||||
public func unzipItem(at sourceURL: URL, to destinationURL: URL,
|
||||
skipCRC32: Bool = false, allowUncontainedSymlinks: Bool = false,
|
||||
progress: Progress? = nil, pathEncoding: String.Encoding? = nil) throws {
|
||||
let fileManager = FileManager()
|
||||
guard fileManager.itemExists(at: sourceURL) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: sourceURL.path])
|
||||
}
|
||||
let archive = try Archive(url: sourceURL, accessMode: .read, pathEncoding: pathEncoding)
|
||||
var totalUnitCount = Int64(0)
|
||||
if let progress = progress {
|
||||
totalUnitCount = archive.reduce(0, { $0 + archive.totalUnitCountForReading($1) })
|
||||
progress.totalUnitCount = totalUnitCount
|
||||
}
|
||||
|
||||
for entry in archive {
|
||||
let path = pathEncoding == nil ? entry.path : entry.path(using: pathEncoding!)
|
||||
let entryURL = destinationURL.appendingPathComponent(path)
|
||||
guard entryURL.isContained(in: destinationURL) else {
|
||||
throw CocoaError(.fileReadInvalidFileName,
|
||||
userInfo: [NSFilePathErrorKey: entryURL.path])
|
||||
}
|
||||
let crc32: CRC32
|
||||
if let progress = progress {
|
||||
let entryProgress = archive.makeProgressForReading(entry)
|
||||
progress.addChild(entryProgress, withPendingUnitCount: entryProgress.totalUnitCount)
|
||||
crc32 = try archive.extract(entry, to: entryURL,
|
||||
skipCRC32: skipCRC32, allowUncontainedSymlinks: allowUncontainedSymlinks,
|
||||
progress: entryProgress)
|
||||
} else {
|
||||
crc32 = try archive.extract(entry, to: entryURL,
|
||||
skipCRC32: skipCRC32, allowUncontainedSymlinks: allowUncontainedSymlinks)
|
||||
}
|
||||
|
||||
func verifyChecksumIfNecessary() throws {
|
||||
if skipCRC32 == false, crc32 != entry.checksum {
|
||||
throw Archive.ArchiveError.invalidCRC32
|
||||
}
|
||||
}
|
||||
try verifyChecksumIfNecessary()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
func itemExists(at url: URL) -> Bool {
|
||||
// Use `URL.checkResourceIsReachable()` instead of `FileManager.fileExists()` here
|
||||
// because we don't want implicit symlink resolution.
|
||||
// As per documentation, `FileManager.fileExists()` traverses symlinks and therefore a broken symlink
|
||||
// would throw a `.fileReadNoSuchFile` false positive error.
|
||||
// For ZIP files it may be intended to archive "broken" symlinks because they might be
|
||||
// resolvable again when extracting the archive to a different destination.
|
||||
return (try? url.checkResourceIsReachable()) == true
|
||||
}
|
||||
|
||||
func createParentDirectoryStructure(for url: URL) throws {
|
||||
let parentDirectoryURL = url.deletingLastPathComponent()
|
||||
try self.createDirectory(at: parentDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
func transferAttributes(from entry: Entry, toItemAtURL url: URL) throws {
|
||||
let attributes = FileManager.attributes(from: entry)
|
||||
switch entry.type {
|
||||
case .directory, .file:
|
||||
try self.setAttributes(attributes, ofItemAtURL: url)
|
||||
case .symlink:
|
||||
try self.setAttributes(attributes, ofItemAtURL: url, traverseLink: false)
|
||||
}
|
||||
}
|
||||
|
||||
func setAttributes(_ attributes: [FileAttributeKey: Any], ofItemAtURL url: URL, traverseLink: Bool = true) throws {
|
||||
// `FileManager.setAttributes` traverses symlinks and applies the attributes to
|
||||
// the symlink destination. Since we want to be able to create symlinks where
|
||||
// the destination isn't available (yet), we want to directly apply entry attributes
|
||||
// to the symlink (vs. the destination file).
|
||||
guard traverseLink == false else {
|
||||
try self.setAttributes(attributes, ofItemAtPath: url.path)
|
||||
return
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
|
||||
guard let posixPermissions = attributes[.posixPermissions] as? NSNumber else {
|
||||
throw Entry.EntryError.missingPermissionsAttributeError
|
||||
}
|
||||
|
||||
try self.setSymlinkPermissions(posixPermissions, ofItemAtURL: url)
|
||||
|
||||
guard let modificationDate = attributes[.modificationDate] as? Date else {
|
||||
throw Entry.EntryError.missingModificationDateAttributeError
|
||||
}
|
||||
|
||||
try self.setSymlinkModificationDate(modificationDate, ofItemAtURL: url)
|
||||
#else
|
||||
// Since non-Darwin POSIX platforms ignore permissions on symlinks and swift-corelibs-foundation
|
||||
// currently doesn't support setting the modification date, this codepath is currently a no-op
|
||||
// on these platforms.
|
||||
return
|
||||
#endif
|
||||
}
|
||||
|
||||
func setSymlinkPermissions(_ posixPermissions: NSNumber, ofItemAtURL url: URL) throws {
|
||||
let fileSystemRepresentation = self.fileSystemRepresentation(withPath: url.path)
|
||||
let modeT = posixPermissions.uint16Value
|
||||
guard lchmod(fileSystemRepresentation, mode_t(modeT)) == 0 else {
|
||||
throw POSIXError(errno, path: url.path)
|
||||
}
|
||||
}
|
||||
|
||||
func setSymlinkModificationDate(_ modificationDate: Date, ofItemAtURL url: URL) throws {
|
||||
let fileSystemRepresentation = self.fileSystemRepresentation(withPath: url.path)
|
||||
var fileStat = stat()
|
||||
guard lstat(fileSystemRepresentation, &fileStat) == 0 else {
|
||||
throw POSIXError(errno, path: url.path)
|
||||
}
|
||||
|
||||
let accessDate = fileStat.lastAccessDate
|
||||
let array = [
|
||||
timeval(timeIntervalSince1970: accessDate.timeIntervalSince1970),
|
||||
timeval(timeIntervalSince1970: modificationDate.timeIntervalSince1970)
|
||||
]
|
||||
try array.withUnsafeBufferPointer {
|
||||
guard lutimes(fileSystemRepresentation, $0.baseAddress) == 0 else {
|
||||
throw POSIXError(errno, path: url.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class func attributes(from entry: Entry) -> [FileAttributeKey: Any] {
|
||||
let centralDirectoryStructure = entry.centralDirectoryStructure
|
||||
let entryType = entry.type
|
||||
let fileTime = centralDirectoryStructure.lastModFileTime
|
||||
let fileDate = centralDirectoryStructure.lastModFileDate
|
||||
let defaultPermissions = entryType == .directory ? defaultDirectoryPermissions : defaultFilePermissions
|
||||
var attributes = [.posixPermissions: defaultPermissions] as [FileAttributeKey: Any]
|
||||
attributes[.modificationDate] = Date(dateTime: (fileDate, fileTime))
|
||||
let versionMadeBy = centralDirectoryStructure.versionMadeBy
|
||||
guard let osType = Entry.OSType(rawValue: UInt(versionMadeBy >> 8)) else { return attributes }
|
||||
|
||||
let externalFileAttributes = centralDirectoryStructure.externalFileAttributes
|
||||
let permissions = self.permissions(for: externalFileAttributes, osType: osType, entryType: entryType)
|
||||
attributes[.posixPermissions] = NSNumber(value: permissions)
|
||||
return attributes
|
||||
}
|
||||
|
||||
class func permissions(for externalFileAttributes: UInt32, osType: Entry.OSType,
|
||||
entryType: Entry.EntryType) -> UInt16 {
|
||||
switch osType {
|
||||
case .unix, .osx:
|
||||
let permissions = mode_t(externalFileAttributes >> 16) & (~S_IFMT)
|
||||
let defaultPermissions = entryType == .directory ? defaultDirectoryPermissions : defaultFilePermissions
|
||||
return permissions == 0 ? defaultPermissions : UInt16(permissions)
|
||||
default:
|
||||
return entryType == .directory ? defaultDirectoryPermissions : defaultFilePermissions
|
||||
}
|
||||
}
|
||||
|
||||
class func externalFileAttributesForEntry(of type: Entry.EntryType, permissions: UInt16) -> UInt32 {
|
||||
var typeInt: UInt16
|
||||
switch type {
|
||||
case .file:
|
||||
typeInt = UInt16(S_IFREG)
|
||||
case .directory:
|
||||
typeInt = UInt16(S_IFDIR)
|
||||
case .symlink:
|
||||
typeInt = UInt16(S_IFLNK)
|
||||
}
|
||||
var externalFileAttributes = UInt32(typeInt|UInt16(permissions))
|
||||
externalFileAttributes = (externalFileAttributes << 16)
|
||||
return externalFileAttributes
|
||||
}
|
||||
|
||||
class func permissionsForItem(at URL: URL) throws -> UInt16 {
|
||||
let fileManager = FileManager()
|
||||
let entryFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: URL.path)
|
||||
var fileStat = stat()
|
||||
lstat(entryFileSystemRepresentation, &fileStat)
|
||||
let permissions = fileStat.st_mode
|
||||
return UInt16(permissions)
|
||||
}
|
||||
|
||||
class func fileModificationDateTimeForItem(at url: URL) throws -> Date {
|
||||
let fileManager = FileManager()
|
||||
guard fileManager.itemExists(at: url) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
let entryFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
var fileStat = stat()
|
||||
lstat(entryFileSystemRepresentation, &fileStat)
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
|
||||
let modTimeSpec = fileStat.st_mtimespec
|
||||
#else
|
||||
let modTimeSpec = fileStat.st_mtim
|
||||
#endif
|
||||
|
||||
let timeStamp = TimeInterval(modTimeSpec.tv_sec) + TimeInterval(modTimeSpec.tv_nsec)/1000000000.0
|
||||
let modDate = Date(timeIntervalSince1970: timeStamp)
|
||||
return modDate
|
||||
}
|
||||
|
||||
class func fileSizeForItem(at url: URL) throws -> Int64 {
|
||||
let fileManager = FileManager()
|
||||
guard fileManager.itemExists(at: url) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
|
||||
let entryFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
var stats = stat()
|
||||
lstat(entryFileSystemRepresentation, &stats)
|
||||
guard stats.st_size >= 0 else { throw CocoaError(.fileReadTooLarge, userInfo: [NSFilePathErrorKey: url.path]) }
|
||||
|
||||
// `st_size` is a signed int value
|
||||
return Int64(stats.st_size)
|
||||
}
|
||||
|
||||
class func typeForItem(at url: URL) throws -> Entry.EntryType {
|
||||
let fileManager = FileManager()
|
||||
guard url.isFileURL, fileManager.itemExists(at: url) else {
|
||||
throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: url.path])
|
||||
}
|
||||
let entryFileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: url.path)
|
||||
var fileStat = stat()
|
||||
lstat(entryFileSystemRepresentation, &fileStat)
|
||||
return Entry.EntryType(mode: mode_t(fileStat.st_mode))
|
||||
}
|
||||
}
|
||||
|
||||
extension POSIXError {
|
||||
|
||||
init(_ code: Int32, path: String) {
|
||||
let errorCode = POSIXError.Code(rawValue: code) ?? .EPERM
|
||||
self = .init(errorCode, userInfo: [NSFilePathErrorKey: path])
|
||||
}
|
||||
}
|
||||
|
||||
extension CocoaError {
|
||||
|
||||
#if swift(>=4.2)
|
||||
#else
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
|
||||
#else
|
||||
|
||||
// The swift-corelibs-foundation version of NSError.swift was missing a convenience method to create
|
||||
// error objects from error codes. (https://github.com/apple/swift-corelibs-foundation/pull/1420)
|
||||
// We have to provide an implementation for non-Darwin platforms using Swift versions < 4.2.
|
||||
|
||||
public static func error(_ code: CocoaError.Code, userInfo: [AnyHashable: Any]? = nil, url: URL? = nil) -> Error {
|
||||
var info: [String: Any] = userInfo as? [String: Any] ?? [:]
|
||||
if let url = url {
|
||||
info[NSURLErrorKey] = url
|
||||
}
|
||||
return NSError(domain: NSCocoaErrorDomain, code: code.rawValue, userInfo: info)
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
public extension URL {
|
||||
|
||||
func isContained(in parentDirectoryURL: URL) -> Bool {
|
||||
// Ensure this URL is contained in the passed in URL
|
||||
let parentDirectoryURL = URL(fileURLWithPath: parentDirectoryURL.path, isDirectory: true).standardized
|
||||
// Maliciously crafted ZIP files can contain entries using a prepended path delimiter `/` in combination
|
||||
// with the parent directory shorthand `..` to bypass our containment check.
|
||||
// When a malicious entry path like e.g. `/../secret.txt` gets appended to the destination
|
||||
// directory URL (e.g. `file:///tmp/`), the resulting URL `file:///tmp//../secret.txt` gets expanded
|
||||
// to `file:///tmp/secret` when using `URL.standardized`. This URL would pass the check performed
|
||||
// in `isContained(in:)`.
|
||||
// Lower level API like POSIX `fopen` - which is used at a later point during extraction - expands
|
||||
// `/tmp//../secret.txt` to `/secret.txt` though. This would lead to an escape to the parent directory.
|
||||
// To avoid that, we replicate the behavior of `fopen`s path expansion and replace all double delimiters
|
||||
// with single delimiters.
|
||||
// More details: https://github.com/weichsel/ZIPFoundation/issues/281
|
||||
let sanitizedEntryPathURL: URL = {
|
||||
let sanitizedPath = self.path.replacingOccurrences(of: "//", with: "/")
|
||||
return URL(fileURLWithPath: sanitizedPath)
|
||||
}()
|
||||
return sanitizedEntryPathURL.standardized.absoluteString.hasPrefix(parentDirectoryURL.absoluteString)
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// FileManager+ZIPDeprecated.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Created by Thomas Zoechling on 06.02.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension FileManager {
|
||||
|
||||
@available(*, deprecated, renamed: "unzipItem(at:to:skipCRC32:progress:pathEncoding:)")
|
||||
func unzipItem(at sourceURL: URL, to destinationURL: URL, skipCRC32: Bool = false,
|
||||
progress: Progress? = nil, preferredEncoding: String.Encoding?) throws {
|
||||
try self.unzipItem(at: sourceURL, to: destinationURL, skipCRC32: skipCRC32,
|
||||
progress: progress, pathEncoding: preferredEncoding)
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>0A2A.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// URL+ZIP.swift
|
||||
// ZIPFoundation
|
||||
//
|
||||
// Copyright © 2017-2025 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
|
||||
|
||||
extension URL {
|
||||
|
||||
static func temporaryReplacementDirectoryURL(for archive: Archive) -> URL {
|
||||
#if swift(>=5.0) || os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
|
||||
if archive.url.isFileURL,
|
||||
let tempDir = try? FileManager().url(for: .itemReplacementDirectory, in: .userDomainMask,
|
||||
appropriateFor: archive.url, create: true) {
|
||||
return tempDir
|
||||
}
|
||||
#endif
|
||||
|
||||
return URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(
|
||||
ProcessInfo.processInfo.globallyUniqueString)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user