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

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 (113) hide show
  1. package/ReactNativeRangeDownloader.podspec +8 -0
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRules.kt +284 -0
  4. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +842 -0
  5. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +148 -0
  6. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRulesTest.kt +75 -0
  7. package/ios/FirmwareArchiveMinizipBridge.h +33 -0
  8. package/ios/FirmwareArchiveMinizipBridge.mm +213 -0
  9. package/ios/FirmwareArtifactStore.swift +1276 -0
  10. package/ios/FirmwareBackgroundSessionEventRouter.swift +17 -0
  11. package/ios/ReactNativeRangeDownloader.swift +638 -6
  12. package/lib/module/index.js +2 -1
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +95 -1
  15. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
  16. package/lib/typescript/src/index.d.ts +1 -0
  17. package/lib/typescript/src/index.d.ts.map +1 -1
  18. package/nitrogen/generated/android/c++/JDownloadChannel.hpp +3 -0
  19. package/nitrogen/generated/android/c++/JFirmwareArchiveExpectedEntry.hpp +69 -0
  20. package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeParams.hpp +86 -0
  21. package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeResult.hpp +80 -0
  22. package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializedArtifact.hpp +63 -0
  23. package/nitrogen/generated/android/c++/JFirmwareArtifactCancelParams.hpp +57 -0
  24. package/nitrogen/generated/android/c++/JFirmwareArtifactCapabilities.hpp +88 -0
  25. package/nitrogen/generated/android/c++/JFirmwareArtifactDownloadParams.hpp +98 -0
  26. package/nitrogen/generated/android/c++/JFirmwareArtifactLease.hpp +57 -0
  27. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseCreateParams.hpp +57 -0
  28. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseReconcileParams.hpp +76 -0
  29. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseReleaseParams.hpp +61 -0
  30. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseRetainParams.hpp +61 -0
  31. package/nitrogen/generated/android/c++/JFirmwareArtifactReaderCloseParams.hpp +57 -0
  32. package/nitrogen/generated/android/c++/JFirmwareArtifactReaderInfo.hpp +61 -0
  33. package/nitrogen/generated/android/c++/JFirmwareArtifactReaderReadParams.hpp +65 -0
  34. package/nitrogen/generated/android/c++/JFirmwareArtifactReceipt.hpp +65 -0
  35. package/nitrogen/generated/android/c++/JFirmwareArtifactRefParams.hpp +57 -0
  36. package/nitrogen/generated/android/c++/JFirmwareArtifactSweepResult.hpp +61 -0
  37. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +267 -0
  38. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +13 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/DownloadChannel.kt +2 -1
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveExpectedEntry.kt +47 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeParams.kt +44 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeResult.kt +38 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializedArtifact.kt +41 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactCancelParams.kt +38 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactCapabilities.kt +47 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDownloadParams.kt +68 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLease.kt +38 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseCreateParams.kt +38 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseReconcileParams.kt +38 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseReleaseParams.kt +41 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseRetainParams.kt +41 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderCloseParams.kt +38 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderInfo.kt +41 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderReadParams.kt +44 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReceipt.kt +44 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactRefParams.kt +38 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactSweepResult.kt +41 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +53 -0
  59. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.cpp +48 -0
  60. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +329 -0
  61. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +56 -0
  62. package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +163 -0
  63. package/nitrogen/generated/ios/swift/DownloadChannel.swift +4 -0
  64. package/nitrogen/generated/ios/swift/FirmwareArchiveExpectedEntry.swift +69 -0
  65. package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeParams.swift +70 -0
  66. package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeResult.swift +48 -0
  67. package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializedArtifact.swift +47 -0
  68. package/nitrogen/generated/ios/swift/FirmwareArtifactCancelParams.swift +36 -0
  69. package/nitrogen/generated/ios/swift/FirmwareArtifactCapabilities.swift +81 -0
  70. package/nitrogen/generated/ios/swift/FirmwareArtifactDownloadParams.swift +177 -0
  71. package/nitrogen/generated/ios/swift/FirmwareArtifactLease.swift +36 -0
  72. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseCreateParams.swift +36 -0
  73. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseReconcileParams.swift +48 -0
  74. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseReleaseParams.swift +47 -0
  75. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseRetainParams.swift +47 -0
  76. package/nitrogen/generated/ios/swift/FirmwareArtifactReaderCloseParams.swift +36 -0
  77. package/nitrogen/generated/ios/swift/FirmwareArtifactReaderInfo.swift +47 -0
  78. package/nitrogen/generated/ios/swift/FirmwareArtifactReaderReadParams.swift +58 -0
  79. package/nitrogen/generated/ios/swift/FirmwareArtifactReceipt.swift +58 -0
  80. package/nitrogen/generated/ios/swift/FirmwareArtifactRefParams.swift +36 -0
  81. package/nitrogen/generated/ios/swift/FirmwareArtifactSweepResult.swift +47 -0
  82. package/nitrogen/generated/ios/swift/Func_void_FirmwareArchiveMaterializeResult.swift +47 -0
  83. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactLease.swift +47 -0
  84. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactReaderInfo.swift +47 -0
  85. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactReceipt.swift +47 -0
  86. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactSweepResult.swift +47 -0
  87. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +47 -0
  88. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +13 -0
  89. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +240 -0
  90. package/nitrogen/generated/shared/c++/DownloadChannel.hpp +4 -0
  91. package/nitrogen/generated/shared/c++/FirmwareArchiveExpectedEntry.hpp +87 -0
  92. package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeParams.hpp +86 -0
  93. package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeResult.hpp +77 -0
  94. package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializedArtifact.hpp +81 -0
  95. package/nitrogen/generated/shared/c++/FirmwareArtifactCancelParams.hpp +75 -0
  96. package/nitrogen/generated/shared/c++/FirmwareArtifactCapabilities.hpp +88 -0
  97. package/nitrogen/generated/shared/c++/FirmwareArtifactDownloadParams.hpp +116 -0
  98. package/nitrogen/generated/shared/c++/FirmwareArtifactLease.hpp +75 -0
  99. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseCreateParams.hpp +75 -0
  100. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseReconcileParams.hpp +76 -0
  101. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseReleaseParams.hpp +79 -0
  102. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseRetainParams.hpp +79 -0
  103. package/nitrogen/generated/shared/c++/FirmwareArtifactReaderCloseParams.hpp +75 -0
  104. package/nitrogen/generated/shared/c++/FirmwareArtifactReaderInfo.hpp +79 -0
  105. package/nitrogen/generated/shared/c++/FirmwareArtifactReaderReadParams.hpp +83 -0
  106. package/nitrogen/generated/shared/c++/FirmwareArtifactReceipt.hpp +83 -0
  107. package/nitrogen/generated/shared/c++/FirmwareArtifactRefParams.hpp +75 -0
  108. package/nitrogen/generated/shared/c++/FirmwareArtifactSweepResult.hpp +79 -0
  109. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +13 -0
  110. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +62 -0
  111. package/package.json +4 -2
  112. package/src/ReactNativeRangeDownloader.nitro.ts +134 -1
  113. package/src/index.tsx +3 -1
