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
@@ -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)
}
}