Epub阅读器0.0.1

This commit is contained in:
shen
2026-05-21 19:40:51 +08:00
commit daa36d8fe7
559 changed files with 106266 additions and 0 deletions
+21
View File
@@ -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
View File
@@ -0,0 +1,297 @@
<img src="https://user-images.githubusercontent.com/1577319/27564151-1d99e3a0-5ad6-11e7-8ab6-417c5b5a3ff2.png" width="489"/>
[![Swift Package Manager compatible](https://img.shields.io/badge/SPM-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/ZIPFoundation.svg)](https://cocoapods.org/pods/ZIPFoundation)
[![Platform](https://img.shields.io/badge/Platforms-macOS%20|%20iOS%20|%20tvOS%20|%20watchOS%20|%20visionOS%20|%20Linux-lightgrey.svg)](https://github.com/weichsel/ZIPFoundation)
[![Twitter](https://img.shields.io/badge/twitter-@weichsel-blue.svg?style=flat)](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.
@@ -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
}
@@ -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
}
@@ -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
}
}
@@ -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
@@ -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))
}
}
@@ -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
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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
}
}
@@ -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
@@ -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)
}
}
@@ -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))
}
}
@@ -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: &centralDirectorySignature, { 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
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}