@@ -95,6 +95,173 @@ class ReactNativeRangeDownloader: HybridReactNativeRangeDownloaderSpec {
95
95
  }
96
96
  return NSTemporaryDirectory()
97
97
  }
98
+
99
+ func getFirmwareArtifactCapabilities() throws -> FirmwareArtifactCapabilities {
100
+ FirmwareArtifactCapabilities(
101
+ firmwareArtifactProtocolVersion: 1,
102
+ supportedRouteTypes: ["domain", "pinnedIp"],
103
+ supportsArchiveMaterialization: true,
104
+ maxReadBytes: Double(FirmwareArtifactStore.maxReadBytes)
105
+ )
106
+ }
107
+
108
+ func downloadFirmwareArtifact(
109
+ params: FirmwareArtifactDownloadParams
110
+ ) throws -> Promise<FirmwareArtifactReceipt> {
111
+ Promise.async {
112
+ let artifact = try await FirmwareArtifactStore.shared.download(params)
113
+ return FirmwareArtifactReceipt(
114
+ artifactRef: artifact.artifactRef,
115
+ size: Double(artifact.size),
116
+ sha256: artifact.sha256
117
+ )
118
+ }
119
+ }
120
+
121
+ func cancelFirmwareArtifactDownloads(
122
+ params: FirmwareArtifactCancelParams
123
+ ) throws -> Promise<Void> {
124
+ Promise.async {
125
+ try await FirmwareArtifactStore.shared.cancelDownloads(
126
+ transactionId: params.transactionId
127
+ )
128
+ }
129
+ }
130
+
131
+ func discardFirmwareArtifact(
132
+ params: FirmwareArtifactRefParams
133
+ ) throws -> Promise<Void> {
134
+ Promise.async {
135
+ try FirmwareArtifactStore.shared.discard(artifactRef: params.artifactRef)
136
+ }
137
+ }
138
+
139
+ func openFirmwareArtifact(
140
+ params: FirmwareArtifactRefParams
141
+ ) throws -> Promise<FirmwareArtifactReaderInfo> {
142
+ Promise.async {
143
+ let reader = try FirmwareArtifactStore.shared.open(
144
+ artifactRef: params.artifactRef
145
+ )
146
+ return FirmwareArtifactReaderInfo(
147
+ readerId: reader.readerId,
148
+ size: Double(reader.size)
149
+ )
150
+ }
151
+ }
152
+
153
+ func readFirmwareArtifact(
154
+ params: FirmwareArtifactReaderReadParams
155
+ ) throws -> Promise<ArrayBuffer> {
156
+ Promise.async {
157
+ guard
158
+ params.offset.isFinite,
159
+ params.offset >= 0,
160
+ params.offset <= Double(Int64.max),
161
+ params.offset.rounded() == params.offset,
162
+ params.length.isFinite,
163
+ params.length > 0,
164
+ params.length <= Double(Int.max),
165
+ params.length.rounded() == params.length
166
+ else {
167
+ throw FirmwareArtifactStoreError.readerInvalid(
168
+ "Invalid firmware artifact read"
169
+ )
170
+ }
171
+ let data = try FirmwareArtifactStore.shared.read(
172
+ readerId: params.readerId,
173
+ offset: Int64(params.offset),
174
+ length: Int(params.length)
175
+ )
176
+ return try ArrayBuffer.copy(data: data)
177
+ }
178
+ }
179
+
180
+ func closeFirmwareArtifact(
181
+ params: FirmwareArtifactReaderCloseParams
182
+ ) throws -> Promise<Void> {
183
+ Promise.async {
184
+ try FirmwareArtifactStore.shared.close(readerId: params.readerId)
185
+ }
186
+ }
187
+
188
+ func materializeFirmwareArchive(
189
+ params: FirmwareArchiveMaterializeParams
190
+ ) throws -> Promise<FirmwareArchiveMaterializeResult> {
191
+ Promise.async {
192
+ let entries = try FirmwareArtifactStore.shared.materializeArchive(
193
+ leaseRef: params.leaseRef,
194
+ artifactRef: params.archiveArtifactRef,
195
+ expectedEntries: params.expectedEntries
196
+ )
197
+ return FirmwareArchiveMaterializeResult(
198
+ artifacts: entries.map { entry in
199
+ FirmwareArchiveMaterializedArtifact(
200
+ entryName: entry.entryName,
201
+ receipt: FirmwareArtifactReceipt(
202
+ artifactRef: entry.artifact.artifactRef,
203
+ size: Double(entry.artifact.size),
204
+ sha256: entry.artifact.sha256
205
+ )
206
+ )
207
+ }
208
+ )
209
+ }
210
+ }
211
+
212
+ func createFirmwareArtifactLease(
213
+ params: FirmwareArtifactLeaseCreateParams
214
+ ) throws -> Promise<FirmwareArtifactLease> {
215
+ Promise.async {
216
+ FirmwareArtifactLease(
217
+ leaseRef: try FirmwareArtifactStore.shared.createLease(
218
+ transactionId: params.transactionId
219
+ )
220
+ )
221
+ }
222
+ }
223
+
224
+ func retainFirmwareArtifact(
225
+ params: FirmwareArtifactLeaseRetainParams
226
+ ) throws -> Promise<Void> {
227
+ Promise.async {
228
+ try FirmwareArtifactStore.shared.retain(
229
+ leaseRef: params.leaseRef,
230
+ artifactRef: params.artifactRef
231
+ )
232
+ }
233
+ }
234
+
235
+ func releaseFirmwareArtifactLease(
236
+ params: FirmwareArtifactLeaseReleaseParams
237
+ ) throws -> Promise<Void> {
238
+ Promise.async {
239
+ try FirmwareArtifactStore.shared.releaseLease(
240
+ leaseRef: params.leaseRef,
241
+ disposition: params.disposition
242
+ )
243
+ }
244
+ }
245
+
246
+ func reconcileFirmwareArtifactLeases(
247
+ params: FirmwareArtifactLeaseReconcileParams
248
+ ) throws -> Promise<Void> {
249
+ Promise.async {
250
+ try FirmwareArtifactStore.shared.reconcileLeases(
251
+ activeLeaseRefs: params.activeLeaseRefs
252
+ )
253
+ }
254
+ }
255
+
256
+ func sweepFirmwareArtifactOrphans() throws -> Promise<FirmwareArtifactSweepResult> {
257
+ Promise.async {
258
+ let result = try FirmwareArtifactStore.shared.sweepOrphans()
259
+ return FirmwareArtifactSweepResult(
260
+ deletedFiles: Double(result.deletedFiles),
261
+ deletedBytes: Double(result.deletedBytes)
262
+ )
263
+ }
264
+ }
98
265
  }
