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

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.
@@ -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
 
@@ -286,7 +565,11 @@ final class FirmwareArtifactStore {
286
565
  }
287
566
 
288
567
  let partialURL = rootURL.appendingPathComponent(
289
- "\(expectedSha256).\(params.taskId).partial",
568
+ firmwareArtifactPartialFileName(
569
+ transactionId: params.transactionId,
570
+ taskId: params.taskId,
571
+ expectedSha256: expectedSha256
572
+ ),
290
573
  isDirectory: false
291
574
  )
292
575
  if !fileManager.fileExists(atPath: partialURL.path) {
@@ -953,19 +1236,30 @@ final class FirmwareArtifactStore {
953
1236
  request.setValue("bytes=\(resumeOffset)-", forHTTPHeaderField: "Range")
954
1237
  }
955
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
+ )
956
1250
  let pinnedSession: SniConnectPinnedSession?
957
1251
  if params.routeType == "pinnedIp" {
958
1252
  pinnedSession = try SniConnectPinnedTransport.makeSession(
959
1253
  hostname: hostname,
960
- ip: params.resolvedIp!
1254
+ ip: params.resolvedIp!,
1255
+ dataDelegate: streamDelegate
961
1256
  )
962
1257
  } else {
963
1258
  pinnedSession = nil
964
1259
  }
965
- let domainDelegate = FirmwareArtifactRedirectDelegate()
966
1260
  let session = pinnedSession?.session ?? URLSession(
967
1261
  configuration: .ephemeral,
968
- delegate: domainDelegate,
1262
+ delegate: streamDelegate,
969
1263
  delegateQueue: nil
970
1264
  )
971
1265
  defer {
@@ -976,91 +1270,8 @@ final class FirmwareArtifactStore {
976
1270
  }
977
1271
  }
978
1272
 
979
- let downloadedURL: URL
980
- let response: URLResponse
981
- do {
982
- (downloadedURL, response) = try await session.download(for: request)
983
- } catch {
984
- if isCancellationError(
985
- error,
986
- transactionId: params.transactionId
987
- ) {
988
- throw FirmwareArtifactStoreError.downloadFailed(
989
- "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
990
- )
991
- }
992
- if isFirmwareArtifactTLSError(error) {
993
- throw FirmwareArtifactStoreError.downloadFailed(
994
- "ARTIFACT_TLS_FAILED: firmware TLS validation failed"
995
- )
996
- }
997
- throw FirmwareArtifactStoreError.downloadFailed(
998
- "ARTIFACT_NETWORK_FAILED: firmware request failed"
999
- )
1000
- }
1001
- defer { try? fileManager.removeItem(at: downloadedURL) }
1002
- guard let httpResponse = response as? HTTPURLResponse else {
1003
- throw FirmwareArtifactStoreError.downloadFailed("Firmware response is not HTTP")
1004
- }
1005
- guard
1006
- let responseURL = httpResponse.url,
1007
- responseURL.scheme?.lowercased() == "https",
1008
- responseURL.host?.lowercased() == hostname.lowercased(),
1009
- responseURL.port == nil || responseURL.port == 443
1010
- else {
1011
- throw FirmwareArtifactStoreError.downloadFailed(
1012
- "ARTIFACT_REDIRECT_REJECTED: firmware response changed canonical identity"
1013
- )
1014
- }
1015
- guard httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else {
1016
- throw FirmwareArtifactStoreError.downloadFailed(
1017
- "ARTIFACT_HTTP_\(httpResponse.statusCode): firmware request failed"
1018
- )
1019
- }
1020
- let append = resumeOffset > 0 && httpResponse.statusCode == 206
1021
- if httpResponse.statusCode == 206 {
1022
- guard
1023
- let contentRange = httpResponse.value(forHTTPHeaderField: "Content-Range"),
1024
- validateContentRange(
1025
- contentRange,
1026
- expectedStart: append ? resumeOffset : 0,
1027
- expectedTotal: Int64(params.expectedSize)
1028
- )
1029
- else {
1030
- throw FirmwareArtifactStoreError.downloadFailed(
1031
- "ARTIFACT_PROTOCOL_INVALID: firmware resume Content-Range is invalid"
1032
- )
1033
- }
1034
- }
1035
- let handle = try FileHandle(forWritingTo: partialURL)
1036
- defer { try? handle.close() }
1037
- if append {
1038
- try handle.seekToEnd()
1039
- } else {
1040
- try handle.truncate(atOffset: 0)
1041
- }
1042
-
1043
- var written = append ? resumeOffset : 0
1044
- let source = try FileHandle(forReadingFrom: downloadedURL)
1045
- defer { try? source.close() }
1046
1273
  do {
1047
- while true {
1048
- try Task.checkCancellation()
1049
- try rejectIfCancelled(transactionId: params.transactionId)
1050
- guard
1051
- let buffer = try source.read(upToCount: 64 * 1024),
1052
- !buffer.isEmpty
1053
- else {
1054
- break
1055
- }
1056
- written += Int64(buffer.count)
1057
- guard written <= Int64(params.maxBytes) else {
1058
- throw FirmwareArtifactStoreError.downloadFailed(
1059
- "ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
1060
- )
1061
- }
1062
- try handle.write(contentsOf: buffer)
1063
- }
1274
+ try await streamDelegate.run(session: session, request: request)
1064
1275
  } catch let error as FirmwareArtifactStoreError {
1065
1276
  throw error
1066
1277
  } catch {
@@ -1078,10 +1289,9 @@ final class FirmwareArtifactStore {
1078
1289
  )
1079
1290
  }
