@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.
Files changed (55) hide show
  1. package/HotUpdater.podspec +0 -4
  2. package/android/src/oldarch/HotUpdaterModule.kt +12 -4
  3. package/android/src/oldarch/HotUpdaterSpec.kt +3 -5
  4. package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +178 -0
  5. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +82 -47
  6. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +359 -0
  7. package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +386 -0
  8. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +7 -213
  9. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +8 -126
  10. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -2
  11. package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +462 -0
  12. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +4 -113
  13. package/lib/commonjs/DefaultResolver.js.map +1 -1
  14. package/lib/commonjs/checkForUpdate.js.map +1 -1
  15. package/lib/commonjs/index.js +0 -7
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/native.js.map +1 -1
  18. package/lib/commonjs/native.spec.js.map +1 -1
  19. package/lib/commonjs/store.js.map +1 -1
  20. package/lib/commonjs/types.js.map +1 -1
  21. package/lib/commonjs/wrap.js.map +1 -1
  22. package/lib/module/DefaultResolver.js.map +1 -1
  23. package/lib/module/checkForUpdate.js.map +1 -1
  24. package/lib/module/index.js +0 -7
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/native.js.map +1 -1
  27. package/lib/module/native.spec.js.map +1 -1
  28. package/lib/module/store.js.map +1 -1
  29. package/lib/module/types.js.map +1 -1
  30. package/lib/module/wrap.js.map +1 -1
  31. package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  38. package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
  39. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  40. package/lib/typescript/module/index.d.ts.map +1 -1
  41. package/lib/typescript/module/native.d.ts.map +1 -1
  42. package/lib/typescript/module/store.d.ts.map +1 -1
  43. package/lib/typescript/module/types.d.ts.map +1 -1
  44. package/lib/typescript/module/wrap.d.ts.map +1 -1
  45. package/package.json +9 -9
  46. package/plugin/build/transformers.js +83 -97
  47. package/plugin/build/withHotUpdater.js +159 -239
  48. package/src/DefaultResolver.ts +1 -0
  49. package/src/checkForUpdate.ts +1 -0
  50. package/src/index.ts +0 -7
  51. package/src/native.spec.ts +4 -6
  52. package/src/native.ts +1 -0
  53. package/src/store.ts +1 -0
  54. package/src/types.ts +1 -0
  55. 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
- guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: file)) else {
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
- guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: file)) else {
50
- throw NSError(
51
- domain: "TarBrDecompressionStrategy",
52
- code: 1,
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
  }