99
266
 
100
267
  // MARK: - RangeDownloader (migrated core)
@@ -156,10 +323,79 @@ extension RangeFallbackClass {
156
323
  }
157
324
  }
158
325
 
326
+ private struct FirmwareBackgroundTaskDescriptor: Codable, Equatable {
327
+ let schemaVersion: Int
328
+ let taskId: String
329
+ let transactionId: String
330
+ let leaseRef: String
331
+ let expectedSize: Int64
332
+ let expectedSha256: String
333
+ let hostname: String
334
+ let deadlineAt: TimeInterval
335
+
336
+ var key: String {
337
+ "\(leaseRef)|\(taskId)|\(expectedSha256)"
338
+ }
339
+
340
+ func hasSameArtifactIdentity(
341
+ as other: FirmwareBackgroundTaskDescriptor
342
+ ) -> Bool {
343
+ taskId == other.taskId &&
344
+ transactionId == other.transactionId &&
345
+ leaseRef == other.leaseRef &&
346
+ expectedSize == other.expectedSize &&
347
+ expectedSha256 == other.expectedSha256 &&
348
+ hostname == other.hostname
349
+ }
350
+ }
351
+
352
+ private enum FirmwareBackgroundDownloadError: LocalizedError {
353
+ case cancelled
354
+ case invalidTask
355
+ case deadlineExceeded
356
+ case redirectRejected
357
+ case responseRejected
358
+ case sizeRejected
359
+ case transferFailed
360
+
361
+ var errorDescription: String? {
362
+ switch self {
363
+ case .cancelled:
364
+ return "ARTIFACT_CANCELLED: firmware background download was cancelled"
365
+ case .invalidTask:
366
+ return "ARTIFACT_PROTOCOL_INVALID: firmware background task is invalid"
367
+ case .deadlineExceeded:
368
+ return "ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
369
+ case .redirectRejected:
370
+ return "ARTIFACT_REDIRECT_REJECTED: firmware redirect changed canonical identity"
371
+ case .responseRejected:
372
+ return "ARTIFACT_PROTOCOL_INVALID: firmware response is invalid"
373
+ case .sizeRejected:
374
+ return "ARTIFACT_PROTOCOL_INVALID: firmware artifact size is invalid"
375
+ case .transferFailed:
376
+ return "ARTIFACT_NETWORK_FAILED: firmware background transfer failed"
377
+ }
378
+ }
379
+ }
380
+
159
381
  public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
