@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.
- package/ReactNativeRangeDownloader.podspec +1 -1
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRules.kt +12 -5
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweep.kt +111 -0
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +132 -68
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRulesTest.kt +39 -0
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweepTest.kt +107 -0
- package/ios/FirmwareArtifactStore.swift +242 -172
- package/ios/RangeDownloadLogic.swift +194 -36
- package/ios/ReactNativeRangeDownloader.swift +9 -423
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +3 -3
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeParams.hpp +7 -6
- package/nitrogen/generated/android/c++/JFirmwareArtifactDownloadParams.hpp +7 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeParams.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDownloadParams.kt +3 -3
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +15 -0
- package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeParams.swift +32 -13
- package/nitrogen/generated/ios/swift/FirmwareArtifactDownloadParams.swift +39 -8
- package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeParams.hpp +6 -5
- package/nitrogen/generated/shared/c++/FirmwareArtifactDownloadParams.hpp +9 -9
- package/package.json +2 -2
- package/src/ReactNativeRangeDownloader.nitro.ts +3 -3
- 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
|
-
|
|
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
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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 =
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
505
|
-
|
|
550
|
+
taskId: params.taskId,
|
|
551
|
+
expectedSize: validated.expectedSize,
|
|
552
|
+
expectedSha256: validated.expectedSha256
|
|
506
553
|
)
|
|
507
|
-
markDownloadActive(
|
|
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
|
-
|
|
563
|
+
validated: validated
|
|
517
564
|
)
|
|
518
565
|
}
|
|
519
566
|
try rejectIfCancelled(transactionId: params.transactionId)
|
|
520
|
-
|
|
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(
|
|
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 =
|
|
595
|
+
guard let expectedSha256 = validated.expectedSha256 else {
|
|
596
|
+
return nil
|
|
597
|
+
}
|
|
544
598
|
let finalURL = artifactURL(sha256: expectedSha256)
|
|
545
|
-
guard let artifact = try?
|
|
599
|
+
guard let artifact = try? validateDownloadedArtifact(
|
|
546
600
|
fileURL: finalURL,
|
|
547
|
-
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
|
-
|
|
630
|
+
validated: ValidatedDownload
|
|
579
631
|
) async throws -> StoredFirmwareArtifact {
|
|
580
|
-
let
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
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 >
|
|
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 ==
|
|
609
|
-
if let completed = try?
|
|
667
|
+
if let expectedSize = validated.expectedSize, currentSize == expectedSize {
|
|
668
|
+
if let completed = try? validateDownloadedArtifact(
|
|
610
669
|
fileURL: partialURL,
|
|
611
|
-
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=\(
|
|
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,
|
|
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=\(
|
|
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
|
|
704
|
+
artifact = try validateDownloadedArtifact(
|
|
643
705
|
fileURL: partialURL,
|
|
644
|
-
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 <=
|
|
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 ==
|
|
857
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
1077
|
-
value
|
|
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 -> [
|
|
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: [
|
|
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
|
-
|
|
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:
|
|
1276
|
-
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")
|