1080
1291
  throw FirmwareArtifactStoreError.downloadFailed(
1081
- "ARTIFACT_NETWORK_FAILED: firmware response stream failed"
1292
+ "ARTIFACT_NETWORK_FAILED: firmware request failed"
1082
1293
  )
1083
1294
  }
1084
- try handle.synchronize()
1085
1295
  try rejectIfCancelled(transactionId: params.transactionId)
1086
1296
  }
1087
1297
 
@@ -1112,34 +1322,6 @@ final class FirmwareArtifactStore {
1112
1322
  nsError.code == NSURLErrorCancelled
1113
1323
  }
1114
1324
 
1115
- private func validateContentRange(
1116
- _ value: String,
1117
- expectedStart: Int64,
1118
- expectedTotal: Int64
1119
- ) -> Bool {
1120
- let pattern = #"^bytes ([0-9]+)-([0-9]+)/([0-9]+)$"#
1121
- guard
1122
- let expression = try? NSRegularExpression(pattern: pattern),
1123
- let match = expression.firstMatch(
1124
- in: value.lowercased(),
1125
- range: NSRange(value.startIndex..., in: value)
1126
- ),
1127
- match.range.location != NSNotFound,
1128
- let startRange = Range(match.range(at: 1), in: value),
1129
- let endRange = Range(match.range(at: 2), in: value),
1130
- let totalRange = Range(match.range(at: 3), in: value),
1131
- let start = Int64(value[startRange]),
1132
- let end = Int64(value[endRange]),
1133
- let total = Int64(value[totalRange])
1134
- else {
1135
- return false
1136
- }
1137
- return start == expectedStart &&
1138
- end >= start &&
1139
- end < total &&
1140
- total == expectedTotal
1141
- }
1142
-
1143
1325
  private func validateStoredArtifact(
1144
1326
  fileURL: URL,
1145
1327
  expectedSize: Int64,
@@ -36,6 +36,55 @@ func firmwareArtifactDownloadKey(
36
36
  "\(transactionId)|\(expectedSize)|\(expectedSha256)"
37
37
  }
38
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
+
39
88
  enum FirmwareBackgroundDownloadError: LocalizedError, CustomStringConvertible {
40
89
  case cancelled
41
90
  case invalidTask
@@ -1634,24 +1634,24 @@ public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
1634
1634
  if completion.completed {
1635
1635
  return
1636
1636
  }
1637
+ let failure: Error
1638
+ if let recordedError = completion.error {
1639
+ failure = recordedError
1640
+ } else if FirmwareArtifactStore.shared.isTransactionCancelled(
1641
+ descriptor.transactionId
1642
+ ) {
1643
+ failure = FirmwareBackgroundDownloadError.cancelled
1644
+ } else if let taskError = error {
1645
+ failure =
1646
+ isFirmwareArtifactTLSError(taskError)
1647
+ ? FirmwareBackgroundDownloadError.tlsRejected
1648
+ : FirmwareBackgroundDownloadError.transferFailed
1649
+ } else {
1650
+ failure = FirmwareBackgroundDownloadError.transferFailed
1651
+ }
1637
1652
  finishFirmwareTask(
1638
1653
  key: descriptor.key,
1639
- result: .failure(
1640
- completion.error ??
1641
- (
1642
- FirmwareArtifactStore.shared.isTransactionCancelled(
1643
- descriptor.transactionId
1644
- )
1645
- ? FirmwareBackgroundDownloadError.cancelled
1646
- : nil
1647
- ) ??
1648
- error.map {
1649
- isFirmwareArtifactTLSError($0)
1650
- ? FirmwareBackgroundDownloadError.tlsRejected
1651
- : FirmwareBackgroundDownloadError.transferFailed
1652
- } ??
1653
- FirmwareBackgroundDownloadError.transferFailed
1654
- )
1654
+ result: .failure(failure)
1655
1655
  )
1656
1656
  return
1657
1657
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-range-downloader",
3
- "version": "3.0.81-alpha.3",
3
+ "version": "3.0.81-alpha.5",
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.3",
91
+ "@onekeyfe/react-native-sni-connect": "3.0.81-alpha.5",
92
92
  "react": "*",
93
93
  "react-native": "*",
94
94
  "react-native-nitro-modules": "0.33.2"