160
382
 
161
383
  public static let shared = RangeDownloader()
162
384
 
385
+ public static func routeFirmwareBackgroundEvents(
386
+ identifier: String,
387
+ completionHandler: @escaping () -> Void
388
+ ) -> Bool {
389
+ guard identifier == sessionIdentifier(for: .firmware) else {
390
+ return false
391
+ }
392
+ shared.attachBackgroundEvents(
393
+ identifier: identifier,
394
+ completionHandler: completionHandler
395
+ )
396
+ return true
397
+ }
398
+
163
399
  /// Posted by the AppDelegate from
164
400
  /// application(_:handleEventsForBackgroundURLSession:completionHandler:).
165
401
  /// userInfo: ["identifier": String, "completionHandler": () -> Void].
@@ -196,6 +432,11 @@ public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
196
432
  /// notification), keyed by session identifier, so we can call each back once
197
433
  /// all its queued background events have been delivered.
198
434
  private var backgroundCompletionHandlers: [String: () -> Void] = [:]
435
+ private var firmwareWaiters: [
436
+ String: [CheckedContinuation<StoredFirmwareArtifact, Error>]
437
+ ] = [:]
438
+ private var firmwareTaskErrors: [Int: Error] = [:]
439
+ private var completedFirmwareTasks: Set<Int> = []
199
440
 
200
441
  // Per-run mutable state.
201
442
  private final class RunState {
@@ -326,14 +567,30 @@ public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
326
567
  @objc private func handleBackgroundEventsNotification(_ note: Notification) {
327
568
  guard let identifier = note.userInfo?["identifier"] as? String,
328
569
  Self.channel(forIdentifier: identifier) != nil else { return }
329
- // Re-create the session with this delegate so queued completion events are
330
- // delivered here on a background relaunch.
331
- _ = session(forIdentifier: identifier)
332
570
  if let handler = note.userInfo?["completionHandler"] as? () -> Void {
333
- lock.lock()
334
- backgroundCompletionHandlers[identifier] = handler
335
- lock.unlock()
571
+ attachBackgroundEvents(
572
+ identifier: identifier,
573
+ completionHandler: handler
574
+ )
575
+ }
576
+ }
577
+
578
+ private func attachBackgroundEvents(
579
+ identifier: String,
580
+ completionHandler: @escaping () -> Void
581
+ ) {
582
+ let replacedHandler: (() -> Void)? = lock.withLockValue {
583
+ backgroundCompletionHandlers.updateValue(
584
+ completionHandler,
585
+ forKey: identifier
586
+ )
336
587
  }
588
+ // UIKit should provide one live handler per session. Complete an older
589
+ // handler instead of leaking it if the callback is unexpectedly repeated.
590
+ replacedHandler?()
591
+ // Store the handler before creating the session. Delegate delivery can
592
+ // begin immediately when a background relaunch reattaches this identifier.
593
+ _ = session(forIdentifier: identifier)
337
594
  }
338
595
 
339
596
  // MARK: - Session cache
@@ -437,6 +694,240 @@ public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
437
694
  return (r, decoded.segIndex)
438
695
  }
439
696
 
