@onekeyfe/react-native-range-downloader 3.0.81-alpha.7 → 3.0.81

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 (111) hide show
  1. package/ReactNativeRangeDownloader.podspec +0 -8
  2. package/android/build.gradle +0 -1
  3. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +0 -144
  4. package/ios/RangeDownloadLogic.swift +0 -120
  5. package/ios/ReactNativeRangeDownloader.swift +6 -623
  6. package/lib/module/index.js +1 -2
  7. package/lib/module/index.js.map +1 -1
  8. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +1 -91
  9. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
  10. package/lib/typescript/src/index.d.ts +0 -1
  11. package/lib/typescript/src/index.d.ts.map +1 -1
  12. package/nitrogen/generated/android/c++/JDownloadChannel.hpp +0 -3
  13. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +0 -248
  14. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +0 -12
  15. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/DownloadChannel.kt +1 -2
  16. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +0 -49
  17. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.cpp +0 -48
  18. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +0 -329
  19. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +0 -53
  20. package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +0 -152
  21. package/nitrogen/generated/ios/swift/DownloadChannel.swift +0 -4
  22. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +0 -12
  23. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +0 -221
  24. package/nitrogen/generated/shared/c++/DownloadChannel.hpp +0 -4
  25. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +0 -12
  26. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +0 -58
  27. package/package.json +2 -4
  28. package/src/ReactNativeRangeDownloader.nitro.ts +1 -127
  29. package/src/index.tsx +1 -3
  30. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRules.kt +0 -284
  31. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +0 -774
  32. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRulesTest.kt +0 -75
  33. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDeadlineTest.kt +0 -26
  34. package/ios/FirmwareArchiveMinizipBridge.h +0 -33
  35. package/ios/FirmwareArchiveMinizipBridge.mm +0 -213
  36. package/ios/FirmwareArtifactStore.swift +0 -1445
  37. package/ios/FirmwareBackgroundSessionEventRouter.swift +0 -17
  38. package/nitrogen/generated/android/c++/JFirmwareArchiveExpectedEntry.hpp +0 -69
  39. package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeParams.hpp +0 -86
  40. package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeResult.hpp +0 -80
  41. package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializedArtifact.hpp +0 -63
  42. package/nitrogen/generated/android/c++/JFirmwareArtifactCancelParams.hpp +0 -57
  43. package/nitrogen/generated/android/c++/JFirmwareArtifactCapabilities.hpp +0 -88
  44. package/nitrogen/generated/android/c++/JFirmwareArtifactDownloadParams.hpp +0 -98
  45. package/nitrogen/generated/android/c++/JFirmwareArtifactLease.hpp +0 -57
  46. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseCreateParams.hpp +0 -57
  47. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseReleaseParams.hpp +0 -61
  48. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseRetainParams.hpp +0 -61
  49. package/nitrogen/generated/android/c++/JFirmwareArtifactReaderCloseParams.hpp +0 -57
  50. package/nitrogen/generated/android/c++/JFirmwareArtifactReaderInfo.hpp +0 -61
  51. package/nitrogen/generated/android/c++/JFirmwareArtifactReaderReadParams.hpp +0 -65
  52. package/nitrogen/generated/android/c++/JFirmwareArtifactReceipt.hpp +0 -65
  53. package/nitrogen/generated/android/c++/JFirmwareArtifactRefParams.hpp +0 -57
  54. package/nitrogen/generated/android/c++/JFirmwareArtifactSweepResult.hpp +0 -61
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveExpectedEntry.kt +0 -47
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeParams.kt +0 -44
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeResult.kt +0 -38
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializedArtifact.kt +0 -41
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactCancelParams.kt +0 -38
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactCapabilities.kt +0 -47
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDownloadParams.kt +0 -68
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLease.kt +0 -38
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseCreateParams.kt +0 -38
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseReleaseParams.kt +0 -41
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseRetainParams.kt +0 -41
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderCloseParams.kt +0 -38
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderInfo.kt +0 -41
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderReadParams.kt +0 -44
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReceipt.kt +0 -44
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactRefParams.kt +0 -38
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactSweepResult.kt +0 -41
  72. package/nitrogen/generated/ios/swift/FirmwareArchiveExpectedEntry.swift +0 -69
  73. package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeParams.swift +0 -70
  74. package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeResult.swift +0 -48
  75. package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializedArtifact.swift +0 -47
  76. package/nitrogen/generated/ios/swift/FirmwareArtifactCancelParams.swift +0 -36
  77. package/nitrogen/generated/ios/swift/FirmwareArtifactCapabilities.swift +0 -81
  78. package/nitrogen/generated/ios/swift/FirmwareArtifactDownloadParams.swift +0 -177
  79. package/nitrogen/generated/ios/swift/FirmwareArtifactLease.swift +0 -36
  80. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseCreateParams.swift +0 -36
  81. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseReleaseParams.swift +0 -47
  82. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseRetainParams.swift +0 -47
  83. package/nitrogen/generated/ios/swift/FirmwareArtifactReaderCloseParams.swift +0 -36
  84. package/nitrogen/generated/ios/swift/FirmwareArtifactReaderInfo.swift +0 -47
  85. package/nitrogen/generated/ios/swift/FirmwareArtifactReaderReadParams.swift +0 -58
  86. package/nitrogen/generated/ios/swift/FirmwareArtifactReceipt.swift +0 -58
  87. package/nitrogen/generated/ios/swift/FirmwareArtifactRefParams.swift +0 -36
  88. package/nitrogen/generated/ios/swift/FirmwareArtifactSweepResult.swift +0 -47
  89. package/nitrogen/generated/ios/swift/Func_void_FirmwareArchiveMaterializeResult.swift +0 -47
  90. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactLease.swift +0 -47
  91. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactReaderInfo.swift +0 -47
  92. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactReceipt.swift +0 -47
  93. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactSweepResult.swift +0 -47
  94. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +0 -47
  95. package/nitrogen/generated/shared/c++/FirmwareArchiveExpectedEntry.hpp +0 -87
  96. package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeParams.hpp +0 -86
  97. package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeResult.hpp +0 -77
  98. package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializedArtifact.hpp +0 -81
  99. package/nitrogen/generated/shared/c++/FirmwareArtifactCancelParams.hpp +0 -75
  100. package/nitrogen/generated/shared/c++/FirmwareArtifactCapabilities.hpp +0 -88
  101. package/nitrogen/generated/shared/c++/FirmwareArtifactDownloadParams.hpp +0 -116
  102. package/nitrogen/generated/shared/c++/FirmwareArtifactLease.hpp +0 -75
  103. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseCreateParams.hpp +0 -75
  104. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseReleaseParams.hpp +0 -79
  105. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseRetainParams.hpp +0 -79
  106. package/nitrogen/generated/shared/c++/FirmwareArtifactReaderCloseParams.hpp +0 -75
  107. package/nitrogen/generated/shared/c++/FirmwareArtifactReaderInfo.hpp +0 -79
  108. package/nitrogen/generated/shared/c++/FirmwareArtifactReaderReadParams.hpp +0 -83
  109. package/nitrogen/generated/shared/c++/FirmwareArtifactReceipt.hpp +0 -83
  110. package/nitrogen/generated/shared/c++/FirmwareArtifactRefParams.hpp +0 -75
  111. package/nitrogen/generated/shared/c++/FirmwareArtifactSweepResult.hpp +0 -79
