@onekeyfe/react-native-range-downloader 3.0.81-alpha.7 → 3.0.81-alpha.9

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 (24) hide show
  1. package/ReactNativeRangeDownloader.podspec +1 -1
  2. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRules.kt +12 -5
  3. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweep.kt +111 -0
  4. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +132 -68
  5. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +1 -1
  6. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRulesTest.kt +39 -0
  7. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweepTest.kt +107 -0
  8. package/ios/FirmwareArtifactStore.swift +242 -172
  9. package/ios/RangeDownloadLogic.swift +194 -36
  10. package/ios/ReactNativeRangeDownloader.swift +9 -423
  11. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +3 -3
  12. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
  13. package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeParams.hpp +7 -6
  14. package/nitrogen/generated/android/c++/JFirmwareArtifactDownloadParams.hpp +7 -7
  15. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeParams.kt +2 -2
  16. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDownloadParams.kt +3 -3
  17. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +15 -0
  18. package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeParams.swift +32 -13
  19. package/nitrogen/generated/ios/swift/FirmwareArtifactDownloadParams.swift +39 -8
  20. package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeParams.hpp +6 -5
  21. package/nitrogen/generated/shared/c++/FirmwareArtifactDownloadParams.hpp +9 -9
  22. package/package.json +2 -2
  23. package/src/ReactNativeRangeDownloader.nitro.ts +3 -3
  24. package/ios/FirmwareBackgroundSessionEventRouter.swift +0 -17
@@ -30,14 +30,6 @@ func isFirmwareArtifactTLSError(_ error: Error) -> Bool {
30
30
  return false
31
31
  }
32
32
 