697
+ private static let firmwareTaskDescriptionPrefix = "firmware-v1:"
698
+
699
+ private static func encodeFirmwareTaskDescription(
700
+ _ descriptor: FirmwareBackgroundTaskDescriptor
701
+ ) throws -> String {
702
+ let data = try JSONEncoder().encode(descriptor)
703
+ return firmwareTaskDescriptionPrefix + data.base64EncodedString()
704
+ }
705
+
706
+ private static func decodeFirmwareTaskDescription(
707
+ _ description: String?
708
+ ) -> FirmwareBackgroundTaskDescriptor? {
709
+ guard
710
+ let description,
711
+ description.hasPrefix(firmwareTaskDescriptionPrefix),
712
+ let data = Data(
713
+ base64Encoded: String(
714
+ description.dropFirst(firmwareTaskDescriptionPrefix.count)
715
+ )
716
+ ),
717
+ data.count <= 2048,
718
+ let descriptor = try? JSONDecoder().decode(
719
+ FirmwareBackgroundTaskDescriptor.self,
720
+ from: data
721
+ ),
722
+ descriptor.schemaVersion == 1,
723
+ descriptor.taskId.range(
724
+ of: "^[A-Za-z0-9._-]{1,100}$",
725
+ options: .regularExpression
726
+ ) != nil,
727
+ descriptor.transactionId.range(
728
+ of: "^[A-Za-z0-9._:-]{1,160}$",
729
+ options: .regularExpression
730
+ ) != nil,
731
+ descriptor.leaseRef.range(
732
+ of: "^fwlease:[a-f0-9-]{36}$",
733
+ options: .regularExpression
734
+ ) != nil,
735
+ descriptor.expectedSize > 0,
736
+ descriptor.expectedSize <= 512 * 1024 * 1024,
737
+ descriptor.expectedSha256.range(
738
+ of: "^[a-f0-9]{64}$",
739
+ options: .regularExpression
740
+ ) != nil,
741
+ !descriptor.hostname.isEmpty,
742
+ descriptor.hostname.count <= 253,
743
+ descriptor.deadlineAt.isFinite,
744
+ descriptor.deadlineAt > 0
745
+ else {
746
+ return nil
747
+ }
748
+ return descriptor
749
+ }
750
+
751
+ func downloadFirmwareArtifact(
752
+ params: FirmwareArtifactDownloadParams
753
+ ) async throws -> StoredFirmwareArtifact {
754
+ guard
755
+ params.routeType == "domain",
756
+ let url = URL(string: params.url),
757
+ let hostname = url.host?.lowercased()
758
+ else {
759
+ throw FirmwareBackgroundDownloadError.invalidTask
760
+ }
761
+ let deadlineSeconds = params.overallDeadlineSeconds ?? 180
762
+ guard
763
+ deadlineSeconds.isFinite,
764
+ deadlineSeconds > 0,
765
+ deadlineSeconds <= 24 * 60 * 60
766
+ else {
767
+ throw FirmwareBackgroundDownloadError.invalidTask
768
+ }
769
+ let descriptor = FirmwareBackgroundTaskDescriptor(
770
+ schemaVersion: 1,
771
+ taskId: params.taskId,
772
+ transactionId: params.transactionId,
773
+ leaseRef: params.leaseRef,
774
+ expectedSize: Int64(params.expectedSize),
775
+ expectedSha256: params.expectedSha256.lowercased(),
776
+ hostname: hostname,
777
+ deadlineAt: Date().timeIntervalSince1970 + deadlineSeconds
778
+ )
779
+ guard !FirmwareArtifactStore.shared.isTransactionCancelled(
780
+ descriptor.transactionId
781
+ ) else {
782
+ throw FirmwareBackgroundDownloadError.cancelled
783
+ }
784
+ if let stored = try FirmwareArtifactStore.shared.storedArtifact(
785
+ expectedSize: descriptor.expectedSize,
786
+ expectedSha256: descriptor.expectedSha256
787
+ ) {
788
+ return stored
789
+ }
790
+
791
+ return try await withCheckedThrowingContinuation { continuation in
792
+ lock.withLockValue {
793
+ firmwareWaiters[descriptor.key, default: []].append(continuation)
794
+ }
795
+ Task { [weak self] in
796
+ await self?.reconcileOrStartFirmwareTask(
797
+ descriptor: descriptor,
798
+ url: url
799
+ )
800
+ }
801
+ }
802
+ }
803
+
804
+ func cancelFirmwareArtifactDownloads(
805
+ transactionId: String
806
+ ) async throws {
807
+ guard transactionId.range(
808
+ of: "^[A-Za-z0-9._:-]{1,160}$",
809
+ options: .regularExpression
810
+ ) != nil else {
811
+ throw FirmwareBackgroundDownloadError.invalidTask
812
+ }
813
+ let session = session(forChannel: .firmware, segmentCount: 1)
814
+ let tasks = await allTasks(in: session)
815
+ for task in tasks {
816
+ guard
817
+ let descriptor = Self.decodeFirmwareTaskDescription(
818
+ task.taskDescription
819
+ ),
820
+ descriptor.transactionId == transactionId
821
+ else {
822
+ continue
823
+ }
824
+ recordFirmwareTaskError(
825
+ taskIdentifier: task.taskIdentifier,
826
+ error: FirmwareBackgroundDownloadError.cancelled
827
+ )
828
+ task.cancel()
829
+ }
830
+ }
831
+
832
+ private func reconcileOrStartFirmwareTask(
833
+ descriptor: FirmwareBackgroundTaskDescriptor,
834
+ url: URL
835
+ ) async {
836
+ guard !FirmwareArtifactStore.shared.isTransactionCancelled(
837
+ descriptor.transactionId
838
+ ) else {
839
+ finishFirmwareTask(
840
+ key: descriptor.key,
841
+ result: .failure(FirmwareBackgroundDownloadError.cancelled)
842
+ )
843
+ return
844
+ }
845
+ let session = session(forChannel: .firmware, segmentCount: 1)
846
+ let tasks = await allTasks(in: session)
847
+ var matchingTaskFound = false
848
+ for task in tasks {
849
+ guard
850
+ let candidate = Self.decodeFirmwareTaskDescription(
851
+ task.taskDescription
852
+ )
853
+ else {
854
+ continue
855
+ }
856
+ if candidate.hasSameArtifactIdentity(as: descriptor) {
857
+ matchingTaskFound = true
858
+ } else if candidate.taskId == descriptor.taskId {
859
+ task.cancel()
860
+ }
861
+ }
862
+ if matchingTaskFound {
863
+ return
864
+ }
865
+ do {
866
+ if let stored = try FirmwareArtifactStore.shared.storedArtifact(
867
+ expectedSize: descriptor.expectedSize,
868
+ expectedSha256: descriptor.expectedSha256
869
+ ) {
870
+ finishFirmwareTask(
871
+ key: descriptor.key,
872
+ result: .success(stored)
873
+ )
874
+ return
875
+ }
876
+ guard Date().timeIntervalSince1970 < descriptor.deadlineAt else {
877
+ throw FirmwareBackgroundDownloadError.deadlineExceeded
878
+ }
879
+ guard !FirmwareArtifactStore.shared.isTransactionCancelled(
880
+ descriptor.transactionId
881
+ ) else {
882
+ throw FirmwareBackgroundDownloadError.cancelled
883
+ }
884
+ var request = URLRequest(url: url)
885
+ request.cachePolicy = .reloadIgnoringLocalCacheData
886
+ request.timeoutInterval = max(
887
+ 1,
888
+ descriptor.deadlineAt - Date().timeIntervalSince1970
889
+ )
890
+ request.setValue("identity", forHTTPHeaderField: "Accept-Encoding")
891
+ let task = session.downloadTask(with: request)
892
+ task.taskDescription = try Self.encodeFirmwareTaskDescription(
893
+ descriptor
894
+ )
895
+ task.resume()
896
+ } catch {
897
+ finishFirmwareTask(
898
+ key: descriptor.key,
899
+ result: .failure(error)
900
+ )
901
+ }
902
+ }
903
+
904
+ private func allTasks(in session: URLSession) async -> [URLSessionTask] {
905
+ await withCheckedContinuation { continuation in
906
+ session.getAllTasks { tasks in
907
+ continuation.resume(returning: tasks)
908
+ }
909
+ }
910
+ }
911
+
912
+ private func finishFirmwareTask(
913
+ key: String,
914
+ result: Result<StoredFirmwareArtifact, Error>
915
+ ) {
916
+ let continuations: [
917
+ CheckedContinuation<StoredFirmwareArtifact, Error>
918
+ ] = lock.withLockValue {
919
+ firmwareWaiters.removeValue(forKey: key) ?? []
920
+ }
921
+ for continuation in continuations {
922
+ switch result {
923
+ case let .success(artifact):
924
+ continuation.resume(returning: artifact)
925
+ case let .failure(error):
926
+ continuation.resume(throwing: error)
927
+ }
928
+ }
929
+ }
930
+
440
931
  // MARK: - Public entry
