@hot-updater/react-native 0.29.2 → 0.29.4
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/android/src/oldarch/HotUpdaterModule.kt +12 -4
- package/android/src/oldarch/HotUpdaterSpec.kt +3 -5
- 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/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +0 -7
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +0 -7
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/store.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +9 -9
- package/plugin/build/transformers.js +83 -97
- package/plugin/build/withHotUpdater.js +159 -239
- package/src/DefaultResolver.ts +1 -0
- package/src/checkForUpdate.ts +1 -0
- package/src/index.ts +0 -7
- package/src/native.spec.ts +4 -6
- package/src/native.ts +1 -0
- package/src/store.ts +1 -0
- package/src/types.ts +1 -0
- package/src/wrap.tsx +1 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum TarArchiveExtractor {
|
|
4
|
+
private static let blockSize = 512
|
|
5
|
+
|
|
6
|
+
private static let regularFileType: UInt8 = 48
|
|
7
|
+
private static let alternateRegularFileType: UInt8 = 0
|
|
8
|
+
private static let hardLinkType: UInt8 = 49
|
|
9
|
+
private static let symbolicLinkType: UInt8 = 50
|
|
10
|
+
private static let directoryType: UInt8 = 53
|
|
11
|
+
private static let contiguousFileType: UInt8 = 55
|
|
12
|
+
private static let globalPaxHeaderType: UInt8 = 103
|
|
13
|
+
private static let paxHeaderType: UInt8 = 120
|
|
14
|
+
private static let gnuLongNameType: UInt8 = 76
|
|
15
|
+
private static let gnuLongLinkType: UInt8 = 75
|
|
16
|
+
|
|
17
|
+
private struct Header {
|
|
18
|
+
let path: String
|
|
19
|
+
let size: UInt64
|
|
20
|
+
let typeFlag: UInt8
|
|
21
|
+
let linkName: String
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static func containsEntries(at tarPath: String) throws -> Bool {
|
|
25
|
+
let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: tarPath))
|
|
26
|
+
|
|
27
|
+
defer {
|
|
28
|
+
try? handle.close()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var globalPaxHeaders: [String: String] = [:]
|
|
32
|
+
var pendingPaxHeaders: [String: String] = [:]
|
|
33
|
+
var pendingLongPath: String?
|
|
34
|
+
var pendingLongLink: String?
|
|
35
|
+
|
|
36
|
+
while true {
|
|
37
|
+
let headerBlock = try ArchiveExtractionUtilities.readExactly(from: handle, count: blockSize)
|
|
38
|
+
guard !isZeroBlock(headerBlock) else {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let header = try parseHeader(from: headerBlock)
|
|
43
|
+
|
|
44
|
+
switch header.typeFlag {
|
|
45
|
+
case globalPaxHeaderType:
|
|
46
|
+
let paxData = try readEntryPayloadData(from: handle, size: header.size)
|
|
47
|
+
globalPaxHeaders.merge(parsePaxHeaders(from: paxData)) { _, newValue in
|
|
48
|
+
newValue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
case paxHeaderType:
|
|
52
|
+
let paxData = try readEntryPayloadData(from: handle, size: header.size)
|
|
53
|
+
pendingPaxHeaders.merge(parsePaxHeaders(from: paxData)) { _, newValue in
|
|
54
|
+
newValue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case gnuLongNameType:
|
|
58
|
+
pendingLongPath = decodeLongPath(from: try readEntryPayloadData(from: handle, size: header.size))
|
|
59
|
+
|
|
60
|
+
case gnuLongLinkType:
|
|
61
|
+
pendingLongLink = decodeLongPath(from: try readEntryPayloadData(from: handle, size: header.size))
|
|
62
|
+
|
|
63
|
+
default:
|
|
64
|
+
let effectiveHeaders = globalPaxHeaders.merging(pendingPaxHeaders) { _, newValue in
|
|
65
|
+
newValue
|
|
66
|
+
}
|
|
67
|
+
let resolvedPath = pendingLongPath ?? effectiveHeaders["path"] ?? header.path
|
|
68
|
+
_ = pendingLongLink ?? effectiveHeaders["linkpath"] ?? header.linkName
|
|
69
|
+
|
|
70
|
+
defer {
|
|
71
|
+
pendingPaxHeaders.removeAll()
|
|
72
|
+
pendingLongPath = nil
|
|
73
|
+
pendingLongLink = nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if let normalizedPath = ArchiveExtractionUtilities.normalizedRelativePath(from: resolvedPath),
|
|
77
|
+
!normalizedPath.isEmpty {
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try skipEntryPayload(in: handle, size: header.size)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static func extract(
|
|
87
|
+
from tarPath: String,
|
|
88
|
+
to destination: String,
|
|
89
|
+
progressHandler: @escaping (Double) -> Void
|
|
90
|
+
) throws {
|
|
91
|
+
let fileManager = FileManager.default
|
|
92
|
+
let destinationRoot = URL(fileURLWithPath: destination).standardizedFileURL.path
|
|
93
|
+
try ArchiveExtractionUtilities.ensureDirectory(at: URL(fileURLWithPath: destinationRoot), fileManager: fileManager)
|
|
94
|
+
|
|
95
|
+
let tarSize = try archiveFileSize(at: tarPath)
|
|
96
|
+
let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: tarPath))
|
|
97
|
+
|
|
98
|
+
defer {
|
|
99
|
+
try? handle.close()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
var globalPaxHeaders: [String: String] = [:]
|
|
103
|
+
var pendingPaxHeaders: [String: String] = [:]
|
|
104
|
+
var pendingLongPath: String?
|
|
105
|
+
var pendingLongLink: String?
|
|
106
|
+
|
|
107
|
+
while true {
|
|
108
|
+
let headerBlock = try ArchiveExtractionUtilities.readExactly(from: handle, count: blockSize)
|
|
109
|
+
guard !isZeroBlock(headerBlock) else {
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let header = try parseHeader(from: headerBlock)
|
|
114
|
+
|
|
115
|
+
switch header.typeFlag {
|
|
116
|
+
case globalPaxHeaderType:
|
|
117
|
+
let paxData = try readEntryPayloadData(from: handle, size: header.size)
|
|
118
|
+
globalPaxHeaders.merge(parsePaxHeaders(from: paxData)) { _, newValue in
|
|
119
|
+
newValue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case paxHeaderType:
|
|
123
|
+
let paxData = try readEntryPayloadData(from: handle, size: header.size)
|
|
124
|
+
pendingPaxHeaders.merge(parsePaxHeaders(from: paxData)) { _, newValue in
|
|
125
|
+
newValue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case gnuLongNameType:
|
|
129
|
+
pendingLongPath = decodeLongPath(from: try readEntryPayloadData(from: handle, size: header.size))
|
|
130
|
+
|
|
131
|
+
case gnuLongLinkType:
|
|
132
|
+
pendingLongLink = decodeLongPath(from: try readEntryPayloadData(from: handle, size: header.size))
|
|
133
|
+
|
|
134
|
+
default:
|
|
135
|
+
let effectiveHeaders = globalPaxHeaders.merging(pendingPaxHeaders) { _, newValue in
|
|
136
|
+
newValue
|
|
137
|
+
}
|
|
138
|
+
let resolvedPath = pendingLongPath ?? effectiveHeaders["path"] ?? header.path
|
|
139
|
+
let resolvedLinkPath = pendingLongLink ?? effectiveHeaders["linkpath"] ?? header.linkName
|
|
140
|
+
|
|
141
|
+
defer {
|
|
142
|
+
pendingPaxHeaders.removeAll()
|
|
143
|
+
pendingLongPath = nil
|
|
144
|
+
pendingLongLink = nil
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try extractEntry(
|
|
148
|
+
path: resolvedPath,
|
|
149
|
+
typeFlag: header.typeFlag,
|
|
150
|
+
size: header.size,
|
|
151
|
+
linkPath: resolvedLinkPath,
|
|
152
|
+
from: handle,
|
|
153
|
+
to: destinationRoot
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if tarSize > 0 {
|
|
158
|
+
let offset = ArchiveExtractionUtilities.currentOffset(for: handle)
|
|
159
|
+
let progress = min(Double(offset) / Double(tarSize), 1.0)
|
|
160
|
+
progressHandler(progress)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
progressHandler(1.0)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private static func extractEntry(
|
|
168
|
+
path rawPath: String,
|
|
169
|
+
typeFlag: UInt8,
|
|
170
|
+
size: UInt64,
|
|
171
|
+
linkPath: String,
|
|
172
|
+
from handle: FileHandle,
|
|
173
|
+
to destinationRoot: String
|
|
174
|
+
) throws {
|
|
175
|
+
guard let relativePath = ArchiveExtractionUtilities.normalizedRelativePath(from: rawPath) else {
|
|
176
|
+
try skipEntryPayload(in: handle, size: size)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let targetURL = try ArchiveExtractionUtilities.extractionURL(
|
|
181
|
+
for: relativePath,
|
|
182
|
+
destinationRoot: destinationRoot
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
switch typeFlag {
|
|
186
|
+
case directoryType:
|
|
187
|
+
try ArchiveExtractionUtilities.ensureDirectory(at: targetURL)
|
|
188
|
+
try skipEntryPayload(in: handle, size: size)
|
|
189
|
+
|
|
190
|
+
case regularFileType, alternateRegularFileType, contiguousFileType:
|
|
191
|
+
let outputHandle = try ArchiveExtractionUtilities.createOutputFile(at: targetURL)
|
|
192
|
+
|
|
193
|
+
defer {
|
|
194
|
+
try? outputHandle.close()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try copyEntryPayload(from: handle, size: size, to: outputHandle)
|
|
198
|
+
try skipPadding(in: handle, size: size)
|
|
199
|
+
|
|
200
|
+
case hardLinkType, symbolicLinkType:
|
|
201
|
+
NSLog("[TarArchiveExtractor] Skipping link entry: \(rawPath) -> \(linkPath)")
|
|
202
|
+
try skipEntryPayload(in: handle, size: size)
|
|
203
|
+
|
|
204
|
+
default:
|
|
205
|
+
NSLog("[TarArchiveExtractor] Skipping unsupported TAR entry type: \(typeFlag) (\(rawPath))")
|
|
206
|
+
try skipEntryPayload(in: handle, size: size)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private static func copyEntryPayload(
|
|
211
|
+
from handle: FileHandle,
|
|
212
|
+
size: UInt64,
|
|
213
|
+
to outputHandle: FileHandle
|
|
214
|
+
) throws {
|
|
215
|
+
var remainingBytes = size
|
|
216
|
+
|
|
217
|
+
while remainingBytes > 0 {
|
|
218
|
+
let chunkSize = Int(min(remainingBytes, UInt64(ArchiveExtractionUtilities.bufferSize)))
|
|
219
|
+
let chunk = try ArchiveExtractionUtilities.readExactly(from: handle, count: chunkSize)
|
|
220
|
+
outputHandle.write(chunk)
|
|
221
|
+
remainingBytes -= UInt64(chunk.count)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private static func readEntryPayloadData(from handle: FileHandle, size: UInt64) throws -> Data {
|
|
226
|
+
guard size > 0 else {
|
|
227
|
+
return Data()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
guard size <= UInt64(Int.max) else {
|
|
231
|
+
throw NSError(
|
|
232
|
+
domain: "TarArchiveExtractor",
|
|
233
|
+
code: 3,
|
|
234
|
+
userInfo: [NSLocalizedDescriptionKey: "TAR payload exceeds supported in-memory size: \(size) bytes"]
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let payload = try ArchiveExtractionUtilities.readExactly(from: handle, count: Int(size))
|
|
239
|
+
try skipPadding(in: handle, size: size)
|
|
240
|
+
return payload
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private static func skipEntryPayload(in handle: FileHandle, size: UInt64) throws {
|
|
244
|
+
try ArchiveExtractionUtilities.skipBytes(size, in: handle)
|
|
245
|
+
try skipPadding(in: handle, size: size)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private static func skipPadding(in handle: FileHandle, size: UInt64) throws {
|
|
249
|
+
let padding = (UInt64(blockSize) - (size % UInt64(blockSize))) % UInt64(blockSize)
|
|
250
|
+
try ArchiveExtractionUtilities.skipBytes(padding, in: handle)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private static func parseHeader(from block: Data) throws -> Header {
|
|
254
|
+
guard block.count == blockSize else {
|
|
255
|
+
throw NSError(
|
|
256
|
+
domain: "TarArchiveExtractor",
|
|
257
|
+
code: 1,
|
|
258
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid TAR block size: \(block.count)"]
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return Header(
|
|
263
|
+
path: parseTarPath(from: block),
|
|
264
|
+
size: try parseTarNumber(block[124..<136]),
|
|
265
|
+
typeFlag: block[156],
|
|
266
|
+
linkName: parseCString(block[157..<257])
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private static func parseTarPath(from block: Data) -> String {
|
|
271
|
+
let name = parseCString(block[0..<100])
|
|
272
|
+
let prefix = parseCString(block[345..<500])
|
|
273
|
+
|
|
274
|
+
guard !prefix.isEmpty else {
|
|
275
|
+
return name
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
guard !name.isEmpty else {
|
|
279
|
+
return prefix
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return "\(prefix)/\(name)"
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private static func parseCString(_ data: Data.SubSequence) -> String {
|
|
286
|
+
let bytes = data.prefix { $0 != 0 }
|
|
287
|
+
guard !bytes.isEmpty else {
|
|
288
|
+
return ""
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if let decoded = String(data: Data(bytes), encoding: .utf8) {
|
|
292
|
+
return decoded
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return String(decoding: bytes, as: UTF8.self)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private static func parseTarNumber(_ data: Data.SubSequence) throws -> UInt64 {
|
|
299
|
+
let bytes = [UInt8](data)
|
|
300
|
+
guard !bytes.allSatisfy({ $0 == 0 || $0 == 32 }) else {
|
|
301
|
+
return 0
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if let first = bytes.first, first & 0x80 != 0 {
|
|
305
|
+
var value: UInt64 = UInt64(first & 0x7F)
|
|
306
|
+
for byte in bytes.dropFirst() {
|
|
307
|
+
value = (value << 8) | UInt64(byte)
|
|
308
|
+
}
|
|
309
|
+
return value
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let stringValue = String(bytes: bytes, encoding: .ascii)?
|
|
313
|
+
.trimmingCharacters(in: CharacterSet(charactersIn: "\0 "))
|
|
314
|
+
|
|
315
|
+
guard let stringValue, !stringValue.isEmpty,
|
|
316
|
+
let parsedValue = UInt64(stringValue, radix: 8) else {
|
|
317
|
+
throw NSError(
|
|
318
|
+
domain: "TarArchiveExtractor",
|
|
319
|
+
code: 2,
|
|
320
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid TAR numeric field"]
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return parsedValue
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private static func parsePaxHeaders(from data: Data) -> [String: String] {
|
|
328
|
+
var headers: [String: String] = [:]
|
|
329
|
+
var index = data.startIndex
|
|
330
|
+
|
|
331
|
+
while index < data.endIndex {
|
|
332
|
+
guard let spaceIndex = data[index...].firstIndex(of: 0x20),
|
|
333
|
+
let lengthString = String(data: data[index..<spaceIndex], encoding: .ascii),
|
|
334
|
+
let recordLength = Int(lengthString),
|
|
335
|
+
recordLength > 0 else {
|
|
336
|
+
break
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let recordEnd = index + recordLength
|
|
340
|
+
guard recordEnd <= data.endIndex else {
|
|
341
|
+
break
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let recordBodyStart = data.index(after: spaceIndex)
|
|
345
|
+
let recordBody = data[recordBodyStart..<recordEnd]
|
|
346
|
+
|
|
347
|
+
if let newlineIndex = recordBody.lastIndex(of: 0x0A),
|
|
348
|
+
let separatorIndex = recordBody[..<newlineIndex].firstIndex(of: 0x3D),
|
|
349
|
+
let key = String(data: recordBody[..<separatorIndex], encoding: .utf8),
|
|
350
|
+
let value = String(data: recordBody[recordBody.index(after: separatorIndex)..<newlineIndex], encoding: .utf8) {
|
|
351
|
+
headers[key] = value
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
index = recordEnd
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return headers
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private static func decodeLongPath(from data: Data) -> String {
|
|
361
|
+
let trimmedData = data.prefix { $0 != 0 }
|
|
362
|
+
guard !trimmedData.isEmpty else {
|
|
363
|
+
return ""
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return String(decoding: trimmedData, as: UTF8.self)
|
|
367
|
+
.trimmingCharacters(in: .newlines)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private static func isZeroBlock(_ data: Data) -> Bool {
|
|
371
|
+
data.allSatisfy { $0 == 0 }
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private static func archiveFileSize(at path: String) throws -> UInt64 {
|
|
375
|
+
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
|
376
|
+
if let number = attributes[.size] as? NSNumber {
|
|
377
|
+
return number.uint64Value
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if let value = attributes[.size] as? UInt64 {
|
|
381
|
+
return value
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return 0
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
-
import SWCompression
|
|
3
|
-
import Compression
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Strategy for handling TAR+Brotli compressed files
|
|
@@ -25,221 +23,17 @@ class TarBrDecompressionStrategy: DecompressionStrategy {
|
|
|
25
23
|
return false
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
NSLog("[TarBrStrategy] Invalid file: cannot read file data")
|
|
30
|
-
return false
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
do {
|
|
34
|
-
let tarEntries = try readTarEntries(from: compressedData)
|
|
35
|
-
if tarEntries.isEmpty {
|
|
36
|
-
NSLog("[TarBrStrategy] Invalid file: tar archive has no entries")
|
|
37
|
-
return false
|
|
38
|
-
}
|
|
39
|
-
return true
|
|
40
|
-
} catch {
|
|
41
|
-
NSLog("[TarBrStrategy] Invalid file: Brotli/TAR validation failed - \(error.localizedDescription)")
|
|
42
|
-
return false
|
|
43
|
-
}
|
|
26
|
+
return StreamingTarArchiveExtractor.containsTarEntries(file: file, algorithm: .brotli)
|
|
44
27
|
}
|
|
45
28
|
|
|
46
29
|
func decompress(file: String, to destination: String, progressHandler: @escaping (Double) -> Void) throws {
|
|
47
30
|
NSLog("[TarBrStrategy] Starting extraction of \(file) to \(destination)")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
userInfo: [NSLocalizedDescriptionKey: "Failed to read tar.br file at: \(file)"]
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
progressHandler(0.3)
|
|
58
|
-
let decompressedData: Data
|
|
59
|
-
do {
|
|
60
|
-
decompressedData = try decompressBrotli(compressedData)
|
|
61
|
-
NSLog("[TarBrStrategy] Brotli decompression successful, size: \(decompressedData.count) bytes")
|
|
62
|
-
progressHandler(0.6)
|
|
63
|
-
} catch {
|
|
64
|
-
throw NSError(
|
|
65
|
-
domain: "TarBrDecompressionStrategy",
|
|
66
|
-
code: 2,
|
|
67
|
-
userInfo: [NSLocalizedDescriptionKey: "Brotli decompression failed: \(error.localizedDescription)"]
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let tarEntries: [TarEntry]
|
|
72
|
-
do {
|
|
73
|
-
tarEntries = try readTarEntries(fromDecompressedData: decompressedData)
|
|
74
|
-
NSLog("[TarBrStrategy] Tar extraction successful, found \(tarEntries.count) entries")
|
|
75
|
-
} catch {
|
|
76
|
-
throw NSError(
|
|
77
|
-
domain: "TarBrDecompressionStrategy",
|
|
78
|
-
code: 3,
|
|
79
|
-
userInfo: [NSLocalizedDescriptionKey: "Tar extraction failed: \(error.localizedDescription)"]
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
let destinationURL = URL(fileURLWithPath: destination)
|
|
84
|
-
let canonicalDestination = destinationURL.standardized.path
|
|
85
|
-
|
|
86
|
-
let fileManager = FileManager.default
|
|
87
|
-
if !fileManager.fileExists(atPath: canonicalDestination) {
|
|
88
|
-
try fileManager.createDirectory(
|
|
89
|
-
atPath: canonicalDestination,
|
|
90
|
-
withIntermediateDirectories: true,
|
|
91
|
-
attributes: nil
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let totalEntries = Double(tarEntries.count)
|
|
96
|
-
for (index, entry) in tarEntries.enumerated() {
|
|
97
|
-
try extractTarEntry(entry, to: canonicalDestination)
|
|
98
|
-
progressHandler(0.6 + (Double(index + 1) / totalEntries * 0.4))
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
NSLog("[TarBrStrategy] Successfully extracted all entries")
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private func readTarEntries(from compressedData: Data) throws -> [TarEntry] {
|
|
105
|
-
let decompressedData = try decompressBrotli(compressedData)
|
|
106
|
-
return try readTarEntries(fromDecompressedData: decompressedData)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private func readTarEntries(fromDecompressedData decompressedData: Data) throws -> [TarEntry] {
|
|
110
|
-
return try TarContainer.open(container: decompressedData)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private func decompressBrotli(_ data: Data) throws -> Data {
|
|
114
|
-
let bufferSize = 64 * 1024
|
|
115
|
-
var decompressedData = Data()
|
|
116
|
-
let count = data.count
|
|
117
|
-
|
|
118
|
-
var stream = compression_stream(
|
|
119
|
-
dst_ptr: UnsafeMutablePointer<UInt8>(bitPattern: 1)!,
|
|
120
|
-
dst_size: 0,
|
|
121
|
-
src_ptr: UnsafePointer<UInt8>(bitPattern: 1)!,
|
|
122
|
-
src_size: 0,
|
|
123
|
-
state: nil
|
|
31
|
+
try StreamingTarArchiveExtractor.extractCompressedTar(
|
|
32
|
+
file: file,
|
|
33
|
+
to: destination,
|
|
34
|
+
algorithm: .brotli,
|
|
35
|
+
progressHandler: progressHandler
|
|
124
36
|
)
|
|
125
|
-
|
|
126
|
-
let status = compression_stream_init(&stream, COMPRESSION_STREAM_DECODE, COMPRESSION_BROTLI)
|
|
127
|
-
|
|
128
|
-
guard status == COMPRESSION_STATUS_OK else {
|
|
129
|
-
throw NSError(
|
|
130
|
-
domain: "TarBrDecompressionStrategy",
|
|
131
|
-
code: 5,
|
|
132
|
-
userInfo: [NSLocalizedDescriptionKey: "Failed to initialize brotli decompression stream"]
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
defer {
|
|
137
|
-
compression_stream_destroy(&stream)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let outputBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
|
141
|
-
defer {
|
|
142
|
-
outputBuffer.deallocate()
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
data.withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) in
|
|
146
|
-
guard let baseAddress = rawBufferPointer.baseAddress else {
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
stream.src_ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
|
|
151
|
-
stream.src_size = count
|
|
152
|
-
|
|
153
|
-
var processStatus: compression_status
|
|
154
|
-
repeat {
|
|
155
|
-
stream.dst_ptr = outputBuffer
|
|
156
|
-
stream.dst_size = bufferSize
|
|
157
|
-
|
|
158
|
-
let flags = (stream.src_size == 0) ? Int32(bitPattern: COMPRESSION_STREAM_FINALIZE.rawValue) : Int32(0)
|
|
159
|
-
processStatus = compression_stream_process(&stream, flags)
|
|
160
|
-
|
|
161
|
-
switch processStatus {
|
|
162
|
-
case COMPRESSION_STATUS_OK, COMPRESSION_STATUS_END:
|
|
163
|
-
let outputSize = bufferSize - stream.dst_size
|
|
164
|
-
decompressedData.append(outputBuffer, count: outputSize)
|
|
165
|
-
case COMPRESSION_STATUS_ERROR:
|
|
166
|
-
break
|
|
167
|
-
default:
|
|
168
|
-
break
|
|
169
|
-
}
|
|
170
|
-
} while processStatus == COMPRESSION_STATUS_OK
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if decompressedData.isEmpty && !data.isEmpty {
|
|
174
|
-
throw NSError(
|
|
175
|
-
domain: "TarBrDecompressionStrategy",
|
|
176
|
-
code: 6,
|
|
177
|
-
userInfo: [NSLocalizedDescriptionKey: "Brotli decompression produced no output"]
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return decompressedData
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
private func extractTarEntry(_ entry: TarEntry, to destination: String) throws {
|
|
185
|
-
let entryInfo = entry.info
|
|
186
|
-
let entryName = entryInfo.name
|
|
187
|
-
|
|
188
|
-
if entryName.isEmpty || entryName == "./" || entryName == "." {
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
let targetURL = URL(fileURLWithPath: destination).appendingPathComponent(entryName)
|
|
193
|
-
let targetPath = targetURL.standardized.path
|
|
194
|
-
|
|
195
|
-
if !targetPath.hasPrefix(destination) {
|
|
196
|
-
throw NSError(
|
|
197
|
-
domain: "TarBrDecompressionStrategy",
|
|
198
|
-
code: 4,
|
|
199
|
-
userInfo: [
|
|
200
|
-
NSLocalizedDescriptionKey: "Path traversal detected",
|
|
201
|
-
"entry": entryName,
|
|
202
|
-
"targetPath": targetPath,
|
|
203
|
-
"destination": destination
|
|
204
|
-
]
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
let fileManager = FileManager.default
|
|
209
|
-
|
|
210
|
-
switch entryInfo.type {
|
|
211
|
-
case .directory:
|
|
212
|
-
if !fileManager.fileExists(atPath: targetPath) {
|
|
213
|
-
try fileManager.createDirectory(
|
|
214
|
-
atPath: targetPath,
|
|
215
|
-
withIntermediateDirectories: true,
|
|
216
|
-
attributes: nil
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
NSLog("[TarBrStrategy] Created directory: \(entryName)")
|
|
220
|
-
|
|
221
|
-
case .regular:
|
|
222
|
-
let parentPath = targetURL.deletingLastPathComponent().path
|
|
223
|
-
if !fileManager.fileExists(atPath: parentPath) {
|
|
224
|
-
try fileManager.createDirectory(
|
|
225
|
-
atPath: parentPath,
|
|
226
|
-
withIntermediateDirectories: true,
|
|
227
|
-
attributes: nil
|
|
228
|
-
)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if let data = entry.data {
|
|
232
|
-
try data.write(to: targetURL, options: .atomic)
|
|
233
|
-
NSLog("[TarBrStrategy] Extracted file: \(entryName) (\(data.count) bytes)")
|
|
234
|
-
} else {
|
|
235
|
-
NSLog("[TarBrStrategy] Warning: No data for file entry: \(entryName)")
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
case .symbolicLink:
|
|
239
|
-
NSLog("[TarBrStrategy] Skipping symbolic link: \(entryName)")
|
|
240
|
-
|
|
241
|
-
default:
|
|
242
|
-
NSLog("[TarBrStrategy] Skipping unsupported entry type: \(entryName)")
|
|
243
|
-
}
|
|
37
|
+
NSLog("[TarBrStrategy] Successfully extracted all entries")
|
|
244
38
|
}
|
|
245
39
|
}
|