@@ -1,1445 +0,0 @@
1
- import CryptoKit
2
- import Foundation
3
- import ReactNativeNativeLogger
4
- import SniConnect
5
-
6
- func isFirmwareArtifactTLSError(_ error: Error) -> Bool {
7
- var current: NSError? = error as NSError
8
- var visited = Set<ObjectIdentifier>()
9
- while let candidate = current {
10
- let identifier = ObjectIdentifier(candidate)
11
- guard visited.insert(identifier).inserted else {
12
- break
13
- }
14
- if candidate.domain == NSURLErrorDomain,
15
- [
16
- NSURLErrorSecureConnectionFailed,
17
- NSURLErrorServerCertificateHasBadDate,
18
- NSURLErrorServerCertificateUntrusted,
19
- NSURLErrorServerCertificateHasUnknownRoot,
20
- NSURLErrorServerCertificateNotYetValid,
21
- NSURLErrorClientCertificateRejected,
22
- NSURLErrorClientCertificateRequired,
23
- NSURLErrorAppTransportSecurityRequiresSecureConnection,
24
- ].contains(candidate.code)
25
- {
26
- return true
27
- }
28
- current = candidate.userInfo[NSUnderlyingErrorKey] as? NSError
29
- }
30
- return false
31
- }
32
-
33
- enum FirmwareArtifactStoreError: Error {
34
- case invalidInput(String)
35
- case downloadFailed(String)
36
- case integrityMismatch(String)
37
- case readerInvalid(String)
38
- case archiveInvalid(String)
39
- }
40
-
41
- struct StoredFirmwareArtifact: Sendable {
42
- let artifactRef: String
43
- let size: Int64
44
- let sha256: String
45
- let fileURL: URL
46
- }
47
-
48
- struct StoredFirmwareArchiveEntry {
49
- let entryName: String
50
- let artifact: StoredFirmwareArtifact
51
- }
52
-
53
- private struct StagedFirmwareArchiveEntry {
54
- let entryName: String
55
- let size: Int64
56
- let sha256: String
57
- let stagingURL: URL
58
- }
59
-
60
- private final class FirmwareArtifactStreamDelegate: NSObject, URLSessionDataDelegate {
61
- private let partialURL: URL
62
- private let hostname: String
63
- private let resumeOffset: Int64
64
- private let expectedSize: Int64
65
- private let maxBytes: Int64
66
- private let isCancelled: () -> Bool
67
- private let stateLock = NSLock()
68
- private var continuation: CheckedContinuation<Void, Error>?
69
- private var dataTask: URLSessionDataTask?
70
- private var cancellationRequested = false
71
- private var completed = false
72
- private var responseAccepted = false
73
- private var handle: FileHandle?
74
- private var written: Int64 = 0
75
-
76
- init(
77
- partialURL: URL,
78
- hostname: String,
79
- resumeOffset: Int64,
80
- expectedSize: Int64,
81
- maxBytes: Int64,
82
- isCancelled: @escaping () -> Bool
83
- ) {
84
- self.partialURL = partialURL
85
- self.hostname = hostname
86
- self.resumeOffset = resumeOffset
87
- self.expectedSize = expectedSize
88
- self.maxBytes = maxBytes
89
- self.isCancelled = isCancelled
90
- }
91
-
92
- func run(session: URLSession, request: URLRequest) async throws {
93
- try await withTaskCancellationHandler {
94
- try await withCheckedThrowingContinuation {
95
- (continuation: CheckedContinuation<Void, Error>) in
96
- let task = session.dataTask(with: request)
97
- stateLock.lock()
98
- if cancellationRequested || completed {
99
- stateLock.unlock()
100
- task.cancel()
101
- continuation.resume(throwing: CancellationError())
102
- return
103
- }
104
- self.continuation = continuation
105
- dataTask = task
106
- stateLock.unlock()
107
- task.resume()
108
- }
109
- } onCancel: {
110
- self.cancel()
111
- }
112
- }
113
-
114
- private func cancel() {
115
- stateLock.lock()
116
- cancellationRequested = true
117
- let task = dataTask
118
- stateLock.unlock()
119
- task?.cancel()
120
- }
121
-
122
- private func finish(_ result: Result<Void, Error>) {
123
- stateLock.lock()
124
- guard !completed else {
125
- stateLock.unlock()
126
- return
127
- }
128
- completed = true
129
- let continuation = continuation
130
- self.continuation = nil
131
- dataTask = nil
132
- let handle = handle
133
- self.handle = nil
134
- stateLock.unlock()
135
-
136
- try? handle?.close()
137
- continuation?.resume(with: result)
138
- }
139
-
140
- private func reject(
141
- _ dataTask: URLSessionDataTask,
142
- completionHandler: @escaping (URLSession.ResponseDisposition) -> Void,
143
- message: String
144
- ) {
145
- completionHandler(.cancel)
146
- fail(dataTask, message: message)
147
- }
148
-
149
- private func fail(_ task: URLSessionTask, message: String) {
150
- task.cancel()
151
- finish(.failure(FirmwareArtifactStoreError.downloadFailed(message)))
152
- }
153
-
154
- func urlSession(
155
- _ session: URLSession,
156
- task: URLSessionTask,
157
- willPerformHTTPRedirection response: HTTPURLResponse,
158
- newRequest request: URLRequest,
159
- completionHandler: @escaping (URLRequest?) -> Void
160
- ) {
161
- completionHandler(nil)
162
- fail(
163
- task,
164
- message:
165
- "ARTIFACT_REDIRECT_REJECTED: firmware response changed canonical identity"
166
- )
167
- }
168
-
169
- func urlSession(
170
- _ session: URLSession,
171
- dataTask: URLSessionDataTask,
172
- didReceive response: URLResponse,
173
- completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
174
- ) {
175
- guard !isCancelled() else {
176
- reject(
177
- dataTask,
178
- completionHandler: completionHandler,
179
- message: "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
180
- )
181
- return
182
- }
183
- guard let httpResponse = response as? HTTPURLResponse else {
184
- reject(
185
- dataTask,
186
- completionHandler: completionHandler,
187
- message: "Firmware response is not HTTP"
188
- )
189
- return
190
- }
191
- guard
192
- let responseURL = httpResponse.url,
193
- responseURL.scheme?.lowercased() == "https",
194
- responseURL.host?.lowercased() == hostname.lowercased(),
195
- responseURL.port == nil || responseURL.port == 443
196
- else {
197
- reject(
198
- dataTask,
199
- completionHandler: completionHandler,
200
- message:
201
- "ARTIFACT_REDIRECT_REJECTED: firmware response changed canonical identity"
202
- )
203
- return
204
- }
205
- guard httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else {
206
- reject(
207
- dataTask,
208
- completionHandler: completionHandler,
209
- message:
210
- "ARTIFACT_HTTP_\(httpResponse.statusCode): firmware request failed"
211
- )
212
- return
213
- }
214
-
215
- let append = resumeOffset > 0 && httpResponse.statusCode == 206
216
- if httpResponse.statusCode == 206 {
217
- guard
218
- let contentRange = httpResponse.value(
219
- forHTTPHeaderField: "Content-Range"
220
- ),
221
- firmwareArtifactContentRangeIsValid(
222
- contentRange,
223
- expectedStart: append ? resumeOffset : 0,
224
- expectedTotal: expectedSize
225
- )
226
- else {
227
- reject(
228
- dataTask,
229
- completionHandler: completionHandler,
230
- message:
231
- "ARTIFACT_PROTOCOL_INVALID: firmware resume Content-Range is invalid"
232
- )
233
- return
234
- }
235
- }
236
-
237
- let baseOffset = append ? resumeOffset : 0
238
- guard
239
- firmwareArtifactResponseFits(
240
- expectedContentLength: httpResponse.expectedContentLength,
241
- baseOffset: baseOffset,
242
- maxBytes: maxBytes
243
- )
244
- else {
245
- reject(
246
- dataTask,
247
- completionHandler: completionHandler,
248
- message:
249
- "ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
250
- )
251
- return
252
- }
253
-
254
- do {
255
- let handle = try FileHandle(forWritingTo: partialURL)
256
- if append {
257
- try handle.seekToEnd()
258
- } else {
259
- try handle.truncate(atOffset: 0)
260
- }
261
- self.handle = handle
262
- written = baseOffset
263
- responseAccepted = true
264
- completionHandler(.allow)
265
- } catch {
266
- reject(
267
- dataTask,
268
- completionHandler: completionHandler,
269
- message:
270
- "ARTIFACT_NETWORK_FAILED: firmware partial file could not be opened"
271
- )
272
- }
273
- }
274
-
275
- func urlSession(
276
- _ session: URLSession,
277
- dataTask: URLSessionDataTask,
278
- didReceive data: Data
279
- ) {
280
- guard responseAccepted, let handle else {
281
- fail(
282
- dataTask,
283
- message:
284
- "ARTIFACT_PROTOCOL_INVALID: firmware response stream was not accepted"
285
- )
286
- return
287
- }
288
- guard !isCancelled() else {
289
- fail(
290
- dataTask,
291
- message: "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
292
- )
293
- return
294
- }
295
- guard
296
- firmwareArtifactResponseFits(
297
- expectedContentLength: Int64(data.count),
298
- baseOffset: written,
299
- maxBytes: maxBytes
300
- )
301
- else {
302
- fail(
303
- dataTask,
304
- message:
305
- "ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
306
- )
307
- return
308
- }
309
- do {
310
- try handle.write(contentsOf: data)
311
- written += Int64(data.count)
312
- } catch {
313
- fail(
314
- dataTask,
315
- message:
316
- "ARTIFACT_NETWORK_FAILED: firmware response stream could not be persisted"
317
- )
318
- }
319
- }
320
-
321
- func urlSession(
322
- _ session: URLSession,
323
- task: URLSessionTask,
324
- didCompleteWithError error: Error?
325
- ) {
326
- if let error {
327
- finish(.failure(error))
328
- return
329
- }
330
- guard responseAccepted, let handle else {
331
- finish(
332
- .failure(FirmwareArtifactStoreError.downloadFailed(
333
- "ARTIFACT_PROTOCOL_INVALID: firmware response stream was not accepted"
334
- ))
335
- )
336
- return
337
- }
338
- do {
339
- try handle.synchronize()
340
- finish(.success(()))
341
- } catch {
342
- finish(
343
- .failure(FirmwareArtifactStoreError.downloadFailed(
344
- "ARTIFACT_NETWORK_FAILED: firmware partial file could not be synchronized"
345
- ))
346
- )
347
- }
348
- }
349
- }
350
-
351
- private actor FirmwareArtifactDownloadCoordinator {
352
- private struct ActiveDownload {
353
- let transactionId: String
354
- let task: Task<StoredFirmwareArtifact, Error>
355
- }
356
-
357
- private var downloads: [String: ActiveDownload] = [:]
358
-
359
- func run(
360
- key: String,
361
- transactionId: String,
362
- operation: @escaping () async throws -> StoredFirmwareArtifact
363
- ) async throws -> StoredFirmwareArtifact {
364
- if let download = downloads[key] {
365
- return try await download.task.value
366
- }
367
- let task = Task {
368
- try await operation()
369
- }
370
- downloads[key] = ActiveDownload(
371
- transactionId: transactionId,
372
- task: task
373
- )
374
- defer { downloads[key] = nil }
375
- return try await task.value
376
- }
377
-
378
- func cancel(transactionId: String) {
379
- for download in downloads.values
380
- where download.transactionId == transactionId {
381
- download.task.cancel()
382
- }
383
- }
384
- }
385
-
386
- final class FirmwareArtifactStore {
387
- static let shared = FirmwareArtifactStore()
388
- static let maxReadBytes = 256 * 1024
389
- private static let defaultDownloadDeadline: TimeInterval = 180
390
- private static let maxDownloadDeadline: TimeInterval = 24 * 60 * 60
391
- private static let finalArtifactGrace: TimeInterval = 24 * 60 * 60
392
- private static let partialArtifactGrace: TimeInterval = 7 * 24 * 60 * 60
393
-
394
- private struct OpenReader {
395
- let handle: FileHandle
396
- let fileURL: URL
397
- let size: Int64
398
- }
399
-
400
- private struct LeaseState {
401
- let transactionId: String
402
- var artifactRefs: Set<String>
403
- }
404
-
405
- private let fileManager = FileManager.default
406
- private let downloadCoordinator = FirmwareArtifactDownloadCoordinator()
407
- private let leaseLock = NSLock()
408
- private let activeDownloadLock = NSLock()
409
- private var activeDownloadCounts: [String: Int] = [:]
410
- private let cancellationLock = NSLock()
411
- private var cancelledTransactions: Set<String> = []
412
- private let readerLock = NSLock()
413
- private var readers: [String: OpenReader] = [:]
414
- private var leases: [String: LeaseState] = [:]
415
-
416
- private lazy var rootURL: URL = {
417
- let base = fileManager.urls(
418
- for: .applicationSupportDirectory,
419
- in: .userDomainMask
420
- ).first!
421
- let root = base.appendingPathComponent("onekey-firmware-artifacts", isDirectory: true)
422
- try? fileManager.createDirectory(at: root, withIntermediateDirectories: true)
423
- return root
424
- }()
425
-
426
- private init() {}
427
-
428
- static func validateDownloadParams(_ params: FirmwareArtifactDownloadParams) throws {
429
- guard
430
- !params.taskId.isEmpty,
431
- params.taskId.count <= 100,
432
- params.taskId.allSatisfy({ $0.isLetter || $0.isNumber || "._-".contains($0) })
433
- else {
434
- throw FirmwareArtifactStoreError.invalidInput("Invalid firmware taskId")
435
- }
436
- guard
437
- isSafeIdentifier(params.transactionId),
438
- isSafeIdentifier(params.artifactId),
439
- isValidLeaseRef(params.leaseRef)
440
- else {
441
- throw FirmwareArtifactStoreError.invalidInput(
442
- "Invalid firmware transaction or lease identity"
443
- )
444
- }
445
- guard
446
- let url = URL(string: params.url),
447
- url.scheme?.lowercased() == "https",
448
- url.host?.isEmpty == false,
449
- url.user == nil,
450
- url.password == nil,
451
- url.fragment == nil,
452
- url.port == nil || url.port == 443
453
- else {
454
- throw FirmwareArtifactStoreError.invalidInput("Firmware URL must use HTTPS port 443")
455
- }
456
- guard
457
- params.expectedSize.isFinite,
458
- params.expectedSize > 0,
459
- params.expectedSize <= Double(Int64.max),
460
- params.expectedSize.rounded() == params.expectedSize,
461
- params.maxBytes.isFinite,
462
- params.maxBytes == params.expectedSize,
463
- params.maxBytes <= Double(512 * 1024 * 1024)
464
- else {
465
- throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifact size")
466
- }
467
- guard params.expectedSha256.range(
468
- of: "^[a-fA-F0-9]{64}$",
469
- options: .regularExpression
470
- ) != nil else {
471
- throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifact SHA-256")
472
- }
473
- guard params.routeType == "domain" || params.routeType == "pinnedIp" else {
474
- throw FirmwareArtifactStoreError.invalidInput("Invalid firmware route type")
475
- }
476
- let deadline = params.overallDeadlineSeconds ?? defaultDownloadDeadline
477
- guard
478
- deadline.isFinite,
479
- deadline > 0,
480
- deadline <= maxDownloadDeadline
481
- else {
482
- throw FirmwareArtifactStoreError.invalidInput("Invalid firmware download deadline")
483
- }
484
- if params.routeType == "pinnedIp" {
485
- guard let resolvedIp = params.resolvedIp, !resolvedIp.isEmpty else {
486
- throw FirmwareArtifactStoreError.invalidInput("Pinned route requires resolvedIp")
487
- }
488
- } else if params.resolvedIp != nil {
489
- throw FirmwareArtifactStoreError.invalidInput("Domain route must not include resolvedIp")
490
- }
491
- }
492
-
493
- func download(_ params: FirmwareArtifactDownloadParams) async throws -> StoredFirmwareArtifact {
494
- try Self.validateDownloadParams(params)
495
- try rejectIfCancelled(transactionId: params.transactionId)
496
- let expectedSha256 = params.expectedSha256.lowercased()
497
- try retainExpectedArtifact(
498
- leaseRef: params.leaseRef,
499
- transactionId: params.transactionId,
500
- artifactRef: "fw:\(expectedSha256)"
501
- )
502
- let key = firmwareArtifactDownloadKey(
503
- transactionId: params.transactionId,
504
- expectedSize: Int64(params.expectedSize),
505
- expectedSha256: expectedSha256
506
- )
507
- markDownloadActive(expectedSha256, delta: 1)
508
- do {
509
- let artifact = try await downloadCoordinator.run(
510
- key: key,
511
- transactionId: params.transactionId
512
- ) { [self] in
513
- try rejectIfCancelled(transactionId: params.transactionId)
514
- return try await downloadLocked(
515
- params,
516
- expectedSha256: expectedSha256
517
- )
518
- }
519
- try rejectIfCancelled(transactionId: params.transactionId)
520
- markDownloadActive(expectedSha256, delta: -1)
521
- return artifact
522
- } catch {
523
- markDownloadActive(expectedSha256, delta: -1)
524
- OneKeyLog.error(
525
- "FirmwareArtifact",
526
- "event=download_failed transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) errorType=\(String(describing: type(of: error)))"
527
- )
528
- if error is CancellationError ||
529
- isTransactionCancelled(params.transactionId) {
530
- throw FirmwareArtifactStoreError.downloadFailed(
531
- "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
532
- )
533
- }
534
- throw error
535
- }
536
- }
537
-
538
- func cachedArtifact(
539
- _ params: FirmwareArtifactDownloadParams
540
- ) throws -> StoredFirmwareArtifact? {
541
- try Self.validateDownloadParams(params)
542
- try rejectIfCancelled(transactionId: params.transactionId)
543
- let expectedSha256 = params.expectedSha256.lowercased()
544
- let finalURL = artifactURL(sha256: expectedSha256)
545
- guard let artifact = try? validateStoredArtifact(
546
- fileURL: finalURL,
547
- expectedSize: Int64(params.expectedSize),
548
- expectedSha256: expectedSha256
549
- ) else {
550
- return nil
551
- }
552
- try retainExpectedArtifact(
553
- leaseRef: params.leaseRef,
554
- transactionId: params.transactionId,
555
- artifactRef: artifact.artifactRef
556
- )
557
- try rejectIfCancelled(transactionId: params.transactionId)
558
- return artifact
559
- }
560
-
561
- func cancelDownloads(transactionId: String) async throws {
562
- guard Self.isSafeIdentifier(transactionId) else {
563
- throw FirmwareArtifactStoreError.invalidInput(
564
- "Invalid firmware transactionId"
565
- )
566
- }
567
- cancellationLock.withFirmwareArtifactLock {
568
- cancelledTransactions.insert(transactionId)
569
- }
570
- await downloadCoordinator.cancel(transactionId: transactionId)
571
- try await RangeDownloader.shared.cancelFirmwareArtifactDownloads(
572
- transactionId: transactionId
573
- )
574
- }
575
-
576
- private func downloadLocked(
577
- _ params: FirmwareArtifactDownloadParams,
578
- expectedSha256: String
579
- ) async throws -> StoredFirmwareArtifact {
580
- let finalURL = artifactURL(sha256: expectedSha256)
581
- if let existing = try? validateStoredArtifact(
582
- fileURL: finalURL,
583
- expectedSize: Int64(params.expectedSize),
584
- expectedSha256: expectedSha256
585
- ) {
586
- return existing
587
- }
588
-
589
- // Firmware preflight is foreground-bound, so both routes use the same
590
- // cancellable stream instead of waiting on process-owned background tasks.
591
- let partialURL = rootURL.appendingPathComponent(
592
- firmwareArtifactPartialFileName(
593
- transactionId: params.transactionId,
594
- taskId: params.taskId,
595
- expectedSha256: expectedSha256
596
- ),
597
- isDirectory: false
598
- )
599
- if !fileManager.fileExists(atPath: partialURL.path) {
600
- fileManager.createFile(atPath: partialURL.path, contents: nil)
601
- }
602
- var currentSize = try fileSize(partialURL)
603
- if currentSize > Int64(params.expectedSize) {
604
- try fileManager.removeItem(at: partialURL)
605
- fileManager.createFile(atPath: partialURL.path, contents: nil)
606
- currentSize = 0
607
- }
608
- if currentSize == Int64(params.expectedSize) {
609
- if let completed = try? validateStoredArtifact(
610
- fileURL: partialURL,
611
- expectedSize: Int64(params.expectedSize),
612
- expectedSha256: expectedSha256
613
- ) {
614
- try promote(source: partialURL, destination: finalURL)
615
- return StoredFirmwareArtifact(
616
- artifactRef: completed.artifactRef,
617
- size: completed.size,
618
- sha256: completed.sha256,
619
- fileURL: finalURL
620
- )
621
- }
622
- try fileManager.removeItem(at: partialURL)
623
- fileManager.createFile(atPath: partialURL.path, contents: nil)
624
- currentSize = 0
625
- }
626
-
627
- OneKeyLog.info(
628
- "FirmwareArtifact",
629
- "event=stream_start transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) expectedBytes=\(Int64(params.expectedSize)) resumeBytes=\(currentSize)"
630
- )
631
- try await streamDownload(
632
- params,
633
- partialURL: partialURL,
634
- resumeOffset: min(currentSize, Int64(params.expectedSize))
635
- )
636
- OneKeyLog.info(
637
- "FirmwareArtifact",
638
- "event=stream_complete transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) expectedBytes=\(Int64(params.expectedSize))"
639
- )
640
- let artifact: StoredFirmwareArtifact
641
- do {
642
- artifact = try validateStoredArtifact(
643
- fileURL: partialURL,
644
- expectedSize: Int64(params.expectedSize),
645
- expectedSha256: expectedSha256
646
- )
647
- } catch {
648
- try? fileManager.removeItem(at: partialURL)
649
- throw error
650
- }
651
- try promote(source: partialURL, destination: finalURL)
652
- return StoredFirmwareArtifact(
653
- artifactRef: artifact.artifactRef,
654
- size: artifact.size,
655
- sha256: artifact.sha256,
656
- fileURL: finalURL
657
- )
658
- }
659
-
660
- func acceptBackgroundDownload(
661
- temporaryURL: URL,
662
- transactionId: String,
663
- expectedSize: Int64,
664
- expectedSha256: String
665
- ) throws -> StoredFirmwareArtifact {
666
- let normalizedExpectedSha256 = expectedSha256.lowercased()
667
- try rejectIfCancelled(transactionId: transactionId)
668
- let finalURL = artifactURL(sha256: normalizedExpectedSha256)
669
- if let existing = try? validateStoredArtifact(
670
- fileURL: finalURL,
671
- expectedSize: expectedSize,
672
- expectedSha256: normalizedExpectedSha256
673
- ) {
674
- return existing
675
- }
676
- let artifact = try validateStoredArtifact(
677
- fileURL: temporaryURL,
678
- expectedSize: expectedSize,
679
- expectedSha256: normalizedExpectedSha256
680
- )
681
- try promote(source: temporaryURL, destination: finalURL)
682
- return StoredFirmwareArtifact(
683
- artifactRef: artifact.artifactRef,
684
- size: artifact.size,
685
- sha256: artifact.sha256,
686
- fileURL: finalURL
687
- )
688
- }
689
-
690
- func storedArtifact(
691
- expectedSize: Int64,
692
- expectedSha256: String
693
- ) throws -> StoredFirmwareArtifact? {
694
- let finalURL = artifactURL(sha256: expectedSha256)
695
- guard fileManager.fileExists(atPath: finalURL.path) else {
696
- return nil
697
- }
698
- return try validateStoredArtifact(
699
- fileURL: finalURL,
700
- expectedSize: expectedSize,
701
- expectedSha256: expectedSha256
702
- )
703
- }
704
-
705
- func discard(artifactRef: String) throws {
706
- let fileURL = try resolveArtifactURL(artifactRef)
707
- leaseLock.lock()
708
- let isRetained = leases.values.contains {
709
- $0.artifactRefs.contains(artifactRef)
710
- }
711
- leaseLock.unlock()
712
- guard !isRetained else {
713
- throw FirmwareArtifactStoreError.invalidInput(
714
- "ARTIFACT_LEASED: firmware artifact is retained"
715
- )
716
- }
717
- readerLock.lock()
718
- let hasMatchingReader = readers.values.contains {
719
- $0.fileURL == fileURL
720
- }
721
- readerLock.unlock()
722
- guard !hasMatchingReader else {
723
- throw FirmwareArtifactStoreError.readerInvalid(
724
- "ARTIFACT_BUSY: firmware artifact has an open reader"
725
- )
726
- }
727
- if fileManager.fileExists(atPath: fileURL.path) {
728
- try fileManager.removeItem(at: fileURL)
729
- }
730
- }
731
-
732
- func open(artifactRef: String) throws -> (readerId: String, size: Int64) {
733
- let fileURL = try resolveArtifactURL(artifactRef)
734
- let size = try fileSize(fileURL)
735
- guard size > 0 else {
736
- throw FirmwareArtifactStoreError.readerInvalid("Firmware artifact is empty")
737
- }
738
- let expectedSha256 = String(artifactRef.dropFirst(3))
739
- guard try hashFile(fileURL) == expectedSha256 else {
740
- throw FirmwareArtifactStoreError.integrityMismatch(
741
- "Firmware artifact SHA-256 mismatch"
742
- )
743
- }
744
- let readerId = UUID().uuidString
745
- let reader = OpenReader(
746
- handle: try FileHandle(forReadingFrom: fileURL),
747
- fileURL: fileURL,
748
- size: size
749
- )
750
- readerLock.lock()
751
- readers[readerId] = reader
752
- readerLock.unlock()
753
- return (readerId, size)
754
- }
755
-
756
- func read(readerId: String, offset: Int64, length: Int) throws -> Data {
757
- guard length > 0, length <= Self.maxReadBytes, offset >= 0 else {
758
- throw FirmwareArtifactStoreError.readerInvalid("Invalid firmware artifact read")
759
- }
760
- readerLock.lock()
761
- defer { readerLock.unlock() }
762
- guard
763
- let reader = readers[readerId],
764
- offset <= reader.size - Int64(length)
765
- else {
766
- throw FirmwareArtifactStoreError.readerInvalid("Firmware artifact read is out of bounds")
767
- }
768
- try reader.handle.seek(toOffset: UInt64(offset))
769
- let data = try reader.handle.read(upToCount: length) ?? Data()
770
- guard data.count == length else {
771
- throw FirmwareArtifactStoreError.readerInvalid("Firmware artifact returned a short read")
772
- }
773
- return data
774
- }
775
-
776
- func close(readerId: String) throws {
777
- readerLock.lock()
778
- let reader = readers.removeValue(forKey: readerId)
779
- readerLock.unlock()
780
- guard let reader else {
781
- return
782
- }
783
- try reader.handle.close()
784
- }
785
-
786
- func materializeArchive(
787
- leaseRef: String,
788
- artifactRef: String,
789
- expectedEntries: [FirmwareArchiveExpectedEntry]
790
- ) throws -> [StoredFirmwareArchiveEntry] {
791
- try requireLease(leaseRef)
792
- let archiveURL = try resolveArtifactURL(artifactRef)
793
- let scratchURL = rootURL.appendingPathComponent(
794
- "archive-\(UUID().uuidString)",
795
- isDirectory: true
796
- )
797
- try fileManager.createDirectory(at: scratchURL, withIntermediateDirectories: true)
798
- defer { try? fileManager.removeItem(at: scratchURL) }
799
-
800
- let requirements = try validateArchiveRequirements(expectedEntries)
801
- let archiveEntries = try FirmwareArchiveMinizipBridge.scanArchive(
802
- atPath: archiveURL.path
803
- )
804
- try validateArchiveEntries(
805
- archiveEntries,
806
- requirements: requirements
807
- )
808
-
809
- var staged: [StagedFirmwareArchiveEntry] = []
810
- for (index, requirement) in requirements.enumerated() {
811
- let stagingURL = scratchURL.appendingPathComponent(
812
- "\(index).entry",
813
- isDirectory: false
814
- )
815
- fileManager.createFile(atPath: stagingURL.path, contents: nil)
816
- let handle = try FileHandle(forWritingTo: stagingURL)
817
- var hasher = SHA256()
818
- var actualSize: Int64 = 0
819
- var consumerError: Error?
820
- do {
821
- try FirmwareArchiveMinizipBridge.extractEntryNamed(
822
- requirement.entryName,
823
- archivePath: archiveURL.path
824
- ) { chunk in
825
- guard consumerError == nil else {
826
- return false
827
- }
828
- do {
829
- actualSize += Int64(chunk.count)
830
- guard actualSize <= Int64(requirement.expectedSize) else {
831
- throw FirmwareArtifactStoreError.archiveInvalid(
832
- "Firmware archive entry exceeds its expected size"
833
- )
834
- }
835
- hasher.update(data: chunk)
836
- try handle.write(contentsOf: chunk)
837
- return true
838
- } catch {
839
- consumerError = error
840
- return false
841
- }
842
- }
843
- if let consumerError {
844
- throw consumerError
845
- }
846
- try handle.synchronize()
847
- try handle.close()
848
- } catch {
849
- try? handle.close()
850
- throw error
851
- }
852
- let sha256 = hasher.finalize().map {
853
- String(format: "%02x", $0)
854
- }.joined()
855
- guard
856
- actualSize == Int64(requirement.expectedSize),
857
- sha256 == requirement.expectedSha256.lowercased()
858
- else {
859
- throw FirmwareArtifactStoreError.archiveInvalid(
860
- "Firmware archive entry integrity mismatch"
861
- )
862
- }
863
- staged.append(
864
- StagedFirmwareArchiveEntry(
865
- entryName: requirement.entryName,
866
- size: actualSize,
867
- sha256: sha256,
868
- stagingURL: stagingURL
869
- )
870
- )
871
- }
872
-
873
- return try staged.map { entry in
874
- let destination = artifactURL(sha256: entry.sha256)
875
- if (try? validateStoredArtifact(
876
- fileURL: destination,
877
- expectedSize: entry.size,
878
- expectedSha256: entry.sha256
879
- )) == nil {
880
- try promote(source: entry.stagingURL, destination: destination)
881
- }
882
- let stored = StoredFirmwareArtifact(
883
- artifactRef: "fw:\(entry.sha256)",
884
- size: entry.size,
885
- sha256: entry.sha256,
886
- fileURL: destination
887
- )
888
- try retainExpectedArtifact(
889
- leaseRef: leaseRef,
890
- transactionId: nil,
891
- artifactRef: stored.artifactRef
892
- )
893
- return StoredFirmwareArchiveEntry(
894
- entryName: entry.entryName,
895
- artifact: stored
896
- )
897
- }
898
- }
899
-
900
- func createLease(transactionId: String) throws -> String {
901
- guard Self.isSafeIdentifier(transactionId) else {
902
- throw FirmwareArtifactStoreError.invalidInput(
903
- "Invalid firmware transactionId"
904
- )
905
- }
906
- leaseLock.lock()
907
- defer { leaseLock.unlock() }
908
- guard leases.count < 32 else {
909
- throw FirmwareArtifactStoreError.invalidInput(
910
- "Too many firmware artifact leases"
911
- )
912
- }
913
- let leaseRef = "fwlease:\(UUID().uuidString.lowercased())"
914
- leases[leaseRef] = LeaseState(
915
- transactionId: transactionId,
916
- artifactRefs: []
917
- )
918
- return leaseRef
919
- }
920
-
921
- func retain(leaseRef: String, artifactRef: String) throws {
922
- _ = try resolveArtifactURL(artifactRef)
923
- try retainExpectedArtifact(
924
- leaseRef: leaseRef,
925
- transactionId: nil,
926
- artifactRef: artifactRef
927
- )
928
- }
929
-
930
- func releaseLease(leaseRef: String, disposition: String) throws {
931
- guard
932
- disposition == "completed" ||
933
- disposition == "safeCancelled" ||
934
- disposition == "safeAbandoned"
935
- else {
936
- throw FirmwareArtifactStoreError.invalidInput(
937
- "Invalid firmware lease disposition"
938
- )
939
- }
940
- let transactionId: String = try {
941
- leaseLock.lock()
942
- defer { leaseLock.unlock() }
943
- guard
944
- let lease = leases.removeValue(
945
- forKey: try validateLeaseRef(leaseRef)
946
- )
947
- else {
948
- throw FirmwareArtifactStoreError.invalidInput(
949
- "Firmware artifact lease is unavailable"
950
- )
951
- }
952
- return lease.transactionId
953
- }()
954
- cancellationLock.withFirmwareArtifactLock {
955
- cancelledTransactions.remove(transactionId)
956
- }
957
- }
958
-
959
- func sweepOrphans() throws -> (deletedFiles: Int, deletedBytes: Int64) {
960
- leaseLock.lock()
961
- let retained = Set(
962
- leases.values
963
- .flatMap(\.artifactRefs)
964
- .map { String($0.dropFirst(3)) }
965
- )
966
- leaseLock.unlock()
967
- activeDownloadLock.lock()
968
- let active = Set(activeDownloadCounts.filter { $0.value > 0 }.keys)
969
- activeDownloadLock.unlock()
970
- readerLock.lock()
971
- let openPaths = Set(readers.values.map(\.fileURL.path))
972
- readerLock.unlock()
973
-
974
- let now = Date()
975
- var deletedFiles = 0
976
- var deletedBytes: Int64 = 0
977
- let files = try fileManager.contentsOfDirectory(
978
- at: rootURL,
979
- includingPropertiesForKeys: [.contentModificationDateKey, .fileSizeKey],
980
- options: [.skipsHiddenFiles]
981
- )
982
- for fileURL in files {
983
- let name = fileURL.lastPathComponent
984
- guard name.count >= 64 else { continue }
985
- let sha256 = String(name.prefix(64))
986
- guard
987
- sha256.range(of: "^[a-f0-9]{64}$", options: .regularExpression) != nil,
988
- !retained.contains(sha256),
989
- !active.contains(sha256),
990
- !openPaths.contains(fileURL.path)
991
- else {
992
- continue
993
- }
994
- let grace: TimeInterval
995
- if name.hasSuffix(".bin") {
996
- grace = Self.finalArtifactGrace
997
- } else if name.hasSuffix(".partial") {
998
- grace = Self.partialArtifactGrace
999
- } else {
1000
- continue
1001
- }
1002
- let values = try fileURL.resourceValues(
1003
- forKeys: [.contentModificationDateKey, .fileSizeKey]
1004
- )
1005
- guard
1006
- let modifiedAt = values.contentModificationDate,
1007
- now.timeIntervalSince(modifiedAt) >= grace
1008
- else {
1009
- continue
1010
- }
1011
- let size = Int64(values.fileSize ?? 0)
1012
- try fileManager.removeItem(at: fileURL)
1013
- deletedFiles += 1
1014
- deletedBytes += size
1015
- }
1016
- return (deletedFiles, deletedBytes)
1017
- }
1018
-
1019
- private func requireLease(_ leaseRef: String) throws {
1020
- leaseLock.lock()
1021
- defer { leaseLock.unlock() }
1022
- guard leases[try validateLeaseRef(leaseRef)] != nil else {
1023
- throw FirmwareArtifactStoreError.invalidInput(
1024
- "Firmware artifact lease is unavailable"
1025
- )
1026
- }
1027
- }
1028
-
1029
- private func retainExpectedArtifact(
1030
- leaseRef: String,
1031
- transactionId: String?,
1032
- artifactRef: String
1033
- ) throws {
1034
- guard artifactRef.range(
1035
- of: "^fw:[a-f0-9]{64}$",
1036
- options: .regularExpression
1037
- ) != nil else {
1038
- throw FirmwareArtifactStoreError.invalidInput(
1039
- "Invalid firmware artifactRef"
1040
- )
1041
- }
1042
- leaseLock.lock()
1043
- defer { leaseLock.unlock() }
1044
- let validatedLeaseRef = try validateLeaseRef(leaseRef)
1045
- guard var lease = leases[validatedLeaseRef] else {
1046
- throw FirmwareArtifactStoreError.invalidInput(
1047
- "Firmware artifact lease is unavailable"
1048
- )
1049
- }
1050
- if let transactionId, lease.transactionId != transactionId {
1051
- throw FirmwareArtifactStoreError.invalidInput(
1052
- "Firmware artifact lease transaction mismatch"
1053
- )
1054
- }
1055
- if lease.artifactRefs.insert(artifactRef).inserted {
1056
- leases[validatedLeaseRef] = lease
1057
- }
1058
- }
1059
-
1060
- private func validateLeaseRef(_ leaseRef: String) throws -> String {
1061
- guard Self.isValidLeaseRef(leaseRef) else {
1062
- throw FirmwareArtifactStoreError.invalidInput(
1063
- "Invalid firmware leaseRef"
1064
- )
1065
- }
1066
- return leaseRef
1067
- }
1068
-
1069
- private static func isValidLeaseRef(_ value: String) -> Bool {
1070
- value.range(
1071
- of: "^fwlease:[a-f0-9-]{36}$",
1072
- options: .regularExpression
1073
- ) != nil
1074
- }
1075
-
1076
- private static func isSafeIdentifier(_ value: String) -> Bool {
1077
- value.range(
1078
- of: "^[A-Za-z0-9._:-]{1,160}$",
1079
- options: .regularExpression
1080
- ) != nil
1081
- }
1082
-
1083
- private func markDownloadActive(_ sha256: String, delta: Int) {
1084
- activeDownloadLock.lock()
1085
- let next = (activeDownloadCounts[sha256] ?? 0) + delta
1086
- if next <= 0 {
1087
- activeDownloadCounts.removeValue(forKey: sha256)
1088
- } else {
1089
- activeDownloadCounts[sha256] = next
1090
- }
1091
- activeDownloadLock.unlock()
1092
- }
1093
-
1094
- private func validateArchiveRequirements(
1095
- _ expectedEntries: [FirmwareArchiveExpectedEntry]
1096
- ) throws -> [FirmwareArchiveExpectedEntry] {
1097
- guard !expectedEntries.isEmpty, expectedEntries.count <= 4096 else {
1098
- throw FirmwareArtifactStoreError.archiveInvalid(
1099
- "Firmware archive expected entry count is invalid"
1100
- )
1101
- }
1102
- var names = Set<String>()
1103
- var canonicalNames = Set<String>()
1104
- var totalSize: Int64 = 0
1105
- for entry in expectedEntries {
1106
- guard
1107
- !entry.artifactId.isEmpty,
1108
- entry.artifactId.count <= 160,
1109
- entry.artifactId.allSatisfy({
1110
- $0.isLetter || $0.isNumber || "._-".contains($0)
1111
- }),
1112
- isPortableArchiveEntryName(
1113
- entry.entryName,
1114
- canonicalNames: &canonicalNames
1115
- ),
1116
- names.insert(entry.entryName).inserted,
1117
- entry.expectedSize.isFinite,
1118
- entry.expectedSize > 0,
1119
- entry.expectedSize <= Double(128 * 1024 * 1024),
1120
- entry.expectedSize.rounded() == entry.expectedSize,
1121
- entry.expectedSha256.range(
1122
- of: "^[a-fA-F0-9]{64}$",
1123
- options: .regularExpression
1124
- ) != nil
1125
- else {
1126
- throw FirmwareArtifactStoreError.archiveInvalid(
1127
- "Firmware archive expected entry is invalid"
1128
- )
1129
- }
1130
- totalSize += Int64(entry.expectedSize)
1131
- guard totalSize <= 512 * 1024 * 1024 else {
1132
- throw FirmwareArtifactStoreError.archiveInvalid(
1133
- "Firmware archive expected entries exceed limits"
1134
- )
1135
- }
1136
- }
1137
- return expectedEntries
1138
- }
1139
-
1140
- private func validateArchiveEntries(
1141
- _ entries: [FirmwareArchiveEntryInfo],
1142
- requirements: [FirmwareArchiveExpectedEntry]
1143
- ) throws {
1144
- guard entries.count == requirements.count else {
1145
- throw FirmwareArtifactStoreError.archiveInvalid(
1146
- "Firmware archive has missing or extra entries"
1147
- )
1148
- }
1149
- let requirementsByName = Dictionary(
1150
- uniqueKeysWithValues: requirements.map { ($0.entryName, $0) }
1151
- )
1152
- var names = Set<String>()
1153
- var canonicalNames = Set<String>()
1154
- for entry in entries {
1155
- guard
1156
- names.insert(entry.name).inserted,
1157
- isPortableArchiveEntryName(
1158
- entry.name,
1159
- canonicalNames: &canonicalNames
1160
- ),
1161
- let requirement = requirementsByName[entry.name],
1162
- entry.uncompressedSize == Int64(requirement.expectedSize),
1163
- entry.uncompressedSize > 0,
1164
- entry.compressedSize >= 0,
1165
- entry.compressedSize <= 512 * 1024 * 1024,
1166
- entry.uncompressedSize <= max(entry.compressedSize, 1) * 1000,
1167
- entry.diskNumber == 0,
1168
- entry.flags & 1 == 0,
1169
- entry.compressionMethod == 0 || entry.compressionMethod == 8,
1170
- entry.linkName?.isEmpty != false,
1171
- isRegularArchiveEntry(entry)
1172
- else {
1173
- throw FirmwareArtifactStoreError.archiveInvalid(
1174
- "Firmware archive entry metadata is invalid"
1175
- )
1176
- }
1177
- }
1178
- }
1179
-
1180
- private func isPortableArchiveEntryName(
1181
- _ name: String,
1182
- canonicalNames: inout Set<String>
1183
- ) -> Bool {
1184
- let lowered = name.precomposedStringWithCanonicalMapping.lowercased()
1185
- let components = name.split(
1186
- separator: "/",
1187
- omittingEmptySubsequences: false
1188
- )
1189
- let nestedArchiveExtensions = [
1190
- ".zip", ".7z", ".rar", ".tar", ".gz", ".tgz",
1191
- ]
1192
- guard
1193
- !name.isEmpty,
1194
- name.count <= 512,
1195
- name == name.precomposedStringWithCanonicalMapping,
1196
- !name.hasPrefix("/"),
1197
- !name.hasPrefix("\\"),
1198
- !name.contains("\\"),
1199
- !name.contains(":"),
1200
- name.unicodeScalars.allSatisfy({
1201
- !CharacterSet.controlCharacters.contains($0)
1202
- }),
1203
- components.allSatisfy({
1204
- !$0.isEmpty &&
1205
- $0 != "." &&
1206
- $0 != ".." &&
1207
- !$0.hasSuffix(".") &&
1208
- !$0.hasSuffix(" ")
1209
- }),
1210
- !nestedArchiveExtensions.contains(where: {
1211
- lowered.hasSuffix($0)
1212
- }),
1213
- canonicalNames.insert(lowered).inserted
1214
- else {
1215
- return false
1216
- }
1217
- return true
1218
- }
1219
-
1220
- private func isRegularArchiveEntry(
1221
- _ entry: FirmwareArchiveEntryInfo
1222
- ) -> Bool {
1223
- let hostSystem = UInt8(entry.versionMadeBy >> 8)
1224
- if hostSystem == 3 || hostSystem == 19 {
1225
- let fileType = (entry.externalAttributes >> 16) & 0xF000
1226
- return fileType == 0 || fileType == 0x8000
1227
- }
1228
- return entry.externalAttributes & 0x10 == 0
1229
- }
1230
-
1231
- private func streamDownload(
1232
- _ params: FirmwareArtifactDownloadParams,
1233
- partialURL: URL,
1234
- resumeOffset: Int64
1235
- ) async throws {
1236
- do {
1237
- try await FirmwareArtifactWallClockDeadline.run(
1238
- timeoutSeconds:
1239
- params.overallDeadlineSeconds ?? Self.defaultDownloadDeadline
1240
- ) { [self] in
1241
- try await streamDownloadWithinDeadline(
1242
- params,
1243
- partialURL: partialURL,
1244
- resumeOffset: resumeOffset
1245
- )
1246
- }
1247
- } catch FirmwareArtifactDeadlineError.exceeded {
1248
- throw FirmwareArtifactStoreError.downloadFailed(
1249
- "ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
1250
- )
1251
- }
1252
- }
1253
-
1254
- private func streamDownloadWithinDeadline(
1255
- _ params: FirmwareArtifactDownloadParams,
1256
- partialURL: URL,
1257
- resumeOffset: Int64
1258
- ) async throws {
1259
- guard let url = URL(string: params.url), let hostname = url.host else {
1260
- throw FirmwareArtifactStoreError.invalidInput("Invalid firmware URL")
1261
- }
1262
- var request = URLRequest(url: url)
1263
- request.cachePolicy = .reloadIgnoringLocalCacheData
1264
- request.timeoutInterval =
1265
- params.overallDeadlineSeconds ?? Self.defaultDownloadDeadline
1266
- request.setValue("identity", forHTTPHeaderField: "Accept-Encoding")
1267
- if resumeOffset > 0 {
1268
- request.setValue("bytes=\(resumeOffset)-", forHTTPHeaderField: "Range")
1269
- }
1270
-
1271
- let streamDelegate = FirmwareArtifactStreamDelegate(
1272
- partialURL: partialURL,
1273
- hostname: hostname,
1274
- resumeOffset: resumeOffset,
1275
- expectedSize: Int64(params.expectedSize),
1276
- maxBytes: Int64(params.maxBytes),
1277
- isCancelled: { [weak self] in
1278
- Task.isCancelled ||
1279
- self?.isTransactionCancelled(params.transactionId) == true
1280
- }
1281
- )
1282
- let pinnedSession: SniConnectPinnedSession?
1283
- if params.routeType == "pinnedIp" {
1284
- pinnedSession = try SniConnectPinnedTransport.makeSession(
1285
- hostname: hostname,
1286
- ip: params.resolvedIp!,
1287
- dataDelegate: streamDelegate
1288
- )
1289
- } else {
1290
- pinnedSession = nil
1291
- }
1292
- let session = pinnedSession?.session ?? URLSession(
1293
- configuration: .ephemeral,
1294
- delegate: streamDelegate,
1295
- delegateQueue: nil
1296
- )
1297
- defer {
1298
- if let pinnedSession {
1299
- pinnedSession.close()
1300
- } else {
1301
- session.finishTasksAndInvalidate()
1302
- }
1303
- }
1304
-
1305
- do {
1306
- try await streamDelegate.run(session: session, request: request)
1307
- } catch let error as FirmwareArtifactStoreError {
1308
- throw error
1309
- } catch {
1310
- if isCancellationError(
1311
- error,
1312
- transactionId: params.transactionId
1313
- ) {
1314
- throw FirmwareArtifactStoreError.downloadFailed(
1315
- "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
1316
- )
1317
- }
1318
- if isFirmwareArtifactTLSError(error) {
1319
- throw FirmwareArtifactStoreError.downloadFailed(
1320
- "ARTIFACT_TLS_FAILED: firmware TLS validation failed"
1321
- )
1322
- }
1323
- throw FirmwareArtifactStoreError.downloadFailed(
1324
- "ARTIFACT_NETWORK_FAILED: firmware request failed"
1325
- )
1326
- }
1327
- try rejectIfCancelled(transactionId: params.transactionId)
1328
- }
1329
-
1330
- private func rejectIfCancelled(transactionId: String) throws {
1331
- if isTransactionCancelled(transactionId) {
1332
- throw FirmwareArtifactStoreError.downloadFailed(
1333
- "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
1334
- )
1335
- }
1336
- }
1337
-
1338
- func isTransactionCancelled(_ transactionId: String) -> Bool {
1339
- cancellationLock.withFirmwareArtifactLock {
1340
- cancelledTransactions.contains(transactionId)
1341
- }
1342
- }
1343
-
1344
- private func isCancellationError(
1345
- _ error: Error,
1346
- transactionId: String
1347
- ) -> Bool {
1348
- if error is CancellationError ||
1349
- isTransactionCancelled(transactionId) {
1350
- return true
1351
- }
1352
- let nsError = error as NSError
1353
- return nsError.domain == NSURLErrorDomain &&
1354
- nsError.code == NSURLErrorCancelled
1355
- }
1356
-
1357
- private func validateStoredArtifact(
1358
- fileURL: URL,
1359
- expectedSize: Int64,
1360
- expectedSha256: String
1361
- ) throws -> StoredFirmwareArtifact {
1362
- let size = try fileSize(fileURL)
1363
- guard size == expectedSize else {
1364
- throw FirmwareArtifactStoreError.integrityMismatch(
1365
- "ARTIFACT_INTEGRITY_FAILED: firmware artifact size mismatch"
1366
- )
1367
- }
1368
- let sha256 = try hashFile(fileURL)
1369
- guard sha256 == expectedSha256 else {
1370
- throw FirmwareArtifactStoreError.integrityMismatch(
1371
- "ARTIFACT_INTEGRITY_FAILED: firmware artifact SHA-256 mismatch"
1372
- )
1373
- }
1374
- return StoredFirmwareArtifact(
1375
- artifactRef: "fw:\(sha256)",
1376
- size: size,
1377
- sha256: sha256,
1378
- fileURL: fileURL
1379
- )
1380
- }
1381
-
1382
- private func resolveArtifactURL(_ artifactRef: String) throws -> URL {
1383
- guard artifactRef.hasPrefix("fw:") else {
1384
- throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifactRef")
1385
- }
1386
- let sha256 = String(artifactRef.dropFirst(3))
1387
- guard sha256.range(of: "^[a-f0-9]{64}$", options: .regularExpression) != nil else {
1388
- throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifactRef")
1389
- }
1390
- let url = artifactURL(sha256: sha256)
1391
- guard fileManager.fileExists(atPath: url.path) else {
1392
- throw FirmwareArtifactStoreError.invalidInput("Firmware artifact not found")
1393
- }
1394
- return url
1395
- }
1396
-
1397
- private func artifactURL(sha256: String) -> URL {
1398
- rootURL.appendingPathComponent("\(sha256).bin", isDirectory: false)
1399
- }
1400
-
1401
- private func fileSize(_ url: URL) throws -> Int64 {
1402
- let attributes = try fileManager.attributesOfItem(atPath: url.path)
1403
- return (attributes[.size] as? NSNumber)?.int64Value ?? 0
1404
- }
1405
-
1406
- private func hashFile(_ url: URL) throws -> String {
1407
- let handle = try FileHandle(forReadingFrom: url)
1408
- defer { try? handle.close() }
1409
- var hasher = SHA256()
1410
- while let data = try handle.read(upToCount: 256 * 1024), !data.isEmpty {
1411
- hasher.update(data: data)
1412
- }
1413
- return hasher.finalize().map { String(format: "%02x", $0) }.joined()
1414
- }
1415
-
1416
- private func promote(source: URL, destination: URL) throws {
1417
- let stagingURL = destination
1418
- .deletingLastPathComponent()
1419
- .appendingPathComponent(".promote-\(UUID().uuidString)", isDirectory: false)
1420
- try fileManager.moveItem(at: source, to: stagingURL)
1421
- defer {
1422
- if fileManager.fileExists(atPath: stagingURL.path) {
1423
- try? fileManager.removeItem(at: stagingURL)
1424
- }
1425
- }
1426
- if fileManager.fileExists(atPath: destination.path) {
1427
- _ = try fileManager.replaceItemAt(
1428
- destination,
1429
- withItemAt: stagingURL,
1430
- backupItemName: nil,
1431
- options: []
1432
- )
1433
- } else {
1434
- try fileManager.moveItem(at: stagingURL, to: destination)
1435
- }
1436
- }
1437
- }
1438
-
1439
- private extension NSLock {
1440
- func withFirmwareArtifactLock<T>(_ body: () -> T) -> T {
1441
- lock()
1442
- defer { unlock() }
1443
- return body()
1444
- }
1445
- }