@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.
Files changed (23) hide show
  1. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +59 -127
  2. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +16 -20
  3. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDeadlineTest.kt +26 -0
  4. package/ios/FirmwareArtifactStore.swift +125 -170
  5. package/ios/RangeDownloadLogic.swift +71 -0
  6. package/ios/ReactNativeRangeDownloader.swift +22 -60
  7. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +0 -4
  8. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
  9. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +0 -19
  10. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +0 -1
  11. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +0 -4
  12. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +0 -3
  13. package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +0 -11
  14. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +0 -1
  15. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +0 -19
  16. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +0 -1
  17. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +0 -4
  18. package/package.json +2 -2
  19. package/src/ReactNativeRangeDownloader.nitro.ts +1 -8
  20. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseReconcileParams.hpp +0 -76
  21. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseReconcileParams.kt +0 -38
  22. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseReconcileParams.swift +0 -48
  23. 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 maxLeaseMetadataBytes: Int64 = 1024 * 1024
83
- private static let maxTotalLeaseRefs = 8192
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: Codable {
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
- "\(params.transactionId):\(expectedSha256):\(Int64(params.expectedSize))"
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 retainExpectedArtifact(
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: Bool
364
- do {
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
- var envelope = try loadLeasesLocked()
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
- envelope.leases[leaseRef] = LeaseState(
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 = envelope.leases.removeValue(
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: Set<String>
651
- do {
652
- retained = Set(
653
- try loadLeasesLocked().leases.values
654
- .flatMap(\.artifactRefs)
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 != "leases.json", name.count >= 64 else { continue }
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 loadLeasesLocked().leases[validateLeaseRef(leaseRef)] != nil else {
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 = envelope.leases[validatedLeaseRef] else {
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
- envelope.leases[validatedLeaseRef] = lease
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 = params.overallDeadlineSeconds ?? 180
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 bytes: URLSession.AsyncBytes
979
+ let downloadedURL: URL
1041
980
  let response: URLResponse
1042
981
  do {
1043
- (bytes, response) = try await session.bytes(for: request)
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
- var buffer = Data()
1090
- buffer.reserveCapacity(64 * 1024)
1044
+ let source = try FileHandle(forReadingFrom: downloadedURL)
1045
+ defer { try? source.close() }
1091
1046
  do {
1092
- for try await byte in bytes {
1093
- buffer.append(byte)
1094
- if buffer.count >= 64 * 1024 {
1095
- written += Int64(buffer.count)
1096
- guard written <= Int64(params.maxBytes) else {
1097
- throw FirmwareArtifactStoreError.downloadFailed(
1098
- "ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
1099
- )
1100
- }
1101
- try handle.write(contentsOf: buffer)
1102
- buffer.removeAll(keepingCapacity: true)
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
- throw FirmwareArtifactStoreError.downloadFailed(
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
- "ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
1077
+ "ARTIFACT_TLS_FAILED: firmware TLS validation failed"
1125
1078
  )
1126
1079
  }
1127
- try handle.write(contentsOf: buffer)
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