441
932
 
442
933
  /// Downloads [urlString] into [filePath] using concurrent background ranges.
@@ -972,6 +1463,28 @@ public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
972
1463
  public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
973
1464
  didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
974
1465
  totalBytesExpectedToWrite: Int64) {
1466
+ if let descriptor = Self.decodeFirmwareTaskDescription(
1467
+ downloadTask.taskDescription
1468
+ ) {
1469
+ let exceedsBound =
1470
+ totalBytesWritten > descriptor.expectedSize ||
1471
+ (
1472
+ totalBytesExpectedToWrite != NSURLSessionTransferSizeUnknown &&
1473
+ totalBytesExpectedToWrite != descriptor.expectedSize
1474
+ )
1475
+ let expired =
1476
+ Date().timeIntervalSince1970 >= descriptor.deadlineAt
1477
+ if exceedsBound || expired {
1478
+ recordFirmwareTaskError(
1479
+ taskIdentifier: downloadTask.taskIdentifier,
1480
+ error: exceedsBound
1481
+ ? FirmwareBackgroundDownloadError.sizeRejected
1482
+ : FirmwareBackgroundDownloadError.deadlineExceeded
1483
+ )
1484
+ downloadTask.cancel()
1485
+ }
1486
+ return
1487
+ }
975
1488
  guard let desc = downloadTask.taskDescription,
976
1489
  let (state, idx) = run(for: desc) else { return }
977
1490
  lock.lock()
@@ -1002,6 +1515,53 @@ public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
1002
1515
 
1003
1516
  public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
1004
1517
  didFinishDownloadingTo location: URL) {
1518
+ if let descriptor = Self.decodeFirmwareTaskDescription(
1519
+ downloadTask.taskDescription
1520
+ ) {
1521
+ guard firmwareTaskError(
1522
+ taskIdentifier: downloadTask.taskIdentifier
1523
+ ) == nil else {
1524
+ return
1525
+ }
1526
+ do {
1527
+ guard
1528
+ !FirmwareArtifactStore.shared.isTransactionCancelled(
1529
+ descriptor.transactionId
1530
+ ),
1531
+ Date().timeIntervalSince1970 < descriptor.deadlineAt,
1532
+ let response = downloadTask.response as? HTTPURLResponse,
1533
+ response.statusCode == 200,
1534
+ response.url?.scheme?.lowercased() == "https",
1535
+ response.url?.host?.lowercased() == descriptor.hostname,
1536
+ response.url?.port == nil || response.url?.port == 443
1537
+ else {
1538
+ throw FirmwareBackgroundDownloadError.responseRejected
1539
+ }
1540
+ let artifact = try FirmwareArtifactStore.shared.acceptBackgroundDownload(
1541
+ temporaryURL: location,
1542
+ leaseRef: descriptor.leaseRef,
1543
+ transactionId: descriptor.transactionId,
1544
+ expectedSize: descriptor.expectedSize,
1545
+ expectedSha256: descriptor.expectedSha256
1546
+ )
1547
+ lock.withLockValue {
1548
+ firmwareTaskErrors.removeValue(
1549
+ forKey: downloadTask.taskIdentifier
1550
+ )
1551
+ completedFirmwareTasks.insert(downloadTask.taskIdentifier)
1552
+ }
1553
+ finishFirmwareTask(
1554
+ key: descriptor.key,
1555
+ result: .success(artifact)
1556
+ )
1557
+ } catch {
1558
+ recordFirmwareTaskError(
1559
+ taskIdentifier: downloadTask.taskIdentifier,
1560
+ error: error
1561
+ )
1562
+ }
1563
+ return
1564
+ }
1005
1565
  guard let desc = downloadTask.taskDescription,
1006
1566
  let (state, idx) = run(for: desc) else { return }
1007
1567
  let ranges = lock.withLockValue { state.ranges }
@@ -1100,6 +1660,39 @@ public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
1100
1660
  }
