@onekeyfe/react-native-range-downloader 3.0.81-alpha.1 → 3.0.81-alpha.3
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.
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +59 -127
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +16 -20
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDeadlineTest.kt +26 -0
- package/ios/FirmwareArtifactStore.swift +125 -170
- package/ios/RangeDownloadLogic.swift +71 -0
- package/ios/ReactNativeRangeDownloader.swift +22 -60
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +0 -4
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +0 -19
- package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +0 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +0 -4
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +0 -3
- package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +0 -11
- package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +0 -1
- package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +0 -19
- package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +0 -1
- package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +0 -4
- package/package.json +2 -2
- package/src/ReactNativeRangeDownloader.nitro.ts +1 -8
- package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseReconcileParams.hpp +0 -76
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseReconcileParams.kt +0 -38
- package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseReconcileParams.swift +0 -48
- package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseReconcileParams.hpp +0 -76
|
@@ -2,6 +2,33 @@ import CryptoKit
|
|
|
2
2
|
import Foundation
|
|
3
3
|
import SniConnect
|
|
4
4
|
|
|
5
|
+
func isFirmwareArtifactTLSError(_ error: Error) -> Bool {
|
|
6
|
+
var current: NSError? = error as NSError
|
|
7
|
+
var visited = Set<ObjectIdentifier>()
|
|
8
|
+
while let candidate = current {
|
|
9
|
+
let identifier = ObjectIdentifier(candidate)
|
|
10
|
+
guard visited.insert(identifier).inserted else {
|
|
11
|
+
break
|
|
12
|
+
}
|
|
13
|
+
if candidate.domain == NSURLErrorDomain,
|
|
14
|
+
[
|
|
15
|
+
NSURLErrorSecureConnectionFailed,
|
|
16
|
+
NSURLErrorServerCertificateHasBadDate,
|
|
17
|
+
NSURLErrorServerCertificateUntrusted,
|
|
18
|
+
NSURLErrorServerCertificateHasUnknownRoot,
|
|
19
|
+
NSURLErrorServerCertificateNotYetValid,
|
|
20
|
+
NSURLErrorClientCertificateRejected,
|
|
21
|
+
NSURLErrorClientCertificateRequired,
|
|
22
|
+
NSURLErrorAppTransportSecurityRequiresSecureConnection,
|
|
23
|
+
].contains(candidate.code)
|
|
24
|
+
{
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
current = candidate.userInfo[NSUnderlyingErrorKey] as? NSError
|
|
28
|
+
}
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
5
32
|
enum FirmwareArtifactStoreError: Error {
|
|
6
33
|
case invalidInput(String)
|
|
7
34
|
case downloadFailed(String)
|
|
@@ -79,8 +106,8 @@ private actor FirmwareArtifactDownloadCoordinator {
|
|
|
79
106
|
final class FirmwareArtifactStore {
|
|
80
107
|
static let shared = FirmwareArtifactStore()
|
|
81
108
|
static let maxReadBytes = 256 * 1024
|
|
82
|
-
private static let
|
|
83
|
-
private static let
|
|
109
|
+
private static let defaultDownloadDeadline: TimeInterval = 180
|
|
110
|
+
private static let maxDownloadDeadline: TimeInterval = 24 * 60 * 60
|
|
84
111
|
private static let finalArtifactGrace: TimeInterval = 24 * 60 * 60
|
|
85
112
|
private static let partialArtifactGrace: TimeInterval = 7 * 24 * 60 * 60
|
|
86
113
|
|
|
@@ -90,16 +117,11 @@ final class FirmwareArtifactStore {
|
|
|
90
117
|
let size: Int64
|
|
91
118
|
}
|
|
92
119
|
|
|
93
|
-
private struct LeaseState
|
|
120
|
+
private struct LeaseState {
|
|
94
121
|
let transactionId: String
|
|
95
122
|
var artifactRefs: Set<String>
|
|
96
123
|
}
|
|
97
124
|
|
|
98
|
-
private struct LeaseEnvelope: Codable {
|
|
99
|
-
let schemaVersion: Int
|
|
100
|
-
var leases: [String: LeaseState]
|
|
101
|
-
}
|
|
102
|
-
|
|
103
125
|
private let fileManager = FileManager.default
|
|
104
126
|
private let downloadCoordinator = FirmwareArtifactDownloadCoordinator()
|
|
105
127
|
private let leaseLock = NSLock()
|
|
@@ -109,6 +131,7 @@ final class FirmwareArtifactStore {
|
|
|
109
131
|
private var cancelledTransactions: Set<String> = []
|
|
110
132
|
private let readerLock = NSLock()
|
|
111
133
|
private var readers: [String: OpenReader] = [:]
|
|
134
|
+
private var leases: [String: LeaseState] = [:]
|
|
112
135
|
|
|
113
136
|
private lazy var rootURL: URL = {
|
|
114
137
|
let base = fileManager.urls(
|
|
@@ -170,6 +193,14 @@ final class FirmwareArtifactStore {
|
|
|
170
193
|
guard params.routeType == "domain" || params.routeType == "pinnedIp" else {
|
|
171
194
|
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware route type")
|
|
172
195
|
}
|
|
196
|
+
let deadline = params.overallDeadlineSeconds ?? defaultDownloadDeadline
|
|
197
|
+
guard
|
|
198
|
+
deadline.isFinite,
|
|
199
|
+
deadline > 0,
|
|
200
|
+
deadline <= maxDownloadDeadline
|
|
201
|
+
else {
|
|
202
|
+
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware download deadline")
|
|
203
|
+
}
|
|
173
204
|
if params.routeType == "pinnedIp" {
|
|
174
205
|
guard let resolvedIp = params.resolvedIp, !resolvedIp.isEmpty else {
|
|
175
206
|
throw FirmwareArtifactStoreError.invalidInput("Pinned route requires resolvedIp")
|
|
@@ -188,8 +219,11 @@ final class FirmwareArtifactStore {
|
|
|
188
219
|
transactionId: params.transactionId,
|
|
189
220
|
artifactRef: "fw:\(expectedSha256)"
|
|
190
221
|
)
|
|
191
|
-
let key =
|
|
192
|
-
|
|
222
|
+
let key = firmwareArtifactDownloadKey(
|
|
223
|
+
transactionId: params.transactionId,
|
|
224
|
+
expectedSize: Int64(params.expectedSize),
|
|
225
|
+
expectedSha256: expectedSha256
|
|
226
|
+
)
|
|
193
227
|
markDownloadActive(expectedSha256, delta: 1)
|
|
194
228
|
do {
|
|
195
229
|
let artifact = try await downloadCoordinator.run(
|
|
@@ -202,6 +236,7 @@ final class FirmwareArtifactStore {
|
|
|
202
236
|
expectedSha256: expectedSha256
|
|
203
237
|
)
|
|
204
238
|
}
|
|
239
|
+
try rejectIfCancelled(transactionId: params.transactionId)
|
|
205
240
|
markDownloadActive(expectedSha256, delta: -1)
|
|
206
241
|
return artifact
|
|
207
242
|
} catch {
|
|
@@ -309,17 +344,12 @@ final class FirmwareArtifactStore {
|
|
|
309
344
|
|
|
310
345
|
func acceptBackgroundDownload(
|
|
311
346
|
temporaryURL: URL,
|
|
312
|
-
leaseRef: String,
|
|
313
347
|
transactionId: String,
|
|
314
348
|
expectedSize: Int64,
|
|
315
349
|
expectedSha256: String
|
|
316
350
|
) throws -> StoredFirmwareArtifact {
|
|
317
351
|
let normalizedExpectedSha256 = expectedSha256.lowercased()
|
|
318
|
-
try
|
|
319
|
-
leaseRef: leaseRef,
|
|
320
|
-
transactionId: transactionId,
|
|
321
|
-
artifactRef: "fw:\(normalizedExpectedSha256)"
|
|
322
|
-
)
|
|
352
|
+
try rejectIfCancelled(transactionId: transactionId)
|
|
323
353
|
let finalURL = artifactURL(sha256: normalizedExpectedSha256)
|
|
324
354
|
if let existing = try? validateStoredArtifact(
|
|
325
355
|
fileURL: finalURL,
|
|
@@ -360,14 +390,8 @@ final class FirmwareArtifactStore {
|
|
|
360
390
|
func discard(artifactRef: String) throws {
|
|
361
391
|
let fileURL = try resolveArtifactURL(artifactRef)
|
|
362
392
|
leaseLock.lock()
|
|
363
|
-
let isRetained
|
|
364
|
-
|
|
365
|
-
isRetained = try loadLeasesLocked().leases.values.contains {
|
|
366
|
-
$0.artifactRefs.contains(artifactRef)
|
|
367
|
-
}
|
|
368
|
-
} catch {
|
|
369
|
-
leaseLock.unlock()
|
|
370
|
-
throw error
|
|
393
|
+
let isRetained = leases.values.contains {
|
|
394
|
+
$0.artifactRefs.contains(artifactRef)
|
|
371
395
|
}
|
|
372
396
|
leaseLock.unlock()
|
|
373
397
|
guard !isRetained else {
|
|
@@ -566,18 +590,16 @@ final class FirmwareArtifactStore {
|
|
|
566
590
|
}
|
|
567
591
|
leaseLock.lock()
|
|
568
592
|
defer { leaseLock.unlock() }
|
|
569
|
-
|
|
570
|
-
guard envelope.leases.count < 32 else {
|
|
593
|
+
guard leases.count < 32 else {
|
|
571
594
|
throw FirmwareArtifactStoreError.invalidInput(
|
|
572
595
|
"Too many firmware artifact leases"
|
|
573
596
|
)
|
|
574
597
|
}
|
|
575
598
|
let leaseRef = "fwlease:\(UUID().uuidString.lowercased())"
|
|
576
|
-
|
|
599
|
+
leases[leaseRef] = LeaseState(
|
|
577
600
|
transactionId: transactionId,
|
|
578
601
|
artifactRefs: []
|
|
579
602
|
)
|
|
580
|
-
try saveLeasesLocked(envelope)
|
|
581
603
|
return leaseRef
|
|
582
604
|
}
|
|
583
605
|
|
|
@@ -603,9 +625,8 @@ final class FirmwareArtifactStore {
|
|
|
603
625
|
let transactionId: String = try {
|
|
604
626
|
leaseLock.lock()
|
|
605
627
|
defer { leaseLock.unlock() }
|
|
606
|
-
var envelope = try loadLeasesLocked()
|
|
607
628
|
guard
|
|
608
|
-
let lease =
|
|
629
|
+
let lease = leases.removeValue(
|
|
609
630
|
forKey: try validateLeaseRef(leaseRef)
|
|
610
631
|
)
|
|
611
632
|
else {
|
|
@@ -613,7 +634,6 @@ final class FirmwareArtifactStore {
|
|
|
613
634
|
"Firmware artifact lease is unavailable"
|
|
614
635
|
)
|
|
615
636
|
}
|
|
616
|
-
try saveLeasesLocked(envelope)
|
|
617
637
|
return lease.transactionId
|
|
618
638
|
}()
|
|
619
639
|
cancellationLock.withFirmwareArtifactLock {
|
|
@@ -621,43 +641,13 @@ final class FirmwareArtifactStore {
|
|
|
621
641
|
}
|
|
622
642
|
}
|
|
623
643
|
|
|
624
|
-
func reconcileLeases(activeLeaseRefs: [String]) throws {
|
|
625
|
-
guard activeLeaseRefs.count <= 32 else {
|
|
626
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
627
|
-
"Too many active firmware artifact leases"
|
|
628
|
-
)
|
|
629
|
-
}
|
|
630
|
-
let active = try Set(activeLeaseRefs.map(validateLeaseRef))
|
|
631
|
-
guard active.count == activeLeaseRefs.count else {
|
|
632
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
633
|
-
"Duplicate active firmware artifact lease"
|
|
634
|
-
)
|
|
635
|
-
}
|
|
636
|
-
leaseLock.lock()
|
|
637
|
-
defer { leaseLock.unlock() }
|
|
638
|
-
var envelope = try loadLeasesLocked()
|
|
639
|
-
guard active.allSatisfy({ envelope.leases[$0] != nil }) else {
|
|
640
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
641
|
-
"Firmware artifact lease reconciliation is incomplete"
|
|
642
|
-
)
|
|
643
|
-
}
|
|
644
|
-
envelope.leases = envelope.leases.filter { active.contains($0.key) }
|
|
645
|
-
try saveLeasesLocked(envelope)
|
|
646
|
-
}
|
|
647
|
-
|
|
648
644
|
func sweepOrphans() throws -> (deletedFiles: Int, deletedBytes: Int64) {
|
|
649
645
|
leaseLock.lock()
|
|
650
|
-
let retained
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
.map { String($0.dropFirst(3)) }
|
|
656
|
-
)
|
|
657
|
-
} catch {
|
|
658
|
-
leaseLock.unlock()
|
|
659
|
-
throw error
|
|
660
|
-
}
|
|
646
|
+
let retained = Set(
|
|
647
|
+
leases.values
|
|
648
|
+
.flatMap(\.artifactRefs)
|
|
649
|
+
.map { String($0.dropFirst(3)) }
|
|
650
|
+
)
|
|
661
651
|
leaseLock.unlock()
|
|
662
652
|
activeDownloadLock.lock()
|
|
663
653
|
let active = Set(activeDownloadCounts.filter { $0.value > 0 }.keys)
|
|
@@ -676,7 +666,7 @@ final class FirmwareArtifactStore {
|
|
|
676
666
|
)
|
|
677
667
|
for fileURL in files {
|
|
678
668
|
let name = fileURL.lastPathComponent
|
|
679
|
-
guard name
|
|
669
|
+
guard name.count >= 64 else { continue }
|
|
680
670
|
let sha256 = String(name.prefix(64))
|
|
681
671
|
guard
|
|
682
672
|
sha256.range(of: "^[a-f0-9]{64}$", options: .regularExpression) != nil,
|
|
@@ -714,7 +704,7 @@ final class FirmwareArtifactStore {
|
|
|
714
704
|
private func requireLease(_ leaseRef: String) throws {
|
|
715
705
|
leaseLock.lock()
|
|
716
706
|
defer { leaseLock.unlock() }
|
|
717
|
-
guard try
|
|
707
|
+
guard leases[try validateLeaseRef(leaseRef)] != nil else {
|
|
718
708
|
throw FirmwareArtifactStoreError.invalidInput(
|
|
719
709
|
"Firmware artifact lease is unavailable"
|
|
720
710
|
)
|
|
@@ -736,9 +726,8 @@ final class FirmwareArtifactStore {
|
|
|
736
726
|
}
|
|
737
727
|
leaseLock.lock()
|
|
738
728
|
defer { leaseLock.unlock() }
|
|
739
|
-
var envelope = try loadLeasesLocked()
|
|
740
729
|
let validatedLeaseRef = try validateLeaseRef(leaseRef)
|
|
741
|
-
guard var lease =
|
|
730
|
+
guard var lease = leases[validatedLeaseRef] else {
|
|
742
731
|
throw FirmwareArtifactStoreError.invalidInput(
|
|
743
732
|
"Firmware artifact lease is unavailable"
|
|
744
733
|
)
|
|
@@ -749,8 +738,7 @@ final class FirmwareArtifactStore {
|
|
|
749
738
|
)
|
|
750
739
|
}
|
|
751
740
|
if lease.artifactRefs.insert(artifactRef).inserted {
|
|
752
|
-
|
|
753
|
-
try saveLeasesLocked(envelope)
|
|
741
|
+
leases[validatedLeaseRef] = lease
|
|
754
742
|
}
|
|
755
743
|
}
|
|
756
744
|
|
|
@@ -777,79 +765,6 @@ final class FirmwareArtifactStore {
|
|
|
777
765
|
) != nil
|
|
778
766
|
}
|
|
779
767
|
|
|
780
|
-
private func loadLeasesLocked() throws -> LeaseEnvelope {
|
|
781
|
-
let url = rootURL.appendingPathComponent("leases.json", isDirectory: false)
|
|
782
|
-
guard fileManager.fileExists(atPath: url.path) else {
|
|
783
|
-
return LeaseEnvelope(schemaVersion: 1, leases: [:])
|
|
784
|
-
}
|
|
785
|
-
let fileSize = try url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0
|
|
786
|
-
guard fileSize > 0, Int64(fileSize) <= Self.maxLeaseMetadataBytes else {
|
|
787
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
788
|
-
"Firmware lease metadata is too large"
|
|
789
|
-
)
|
|
790
|
-
}
|
|
791
|
-
let envelope = try JSONDecoder().decode(
|
|
792
|
-
LeaseEnvelope.self,
|
|
793
|
-
from: Data(contentsOf: url)
|
|
794
|
-
)
|
|
795
|
-
guard envelope.schemaVersion == 1, envelope.leases.count <= 32 else {
|
|
796
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
797
|
-
"Unsupported firmware lease schema"
|
|
798
|
-
)
|
|
799
|
-
}
|
|
800
|
-
guard
|
|
801
|
-
envelope.leases.values.reduce(0, { $0 + $1.artifactRefs.count })
|
|
802
|
-
<= Self.maxTotalLeaseRefs
|
|
803
|
-
else {
|
|
804
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
805
|
-
"Firmware lease metadata is too large"
|
|
806
|
-
)
|
|
807
|
-
}
|
|
808
|
-
for (leaseRef, lease) in envelope.leases {
|
|
809
|
-
guard
|
|
810
|
-
Self.isValidLeaseRef(leaseRef),
|
|
811
|
-
Self.isSafeIdentifier(lease.transactionId),
|
|
812
|
-
lease.artifactRefs.count <= 4096,
|
|
813
|
-
lease.artifactRefs.allSatisfy({
|
|
814
|
-
$0.range(
|
|
815
|
-
of: "^fw:[a-f0-9]{64}$",
|
|
816
|
-
options: .regularExpression
|
|
817
|
-
) != nil
|
|
818
|
-
})
|
|
819
|
-
else {
|
|
820
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
821
|
-
"Invalid persisted firmware lease"
|
|
822
|
-
)
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
return envelope
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
private func saveLeasesLocked(_ envelope: LeaseEnvelope) throws {
|
|
829
|
-
guard
|
|
830
|
-
envelope.schemaVersion == 1,
|
|
831
|
-
envelope.leases.count <= 32,
|
|
832
|
-
envelope.leases.values.allSatisfy({
|
|
833
|
-
$0.artifactRefs.count <= 4096
|
|
834
|
-
}),
|
|
835
|
-
envelope.leases.values.reduce(0, {
|
|
836
|
-
$0 + $1.artifactRefs.count
|
|
837
|
-
}) <= Self.maxTotalLeaseRefs
|
|
838
|
-
else {
|
|
839
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
840
|
-
"Firmware lease metadata is too large"
|
|
841
|
-
)
|
|
842
|
-
}
|
|
843
|
-
let data = try JSONEncoder().encode(envelope)
|
|
844
|
-
guard Int64(data.count) <= Self.maxLeaseMetadataBytes else {
|
|
845
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
846
|
-
"Firmware lease metadata is too large"
|
|
847
|
-
)
|
|
848
|
-
}
|
|
849
|
-
let url = rootURL.appendingPathComponent("leases.json", isDirectory: false)
|
|
850
|
-
try data.write(to: url, options: [.atomic])
|
|
851
|
-
}
|
|
852
|
-
|
|
853
768
|
private func markDownloadActive(_ sha256: String, delta: Int) {
|
|
854
769
|
activeDownloadLock.lock()
|
|
855
770
|
let next = (activeDownloadCounts[sha256] ?? 0) + delta
|
|
@@ -1002,13 +917,37 @@ final class FirmwareArtifactStore {
|
|
|
1002
917
|
_ params: FirmwareArtifactDownloadParams,
|
|
1003
918
|
partialURL: URL,
|
|
1004
919
|
resumeOffset: Int64
|
|
920
|
+
) async throws {
|
|
921
|
+
do {
|
|
922
|
+
try await FirmwareArtifactWallClockDeadline.run(
|
|
923
|
+
timeoutSeconds:
|
|
924
|
+
params.overallDeadlineSeconds ?? Self.defaultDownloadDeadline
|
|
925
|
+
) { [self] in
|
|
926
|
+
try await streamDownloadWithinDeadline(
|
|
927
|
+
params,
|
|
928
|
+
partialURL: partialURL,
|
|
929
|
+
resumeOffset: resumeOffset
|
|
930
|
+
)
|
|
931
|
+
}
|
|
932
|
+
} catch FirmwareArtifactDeadlineError.exceeded {
|
|
933
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
934
|
+
"ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
|
|
935
|
+
)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
private func streamDownloadWithinDeadline(
|
|
940
|
+
_ params: FirmwareArtifactDownloadParams,
|
|
941
|
+
partialURL: URL,
|
|
942
|
+
resumeOffset: Int64
|
|
1005
943
|
) async throws {
|
|
1006
944
|
guard let url = URL(string: params.url), let hostname = url.host else {
|
|
1007
945
|
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware URL")
|
|
1008
946
|
}
|
|
1009
947
|
var request = URLRequest(url: url)
|
|
1010
948
|
request.cachePolicy = .reloadIgnoringLocalCacheData
|
|
1011
|
-
request.timeoutInterval =
|
|
949
|
+
request.timeoutInterval =
|
|
950
|
+
params.overallDeadlineSeconds ?? Self.defaultDownloadDeadline
|
|
1012
951
|
request.setValue("identity", forHTTPHeaderField: "Accept-Encoding")
|
|
1013
952
|
if resumeOffset > 0 {
|
|
1014
953
|
request.setValue("bytes=\(resumeOffset)-", forHTTPHeaderField: "Range")
|
|
@@ -1037,10 +976,10 @@ final class FirmwareArtifactStore {
|
|
|
1037
976
|
}
|
|
1038
977
|
}
|
|
1039
978
|
|
|
1040
|
-
let
|
|
979
|
+
let downloadedURL: URL
|
|
1041
980
|
let response: URLResponse
|
|
1042
981
|
do {
|
|
1043
|
-
(
|
|
982
|
+
(downloadedURL, response) = try await session.download(for: request)
|
|
1044
983
|
} catch {
|
|
1045
984
|
if isCancellationError(
|
|
1046
985
|
error,
|
|
@@ -1050,13 +989,29 @@ final class FirmwareArtifactStore {
|
|
|
1050
989
|
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
1051
990
|
)
|
|
1052
991
|
}
|
|
992
|
+
if isFirmwareArtifactTLSError(error) {
|
|
993
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
994
|
+
"ARTIFACT_TLS_FAILED: firmware TLS validation failed"
|
|
995
|
+
)
|
|
996
|
+
}
|
|
1053
997
|
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1054
998
|
"ARTIFACT_NETWORK_FAILED: firmware request failed"
|
|
1055
999
|
)
|
|
1056
1000
|
}
|
|
1001
|
+
defer { try? fileManager.removeItem(at: downloadedURL) }
|
|
1057
1002
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
1058
1003
|
throw FirmwareArtifactStoreError.downloadFailed("Firmware response is not HTTP")
|
|
1059
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
|
+
}
|
|
1060
1015
|
guard httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else {
|
|
1061
1016
|
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1062
1017
|
"ARTIFACT_HTTP_\(httpResponse.statusCode): firmware request failed"
|
|
@@ -1086,21 +1041,25 @@ final class FirmwareArtifactStore {
|
|
|
1086
1041
|
}
|
|
1087
1042
|
|
|
1088
1043
|
var written = append ? resumeOffset : 0
|
|
1089
|
-
|
|
1090
|
-
|
|
1044
|
+
let source = try FileHandle(forReadingFrom: downloadedURL)
|
|
1045
|
+
defer { try? source.close() }
|
|
1091
1046
|
do {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
+
)
|
|
1103
1061
|
}
|
|
1062
|
+
try handle.write(contentsOf: buffer)
|
|
1104
1063
|
}
|
|
1105
1064
|
} catch let error as FirmwareArtifactStoreError {
|
|
1106
1065
|
throw error
|
|
@@ -1113,18 +1072,14 @@ final class FirmwareArtifactStore {
|
|
|
1113
1072
|
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
1114
1073
|
)
|
|
1115
1074
|
}
|
|
1116
|
-
|
|
1117
|
-
"ARTIFACT_NETWORK_FAILED: firmware response stream failed"
|
|
1118
|
-
)
|
|
1119
|
-
}
|
|
1120
|
-
if !buffer.isEmpty {
|
|
1121
|
-
written += Int64(buffer.count)
|
|
1122
|
-
guard written <= Int64(params.maxBytes) else {
|
|
1075
|
+
if isFirmwareArtifactTLSError(error) {
|
|
1123
1076
|
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1124
|
-
"
|
|
1077
|
+
"ARTIFACT_TLS_FAILED: firmware TLS validation failed"
|
|
1125
1078
|
)
|
|
1126
1079
|
}
|
|
1127
|
-
|
|
1080
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1081
|
+
"ARTIFACT_NETWORK_FAILED: firmware response stream failed"
|
|
1082
|
+
)
|
|
1128
1083
|
}
|
|
1129
1084
|
try handle.synchronize()
|
|
1130
1085
|
try rejectIfCancelled(transactionId: params.transactionId)
|
|
@@ -1,6 +1,77 @@
|
|
|
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
|
+
enum FirmwareBackgroundDownloadError: LocalizedError, CustomStringConvertible {
|
|
40
|
+
case cancelled
|
|
41
|
+
case invalidTask
|
|
42
|
+
case deadlineExceeded
|
|
43
|
+
case redirectRejected
|
|
44
|
+
case responseRejected
|
|
45
|
+
case sizeRejected
|
|
46
|
+
case tlsRejected
|
|
47
|
+
case transferFailed
|
|
48
|
+
|
|
49
|
+
var errorDescription: String? {
|
|
50
|
+
switch self {
|
|
51
|
+
case .cancelled:
|
|
52
|
+
return "ARTIFACT_CANCELLED: firmware background download was cancelled"
|
|
53
|
+
case .invalidTask:
|
|
54
|
+
return "ARTIFACT_PROTOCOL_INVALID: firmware background task is invalid"
|
|
55
|
+
case .deadlineExceeded:
|
|
56
|
+
return "ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
|
|
57
|
+
case .redirectRejected:
|
|
58
|
+
return "ARTIFACT_REDIRECT_REJECTED: firmware redirect changed canonical identity"
|
|
59
|
+
case .responseRejected:
|
|
60
|
+
return "ARTIFACT_PROTOCOL_INVALID: firmware response is invalid"
|
|
61
|
+
case .sizeRejected:
|
|
62
|
+
return "ARTIFACT_PROTOCOL_INVALID: firmware artifact size is invalid"
|
|
63
|
+
case .tlsRejected:
|
|
64
|
+
return "ARTIFACT_TLS_FAILED: firmware TLS validation failed"
|
|
65
|
+
case .transferFailed:
|
|
66
|
+
return "ARTIFACT_NETWORK_FAILED: firmware background transfer failed"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var description: String {
|
|
71
|
+
errorDescription ?? "ARTIFACT_NETWORK_FAILED: firmware background transfer failed"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
4
75
|
// MARK: - Dependency-free RangeDownloader logic (OCDS §4 / §5)
|
|
5
76
|
//
|
|
6
77
|
// This file holds the DETERMINISTIC, dependency-light pieces of the range
|