ReadViewSDK/ReadViewDemo/Pods/ZIPFoundation/Sources/ZIPFoundation/Archive+Writing.swift
2026-05-21 19:40:51 +08:00

317 lines
19 KiB
Swift

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