@onekeyfe/react-native-range-downloader 3.0.81-alpha.2 → 3.0.81-alpha.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.
@@ -9,6 +9,9 @@ import java.io.File
9
9
  import java.security.MessageDigest
10
10
  import java.util.concurrent.CopyOnWriteArrayList
11
11
  import java.util.concurrent.atomic.AtomicLong
12
+ import kotlinx.coroutines.CoroutineScope
13
+ import kotlinx.coroutines.Dispatchers
14
+ import kotlinx.coroutines.SupervisorJob
12
15
 
13
16
  // P1: Nitro adapter for the Android concurrent multi-range downloader.
14
17
  //
@@ -34,6 +37,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
34
37
 
35
38
  private val listeners = CopyOnWriteArrayList<Listener>()
36
39
  private val nextListenerId = AtomicLong(1)
40
+ private val firmwareArtifactScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
37
41
 
38
42
  // Active downloads keyed by "channel|taskId" so cancel/discardArtifacts can
39
43
  // flip the abort flag + stop the worker pool BEFORE deleting files, instead of
@@ -278,7 +282,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
278
282
  override fun downloadFirmwareArtifact(
279
283
  params: FirmwareArtifactDownloadParams,
280
284
  ): Promise<FirmwareArtifactReceipt> {
281
- return Promise.async {
285
+ return Promise.async(firmwareArtifactScope) {
282
286
  val artifact = FirmwareArtifactStore.download(params)
283
287
  FirmwareArtifactReceipt(
284
288
  artifactRef = artifact.artifactRef,
@@ -291,7 +295,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
291
295
  override fun cancelFirmwareArtifactDownloads(
292
296
  params: FirmwareArtifactCancelParams,
293
297
  ): Promise<Unit> {
294
- return Promise.async {
298
+ return Promise.async(firmwareArtifactScope) {
295
299
  FirmwareArtifactStore.cancelDownloads(params.transactionId)
296
300
  }
297
301
  }
@@ -299,7 +303,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
299
303
  override fun discardFirmwareArtifact(
300
304
  params: FirmwareArtifactRefParams,
301
305
  ): Promise<Unit> {
302
- return Promise.async {
306
+ return Promise.async(firmwareArtifactScope) {
303
307
  FirmwareArtifactStore.discard(params.artifactRef)
304
308
  }
305
309
  }
@@ -307,7 +311,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
307
311
  override fun openFirmwareArtifact(
308
312
  params: FirmwareArtifactRefParams,
309
313
  ): Promise<FirmwareArtifactReaderInfo> {
310
- return Promise.async {
314
+ return Promise.async(firmwareArtifactScope) {
311
315
  val (readerId, size) = FirmwareArtifactStore.open(params.artifactRef)
312
316
  FirmwareArtifactReaderInfo(readerId = readerId, size = size.toDouble())
313
317
  }
@@ -316,7 +320,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
316
320
  override fun readFirmwareArtifact(
317
321
  params: FirmwareArtifactReaderReadParams,
318
322
  ): Promise<ArrayBuffer> {
319
- return Promise.async {
323
+ return Promise.async(firmwareArtifactScope) {
320
324
  require(
321
325
  params.offset.isFinite() &&
322
326
  params.offset >= 0 &&
@@ -340,7 +344,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
340
344
  override fun closeFirmwareArtifact(
341
345
  params: FirmwareArtifactReaderCloseParams,
342
346
  ): Promise<Unit> {
343
- return Promise.async {
347
+ return Promise.async(firmwareArtifactScope) {
344
348
  FirmwareArtifactStore.close(params.readerId)
345
349
  }
346
350
  }
@@ -348,7 +352,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
348
352
  override fun materializeFirmwareArchive(
349
353
  params: FirmwareArchiveMaterializeParams,
350
354
  ): Promise<FirmwareArchiveMaterializeResult> {
351
- return Promise.async {
355
+ return Promise.async(firmwareArtifactScope) {
352
356
  val artifacts = FirmwareArtifactStore.materializeArchive(
353
357
  params.leaseRef,
354
358
  params.archiveArtifactRef,
@@ -372,7 +376,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
372
376
  override fun createFirmwareArtifactLease(
373
377
  params: FirmwareArtifactLeaseCreateParams,
374
378
  ): Promise<FirmwareArtifactLease> {
375
- return Promise.async {
379
+ return Promise.async(firmwareArtifactScope) {
376
380
  FirmwareArtifactLease(
377
381
  leaseRef = FirmwareArtifactStore.createLease(params.transactionId),
378
382
  )
@@ -382,7 +386,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
382
386
  override fun retainFirmwareArtifact(
383
387
  params: FirmwareArtifactLeaseRetainParams,
384
388
  ): Promise<Unit> {
385
- return Promise.async {
389
+ return Promise.async(firmwareArtifactScope) {
386
390
  FirmwareArtifactStore.retain(params.leaseRef, params.artifactRef)
387
391
  }
388
392
  }
@@ -390,13 +394,13 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
390
394
  override fun releaseFirmwareArtifactLease(
391
395
  params: FirmwareArtifactLeaseReleaseParams,
392
396
  ): Promise<Unit> {
393
- return Promise.async {
397
+ return Promise.async(firmwareArtifactScope) {
394
398
  FirmwareArtifactStore.releaseLease(params.leaseRef, params.disposition)
395
399
  }
396
400
  }
397
401
 
398
402
  override fun sweepFirmwareArtifactOrphans(): Promise<FirmwareArtifactSweepResult> {
399
- return Promise.async {
403
+ return Promise.async(firmwareArtifactScope) {
400
404
  val (deletedFiles, deletedBytes) = FirmwareArtifactStore.sweepOrphans()
401
405
  FirmwareArtifactSweepResult(
402
406
  deletedFiles = deletedFiles.toDouble(),
@@ -56,7 +56,100 @@ private struct StagedFirmwareArchiveEntry {
56
56
  let stagingURL: URL
57
57
  }
58
58
 
59
- private final class FirmwareArtifactRedirectDelegate: NSObject, URLSessionTaskDelegate {
59
+ private final class FirmwareArtifactStreamDelegate: NSObject, URLSessionDataDelegate {
60
+ private let partialURL: URL
61
+ private let hostname: String
62
+ private let resumeOffset: Int64
63
+ private let expectedSize: Int64
64
+ private let maxBytes: Int64
65
+ private let isCancelled: () -> Bool
66
+ private let stateLock = NSLock()
67
+ private var continuation: CheckedContinuation<Void, Error>?
68
+ private var dataTask: URLSessionDataTask?
69
+ private var cancellationRequested = false
70
+ private var completed = false
71
+ private var responseAccepted = false
72
+ private var handle: FileHandle?
73
+ private var written: Int64 = 0
74
+
75
+ init(
76
+ partialURL: URL,
77
+ hostname: String,
78
+ resumeOffset: Int64,
79
+ expectedSize: Int64,
80
+ maxBytes: Int64,
81
+ isCancelled: @escaping () -> Bool
82
+ ) {
83
+ self.partialURL = partialURL
84
+ self.hostname = hostname
85
+ self.resumeOffset = resumeOffset
86
+ self.expectedSize = expectedSize
87
+ self.maxBytes = maxBytes
88
+ self.isCancelled = isCancelled
89
+ }
90
+
91
+ func run(session: URLSession, request: URLRequest) async throws {
92
+ try await withTaskCancellationHandler {
93
+ try await withCheckedThrowingContinuation {
94
+ (continuation: CheckedContinuation<Void, Error>) in
95
+ let task = session.dataTask(with: request)
96
+ stateLock.lock()
97
+ if cancellationRequested || completed {
98
+ stateLock.unlock()
99
+ task.cancel()
100
+ continuation.resume(throwing: CancellationError())
101
+ return
102
+ }
103
+ self.continuation = continuation
104
+ dataTask = task
105
+ stateLock.unlock()
106
+ task.resume()
107
+ }
108
+ } onCancel: {
109
+ self.cancel()
110
+ }
111
+ }
112
+
113
+ private func cancel() {
114
+ stateLock.lock()
115
+ cancellationRequested = true
116
+ let task = dataTask
117
+ stateLock.unlock()
118
+ task?.cancel()
119
+ }
120
+
121
+ private func finish(_ result: Result<Void, Error>) {
122
+ stateLock.lock()
123
+ guard !completed else {
124
+ stateLock.unlock()
125
+ return
126
+ }
127
+ completed = true
128
+ let continuation = continuation
129
+ self.continuation = nil
130
+ dataTask = nil
131
+ let handle = handle
132
+ self.handle = nil
133
+ stateLock.unlock()
134
+
135
+ try? handle?.close()
136
+ continuation?.resume(with: result)
137
+ }
138
+
139
+ private func reject(
140
+ _ dataTask: URLSessionDataTask,
141
+ completionHandler: @escaping (URLSession.ResponseDisposition) -> Void,
142
+ message: String
143
+ ) {
144
+ completionHandler(.cancel)
145
+ fail(dataTask, message: message)
146
+ }
147
+
148
+ private func fail(_ task: URLSessionTask, message: String) {
149
+ task.cancel()
150
+ finish(.failure(FirmwareArtifactStoreError.downloadFailed(message)))
151
+ }
152
+
60
153
  func urlSession(
61
154
  _ session: URLSession,
62
155
  task: URLSessionTask,
@@ -65,6 +158,192 @@ private final class FirmwareArtifactRedirectDelegate: NSObject, URLSessionTaskDe
65
158
  completionHandler: @escaping (URLRequest?) -> Void
66
159
  ) {
67
160
  completionHandler(nil)
161
+ fail(
162
+ task,
163
+ message:
164
+ "ARTIFACT_REDIRECT_REJECTED: firmware response changed canonical identity"
165
+ )
166
+ }
167
+
168
+ func urlSession(
169
+ _ session: URLSession,
170
+ dataTask: URLSessionDataTask,
171
+ didReceive response: URLResponse,
172
+ completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
173
+ ) {
174
+ guard !isCancelled() else {
175
+ reject(
176
+ dataTask,
177
+ completionHandler: completionHandler,
178
+ message: "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
179
+ )
180
+ return
181
+ }
182
+ guard let httpResponse = response as? HTTPURLResponse else {
183
+ reject(
184
+ dataTask,
185
+ completionHandler: completionHandler,
186
+ message: "Firmware response is not HTTP"
187
+ )
188
+ return
189
+ }
190
+ guard
191
+ let responseURL = httpResponse.url,
192
+ responseURL.scheme?.lowercased() == "https",
193
+ responseURL.host?.lowercased() == hostname.lowercased(),
194
+ responseURL.port == nil || responseURL.port == 443
195
+ else {
196
+ reject(
197
+ dataTask,
198
+ completionHandler: completionHandler,
199
+ message:
200
+ "ARTIFACT_REDIRECT_REJECTED: firmware response changed canonical identity"
201
+ )
202
+ return
203
+ }
204
+ guard httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else {
205
+ reject(
206
+ dataTask,
207
+ completionHandler: completionHandler,
208
+ message:
209
+ "ARTIFACT_HTTP_\(httpResponse.statusCode): firmware request failed"
210
+ )
211
+ return
212
+ }
213
+
214
+ let append = resumeOffset > 0 && httpResponse.statusCode == 206
215
+ if httpResponse.statusCode == 206 {
216
+ guard
217
+ let contentRange = httpResponse.value(
218
+ forHTTPHeaderField: "Content-Range"
219
+ ),
220
+ firmwareArtifactContentRangeIsValid(
221
+ contentRange,
222
+ expectedStart: append ? resumeOffset : 0,
223
+ expectedTotal: expectedSize
224
+ )
225
+ else {
226
+ reject(
227
+ dataTask,
228
+ completionHandler: completionHandler,
229
+ message:
230
+ "ARTIFACT_PROTOCOL_INVALID: firmware resume Content-Range is invalid"
231
+ )
232
+ return
233
+ }
234
+ }
235
+
236
+ let baseOffset = append ? resumeOffset : 0
237
+ guard
238
+ firmwareArtifactResponseFits(
239
+ expectedContentLength: httpResponse.expectedContentLength,
240
+ baseOffset: baseOffset,
241
+ maxBytes: maxBytes
242
+ )
243
+ else {
244
+ reject(
245
+ dataTask,
246
+ completionHandler: completionHandler,
247
+ message:
248
+ "ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
249
+ )
250
+ return
251
+ }
252
+
253
+ do {
254
+ let handle = try FileHandle(forWritingTo: partialURL)
255
+ if append {
256
+ try handle.seekToEnd()
257
+ } else {
258
+ try handle.truncate(atOffset: 0)
259
+ }
260
+ self.handle = handle
261
+ written = baseOffset
262
+ responseAccepted = true
263
+ completionHandler(.allow)
264
+ } catch {
265
+ reject(
266
+ dataTask,
267
+ completionHandler: completionHandler,
268
+ message:
269
+ "ARTIFACT_NETWORK_FAILED: firmware partial file could not be opened"
270
+ )
271
+ }
272
+ }
273
+
274
+ func urlSession(
275
+ _ session: URLSession,
276
+ dataTask: URLSessionDataTask,
277
+ didReceive data: Data
278
+ ) {
279
+ guard responseAccepted, let handle else {
280
+ fail(
281
+ dataTask,
282
+ message:
283
+ "ARTIFACT_PROTOCOL_INVALID: firmware response stream was not accepted"
284
+ )
285
+ return
286
+ }
287
+ guard !isCancelled() else {
288
+ fail(
289
+ dataTask,
290
+ message: "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
291
+ )
292
+ return
293
+ }
294
+ guard
295
+ firmwareArtifactResponseFits(
296
+ expectedContentLength: Int64(data.count),
297
+ baseOffset: written,
298
+ maxBytes: maxBytes
299
+ )
300
+ else {
301
+ fail(
302
+ dataTask,
303
+ message:
304
+ "ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
305
+ )
306
+ return
307
+ }
308
+ do {
309
+ try handle.write(contentsOf: data)
310
+ written += Int64(data.count)
311
+ } catch {
312
+ fail(
313
+ dataTask,
314
+ message:
315
+ "ARTIFACT_NETWORK_FAILED: firmware response stream could not be persisted"
316
+ )
317
+ }
318
+ }
319
+
320
+ func urlSession(
321
+ _ session: URLSession,
322
+ task: URLSessionTask,
323
+ didCompleteWithError error: Error?
324
+ ) {
325
+ if let error {
326
+ finish(.failure(error))
327
+ return
328
+ }
329
+ guard responseAccepted, let handle else {
330
+ finish(
331
+ .failure(FirmwareArtifactStoreError.downloadFailed(
332
+ "ARTIFACT_PROTOCOL_INVALID: firmware response stream was not accepted"
333
+ ))
334
+ )
335
+ return
336
+ }
337
+ do {
338
+ try handle.synchronize()
339
+ finish(.success(()))
340
+ } catch {
341
+ finish(
342
+ .failure(FirmwareArtifactStoreError.downloadFailed(
343
+ "ARTIFACT_NETWORK_FAILED: firmware partial file could not be synchronized"
344
+ ))
345
+ )
346
+ }
68
347
  }
69
348
  }
70
349
 
@@ -219,7 +498,11 @@ final class FirmwareArtifactStore {
219
498
  transactionId: params.transactionId,
220
499
  artifactRef: "fw:\(expectedSha256)"
221
500
  )
222
- let key = "\(expectedSha256):\(Int64(params.expectedSize))"
501
+ let key = firmwareArtifactDownloadKey(
502
+ transactionId: params.transactionId,
503
+ expectedSize: Int64(params.expectedSize),
504
+ expectedSha256: expectedSha256
505
+ )
223
506
  markDownloadActive(expectedSha256, delta: 1)
224
507
  do {
225
508
  let artifact = try await downloadCoordinator.run(
@@ -282,7 +565,11 @@ final class FirmwareArtifactStore {
282
565
  }
283
566
 
284
567
  let partialURL = rootURL.appendingPathComponent(
285
- "\(expectedSha256).\(params.taskId).partial",
568
+ firmwareArtifactPartialFileName(
569
+ transactionId: params.transactionId,
570
+ taskId: params.taskId,
571
+ expectedSha256: expectedSha256
572
+ ),
286
573
  isDirectory: false
287
574
  )
288
575
  if !fileManager.fileExists(atPath: partialURL.path) {
@@ -913,6 +1200,29 @@ final class FirmwareArtifactStore {
913
1200
  _ params: FirmwareArtifactDownloadParams,
914
1201
  partialURL: URL,
915
1202
  resumeOffset: Int64
1203
+ ) async throws {
1204
+ do {
1205
+ try await FirmwareArtifactWallClockDeadline.run(
1206
+ timeoutSeconds:
1207
+ params.overallDeadlineSeconds ?? Self.defaultDownloadDeadline
1208
+ ) { [self] in
1209
+ try await streamDownloadWithinDeadline(
1210
+ params,
1211
+ partialURL: partialURL,
1212
+ resumeOffset: resumeOffset
1213
+ )
1214
+ }
1215
+ } catch FirmwareArtifactDeadlineError.exceeded {
1216
+ throw FirmwareArtifactStoreError.downloadFailed(
1217
+ "ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
1218
+ )
1219
+ }
1220
+ }
1221
+
1222
+ private func streamDownloadWithinDeadline(
1223
+ _ params: FirmwareArtifactDownloadParams,
1224
+ partialURL: URL,
1225
+ resumeOffset: Int64
916
1226
  ) async throws {
917
1227
  guard let url = URL(string: params.url), let hostname = url.host else {
918
1228
  throw FirmwareArtifactStoreError.invalidInput("Invalid firmware URL")
@@ -926,19 +1236,30 @@ final class FirmwareArtifactStore {
926
1236
  request.setValue("bytes=\(resumeOffset)-", forHTTPHeaderField: "Range")
927
1237
  }
928
1238
 
1239
+ let streamDelegate = FirmwareArtifactStreamDelegate(
1240
+ partialURL: partialURL,
1241
+ hostname: hostname,
1242
+ resumeOffset: resumeOffset,
1243
+ expectedSize: Int64(params.expectedSize),
1244
+ maxBytes: Int64(params.maxBytes),
1245
+ isCancelled: { [weak self] in
1246
+ Task.isCancelled ||
1247
+ self?.isTransactionCancelled(params.transactionId) == true
1248
+ }
1249
+ )
929
1250
  let pinnedSession: SniConnectPinnedSession?
930
1251
  if params.routeType == "pinnedIp" {
931
1252
  pinnedSession = try SniConnectPinnedTransport.makeSession(
932
1253
  hostname: hostname,
933
- ip: params.resolvedIp!
1254
+ ip: params.resolvedIp!,
1255
+ dataDelegate: streamDelegate
934
1256
  )
935
1257
  } else {
936
1258
  pinnedSession = nil
937
1259
  }
938
- let domainDelegate = FirmwareArtifactRedirectDelegate()
939
1260
  let session = pinnedSession?.session ?? URLSession(
940
1261
  configuration: .ephemeral,
941
- delegate: domainDelegate,
1262
+ delegate: streamDelegate,
942
1263
  delegateQueue: nil
943
1264
  )
944
1265
  defer {
@@ -949,76 +1270,8 @@ final class FirmwareArtifactStore {
949
1270
  }
950
1271
  }
951
1272
 
952
- let bytes: URLSession.AsyncBytes
953
- let response: URLResponse
954
1273
  do {
955
- (bytes, response) = try await session.bytes(for: request)
956
- } catch {
957
- if isCancellationError(
958
- error,
959
- transactionId: params.transactionId
960
- ) {
961
- throw FirmwareArtifactStoreError.downloadFailed(
962
- "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
963
- )
964
- }
965
- if isFirmwareArtifactTLSError(error) {
966
- throw FirmwareArtifactStoreError.downloadFailed(
967
- "ARTIFACT_TLS_FAILED: firmware TLS validation failed"
968
- )
969
- }
970
- throw FirmwareArtifactStoreError.downloadFailed(
971
- "ARTIFACT_NETWORK_FAILED: firmware request failed"
972
- )
973
- }
974
- guard let httpResponse = response as? HTTPURLResponse else {
975
- throw FirmwareArtifactStoreError.downloadFailed("Firmware response is not HTTP")
976
- }
977
- guard httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else {
978
- throw FirmwareArtifactStoreError.downloadFailed(
979
- "ARTIFACT_HTTP_\(httpResponse.statusCode): firmware request failed"
980
- )
981
- }
982
- let append = resumeOffset > 0 && httpResponse.statusCode == 206
983
- if httpResponse.statusCode == 206 {
984
- guard
985
- let contentRange = httpResponse.value(forHTTPHeaderField: "Content-Range"),
986
- validateContentRange(
987
- contentRange,
988
- expectedStart: append ? resumeOffset : 0,
989
- expectedTotal: Int64(params.expectedSize)
990
- )
991
- else {
992
- throw FirmwareArtifactStoreError.downloadFailed(
993
- "ARTIFACT_PROTOCOL_INVALID: firmware resume Content-Range is invalid"
994
- )
995
- }
996
- }
997
- let handle = try FileHandle(forWritingTo: partialURL)
998
- defer { try? handle.close() }
999
- if append {
1000
- try handle.seekToEnd()
1001
- } else {
1002
- try handle.truncate(atOffset: 0)
1003
- }
1004
-
1005
- var written = append ? resumeOffset : 0
1006
- var buffer = Data()
1007
- buffer.reserveCapacity(64 * 1024)
1008
- do {
1009
- for try await byte in bytes {
1010
- buffer.append(byte)
1011
- if buffer.count >= 64 * 1024 {
1012
- written += Int64(buffer.count)
1013
- guard written <= Int64(params.maxBytes) else {
1014
- throw FirmwareArtifactStoreError.downloadFailed(
1015
- "ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
1016
- )
1017
- }
1018
- try handle.write(contentsOf: buffer)
1019
- buffer.removeAll(keepingCapacity: true)
1020
- }
1021
- }
1274
+ try await streamDelegate.run(session: session, request: request)
1022
1275
  } catch let error as FirmwareArtifactStoreError {
1023
1276
  throw error
1024
1277
  } catch {
@@ -1036,19 +1289,9 @@ final class FirmwareArtifactStore {
1036
1289
  )
1037
1290
  }
1038
1291
  throw FirmwareArtifactStoreError.downloadFailed(
1039
- "ARTIFACT_NETWORK_FAILED: firmware response stream failed"
1292
+ "ARTIFACT_NETWORK_FAILED: firmware request failed"
1040
1293
  )
1041
1294
  }
1042
- if !buffer.isEmpty {
1043
- written += Int64(buffer.count)
1044
- guard written <= Int64(params.maxBytes) else {
1045
- throw FirmwareArtifactStoreError.downloadFailed(
1046
- "ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
1047
- )
1048
- }
1049
- try handle.write(contentsOf: buffer)
1050
- }
1051
- try handle.synchronize()
1052
1295
  try rejectIfCancelled(transactionId: params.transactionId)
1053
1296
  }
1054
1297
 
@@ -1079,34 +1322,6 @@ final class FirmwareArtifactStore {
1079
1322
  nsError.code == NSURLErrorCancelled
1080
1323
  }
1081
1324
 
1082
- private func validateContentRange(
1083
- _ value: String,
1084
- expectedStart: Int64,
1085
- expectedTotal: Int64
1086
- ) -> Bool {
1087
- let pattern = #"^bytes ([0-9]+)-([0-9]+)/([0-9]+)$"#
1088
- guard
1089
- let expression = try? NSRegularExpression(pattern: pattern),
1090
- let match = expression.firstMatch(
1091
- in: value.lowercased(),
1092
- range: NSRange(value.startIndex..., in: value)
1093
- ),
1094
- match.range.location != NSNotFound,
1095
- let startRange = Range(match.range(at: 1), in: value),
1096
- let endRange = Range(match.range(at: 2), in: value),
1097
- let totalRange = Range(match.range(at: 3), in: value),
1098
- let start = Int64(value[startRange]),
1099
- let end = Int64(value[endRange]),
1100
- let total = Int64(value[totalRange])
1101
- else {
1102
- return false
1103
- }
1104
- return start == expectedStart &&
1105
- end >= start &&
1106
- end < total &&
1107
- total == expectedTotal
1108
- }
1109
-
1110
1325
  private func validateStoredArtifact(
1111
1326
  fileURL: URL,
1112
1327
  expectedSize: Int64,
@@ -1,6 +1,126 @@
1
1
  import Foundation
2
2
  import CommonCrypto
3
3
 
4
+ enum FirmwareArtifactDeadlineError: Error {
5
+ case exceeded
6
+ }
7
+
8
+ enum FirmwareArtifactWallClockDeadline {
9
+ static func run<T>(
10
+ timeoutSeconds: TimeInterval,
11
+ operation: @escaping @Sendable () async throws -> T
12
+ ) async throws -> T {
13
+ let timeoutNanoseconds = UInt64(max(0.001, timeoutSeconds) * 1_000_000_000.0)
14
+ return try await withThrowingTaskGroup(of: T.self) { group in
15
+ group.addTask {
16
+ try await operation()
17
+ }
18
+ group.addTask {
19
+ try await Task.sleep(nanoseconds: timeoutNanoseconds)
20
+ throw FirmwareArtifactDeadlineError.exceeded
21
+ }
22
+ defer { group.cancelAll() }
23
+ guard let result = try await group.next() else {
24
+ throw FirmwareArtifactDeadlineError.exceeded
25
+ }
26
+ return result
27
+ }
28
+ }
29
+ }
30
+
31
+ func firmwareArtifactDownloadKey(
32
+ transactionId: String,
33
+ expectedSize: Int64,
34
+ expectedSha256: String
35
+ ) -> String {
36
+ "\(transactionId)|\(expectedSize)|\(expectedSha256)"
37
+ }
38
+
39
+ func firmwareArtifactPartialFileName(
40
+ transactionId: String,
41
+ taskId: String,
42
+ expectedSha256: String
43
+ ) -> String {
44
+ let transactionData = Data(transactionId.utf8)
45
+ var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
46
+ transactionData.withUnsafeBytes {
47
+ _ = CC_SHA256($0.baseAddress, CC_LONG(transactionData.count), &hash)
48
+ }
49
+ let transactionToken = hash
50
+ .prefix(8)
51
+ .map { String(format: "%02x", $0) }
52
+ .joined()
53
+ return "\(expectedSha256).\(taskId).\(transactionToken).partial"
54
+ }
55
+
56
+ func firmwareArtifactContentRangeIsValid(
57
+ _ value: String,
58
+ expectedStart: Int64,
59
+ expectedTotal: Int64
60
+ ) -> Bool {
61
+ guard value.lowercased().hasPrefix("bytes ") else {
62
+ return false
63
+ }
64
+ guard
65
+ let bounds = RangeDownloadLogic.parseContentRangeBounds(value),
66
+ let total = RangeDownloadLogic.parseContentRangeTotal(value)
67
+ else {
68
+ return false
69
+ }
70
+ return bounds.start == expectedStart &&
71
+ bounds.end >= bounds.start &&
72
+ bounds.end < total &&
73
+ total == expectedTotal
74
+ }
75
+
76
+ func firmwareArtifactResponseFits(
77
+ expectedContentLength: Int64,
78
+ baseOffset: Int64,
79
+ maxBytes: Int64
80
+ ) -> Bool {
81
+ guard baseOffset >= 0, baseOffset <= maxBytes else {
82
+ return false
83
+ }
84
+ return expectedContentLength < 0 ||
85
+ expectedContentLength <= maxBytes - baseOffset
86
+ }
87
+
88
+ enum FirmwareBackgroundDownloadError: LocalizedError, CustomStringConvertible {
89
+ case cancelled
90
+ case invalidTask
91
+ case deadlineExceeded
92
+ case redirectRejected
93
+ case responseRejected
94
+ case sizeRejected
95
+ case tlsRejected
96
+ case transferFailed
97
+
98
+ var errorDescription: String? {
99
+ switch self {
100
+ case .cancelled:
101
+ return "ARTIFACT_CANCELLED: firmware background download was cancelled"
102
+ case .invalidTask:
103
+ return "ARTIFACT_PROTOCOL_INVALID: firmware background task is invalid"
104
+ case .deadlineExceeded:
105
+ return "ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
106
+ case .redirectRejected:
107
+ return "ARTIFACT_REDIRECT_REJECTED: firmware redirect changed canonical identity"
108
+ case .responseRejected:
109
+ return "ARTIFACT_PROTOCOL_INVALID: firmware response is invalid"
110
+ case .sizeRejected:
111
+ return "ARTIFACT_PROTOCOL_INVALID: firmware artifact size is invalid"
112
+ case .tlsRejected:
113
+ return "ARTIFACT_TLS_FAILED: firmware TLS validation failed"
114
+ case .transferFailed:
115
+ return "ARTIFACT_NETWORK_FAILED: firmware background transfer failed"
116
+ }
117
+ }
118
+
119
+ var description: String {
120
+ errorDescription ?? "ARTIFACT_NETWORK_FAILED: firmware background transfer failed"
121
+ }
122
+ }
123
+
4
124
  // MARK: - Dependency-free RangeDownloader logic (OCDS §4 / §5)
5
125
  //
6
126
  // This file holds the DETERMINISTIC, dependency-light pieces of the range
@@ -323,49 +323,22 @@ private struct FirmwareBackgroundTaskDescriptor: Codable, Equatable {
323
323
  let deadlineAt: TimeInterval
324
324
 
325
325
  var key: String {
326
- "\(expectedSize)|\(expectedSha256)"
326
+ firmwareArtifactDownloadKey(
327
+ transactionId: transactionId,
328
+ expectedSize: expectedSize,
329
+ expectedSha256: expectedSha256
330
+ )
327
331
  }
328
332
 
329
- func hasSameArtifactIdentity(
333
+ func hasSameDownloadIdentity(
330
334
  as other: FirmwareBackgroundTaskDescriptor
331
335
  ) -> Bool {
332
- expectedSize == other.expectedSize &&
336
+ transactionId == other.transactionId &&
337
+ expectedSize == other.expectedSize &&
333
338
  expectedSha256 == other.expectedSha256
334
339
  }
335
340
  }
336
341
 
337
- private enum FirmwareBackgroundDownloadError: LocalizedError {
338
- case cancelled
339
- case invalidTask
340
- case deadlineExceeded
341
- case redirectRejected
342
- case responseRejected
343
- case sizeRejected
344
- case tlsRejected
345
- case transferFailed
346
-
347
- var errorDescription: String? {
348
- switch self {
349
- case .cancelled:
350
- return "ARTIFACT_CANCELLED: firmware background download was cancelled"
351
- case .invalidTask:
352
- return "ARTIFACT_PROTOCOL_INVALID: firmware background task is invalid"
353
- case .deadlineExceeded:
354
- return "ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
355
- case .redirectRejected:
356
- return "ARTIFACT_REDIRECT_REJECTED: firmware redirect changed canonical identity"
357
- case .responseRejected:
358
- return "ARTIFACT_PROTOCOL_INVALID: firmware response is invalid"
359
- case .sizeRejected:
360
- return "ARTIFACT_PROTOCOL_INVALID: firmware artifact size is invalid"
361
- case .tlsRejected:
362
- return "ARTIFACT_TLS_FAILED: firmware TLS validation failed"
363
- case .transferFailed:
364
- return "ARTIFACT_NETWORK_FAILED: firmware background transfer failed"
365
- }
366
- }
367
- }
368
-
369
342
  public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
370
343
 
371
344
  public static let shared = RangeDownloader()
@@ -836,9 +809,12 @@ public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
836
809
  else {
837
810
  continue
838
811
  }
839
- if candidate.hasSameArtifactIdentity(as: descriptor) {
812
+ if candidate.hasSameDownloadIdentity(as: descriptor) {
840
813
  matchingTaskFound = true
841
- } else if candidate.taskId == descriptor.taskId {
814
+ } else if
815
+ candidate.transactionId == descriptor.transactionId &&
816
+ candidate.taskId == descriptor.taskId
817
+ {
842
818
  task.cancel()
843
819
  }
844
820
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-range-downloader",
3
- "version": "3.0.81-alpha.2",
3
+ "version": "3.0.81-alpha.4",
4
4
  "description": "react-native-range-downloader",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -88,7 +88,7 @@
88
88
  "typescript": "^5.9.2"
89
89
  },
90
90
  "peerDependencies": {
91
- "@onekeyfe/react-native-sni-connect": "3.0.81-alpha.2",
91
+ "@onekeyfe/react-native-sni-connect": "3.0.81-alpha.4",
92
92
  "react": "*",
93
93
  "react-native": "*",
94
94
  "react-native-nitro-modules": "0.33.2"