@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
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
326
|
+
firmwareArtifactDownloadKey(
|
|
327
|
+
transactionId: transactionId,
|
|
328
|
+
expectedSize: expectedSize,
|
|
329
|
+
expectedSha256: expectedSha256
|
|
330
|
+
)
|
|
327
331
|
}
|
|
328
332
|
|
|
329
|
-
func
|
|
333
|
+
func hasSameDownloadIdentity(
|
|
330
334
|
as other: FirmwareBackgroundTaskDescriptor
|
|
331
335
|
) -> Bool {
|
|
332
|
-
|
|
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.
|
|
812
|
+
if candidate.hasSameDownloadIdentity(as: descriptor) {
|
|
840
813
|
matchingTaskFound = true
|
|
841
|
-
} else if
|
|
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.
|
|
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.
|
|
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"
|