@hot-updater/react-native 0.29.2 → 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.
@@ -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? fileHandle.read(upToCount: 2), header.count == 2 else {
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
- guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: file)) else {
54
- throw NSError(
55
- domain: "TarGzDecompressionStrategy",
56
- code: 1,
57
- userInfo: [NSLocalizedDescriptionKey: "Failed to read tar.gz file at: \(file)"]
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 FileManager.default.copyItem(at: location, to: destinationURL)
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
+ }