1101
1661
 
1102
1662
  public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
1663
+ if let descriptor = Self.decodeFirmwareTaskDescription(
1664
+ task.taskDescription
1665
+ ) {
1666
+ let completion: (completed: Bool, error: Error?) =
1667
+ lock.withLockValue {
1668
+ let completed = completedFirmwareTasks.remove(
1669
+ task.taskIdentifier
1670
+ ) != nil
1671
+ let recordedError = firmwareTaskErrors.removeValue(
1672
+ forKey: task.taskIdentifier
1673
+ )
1674
+ return (completed, recordedError)
1675
+ }
1676
+ if completion.completed {
1677
+ return
1678
+ }
1679
+ finishFirmwareTask(
1680
+ key: descriptor.key,
1681
+ result: .failure(
1682
+ completion.error ??
1683
+ (
1684
+ FirmwareArtifactStore.shared.isTransactionCancelled(
1685
+ descriptor.transactionId
1686
+ )
1687
+ ? FirmwareBackgroundDownloadError.cancelled
1688
+ : nil
1689
+ ) ??
1690
+ error ??
1691
+ FirmwareBackgroundDownloadError.transferFailed
1692
+ )
1693
+ )
1694
+ return
1695
+ }
1103
1696
  guard let desc = task.taskDescription,
1104
1697
  let (state, idx) = run(for: desc) else { return }
1105
1698
  let ranges = lock.withLockValue { state.ranges }
@@ -1275,6 +1868,26 @@ public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
1275
1868
  willPerformHTTPRedirection response: HTTPURLResponse,
1276
1869
  newRequest request: URLRequest,
1277
1870
  completionHandler: @escaping (URLRequest?) -> Void) {
1871
+ if let descriptor = Self.decodeFirmwareTaskDescription(
1872
+ task.taskDescription
1873
+ ) {
1874
+ guard
1875
+ request.url?.scheme?.lowercased() == "https",
1876
+ request.url?.host?.lowercased() == descriptor.hostname,
1877
+ request.url?.port == nil || request.url?.port == 443,
1878
+ request.url?.user == nil,
1879
+ request.url?.password == nil
1880
+ else {
1881
+ recordFirmwareTaskError(
1882
+ taskIdentifier: task.taskIdentifier,
1883
+ error: FirmwareBackgroundDownloadError.redirectRejected
1884
+ )
1885
+ completionHandler(nil)
1886
+ return
1887
+ }
1888
+ completionHandler(request)
1889
+ return
1890
+ }
1278
1891
  if request.url?.scheme?.lowercased() != "https" {
1279
1892
  OneKeyLog.error("RangeDownloader", "blocked redirect to non-HTTPS URL")
1280
1893
  if let desc = task.taskDescription, let (state, _) = run(for: desc) {
@@ -1286,6 +1899,25 @@ public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
1286
1899
  }
1287
1900
  }
1288
1901
 
1902
+ private func recordFirmwareTaskError(
1903
+ taskIdentifier: Int,
1904
+ error: Error
1905
+ ) {
1906
+ lock.withLockValue {
1907
+ if firmwareTaskErrors[taskIdentifier] == nil {
1908
+ firmwareTaskErrors[taskIdentifier] = error
1909
+ }
1910
+ }
1911
+ }
1912
+
1913
+ private func firmwareTaskError(
1914
+ taskIdentifier: Int
1915
+ ) -> Error? {
1916
+ lock.withLockValue {
1917
+ firmwareTaskErrors[taskIdentifier]
1918
+ }
1919
+ }
1920
+
1289
1921
  /// Called on the session delegate queue when all background events for this
1290
1922
  /// session have been delivered after a background relaunch.
1291
1923
  public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {