@hot-updater/react-native 0.29.1 → 0.29.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HotUpdater.podspec +0 -4
- package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +178 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +82 -47
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +359 -0
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +386 -0
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +7 -213
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +8 -126
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -2
- package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +462 -0
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +4 -113
- package/package.json +6 -6
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
-
import SWCompression
|
|
3
|
-
import Compression
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Strategy for handling TAR+GZIP compressed files
|
|
@@ -35,7 +33,8 @@ class TarGzDecompressionStrategy: DecompressionStrategy {
|
|
|
35
33
|
fileHandle.closeFile()
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
guard let header = try?
|
|
36
|
+
guard let header = try? ArchiveExtractionUtilities.readUpToCount(from: fileHandle, count: 2),
|
|
37
|
+
header.count == 2 else {
|
|
39
38
|
NSLog("[TarGzStrategy] Invalid file: cannot read header")
|
|
40
39
|
return false
|
|
41
40
|
}
|
|
@@ -49,129 +48,12 @@ class TarGzDecompressionStrategy: DecompressionStrategy {
|
|
|
49
48
|
|
|
50
49
|
func decompress(file: String, to destination: String, progressHandler: @escaping (Double) -> Void) throws {
|
|
51
50
|
NSLog("[TarGzStrategy] Starting extraction of \(file) to \(destination)")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
progressHandler(0.3)
|
|
62
|
-
let decompressedData: Data
|
|
63
|
-
do {
|
|
64
|
-
decompressedData = try decompressGzip(compressedData)
|
|
65
|
-
NSLog("[TarGzStrategy] GZIP decompression successful, size: \(decompressedData.count) bytes")
|
|
66
|
-
progressHandler(0.6)
|
|
67
|
-
} catch {
|
|
68
|
-
throw NSError(
|
|
69
|
-
domain: "TarGzDecompressionStrategy",
|
|
70
|
-
code: 2,
|
|
71
|
-
userInfo: [NSLocalizedDescriptionKey: "GZIP decompression failed: \(error.localizedDescription)"]
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let tarEntries: [TarEntry]
|
|
76
|
-
do {
|
|
77
|
-
tarEntries = try TarContainer.open(container: decompressedData)
|
|
78
|
-
NSLog("[TarGzStrategy] Tar extraction successful, found \(tarEntries.count) entries")
|
|
79
|
-
} catch {
|
|
80
|
-
throw NSError(
|
|
81
|
-
domain: "TarGzDecompressionStrategy",
|
|
82
|
-
code: 3,
|
|
83
|
-
userInfo: [NSLocalizedDescriptionKey: "Tar extraction failed: \(error.localizedDescription)"]
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
let destinationURL = URL(fileURLWithPath: destination)
|
|
88
|
-
let canonicalDestination = destinationURL.standardized.path
|
|
89
|
-
|
|
90
|
-
let fileManager = FileManager.default
|
|
91
|
-
if !fileManager.fileExists(atPath: canonicalDestination) {
|
|
92
|
-
try fileManager.createDirectory(
|
|
93
|
-
atPath: canonicalDestination,
|
|
94
|
-
withIntermediateDirectories: true,
|
|
95
|
-
attributes: nil
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let totalEntries = Double(tarEntries.count)
|
|
100
|
-
for (index, entry) in tarEntries.enumerated() {
|
|
101
|
-
try extractTarEntry(entry, to: canonicalDestination)
|
|
102
|
-
progressHandler(0.6 + (Double(index + 1) / totalEntries * 0.4))
|
|
103
|
-
}
|
|
104
|
-
|
|
51
|
+
try StreamingTarArchiveExtractor.extractCompressedTar(
|
|
52
|
+
file: file,
|
|
53
|
+
to: destination,
|
|
54
|
+
algorithm: .gzip,
|
|
55
|
+
progressHandler: progressHandler
|
|
56
|
+
)
|
|
105
57
|
NSLog("[TarGzStrategy] Successfully extracted all entries")
|
|
106
58
|
}
|
|
107
|
-
|
|
108
|
-
private func decompressGzip(_ data: Data) throws -> Data {
|
|
109
|
-
do {
|
|
110
|
-
let decompressedData = try GzipArchive.unarchive(archive: data)
|
|
111
|
-
NSLog("[TarGzStrategy] GZIP decompression successful using SWCompression")
|
|
112
|
-
return decompressedData
|
|
113
|
-
} catch {
|
|
114
|
-
throw NSError(
|
|
115
|
-
domain: "TarGzDecompressionStrategy",
|
|
116
|
-
code: 5,
|
|
117
|
-
userInfo: [NSLocalizedDescriptionKey: "GZIP decompression failed: \(error.localizedDescription)"]
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private func extractTarEntry(_ entry: TarEntry, to destination: String) throws {
|
|
123
|
-
let fileManager = FileManager.default
|
|
124
|
-
let entryPath = entry.info.name.trimmingCharacters(in: .init(charactersIn: "/"))
|
|
125
|
-
|
|
126
|
-
guard !entryPath.isEmpty,
|
|
127
|
-
!entryPath.contains(".."),
|
|
128
|
-
!entryPath.hasPrefix("/") else {
|
|
129
|
-
NSLog("[TarGzStrategy] Skipping suspicious path: \(entry.info.name)")
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let fullPath = (destination as NSString).appendingPathComponent(entryPath)
|
|
134
|
-
let fullURL = URL(fileURLWithPath: fullPath)
|
|
135
|
-
let canonicalFullPath = fullURL.standardized.path
|
|
136
|
-
let canonicalDestination = URL(fileURLWithPath: destination).standardized.path
|
|
137
|
-
|
|
138
|
-
guard canonicalFullPath.hasPrefix(canonicalDestination + "/") ||
|
|
139
|
-
canonicalFullPath == canonicalDestination else {
|
|
140
|
-
throw NSError(
|
|
141
|
-
domain: "TarGzDecompressionStrategy",
|
|
142
|
-
code: 4,
|
|
143
|
-
userInfo: [NSLocalizedDescriptionKey: "Path traversal attempt detected: \(entry.info.name)"]
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if entry.info.type == .directory {
|
|
148
|
-
if !fileManager.fileExists(atPath: canonicalFullPath) {
|
|
149
|
-
try fileManager.createDirectory(
|
|
150
|
-
atPath: canonicalFullPath,
|
|
151
|
-
withIntermediateDirectories: true,
|
|
152
|
-
attributes: nil
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if entry.info.type == .regular {
|
|
159
|
-
let parentPath = (canonicalFullPath as NSString).deletingLastPathComponent
|
|
160
|
-
if !fileManager.fileExists(atPath: parentPath) {
|
|
161
|
-
try fileManager.createDirectory(
|
|
162
|
-
atPath: parentPath,
|
|
163
|
-
withIntermediateDirectories: true,
|
|
164
|
-
attributes: nil
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
guard let data = entry.data else {
|
|
169
|
-
NSLog("[TarGzStrategy] Skipping file with no data: \(entry.info.name)")
|
|
170
|
-
return
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
try data.write(to: URL(fileURLWithPath: canonicalFullPath))
|
|
174
|
-
NSLog("[TarGzStrategy] Extracted: \(entryPath)")
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
59
|
}
|
|
@@ -179,7 +179,7 @@ extension URLSessionDownloadService: URLSessionDownloadDelegate {
|
|
|
179
179
|
try FileManager.default.removeItem(at: destinationURL)
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
try
|
|
182
|
+
try persistDownloadedFile(from: location, to: destinationURL)
|
|
183
183
|
NSLog("[DownloadService] Download completed successfully: \(actualSize ?? 0) bytes")
|
|
184
184
|
completion?(.success(destinationURL))
|
|
185
185
|
} catch {
|
|
@@ -236,4 +236,15 @@ extension URLSessionDownloadService: URLSessionDownloadDelegate {
|
|
|
236
236
|
NotificationCenter.default.post(name: .downloadProgressUpdate, object: downloadTask, userInfo: ["progress": 0.0, "totalBytesReceived": 0, "totalBytesExpected": 0])
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
-
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private extension URLSessionDownloadService {
|
|
242
|
+
func persistDownloadedFile(from location: URL, to destinationURL: URL) throws {
|
|
243
|
+
do {
|
|
244
|
+
try FileManager.default.moveItem(at: location, to: destinationURL)
|
|
245
|
+
} catch {
|
|
246
|
+
NSLog("[DownloadService] Move failed, falling back to copy: \(error.localizedDescription)")
|
|
247
|
+
try FileManager.default.copyItem(at: location, to: destinationURL)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import zlib
|
|
3
|
+
|
|
4
|
+
enum ZipArchiveExtractor {
|
|
5
|
+
private static let localFileHeaderSignature: UInt32 = 0x04034B50
|
|
6
|
+
private static let centralDirectoryHeaderSignature: UInt32 = 0x02014B50
|
|
7
|
+
private static let endOfCentralDirectorySignature: UInt32 = 0x06054B50
|
|
8
|
+
private static let storedMethod: UInt16 = 0
|
|
9
|
+
private static let deflatedMethod: UInt16 = 8
|
|
10
|
+
private static let encryptedFlag: UInt16 = 1 << 0
|
|
11
|
+
private static let maxCommentLength = 0xFFFF
|
|
12
|
+
private static let unixHostIdentifier: UInt8 = 3
|
|
13
|
+
private static let fileTypeMask: UInt16 = 0o170000
|
|
14
|
+
private static let symbolicLinkMode: UInt16 = 0o120000
|
|
15
|
+
private static let directoryMode: UInt16 = 0o040000
|
|
16
|
+
|
|
17
|
+
private struct CentralDirectoryEntry {
|
|
18
|
+
let path: String
|
|
19
|
+
let compressionMethod: UInt16
|
|
20
|
+
let flags: UInt16
|
|
21
|
+
let compressedSize: UInt64
|
|
22
|
+
let uncompressedSize: UInt64
|
|
23
|
+
let checksum: UInt32
|
|
24
|
+
let localHeaderOffset: UInt64
|
|
25
|
+
let isDirectory: Bool
|
|
26
|
+
let isSymbolicLink: Bool
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private struct EndOfCentralDirectoryRecord {
|
|
30
|
+
let totalEntries: UInt16
|
|
31
|
+
let centralDirectoryOffset: UInt64
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static func extract(file: String, to destination: String, progressHandler: @escaping (Double) -> Void) throws {
|
|
35
|
+
let fileURL = URL(fileURLWithPath: file)
|
|
36
|
+
let fileSize = try archiveFileSize(at: file)
|
|
37
|
+
let handle = try FileHandle(forReadingFrom: fileURL)
|
|
38
|
+
|
|
39
|
+
defer {
|
|
40
|
+
try? handle.close()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let entries = try readCentralDirectoryEntries(from: handle, fileSize: fileSize)
|
|
44
|
+
|
|
45
|
+
let destinationURL = URL(fileURLWithPath: destination)
|
|
46
|
+
try ArchiveExtractionUtilities.ensureDirectory(at: destinationURL)
|
|
47
|
+
|
|
48
|
+
let totalCompressedBytes = entries.reduce(UInt64(0)) { $0 + max($1.compressedSize, 1) }
|
|
49
|
+
var processedCompressedBytes: UInt64 = 0
|
|
50
|
+
|
|
51
|
+
for entry in entries {
|
|
52
|
+
try extractEntry(entry, from: handle, to: destinationURL.standardizedFileURL.path)
|
|
53
|
+
processedCompressedBytes += max(entry.compressedSize, 1)
|
|
54
|
+
|
|
55
|
+
guard totalCompressedBytes > 0 else {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let progress = min(Double(processedCompressedBytes) / Double(totalCompressedBytes), 1.0)
|
|
60
|
+
progressHandler(progress)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
progressHandler(1.0)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static func readCentralDirectoryEntries(
|
|
67
|
+
from handle: FileHandle,
|
|
68
|
+
fileSize: UInt64
|
|
69
|
+
) throws -> [CentralDirectoryEntry] {
|
|
70
|
+
let endRecord = try locateEndOfCentralDirectory(in: handle, fileSize: fileSize)
|
|
71
|
+
|
|
72
|
+
try ArchiveExtractionUtilities.seek(handle, to: endRecord.centralDirectoryOffset)
|
|
73
|
+
|
|
74
|
+
var entries: [CentralDirectoryEntry] = []
|
|
75
|
+
entries.reserveCapacity(Int(endRecord.totalEntries))
|
|
76
|
+
|
|
77
|
+
for _ in 0..<endRecord.totalEntries {
|
|
78
|
+
let header = try ArchiveExtractionUtilities.readExactly(from: handle, count: 46)
|
|
79
|
+
|
|
80
|
+
guard header.archiveUInt32LE(at: 0) == centralDirectoryHeaderSignature else {
|
|
81
|
+
throw NSError(
|
|
82
|
+
domain: "ZipArchiveExtractor",
|
|
83
|
+
code: 1,
|
|
84
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid ZIP central directory header"]
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let flags = header.archiveUInt16LE(at: 8)
|
|
89
|
+
let compressionMethod = header.archiveUInt16LE(at: 10)
|
|
90
|
+
let compressedSize = header.archiveUInt32LE(at: 20)
|
|
91
|
+
let uncompressedSize = header.archiveUInt32LE(at: 24)
|
|
92
|
+
let fileNameLength = Int(header.archiveUInt16LE(at: 28))
|
|
93
|
+
let extraFieldLength = Int(header.archiveUInt16LE(at: 30))
|
|
94
|
+
let commentLength = Int(header.archiveUInt16LE(at: 32))
|
|
95
|
+
let checksum = header.archiveUInt32LE(at: 16)
|
|
96
|
+
let localHeaderOffset = header.archiveUInt32LE(at: 42)
|
|
97
|
+
let versionMadeBy = header.archiveUInt16LE(at: 4)
|
|
98
|
+
let externalAttributes = header.archiveUInt32LE(at: 38)
|
|
99
|
+
|
|
100
|
+
guard compressedSize != UInt32.max,
|
|
101
|
+
uncompressedSize != UInt32.max,
|
|
102
|
+
localHeaderOffset != UInt32.max else {
|
|
103
|
+
throw NSError(
|
|
104
|
+
domain: "ZipArchiveExtractor",
|
|
105
|
+
code: 2,
|
|
106
|
+
userInfo: [NSLocalizedDescriptionKey: "ZIP64 archives are not supported"]
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let fileNameData = try ArchiveExtractionUtilities.readExactly(from: handle, count: fileNameLength)
|
|
111
|
+
let extraFieldData = try ArchiveExtractionUtilities.readExactly(from: handle, count: extraFieldLength)
|
|
112
|
+
_ = extraFieldData
|
|
113
|
+
if commentLength > 0 {
|
|
114
|
+
try ArchiveExtractionUtilities.skipBytes(UInt64(commentLength), in: handle)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let path = decodePath(from: fileNameData)
|
|
118
|
+
let mode = unixFileMode(versionMadeBy: versionMadeBy, externalAttributes: externalAttributes)
|
|
119
|
+
let isDirectory = path.hasSuffix("/") || mode == directoryMode
|
|
120
|
+
let isSymbolicLink = mode == symbolicLinkMode
|
|
121
|
+
|
|
122
|
+
entries.append(
|
|
123
|
+
CentralDirectoryEntry(
|
|
124
|
+
path: path,
|
|
125
|
+
compressionMethod: compressionMethod,
|
|
126
|
+
flags: flags,
|
|
127
|
+
compressedSize: UInt64(compressedSize),
|
|
128
|
+
uncompressedSize: UInt64(uncompressedSize),
|
|
129
|
+
checksum: checksum,
|
|
130
|
+
localHeaderOffset: UInt64(localHeaderOffset),
|
|
131
|
+
isDirectory: isDirectory,
|
|
132
|
+
isSymbolicLink: isSymbolicLink
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return entries
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private static func locateEndOfCentralDirectory(
|
|
141
|
+
in handle: FileHandle,
|
|
142
|
+
fileSize: UInt64
|
|
143
|
+
) throws -> EndOfCentralDirectoryRecord {
|
|
144
|
+
let minimumRecordLength = 22
|
|
145
|
+
let searchLength = Int(min(fileSize, UInt64(maxCommentLength + minimumRecordLength)))
|
|
146
|
+
let searchOffset = fileSize - UInt64(searchLength)
|
|
147
|
+
|
|
148
|
+
try ArchiveExtractionUtilities.seek(handle, to: searchOffset)
|
|
149
|
+
let tailData = try ArchiveExtractionUtilities.readExactly(from: handle, count: searchLength)
|
|
150
|
+
|
|
151
|
+
let signatureBytes: [UInt8] = [0x50, 0x4B, 0x05, 0x06]
|
|
152
|
+
|
|
153
|
+
guard let recordIndex = findLastSignature(signatureBytes, in: tailData) else {
|
|
154
|
+
throw NSError(
|
|
155
|
+
domain: "ZipArchiveExtractor",
|
|
156
|
+
code: 3,
|
|
157
|
+
userInfo: [NSLocalizedDescriptionKey: "ZIP end of central directory record not found"]
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let minimumEndIndex = recordIndex + minimumRecordLength
|
|
162
|
+
guard minimumEndIndex <= tailData.count else {
|
|
163
|
+
throw NSError(
|
|
164
|
+
domain: "ZipArchiveExtractor",
|
|
165
|
+
code: 4,
|
|
166
|
+
userInfo: [NSLocalizedDescriptionKey: "ZIP end of central directory record is truncated"]
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let commentLength = Int(tailData.archiveUInt16LE(at: recordIndex + 20))
|
|
171
|
+
guard minimumEndIndex + commentLength == tailData.count else {
|
|
172
|
+
throw NSError(
|
|
173
|
+
domain: "ZipArchiveExtractor",
|
|
174
|
+
code: 5,
|
|
175
|
+
userInfo: [NSLocalizedDescriptionKey: "ZIP central directory comment is malformed"]
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return EndOfCentralDirectoryRecord(
|
|
180
|
+
totalEntries: tailData.archiveUInt16LE(at: recordIndex + 10),
|
|
181
|
+
centralDirectoryOffset: UInt64(tailData.archiveUInt32LE(at: recordIndex + 16))
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private static func extractEntry(
|
|
186
|
+
_ entry: CentralDirectoryEntry,
|
|
187
|
+
from handle: FileHandle,
|
|
188
|
+
to destinationRoot: String
|
|
189
|
+
) throws {
|
|
190
|
+
guard entry.flags & encryptedFlag == 0 else {
|
|
191
|
+
throw NSError(
|
|
192
|
+
domain: "ZipArchiveExtractor",
|
|
193
|
+
code: 6,
|
|
194
|
+
userInfo: [NSLocalizedDescriptionKey: "Encrypted ZIP entries are not supported"]
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
guard let relativePath = ArchiveExtractionUtilities.normalizedRelativePath(from: entry.path) else {
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let targetURL = try ArchiveExtractionUtilities.extractionURL(
|
|
203
|
+
for: relativePath,
|
|
204
|
+
destinationRoot: destinationRoot
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if entry.isDirectory {
|
|
208
|
+
try ArchiveExtractionUtilities.ensureDirectory(at: targetURL)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if entry.isSymbolicLink {
|
|
213
|
+
NSLog("[ZipArchiveExtractor] Skipping symbolic link entry: \(entry.path)")
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try ArchiveExtractionUtilities.seek(handle, to: entry.localHeaderOffset)
|
|
218
|
+
let localHeader = try ArchiveExtractionUtilities.readExactly(from: handle, count: 30)
|
|
219
|
+
|
|
220
|
+
guard localHeader.archiveUInt32LE(at: 0) == localFileHeaderSignature else {
|
|
221
|
+
throw NSError(
|
|
222
|
+
domain: "ZipArchiveExtractor",
|
|
223
|
+
code: 7,
|
|
224
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid ZIP local file header for \(entry.path)"]
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let fileNameLength = UInt64(localHeader.archiveUInt16LE(at: 26))
|
|
229
|
+
let extraFieldLength = UInt64(localHeader.archiveUInt16LE(at: 28))
|
|
230
|
+
let dataOffset = entry.localHeaderOffset + 30 + fileNameLength + extraFieldLength
|
|
231
|
+
|
|
232
|
+
try ArchiveExtractionUtilities.seek(handle, to: dataOffset)
|
|
233
|
+
let outputHandle = try ArchiveExtractionUtilities.createOutputFile(at: targetURL)
|
|
234
|
+
|
|
235
|
+
defer {
|
|
236
|
+
try? outputHandle.close()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let extractionResult: (writtenSize: UInt64, checksum: UInt32)
|
|
240
|
+
|
|
241
|
+
switch entry.compressionMethod {
|
|
242
|
+
case storedMethod:
|
|
243
|
+
extractionResult = try extractStoredEntry(
|
|
244
|
+
from: handle,
|
|
245
|
+
compressedSize: entry.compressedSize,
|
|
246
|
+
to: outputHandle
|
|
247
|
+
)
|
|
248
|
+
case deflatedMethod:
|
|
249
|
+
extractionResult = try extractDeflatedEntry(
|
|
250
|
+
from: handle,
|
|
251
|
+
compressedSize: entry.compressedSize,
|
|
252
|
+
to: outputHandle
|
|
253
|
+
)
|
|
254
|
+
default:
|
|
255
|
+
throw NSError(
|
|
256
|
+
domain: "ZipArchiveExtractor",
|
|
257
|
+
code: 8,
|
|
258
|
+
userInfo: [NSLocalizedDescriptionKey: "Unsupported ZIP compression method \(entry.compressionMethod) for \(entry.path)"]
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
guard extractionResult.writtenSize == entry.uncompressedSize else {
|
|
263
|
+
throw NSError(
|
|
264
|
+
domain: "ZipArchiveExtractor",
|
|
265
|
+
code: 9,
|
|
266
|
+
userInfo: [NSLocalizedDescriptionKey: "ZIP entry size mismatch for \(entry.path)"]
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
guard extractionResult.checksum == entry.checksum else {
|
|
271
|
+
throw NSError(
|
|
272
|
+
domain: "ZipArchiveExtractor",
|
|
273
|
+
code: 10,
|
|
274
|
+
userInfo: [NSLocalizedDescriptionKey: "ZIP entry checksum mismatch for \(entry.path)"]
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private static func extractStoredEntry(
|
|
280
|
+
from handle: FileHandle,
|
|
281
|
+
compressedSize: UInt64,
|
|
282
|
+
to outputHandle: FileHandle
|
|
283
|
+
) throws -> (writtenSize: UInt64, checksum: UInt32) {
|
|
284
|
+
var remainingBytes = compressedSize
|
|
285
|
+
var totalWritten: UInt64 = 0
|
|
286
|
+
var checksum: uLong = crc32(0, nil, 0)
|
|
287
|
+
|
|
288
|
+
while remainingBytes > 0 {
|
|
289
|
+
let chunkSize = Int(min(remainingBytes, UInt64(ArchiveExtractionUtilities.bufferSize)))
|
|
290
|
+
let chunk = try ArchiveExtractionUtilities.readExactly(from: handle, count: chunkSize)
|
|
291
|
+
outputHandle.write(chunk)
|
|
292
|
+
totalWritten += UInt64(chunk.count)
|
|
293
|
+
checksum = updateCRC32(checksum, with: chunk)
|
|
294
|
+
remainingBytes -= UInt64(chunk.count)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return (totalWritten, UInt32(checksum))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private static func extractDeflatedEntry(
|
|
301
|
+
from handle: FileHandle,
|
|
302
|
+
compressedSize: UInt64,
|
|
303
|
+
to outputHandle: FileHandle
|
|
304
|
+
) throws -> (writtenSize: UInt64, checksum: UInt32) {
|
|
305
|
+
let outputBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: ArchiveExtractionUtilities.bufferSize)
|
|
306
|
+
|
|
307
|
+
defer {
|
|
308
|
+
outputBuffer.deallocate()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
var stream = z_stream()
|
|
312
|
+
let initStatus = inflateInit2_(
|
|
313
|
+
&stream,
|
|
314
|
+
-MAX_WBITS,
|
|
315
|
+
ZLIB_VERSION,
|
|
316
|
+
Int32(MemoryLayout<z_stream>.size)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
guard initStatus == Z_OK else {
|
|
320
|
+
throw NSError(
|
|
321
|
+
domain: "ZipArchiveExtractor",
|
|
322
|
+
code: 11,
|
|
323
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to initialize ZIP inflater"]
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
defer {
|
|
328
|
+
inflateEnd(&stream)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
var remainingBytes = compressedSize
|
|
332
|
+
var totalWritten: UInt64 = 0
|
|
333
|
+
var checksum: uLong = crc32(0, nil, 0)
|
|
334
|
+
var reachedStreamEnd = false
|
|
335
|
+
|
|
336
|
+
while remainingBytes > 0 {
|
|
337
|
+
let chunkSize = Int(min(remainingBytes, UInt64(ArchiveExtractionUtilities.bufferSize)))
|
|
338
|
+
let chunk = try ArchiveExtractionUtilities.readExactly(from: handle, count: chunkSize)
|
|
339
|
+
remainingBytes -= UInt64(chunk.count)
|
|
340
|
+
|
|
341
|
+
try chunk.withUnsafeBytes { rawBuffer in
|
|
342
|
+
guard let baseAddress = rawBuffer.bindMemory(to: UInt8.self).baseAddress else {
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
stream.next_in = UnsafeMutablePointer(mutating: baseAddress)
|
|
347
|
+
stream.avail_in = uInt(chunk.count)
|
|
348
|
+
|
|
349
|
+
repeat {
|
|
350
|
+
stream.next_out = outputBuffer
|
|
351
|
+
stream.avail_out = uInt(ArchiveExtractionUtilities.bufferSize)
|
|
352
|
+
|
|
353
|
+
let status = inflate(&stream, Z_NO_FLUSH)
|
|
354
|
+
switch status {
|
|
355
|
+
case Z_OK, Z_STREAM_END:
|
|
356
|
+
let producedBytes = ArchiveExtractionUtilities.bufferSize - Int(stream.avail_out)
|
|
357
|
+
if producedBytes > 0 {
|
|
358
|
+
let outputData = Data(bytes: outputBuffer, count: producedBytes)
|
|
359
|
+
outputHandle.write(outputData)
|
|
360
|
+
totalWritten += UInt64(producedBytes)
|
|
361
|
+
checksum = updateCRC32(checksum, with: outputData)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if status == Z_STREAM_END {
|
|
365
|
+
guard stream.avail_in == 0, remainingBytes == 0 else {
|
|
366
|
+
throw NSError(
|
|
367
|
+
domain: "ZipArchiveExtractor",
|
|
368
|
+
code: 12,
|
|
369
|
+
userInfo: [NSLocalizedDescriptionKey: "ZIP deflate stream ended unexpectedly"]
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
reachedStreamEnd = true
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
default:
|
|
377
|
+
let message = stream.msg.map { String(cString: $0) } ?? "Unknown zlib error"
|
|
378
|
+
throw NSError(
|
|
379
|
+
domain: "ZipArchiveExtractor",
|
|
380
|
+
code: 13,
|
|
381
|
+
userInfo: [NSLocalizedDescriptionKey: "ZIP deflate failed: \(message)"]
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
} while stream.avail_in > 0 || stream.avail_out == 0
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if reachedStreamEnd {
|
|
388
|
+
break
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
guard reachedStreamEnd || compressedSize == 0 else {
|
|
393
|
+
throw NSError(
|
|
394
|
+
domain: "ZipArchiveExtractor",
|
|
395
|
+
code: 14,
|
|
396
|
+
userInfo: [NSLocalizedDescriptionKey: "ZIP deflate stream did not terminate correctly"]
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return (totalWritten, UInt32(checksum))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private static func findLastSignature(_ signature: [UInt8], in data: Data) -> Int? {
|
|
404
|
+
guard signature.count <= data.count else {
|
|
405
|
+
return nil
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let lastStartIndex = data.count - signature.count
|
|
409
|
+
for startIndex in stride(from: lastStartIndex, through: 0, by: -1) {
|
|
410
|
+
if Array(data[startIndex..<(startIndex + signature.count)]) == signature {
|
|
411
|
+
return startIndex
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return nil
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private static func decodePath(from data: Data) -> String {
|
|
419
|
+
if let utf8Path = String(data: data, encoding: .utf8) {
|
|
420
|
+
return utf8Path
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return String(decoding: data, as: UTF8.self)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private static func updateCRC32(_ checksum: uLong, with data: Data) -> uLong {
|
|
427
|
+
data.withUnsafeBytes { rawBuffer in
|
|
428
|
+
guard let baseAddress = rawBuffer.bindMemory(to: Bytef.self).baseAddress else {
|
|
429
|
+
return checksum
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return crc32(checksum, baseAddress, uInt(data.count))
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private static func unixFileMode(versionMadeBy: UInt16, externalAttributes: UInt32) -> UInt16? {
|
|
437
|
+
let hostIdentifier = UInt8((versionMadeBy >> 8) & 0xFF)
|
|
438
|
+
guard hostIdentifier == unixHostIdentifier else {
|
|
439
|
+
return nil
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let mode = UInt16((externalAttributes >> 16) & 0xFFFF) & fileTypeMask
|
|
443
|
+
guard mode != 0 else {
|
|
444
|
+
return nil
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return mode
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private static func archiveFileSize(at path: String) throws -> UInt64 {
|
|
451
|
+
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
|
452
|
+
if let number = attributes[.size] as? NSNumber {
|
|
453
|
+
return number.uint64Value
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if let value = attributes[.size] as? UInt64 {
|
|
457
|
+
return value
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return 0
|
|
461
|
+
}
|
|
462
|
+
}
|