33
- enum FirmwareArtifactStoreError: Error {
34
- case invalidInput(String)
35
- case downloadFailed(String)
36
- case integrityMismatch(String)
37
- case readerInvalid(String)
38
- case archiveInvalid(String)
39
- }
40
-
41
33
  struct StoredFirmwareArtifact: Sendable {
42
34
  let artifactRef: String
43
35
  let size: Int64
@@ -57,11 +49,17 @@ private struct StagedFirmwareArchiveEntry {
57
49
  let stagingURL: URL
58
50
  }
59
51
 
52
+ private struct FirmwareArchiveRequirement {
53
+ let entryName: String
54
+ let expectedSize: Int64
55
+ let expectedSha256: String?
56
+ }
57
+
60
58
  private final class FirmwareArtifactStreamDelegate: NSObject, URLSessionDataDelegate {
61
59
  private let partialURL: URL
62
60
  private let hostname: String
63
61
  private let resumeOffset: Int64
64
- private let expectedSize: Int64
62
+ private let expectedSize: Int64?
65
63
  private let maxBytes: Int64
66
64
  private let isCancelled: () -> Bool
67
65
  private let stateLock = NSLock()
@@ -77,7 +75,7 @@ private final class FirmwareArtifactStreamDelegate: NSObject, URLSessionDataDele
77
75
  partialURL: URL,
78
76
  hostname: String,
79
77
  resumeOffset: Int64,
80
- expectedSize: Int64,
78
+ expectedSize: Int64?,
81
79
  maxBytes: Int64,
82
80
  isCancelled: @escaping () -> Bool
83
81
  ) {
@@ -221,7 +219,8 @@ private final class FirmwareArtifactStreamDelegate: NSObject, URLSessionDataDele
221
219
  firmwareArtifactContentRangeIsValid(
222
220
  contentRange,
223
221
  expectedStart: append ? resumeOffset : 0,
224
- expectedTotal: expectedSize
222
+ expectedTotal: expectedSize,
223
+ maxBytes: maxBytes
225
224
  )
226
225
  else {
227
226
  reject(
@@ -388,8 +387,6 @@ final class FirmwareArtifactStore {
388
387
  static let maxReadBytes = 256 * 1024
389
388
  private static let defaultDownloadDeadline: TimeInterval = 180
390
389
  private static let maxDownloadDeadline: TimeInterval = 24 * 60 * 60
391
- private static let finalArtifactGrace: TimeInterval = 24 * 60 * 60
392
- private static let partialArtifactGrace: TimeInterval = 7 * 24 * 60 * 60
393
390
 
394
391
  private struct OpenReader {
395
392
  let handle: FileHandle
@@ -425,7 +422,16 @@ final class FirmwareArtifactStore {
425
422
 
426
423
  private init() {}
427
424
 
428
- static func validateDownloadParams(_ params: FirmwareArtifactDownloadParams) throws {
425
+ private struct ValidatedDownload {
426
+ let expectedSize: Int64?
427
+ let expectedSha256: String?
428
+ let maxBytes: Int64
429
+ let downloadToken: String
430
+ }
431
+
432
+ private static func validateDownloadParams(
433
+ _ params: FirmwareArtifactDownloadParams
434
+ ) throws -> ValidatedDownload {
429
435
  guard
430
436
  !params.taskId.isEmpty,
431
437
  params.taskId.count <= 100,
@@ -453,22 +459,47 @@ final class FirmwareArtifactStore {
453
459
  else {
454
460
  throw FirmwareArtifactStoreError.invalidInput("Firmware URL must use HTTPS port 443")
455
461
  }
462
+ let expectedSize: Int64?
463
+ if let value = params.expectedSize {
464
+ guard
465
+ value.isFinite,
466
+ value > 0,
467
+ value <= Double(Int64.max),
468
+ value.rounded() == value
469
+ else {
470
+ throw FirmwareArtifactStoreError.invalidInput(
471
+ "Invalid firmware artifact expected size"
472
+ )
473
+ }
474
+ expectedSize = Int64(value)
475
+ } else {
476
+ expectedSize = nil
477
+ }
456
478
  guard
457
- params.expectedSize.isFinite,
458
- params.expectedSize > 0,
459
- params.expectedSize <= Double(Int64.max),
460
- params.expectedSize.rounded() == params.expectedSize,
461
479
  params.maxBytes.isFinite,
462
- params.maxBytes == params.expectedSize,
480
+ params.maxBytes > 0,
481
+ params.maxBytes <= Double(Int64.max),
482
+ params.maxBytes.rounded() == params.maxBytes,
463
483
  params.maxBytes <= Double(512 * 1024 * 1024)
464
484
  else {
465
485
  throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifact size")
466
486
  }
467
- guard params.expectedSha256.range(
468
- of: "^[a-fA-F0-9]{64}$",
469
- options: .regularExpression
470
- ) != nil else {
471
- throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifact SHA-256")
487
+ let maxBytes = Int64(params.maxBytes)
488
+ guard expectedSize.map({ $0 <= maxBytes }) ?? true else {
489
+ throw FirmwareArtifactStoreError.invalidInput(
490
+ "Firmware artifact expected size exceeds maxBytes"
491
+ )
492
+ }
493
+ let expectedSha256 = params.expectedSha256?.lowercased()
494
+ if let expectedSha256 {
495
+ guard expectedSha256.range(
496
+ of: "^[a-f0-9]{64}$",
497
+ options: .regularExpression
498
+ ) != nil else {
499
+ throw FirmwareArtifactStoreError.invalidInput(
500
+ "Invalid firmware artifact SHA-256"
501
+ )
502
+ }
472
503
  }
473
504
  guard params.routeType == "domain" || params.routeType == "pinnedIp" else {
474
505
  throw FirmwareArtifactStoreError.invalidInput("Invalid firmware route type")
@@ -488,23 +519,39 @@ final class FirmwareArtifactStore {
488
519
  } else if params.resolvedIp != nil {
489
520
  throw FirmwareArtifactStoreError.invalidInput("Domain route must not include resolvedIp")
490
521
  }
522
+ return ValidatedDownload(
523
+ expectedSize: expectedSize,
524
+ expectedSha256: expectedSha256,
525
+ maxBytes: maxBytes,
526
+ downloadToken: firmwareArtifactDownloadToken(
527
+ expectedSha256: expectedSha256,
528
+ url: params.url
529
+ )
530
+ )
491
531
  }
492
532
 
493
533
  func download(_ params: FirmwareArtifactDownloadParams) async throws -> StoredFirmwareArtifact {
494
- try Self.validateDownloadParams(params)
534
+ let validated = try Self.validateDownloadParams(params)
495
535
  try rejectIfCancelled(transactionId: params.transactionId)
496
- let expectedSha256 = params.expectedSha256.lowercased()
497
- try retainExpectedArtifact(
498
- leaseRef: params.leaseRef,
499
- transactionId: params.transactionId,
500
- artifactRef: "fw:\(expectedSha256)"
501
- )
536
+ if let expectedSha256 = validated.expectedSha256 {
537
+ try retainExpectedArtifact(
538
+ leaseRef: params.leaseRef,
539
+ transactionId: params.transactionId,
540
+ artifactRef: "fw:\(expectedSha256)"
541
+ )
542
+ } else {
543
+ try requireLeaseTransaction(
544
+ leaseRef: params.leaseRef,
545
+ transactionId: params.transactionId
546
+ )
547
+ }
502
548
  let key = firmwareArtifactDownloadKey(
503
549
  transactionId: params.transactionId,
504
- expectedSize: Int64(params.expectedSize),
505
- expectedSha256: expectedSha256
550
+ taskId: params.taskId,
551
+ expectedSize: validated.expectedSize,
552
+ expectedSha256: validated.expectedSha256
506
553
  )
507
- markDownloadActive(expectedSha256, delta: 1)
554
+ markDownloadActive(validated.downloadToken, delta: 1)
508
555
  do {
509
556
  let artifact = try await downloadCoordinator.run(
510
557
  key: key,
@@ -513,14 +560,19 @@ final class FirmwareArtifactStore {
513
560
  try rejectIfCancelled(transactionId: params.transactionId)
514
561
  return try await downloadLocked(
515
562
  params,
516
- expectedSha256: expectedSha256
563
+ validated: validated
517
564
  )
518
565
  }
519
566
  try rejectIfCancelled(transactionId: params.transactionId)
520
- markDownloadActive(expectedSha256, delta: -1)
567
+ try retainExpectedArtifact(
568
+ leaseRef: params.leaseRef,
569
+ transactionId: params.transactionId,
570
+ artifactRef: artifact.artifactRef
571
+ )
572
+ markDownloadActive(validated.downloadToken, delta: -1)
521
573
  return artifact
522
574
  } catch {
523
- markDownloadActive(expectedSha256, delta: -1)
575
+ markDownloadActive(validated.downloadToken, delta: -1)
524
576
  OneKeyLog.error(
525
577
  "FirmwareArtifact",
526
578
  "event=download_failed transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) errorType=\(String(describing: type(of: error)))"
@@ -538,14 +590,17 @@ final class FirmwareArtifactStore {
538
590
  func cachedArtifact(
539
591
  _ params: FirmwareArtifactDownloadParams
540
592
  ) throws -> StoredFirmwareArtifact? {
541
- try Self.validateDownloadParams(params)
593
+ let validated = try Self.validateDownloadParams(params)
542
594
  try rejectIfCancelled(transactionId: params.transactionId)
543
- let expectedSha256 = params.expectedSha256.lowercased()
595
+ guard let expectedSha256 = validated.expectedSha256 else {
596
+ return nil
597
+ }
544
598
  let finalURL = artifactURL(sha256: expectedSha256)
545
- guard let artifact = try? validateStoredArtifact(
599
+ guard let artifact = try? validateDownloadedArtifact(
546
600
  fileURL: finalURL,
547
- expectedSize: Int64(params.expectedSize),
548
- expectedSha256: expectedSha256
601
+ expectedSize: validated.expectedSize,
602
+ expectedSha256: expectedSha256,
603
+ maxBytes: validated.maxBytes
549
604
  ) else {
550
605
  return nil
551
606
  }
@@ -568,22 +623,22 @@ final class FirmwareArtifactStore {
568
623
  cancelledTransactions.insert(transactionId)
569
624
  }
570
625
  await downloadCoordinator.cancel(transactionId: transactionId)
571
- try await RangeDownloader.shared.cancelFirmwareArtifactDownloads(
572
- transactionId: transactionId
573
- )
574
626
  }
575
627
 
576
628
  private func downloadLocked(
577
629
  _ params: FirmwareArtifactDownloadParams,
578
- expectedSha256: String
630
+ validated: ValidatedDownload
579
631
  ) async throws -> StoredFirmwareArtifact {
580
- let finalURL = artifactURL(sha256: expectedSha256)
581
- if let existing = try? validateStoredArtifact(
582
- fileURL: finalURL,
583
- expectedSize: Int64(params.expectedSize),
584
- expectedSha256: expectedSha256
585
- ) {
586
- return existing
632
+ if let expectedSha256 = validated.expectedSha256 {
633
+ let finalURL = artifactURL(sha256: expectedSha256)
634
+ if let existing = try? validateDownloadedArtifact(
635
+ fileURL: finalURL,
636
+ expectedSize: validated.expectedSize,
637
+ expectedSha256: expectedSha256,
638
+ maxBytes: validated.maxBytes
639
+ ) {
640
+ return existing
641
+ }
587
642
  }
588
643
 
589
644
  // Firmware preflight is foreground-bound, so both routes use the same
@@ -592,7 +647,7 @@ final class FirmwareArtifactStore {
592
647
  firmwareArtifactPartialFileName(
593
648
  transactionId: params.transactionId,
594
649
  taskId: params.taskId,
595
- expectedSha256: expectedSha256
650
+ downloadToken: validated.downloadToken
596
651
  ),
597
652
  isDirectory: false
598
653
  )
@@ -600,17 +655,23 @@ final class FirmwareArtifactStore {
600
655
  fileManager.createFile(atPath: partialURL.path, contents: nil)
601
656
  }
602
657
  var currentSize = try fileSize(partialURL)
603
- if currentSize > Int64(params.expectedSize) {
658
+ if validated.expectedSha256 == nil && currentSize > 0 {
659
+ try fileManager.removeItem(at: partialURL)
660
+ fileManager.createFile(atPath: partialURL.path, contents: nil)
661
+ currentSize = 0
662
+ } else if currentSize > validated.maxBytes {
604
663
  try fileManager.removeItem(at: partialURL)
605
664
  fileManager.createFile(atPath: partialURL.path, contents: nil)
606
665
  currentSize = 0
607
666
  }
608
- if currentSize == Int64(params.expectedSize) {
609
- if let completed = try? validateStoredArtifact(
667
+ if let expectedSize = validated.expectedSize, currentSize == expectedSize {
668
+ if let completed = try? validateDownloadedArtifact(
610
669
  fileURL: partialURL,
611
- expectedSize: Int64(params.expectedSize),
612
- expectedSha256: expectedSha256
670
+ expectedSize: expectedSize,
671
+ expectedSha256: validated.expectedSha256,
672
+ maxBytes: validated.maxBytes
613
673
  ) {
674
+ let finalURL = artifactURL(sha256: completed.sha256)
614
675
  try promote(source: partialURL, destination: finalURL)
615
676
  return StoredFirmwareArtifact(
616
677
  artifactRef: completed.artifactRef,
@@ -626,28 +687,31 @@ final class FirmwareArtifactStore {
626
687
 
627
688
  OneKeyLog.info(
628
689
  "FirmwareArtifact",
629
- "event=stream_start transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) expectedBytes=\(Int64(params.expectedSize)) resumeBytes=\(currentSize)"
690
+ "event=stream_start transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) expectedBytes=\(validated.expectedSize ?? -1) resumeBytes=\(currentSize)"
630
691
  )
631
692
  try await streamDownload(
632
693
  params,
633
694
  partialURL: partialURL,
634
- resumeOffset: min(currentSize, Int64(params.expectedSize))
695
+ resumeOffset: min(currentSize, validated.maxBytes),
696
+ validated: validated
635
697
  )
636
698
  OneKeyLog.info(
637
699
  "FirmwareArtifact",
638
- "event=stream_complete transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) expectedBytes=\(Int64(params.expectedSize))"
700
+ "event=stream_complete transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) expectedBytes=\(validated.expectedSize ?? -1)"
639
701
  )
640
702
  let artifact: StoredFirmwareArtifact
641
703
  do {
642
- artifact = try validateStoredArtifact(
704
+ artifact = try validateDownloadedArtifact(
643
705
  fileURL: partialURL,
644
- expectedSize: Int64(params.expectedSize),
645
- expectedSha256: expectedSha256
706
+ expectedSize: validated.expectedSize,
707
+ expectedSha256: validated.expectedSha256,
708
+ maxBytes: validated.maxBytes
646
709
  )
647
710
  } catch {
648
711
  try? fileManager.removeItem(at: partialURL)
649
712
  throw error
650
713
  }
714
+ let finalURL = artifactURL(sha256: artifact.sha256)
651
715
  try promote(source: partialURL, destination: finalURL)
652
716
  return StoredFirmwareArtifact(
653
717
  artifactRef: artifact.artifactRef,
@@ -657,51 +721,6 @@ final class FirmwareArtifactStore {
657
721
  )
658
722
  }
659
723
 
660
- func acceptBackgroundDownload(
661
- temporaryURL: URL,
662
- transactionId: String,
663
- expectedSize: Int64,
664
- expectedSha256: String
665
- ) throws -> StoredFirmwareArtifact {
666
- let normalizedExpectedSha256 = expectedSha256.lowercased()
667
- try rejectIfCancelled(transactionId: transactionId)
668
- let finalURL = artifactURL(sha256: normalizedExpectedSha256)
669
- if let existing = try? validateStoredArtifact(
670
- fileURL: finalURL,
671
- expectedSize: expectedSize,
672
- expectedSha256: normalizedExpectedSha256
673
- ) {
674
- return existing
675
- }
676
- let artifact = try validateStoredArtifact(
677
- fileURL: temporaryURL,
678
- expectedSize: expectedSize,
679
- expectedSha256: normalizedExpectedSha256
680
- )
681
- try promote(source: temporaryURL, destination: finalURL)
682
- return StoredFirmwareArtifact(
683
- artifactRef: artifact.artifactRef,
684
- size: artifact.size,
685
- sha256: artifact.sha256,
686
- fileURL: finalURL
687
- )
688
- }
689
-
690
- func storedArtifact(
691
- expectedSize: Int64,
692
- expectedSha256: String
693
- ) throws -> StoredFirmwareArtifact? {
694
- let finalURL = artifactURL(sha256: expectedSha256)
695
- guard fileManager.fileExists(atPath: finalURL.path) else {
696
- return nil
697
- }
698
- return try validateStoredArtifact(
699
- fileURL: finalURL,
700
- expectedSize: expectedSize,
701
- expectedSha256: expectedSha256
702
- )
703
- }
704
-
705
724
  func discard(artifactRef: String) throws {
706
725
  let fileURL = try resolveArtifactURL(artifactRef)
707
726
  leaseLock.lock()
@@ -786,7 +805,7 @@ final class FirmwareArtifactStore {
786
805
  func materializeArchive(
787
806
  leaseRef: String,
788
807
  artifactRef: String,
789
- expectedEntries: [FirmwareArchiveExpectedEntry]
808
+ expectedEntries: [FirmwareArchiveExpectedEntry]?
790
809
  ) throws -> [StoredFirmwareArchiveEntry] {
791
810
  try requireLease(leaseRef)
792
811
  let archiveURL = try resolveArtifactURL(artifactRef)
@@ -797,10 +816,13 @@ final class FirmwareArtifactStore {
797
816
  try fileManager.createDirectory(at: scratchURL, withIntermediateDirectories: true)
798
817
  defer { try? fileManager.removeItem(at: scratchURL) }
799
818
 
800
- let requirements = try validateArchiveRequirements(expectedEntries)
801
819
  let archiveEntries = try FirmwareArchiveMinizipBridge.scanArchive(
802
820
  atPath: archiveURL.path
803
821
  )
822
+ let requirements = try resolveArchiveRequirements(
823
+ expectedEntries,
824
+ archiveEntries: archiveEntries
825
+ )
804
826
  try validateArchiveEntries(
805
827
  archiveEntries,
806
828
  requirements: requirements
@@ -827,7 +849,7 @@ final class FirmwareArtifactStore {
827
849
  }
828
850
  do {
829
851
  actualSize += Int64(chunk.count)
830
- guard actualSize <= Int64(requirement.expectedSize) else {
852
+ guard actualSize <= requirement.expectedSize else {
831
853
  throw FirmwareArtifactStoreError.archiveInvalid(
832
854
  "Firmware archive entry exceeds its expected size"
833
855
  )
@@ -853,8 +875,8 @@ final class FirmwareArtifactStore {
853
875
  String(format: "%02x", $0)
854
876
  }.joined()
855
877
  guard
856
- actualSize == Int64(requirement.expectedSize),
857
- sha256 == requirement.expectedSha256.lowercased()
878
+ actualSize == requirement.expectedSize,
879
+ requirement.expectedSha256.map({ sha256 == $0 }) ?? true
858
880
  else {
859
881
  throw FirmwareArtifactStoreError.archiveInvalid(
860
882
  "Firmware archive entry integrity mismatch"
@@ -971,49 +993,13 @@ final class FirmwareArtifactStore {
971
993
  let openPaths = Set(readers.values.map(\.fileURL.path))
972
994
  readerLock.unlock()
973
995
 
974
- let now = Date()
975
- var deletedFiles = 0
976
- var deletedBytes: Int64 = 0
977
- let files = try fileManager.contentsOfDirectory(
978
- at: rootURL,
979
- includingPropertiesForKeys: [.contentModificationDateKey, .fileSizeKey],
980
- options: [.skipsHiddenFiles]
996
+ return try sweepFirmwareArtifactOrphansAtRoot(
997
+ rootURL,
998
+ retainedSha256: retained,
999
+ activeSha256: active,
1000
+ openPaths: openPaths,
1001
+ fileManager: fileManager
981
1002
  )
982
- for fileURL in files {
983
- let name = fileURL.lastPathComponent
984
- guard name.count >= 64 else { continue }
985
- let sha256 = String(name.prefix(64))
986
- guard
987
- sha256.range(of: "^[a-f0-9]{64}$", options: .regularExpression) != nil,
988
- !retained.contains(sha256),
989
- !active.contains(sha256),
990
- !openPaths.contains(fileURL.path)
991
- else {
992
- continue
993
- }
994
- let grace: TimeInterval
995
- if name.hasSuffix(".bin") {
996
- grace = Self.finalArtifactGrace
997
- } else if name.hasSuffix(".partial") {
998
- grace = Self.partialArtifactGrace
999
- } else {
1000
- continue
1001
- }
1002
- let values = try fileURL.resourceValues(
1003
- forKeys: [.contentModificationDateKey, .fileSizeKey]
1004
- )
1005
- guard
1006
- let modifiedAt = values.contentModificationDate,
1007
- now.timeIntervalSince(modifiedAt) >= grace
1008
- else {
1009
- continue
1010
- }
1011
- let size = Int64(values.fileSize ?? 0)
1012
- try fileManager.removeItem(at: fileURL)
1013
- deletedFiles += 1
1014
- deletedBytes += size
1015
- }
1016
- return (deletedFiles, deletedBytes)
1017
1003
  }
1018
1004
 
1019
1005
  private func requireLease(_ leaseRef: String) throws {
@@ -1026,6 +1012,22 @@ final class FirmwareArtifactStore {
1026
1012
  }
1027
1013
  }
1028
1014
 
1015
+ private func requireLeaseTransaction(
1016
+ leaseRef: String,
1017
+ transactionId: String
1018
+ ) throws {
1019
+ leaseLock.lock()
1020
+ defer { leaseLock.unlock() }
1021
+ guard
1022
+ let lease = leases[try validateLeaseRef(leaseRef)],
1023
+ lease.transactionId == transactionId
1024
+ else {
1025
+ throw FirmwareArtifactStoreError.invalidInput(
1026
+ "Firmware artifact lease transaction mismatch"
1027
+ )
1028
+ }
1029
+ }
1030
+
1029
1031
  private func retainExpectedArtifact(
1030
1032
  leaseRef: String,
1031
1033
  transactionId: String?,
@@ -1073,11 +1075,8 @@ final class FirmwareArtifactStore {
1073
1075
  ) != nil
1074
1076
  }
1075
1077
 
1076
- private static func isSafeIdentifier(_ value: String) -> Bool {
1077
- value.range(
1078
- of: "^[A-Za-z0-9._:-]{1,160}$",
1079
- options: .regularExpression
1080
- ) != nil
1078
+ static func isSafeIdentifier(_ value: String) -> Bool {
1079
+ firmwareArtifactIdentifierIsSafe(value)
1081
1080
  }
1082
1081
 
1083
1082
  private func markDownloadActive(_ sha256: String, delta: Int) {
@@ -1093,7 +1092,7 @@ final class FirmwareArtifactStore {
1093
1092
 
1094
1093
  private func validateArchiveRequirements(
1095
1094
  _ expectedEntries: [FirmwareArchiveExpectedEntry]
1096
- ) throws -> [FirmwareArchiveExpectedEntry] {
1095
+ ) throws -> [FirmwareArchiveRequirement] {
1097
1096
  guard !expectedEntries.isEmpty, expectedEntries.count <= 4096 else {
1098
1097
  throw FirmwareArtifactStoreError.archiveInvalid(
1099
1098
  "Firmware archive expected entry count is invalid"
@@ -1134,12 +1133,34 @@ final class FirmwareArtifactStore {
1134
1133
  )
1135
1134
  }
1136
1135
  }
1137
- return expectedEntries
1136
+ return expectedEntries.map {
1137
+ FirmwareArchiveRequirement(
1138
+ entryName: $0.entryName,
1139
+ expectedSize: Int64($0.expectedSize),
1140
+ expectedSha256: $0.expectedSha256.lowercased()
1141
+ )
1142
+ }
1143
+ }
1144
+
1145
+ private func resolveArchiveRequirements(
1146
+ _ expectedEntries: [FirmwareArchiveExpectedEntry]?,
1147
+ archiveEntries: [FirmwareArchiveEntryInfo]
1148
+ ) throws -> [FirmwareArchiveRequirement] {
1149
+ if let expectedEntries {
1150
+ return try validateArchiveRequirements(expectedEntries)
1151
+ }
1152
+ return archiveEntries.map {
1153
+ FirmwareArchiveRequirement(
1154
+ entryName: $0.name,
1155
+ expectedSize: $0.uncompressedSize,
1156
+ expectedSha256: nil
1157
+ )
1158
+ }
1138
1159
  }
1139
1160
 
1140
1161
  private func validateArchiveEntries(
1141
1162
  _ entries: [FirmwareArchiveEntryInfo],
1142
- requirements: [FirmwareArchiveExpectedEntry]
1163
+ requirements: [FirmwareArchiveRequirement]
1143
1164
  ) throws {
1144
1165
  guard entries.count == requirements.count else {
1145
1166
  throw FirmwareArtifactStoreError.archiveInvalid(
@@ -1151,16 +1172,32 @@ final class FirmwareArtifactStore {
1151
1172
  )
1152
1173
  var names = Set<String>()
1153
1174
  var canonicalNames = Set<String>()
1175
+ var totalSize: Int64 = 0
1154
1176
  for entry in entries {
1177
+ guard let requirement = requirementsByName[entry.name] else {
1178
+ throw FirmwareArtifactStoreError.archiveInvalid(
1179
+ "Firmware archive contains an unexpected entry"
1180
+ )
1181
+ }
1182
+ let (nextTotalSize, overflowed) = totalSize.addingReportingOverflow(
1183
+ entry.uncompressedSize
1184
+ )
1185
+ guard !overflowed else {
1186
+ throw FirmwareArtifactStoreError.archiveInvalid(
1187
+ "Firmware archive expanded size is invalid"
1188
+ )
1189
+ }
1190
+ totalSize = nextTotalSize
1155
1191
  guard
1156
1192
  names.insert(entry.name).inserted,
1157
1193
  isPortableArchiveEntryName(
1158
1194
  entry.name,
1159
1195
  canonicalNames: &canonicalNames
1160
1196
  ),
1161
- let requirement = requirementsByName[entry.name],
1162
- entry.uncompressedSize == Int64(requirement.expectedSize),
1197
+ entry.uncompressedSize == requirement.expectedSize,
1163
1198
  entry.uncompressedSize > 0,
1199
+ entry.uncompressedSize <= 128 * 1024 * 1024,
1200
+ totalSize <= 512 * 1024 * 1024,
1164
1201
  entry.compressedSize >= 0,
1165
1202
  entry.compressedSize <= 512 * 1024 * 1024,
1166
1203
  entry.uncompressedSize <= max(entry.compressedSize, 1) * 1000,
@@ -1231,7 +1268,8 @@ final class FirmwareArtifactStore {
1231
1268
  private func streamDownload(
1232
1269
  _ params: FirmwareArtifactDownloadParams,
1233
1270
  partialURL: URL,
1234
- resumeOffset: Int64
1271
+ resumeOffset: Int64,
1272
+ validated: ValidatedDownload
1235
1273
  ) async throws {
1236
1274
  do {
1237
1275
  try await FirmwareArtifactWallClockDeadline.run(
@@ -1241,7 +1279,8 @@ final class FirmwareArtifactStore {
1241
1279
  try await streamDownloadWithinDeadline(
1242
1280
  params,
1243
1281
  partialURL: partialURL,
1244
- resumeOffset: resumeOffset
1282
+ resumeOffset: resumeOffset,
1283
+ validated: validated
1245
1284
  )
1246
1285
  }
1247
1286
  } catch FirmwareArtifactDeadlineError.exceeded {
@@ -1254,7 +1293,8 @@ final class FirmwareArtifactStore {
1254
1293
  private func streamDownloadWithinDeadline(
1255
1294
  _ params: FirmwareArtifactDownloadParams,
1256
1295
  partialURL: URL,
1257
- resumeOffset: Int64
1296
+ resumeOffset: Int64,
1297
+ validated: ValidatedDownload
1258
1298
  ) async throws {
1259
1299
  guard let url = URL(string: params.url), let hostname = url.host else {
1260
1300
  throw FirmwareArtifactStoreError.invalidInput("Invalid firmware URL")
@@ -1272,8 +1312,8 @@ final class FirmwareArtifactStore {
1272
1312
  partialURL: partialURL,
1273
1313
  hostname: hostname,
1274
1314
  resumeOffset: resumeOffset,
1275
- expectedSize: Int64(params.expectedSize),
1276
- maxBytes: Int64(params.maxBytes),
1315
+ expectedSize: validated.expectedSize,
1316
+ maxBytes: validated.maxBytes,
1277
1317
  isCancelled: { [weak self] in
1278
1318
  Task.isCancelled ||
1279
1319
  self?.isTransactionCancelled(params.transactionId) == true
@@ -1379,6 +1419,36 @@ final class FirmwareArtifactStore {
1379
1419
  )
1380
1420
  }
1381
1421
 
1422
+ private func validateDownloadedArtifact(
1423
+ fileURL: URL,
1424
+ expectedSize: Int64?,
1425
+ expectedSha256: String?,
1426
+ maxBytes: Int64
1427
+ ) throws -> StoredFirmwareArtifact {
1428
+ let size = try fileSize(fileURL)
1429
+ guard
1430
+ size > 0,
1431
+ size <= maxBytes,
1432
+ expectedSize.map({ size == $0 }) ?? true
1433
+ else {
1434
+ throw FirmwareArtifactStoreError.integrityMismatch(
1435
+ "ARTIFACT_INTEGRITY_FAILED: firmware artifact size mismatch"
1436
+ )
1437
+ }
1438
+ let sha256 = try hashFile(fileURL)
1439
+ guard expectedSha256.map({ sha256 == $0 }) ?? true else {
1440
+ throw FirmwareArtifactStoreError.integrityMismatch(
1441
+ "ARTIFACT_INTEGRITY_FAILED: firmware artifact SHA-256 mismatch"
1442
+ )
1443
+ }
1444
+ return StoredFirmwareArtifact(
1445
+ artifactRef: "fw:\(sha256)",
1446
+ size: size,
1447
+ sha256: sha256,
1448
+ fileURL: fileURL
1449
+ )
1450
+ }
1451
+
1382
1452
  private func resolveArtifactURL(_ artifactRef: String) throws -> URL {
1383
1453
  guard artifactRef.hasPrefix("fw:") else {
1384
1454
  throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifactRef")