@onekeyfe/react-native-bundle-update 6.7.0-1 → 6.7.0-2
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/reactnativebundleupdate/ReactNativeBundleUpdate.kt +604 -230
- package/ios/ReactNativeBundleUpdate.swift +782 -198
- package/lib/typescript/src/ReactNativeBundleUpdate.nitro.d.ts +0 -5
- package/nitrogen/generated/android/c++/JBundleDownloadASCParams.hpp +1 -5
- package/nitrogen/generated/android/c++/JBundleDownloadResult.hpp +1 -5
- package/nitrogen/generated/android/c++/JBundleInstallParams.hpp +1 -5
- package/nitrogen/generated/android/c++/JBundleVerifyASCParams.hpp +1 -5
- package/nitrogen/generated/android/c++/JBundleVerifyParams.hpp +1 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadASCParams.kt +2 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleDownloadResult.kt +3 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleInstallParams.kt +3 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyASCParams.kt +3 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativebundleupdate/BundleVerifyParams.kt +3 -8
- package/nitrogen/generated/ios/swift/BundleDownloadASCParams.swift +2 -7
- package/nitrogen/generated/ios/swift/BundleDownloadResult.swift +2 -7
- package/nitrogen/generated/ios/swift/BundleInstallParams.swift +2 -7
- package/nitrogen/generated/ios/swift/BundleVerifyASCParams.swift +2 -7
- package/nitrogen/generated/ios/swift/BundleVerifyParams.swift +2 -7
- package/nitrogen/generated/shared/c++/BundleDownloadASCParams.hpp +1 -5
- package/nitrogen/generated/shared/c++/BundleDownloadResult.hpp +1 -5
- package/nitrogen/generated/shared/c++/BundleInstallParams.hpp +1 -5
- package/nitrogen/generated/shared/c++/BundleVerifyASCParams.hpp +1 -5
- package/nitrogen/generated/shared/c++/BundleVerifyParams.hpp +1 -5
- package/package.json +1 -1
- package/src/ReactNativeBundleUpdate.nitro.ts +0 -5
|
@@ -324,6 +324,16 @@ public class BundleUpdateStore: NSObject {
|
|
|
324
324
|
|
|
325
325
|
private static var didProcessPreLaunchTask = false
|
|
326
326
|
|
|
327
|
+
private static func failPendingTask(mmkv: MMKV, taskDict: [String: Any], reason: String) {
|
|
328
|
+
var updatedTask = taskDict
|
|
329
|
+
updatedTask["status"] = "failed"
|
|
330
|
+
updatedTask["lastError"] = reason
|
|
331
|
+
if let jsonData = try? JSONSerialization.data(withJSONObject: updatedTask),
|
|
332
|
+
let jsonStr = String(data: jsonData, encoding: .utf8) {
|
|
333
|
+
mmkv.set(jsonStr, forKey: "onekey_pending_install_task")
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
327
337
|
/// Checks MMKV for a pending bundle-switch task and applies it before JS runtime starts.
|
|
328
338
|
/// Only handles the happy path (status=pending, bundle exists, env matches).
|
|
329
339
|
/// All complex logic (retry, download, error handling) remains in JS layer.
|
|
@@ -366,9 +376,9 @@ public class BundleUpdateStore: NSObject {
|
|
|
366
376
|
}
|
|
367
377
|
|
|
368
378
|
let currentBundleVersionStr: String
|
|
369
|
-
if let
|
|
370
|
-
|
|
371
|
-
currentBundleVersionStr =
|
|
379
|
+
if let currentFolder = currentBundleVersion() {
|
|
380
|
+
guard let parsedVersion = bundleVersion(inFolder: currentFolder, appVersion: currentAppVersion) else { return }
|
|
381
|
+
currentBundleVersionStr = parsedVersion
|
|
372
382
|
} else {
|
|
373
383
|
currentBundleVersionStr = getBuiltinBundleVersion()
|
|
374
384
|
}
|
|
@@ -379,26 +389,76 @@ public class BundleUpdateStore: NSObject {
|
|
|
379
389
|
let appVersion = payload["appVersion"] as? String,
|
|
380
390
|
let bundleVersion = payload["bundleVersion"] as? String,
|
|
381
391
|
let signature = payload["signature"] as? String,
|
|
382
|
-
!appVersion.isEmpty, !bundleVersion.isEmpty, !signature.isEmpty
|
|
383
|
-
appVersion.isSafeVersionString, bundleVersion.isSafeVersionString
|
|
392
|
+
!appVersion.isEmpty, !bundleVersion.isEmpty, !signature.isEmpty
|
|
384
393
|
else { return }
|
|
394
|
+
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
395
|
+
failPendingTask(mmkv: mmkv, taskDict: taskDict, reason: "ERR_VERIFY_FAILED:Invalid version string")
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
guard appVersion == currentAppVersion else {
|
|
399
|
+
OneKeyLog.warn("BundleUpdate", "processPreLaunchPendingTask: target appVersion (\(appVersion)) != currentAppVersion (\(currentAppVersion))")
|
|
400
|
+
failPendingTask(mmkv: mmkv, taskDict: taskDict, reason: "ERR_ENV_CHANGED:Target appVersion mismatch")
|
|
401
|
+
return
|
|
402
|
+
}
|
|
385
403
|
|
|
386
|
-
// 5. Verify bundle directory and
|
|
404
|
+
// 5. Verify bundle directory, canonical path confinement, and required files
|
|
387
405
|
let folderName = "\(appVersion)-\(bundleVersion)"
|
|
388
406
|
let bundleDirPath = (bundleDir() as NSString).appendingPathComponent(folderName)
|
|
407
|
+
let resolvedBundleDir = (bundleDirPath as NSString).resolvingSymlinksInPath
|
|
408
|
+
let baseBundleDir = (bundleDir() as NSString).resolvingSymlinksInPath
|
|
409
|
+
guard resolvedBundleDir.hasPrefix(baseBundleDir + "/") else {
|
|
410
|
+
OneKeyLog.error("BundleUpdate", "processPreLaunchPendingTask: bundle path outside bundle directory: \(resolvedBundleDir)")
|
|
411
|
+
failPendingTask(mmkv: mmkv, taskDict: taskDict, reason: "ERR_VERIFY_FAILED:Bundle path outside bundle directory")
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
let sigPath = (signatureFilePath(folderName) as NSString).resolvingSymlinksInPath
|
|
415
|
+
let baseAscDir = (ascDir() as NSString).resolvingSymlinksInPath
|
|
416
|
+
guard sigPath.hasPrefix(baseAscDir + "/") else {
|
|
417
|
+
OneKeyLog.error("BundleUpdate", "processPreLaunchPendingTask: signature path outside asc directory: \(sigPath)")
|
|
418
|
+
failPendingTask(mmkv: mmkv, taskDict: taskDict, reason: "ERR_VERIFY_FAILED:Signature path outside asc directory")
|
|
419
|
+
return
|
|
420
|
+
}
|
|
389
421
|
guard FileManager.default.fileExists(atPath: bundleDirPath) else { return }
|
|
390
422
|
let entryFilePath = (bundleDirPath as NSString).appendingPathComponent("main.jsbundle.hbc")
|
|
391
|
-
|
|
392
|
-
|
|
423
|
+
let metadataFilePath = (bundleDirPath as NSString).appendingPathComponent("metadata.json")
|
|
424
|
+
guard FileManager.default.fileExists(atPath: entryFilePath),
|
|
425
|
+
FileManager.default.fileExists(atPath: metadataFilePath) else {
|
|
426
|
+
OneKeyLog.warn("BundleUpdate", "processPreLaunchPendingTask: bundle dir exists but required files missing: \(bundleDirPath)")
|
|
427
|
+
failPendingTask(mmkv: mmkv, taskDict: taskDict, reason: "ERR_VERIFY_FAILED:Required bundle files missing")
|
|
393
428
|
return
|
|
394
429
|
}
|
|
395
430
|
|
|
396
|
-
|
|
431
|
+
#if ALLOW_SKIP_GPG_VERIFICATION
|
|
432
|
+
let skipGPG = isDevSettingsEnabled() && isSkipGPGEnabled()
|
|
433
|
+
#else
|
|
434
|
+
let skipGPG = false
|
|
435
|
+
#endif
|
|
436
|
+
if !skipGPG && !validateMetadataFileSha256(folderName, signature: signature, expectedAppVersion: appVersion, expectedBundleVersion: bundleVersion) {
|
|
437
|
+
OneKeyLog.error("BundleUpdate", "processPreLaunchPendingTask: GPG signature verification failed for \(folderName)")
|
|
438
|
+
failPendingTask(mmkv: mmkv, taskDict: taskDict, reason: "ERR_VERIFY_FAILED:Bundle signature verification failed")
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
guard let metadata = getMetadataFileContent(folderName) else {
|
|
443
|
+
OneKeyLog.error("BundleUpdate", "processPreLaunchPendingTask: failed to read metadata.json for \(folderName)")
|
|
444
|
+
failPendingTask(mmkv: mmkv, taskDict: taskDict, reason: "ERR_VERIFY_FAILED:Failed to read metadata.json")
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
if !validateAllFilesInDir(bundleDirPath, metadata: metadata, appVersion: appVersion, bundleVersion: bundleVersion) {
|
|
448
|
+
OneKeyLog.error("BundleUpdate", "processPreLaunchPendingTask: extracted file verification failed for \(folderName)")
|
|
449
|
+
failPendingTask(mmkv: mmkv, taskDict: taskDict, reason: "ERR_VERIFY_FAILED:Extracted file verification failed")
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
if !validateBundlePairCompatibility(bundleDirPath: bundleDirPath, metadata: metadata) {
|
|
453
|
+
OneKeyLog.error("BundleUpdate", "processPreLaunchPendingTask: bundle pair compatibility check failed for \(folderName)")
|
|
454
|
+
failPendingTask(mmkv: mmkv, taskDict: taskDict, reason: "ERR_VERIFY_FAILED:Bundle pair compatibility check failed")
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 6. Apply: write signature file BEFORE flipping currentBundleVersion
|
|
459
|
+
writeSignatureFile(folderName, signature: signature)
|
|
397
460
|
let ud = UserDefaults.standard
|
|
398
461
|
ud.set(folderName, forKey: bundlePrefsKey)
|
|
399
|
-
if !signature.isEmpty {
|
|
400
|
-
writeSignatureFile(folderName, signature: signature)
|
|
401
|
-
}
|
|
402
462
|
setNativeVersion(currentAppVersion)
|
|
403
463
|
setNativeBuildNumber(getCurrentNativeBuildNumber())
|
|
404
464
|
ud.synchronize()
|
|
@@ -525,14 +585,31 @@ public class BundleUpdateStore: NSObject {
|
|
|
525
585
|
#endif
|
|
526
586
|
}
|
|
527
587
|
|
|
528
|
-
public static func
|
|
588
|
+
public static func bundleVersion(inFolder folder: String, appVersion: String) -> String? {
|
|
589
|
+
let prefix = "\(appVersion)-"
|
|
590
|
+
guard appVersion.isSafeVersionString, folder.hasPrefix(prefix) else { return nil }
|
|
591
|
+
let bundleVersion = String(folder.dropFirst(prefix.count))
|
|
592
|
+
return bundleVersion.isSafeVersionString ? bundleVersion : nil
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
public static func readMetadataFileSha256(
|
|
596
|
+
_ signature: String,
|
|
597
|
+
expectedAppVersion: String? = nil,
|
|
598
|
+
expectedBundleVersion: String? = nil,
|
|
599
|
+
expectedAppType: String? = "ios"
|
|
600
|
+
) -> String? {
|
|
529
601
|
guard !signature.isEmpty else { return nil }
|
|
530
602
|
|
|
531
603
|
// GPG cleartext signature verification is required. The actual PGP
|
|
532
604
|
// verification + JSON-body sha256 extraction lives in the shared
|
|
533
605
|
// BundleCryptoCore (react-native-bundle-crypto); preserve the previous
|
|
534
606
|
// "failure => nil" semantics here.
|
|
535
|
-
let r = BundleCryptoCore.verifyGpgCleartext(
|
|
607
|
+
let r = BundleCryptoCore.verifyGpgCleartext(
|
|
608
|
+
signature,
|
|
609
|
+
expectedAppVersion: expectedAppVersion,
|
|
610
|
+
expectedBundleVersion: expectedBundleVersion,
|
|
611
|
+
expectedAppType: expectedAppType
|
|
612
|
+
)
|
|
536
613
|
guard r.valid, let sha256 = r.sha256 else {
|
|
537
614
|
OneKeyLog.error("BundleUpdate", "readMetadataFileSha256: GPG verification failed (\(r.reason ?? "unknown")), rejecting unsigned content")
|
|
538
615
|
return nil
|
|
@@ -540,12 +617,20 @@ public class BundleUpdateStore: NSObject {
|
|
|
540
617
|
return sha256
|
|
541
618
|
}
|
|
542
619
|
|
|
543
|
-
public static func validateMetadataFileSha256(_ currentBundleVersion: String, signature: String) -> Bool {
|
|
620
|
+
public static func validateMetadataFileSha256(_ currentBundleVersion: String, signature: String, expectedAppVersion: String, expectedBundleVersion: String) -> Bool {
|
|
544
621
|
guard let metadataFilePath = getMetadataFilePath(currentBundleVersion) else {
|
|
545
622
|
OneKeyLog.debug("BundleUpdate", "metadataFilePath is null")
|
|
546
623
|
return false
|
|
547
624
|
}
|
|
548
|
-
guard
|
|
625
|
+
guard expectedAppVersion.isSafeVersionString,
|
|
626
|
+
expectedBundleVersion.isSafeVersionString,
|
|
627
|
+
currentBundleVersion == "\(expectedAppVersion)-\(expectedBundleVersion)" else { return false }
|
|
628
|
+
guard let extractedSha256 = readMetadataFileSha256(
|
|
629
|
+
signature,
|
|
630
|
+
expectedAppVersion: expectedAppVersion,
|
|
631
|
+
expectedBundleVersion: expectedBundleVersion,
|
|
632
|
+
expectedAppType: "ios"
|
|
633
|
+
), !extractedSha256.isEmpty else {
|
|
549
634
|
return false
|
|
550
635
|
}
|
|
551
636
|
guard let calculatedSha256 = calculateSHA256(metadataFilePath) else { return false }
|
|
@@ -716,14 +801,15 @@ public class BundleUpdateStore: NSObject {
|
|
|
716
801
|
OneKeyLog.debug("BundleUpdate", "getJsBundlePath: signatureLength=\(signature.count)")
|
|
717
802
|
|
|
718
803
|
#if ALLOW_SKIP_GPG_VERIFICATION
|
|
719
|
-
let
|
|
720
|
-
if
|
|
721
|
-
OneKeyLog.warn("BundleUpdate", "Startup SHA256 validation skipped (DevSettings enabled)")
|
|
804
|
+
let skipGPG = isDevSettingsEnabled() && isSkipGPGEnabled()
|
|
805
|
+
if skipGPG {
|
|
806
|
+
OneKeyLog.warn("BundleUpdate", "Startup SHA256 validation skipped (DevSettings + skip-GPG enabled)")
|
|
722
807
|
}
|
|
723
808
|
#else
|
|
724
|
-
let
|
|
809
|
+
let skipGPG = false
|
|
725
810
|
#endif
|
|
726
|
-
|
|
811
|
+
guard let currentBundleVersion = bundleVersion(inFolder: currentBundleVer, appVersion: currentAppVersion) else { return nil }
|
|
812
|
+
if !skipGPG && !validateMetadataFileSha256(currentBundleVer, signature: signature, expectedAppVersion: currentAppVersion, expectedBundleVersion: currentBundleVersion) {
|
|
727
813
|
OneKeyLog.warn("BundleUpdate", "getJsBundlePath: validateMetadataFileSha256 failed, signatureLength=\(signature.count)")
|
|
728
814
|
return nil
|
|
729
815
|
}
|
|
@@ -851,6 +937,7 @@ public class BundleUpdateStore: NSObject {
|
|
|
851
937
|
}
|
|
852
938
|
|
|
853
939
|
public static func clearUpdateBundleData() {
|
|
940
|
+
didProcessPreLaunchTask = true
|
|
854
941
|
invalidateValidatedBundleInfoCache()
|
|
855
942
|
let bDir = bundleDir()
|
|
856
943
|
let fm = FileManager.default
|
|
@@ -878,22 +965,83 @@ private extension String {
|
|
|
878
965
|
|
|
879
966
|
/// URLSession delegate that handles download progress and HTTPS redirect validation
|
|
880
967
|
private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
968
|
+
private static let maxDownloadBytes: Int64 = 512 * 1024 * 1024
|
|
969
|
+
|
|
881
970
|
/// Called with progress percentage (0-100) during download
|
|
882
971
|
var onProgress: ((Int) -> Void)?
|
|
883
972
|
|
|
884
973
|
/// Continuation to bridge delegate callbacks → async/await
|
|
885
974
|
private var continuation: CheckedContinuation<(URL, URLResponse), Error>?
|
|
886
975
|
private var tempFileURL: URL?
|
|
976
|
+
private var exceededMaxBytes = false
|
|
887
977
|
/// Resume data captured from the most recent failure so the caller can
|
|
888
978
|
/// persist it for the next attempt. Populated in didCompleteWithError
|
|
889
979
|
/// when the system supplies NSURLSessionDownloadTaskResumeData.
|
|
890
980
|
private(set) var lastResumeData: Data?
|
|
891
981
|
private let lock = NSLock()
|
|
982
|
+
private var cancelledByClear = false
|
|
983
|
+
private var rejectedInsecureRedirect = false
|
|
984
|
+
private var runCompleted = true
|
|
985
|
+
private var completionWaiters: [UUID: CheckedContinuation<Bool, Never>] = [:]
|
|
986
|
+
|
|
987
|
+
var wasCancelledByClear: Bool {
|
|
988
|
+
lock.lock()
|
|
989
|
+
defer { lock.unlock() }
|
|
990
|
+
return cancelledByClear
|
|
991
|
+
}
|
|
892
992
|
|
|
893
|
-
func
|
|
993
|
+
func markCancelledByClear() {
|
|
894
994
|
lock.lock()
|
|
995
|
+
cancelledByClear = true
|
|
996
|
+
lock.unlock()
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
var wasInsecureRedirectRejected: Bool {
|
|
1000
|
+
lock.lock()
|
|
1001
|
+
defer { lock.unlock() }
|
|
1002
|
+
return rejectedInsecureRedirect
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
func setContinuation(_ cont: CheckedContinuation<(URL, URLResponse), Error>) -> Bool {
|
|
1006
|
+
lock.lock()
|
|
1007
|
+
if cancelledByClear {
|
|
1008
|
+
lock.unlock()
|
|
1009
|
+
cont.resume(throwing: BundleUpdateError(.cancelled, .download))
|
|
1010
|
+
return false
|
|
1011
|
+
}
|
|
895
1012
|
continuation = cont
|
|
896
1013
|
lock.unlock()
|
|
1014
|
+
return true
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
func finishRun() {
|
|
1018
|
+
lock.lock()
|
|
1019
|
+
runCompleted = true
|
|
1020
|
+
let waiters = Array(completionWaiters.values)
|
|
1021
|
+
completionWaiters.removeAll()
|
|
1022
|
+
lock.unlock()
|
|
1023
|
+
waiters.forEach { $0.resume(returning: true) }
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
func waitForRunCompletion(timeout: TimeInterval) async -> Bool {
|
|
1027
|
+
await withCheckedContinuation { continuation in
|
|
1028
|
+
let id = UUID()
|
|
1029
|
+
lock.lock()
|
|
1030
|
+
if runCompleted {
|
|
1031
|
+
lock.unlock()
|
|
1032
|
+
continuation.resume(returning: true)
|
|
1033
|
+
return
|
|
1034
|
+
}
|
|
1035
|
+
completionWaiters[id] = continuation
|
|
1036
|
+
lock.unlock()
|
|
1037
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + timeout) { [weak self] in
|
|
1038
|
+
guard let self else { return }
|
|
1039
|
+
self.lock.lock()
|
|
1040
|
+
let waiter = self.completionWaiters.removeValue(forKey: id)
|
|
1041
|
+
self.lock.unlock()
|
|
1042
|
+
waiter?.resume(returning: false)
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
897
1045
|
}
|
|
898
1046
|
|
|
899
1047
|
// MARK: - URLSessionDownloadDelegate
|
|
@@ -901,6 +1049,11 @@ private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
|
901
1049
|
private var prevProgress: Int = -1
|
|
902
1050
|
|
|
903
1051
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
|
1052
|
+
if totalBytesWritten > Self.maxDownloadBytes || totalBytesExpectedToWrite > Self.maxDownloadBytes {
|
|
1053
|
+
exceededMaxBytes = true
|
|
1054
|
+
downloadTask.cancel()
|
|
1055
|
+
return
|
|
1056
|
+
}
|
|
904
1057
|
guard totalBytesExpectedToWrite > 0 else { return }
|
|
905
1058
|
let progress = Int((totalBytesWritten * 100) / totalBytesExpectedToWrite)
|
|
906
1059
|
if progress != prevProgress {
|
|
@@ -911,6 +1064,13 @@ private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
|
911
1064
|
}
|
|
912
1065
|
|
|
913
1066
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
|
1067
|
+
if exceededMaxBytes { return }
|
|
1068
|
+
if let attrs = try? FileManager.default.attributesOfItem(atPath: location.path),
|
|
1069
|
+
let fileSize = attrs[.size] as? Int64,
|
|
1070
|
+
fileSize > Self.maxDownloadBytes {
|
|
1071
|
+
exceededMaxBytes = true
|
|
1072
|
+
return
|
|
1073
|
+
}
|
|
914
1074
|
// Copy to a temp location because the file at `location` is deleted after this method returns
|
|
915
1075
|
let tempPath = NSTemporaryDirectory() + UUID().uuidString
|
|
916
1076
|
let dest = URL(fileURLWithPath: tempPath)
|
|
@@ -930,6 +1090,12 @@ private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
|
930
1090
|
continuation = nil
|
|
931
1091
|
lock.unlock()
|
|
932
1092
|
|
|
1093
|
+
if exceededMaxBytes {
|
|
1094
|
+
self.lastResumeData = nil
|
|
1095
|
+
cont?.resume(throwing: BundleUpdateError(.security, .download))
|
|
1096
|
+
return
|
|
1097
|
+
}
|
|
1098
|
+
|
|
933
1099
|
if let error = error {
|
|
934
1100
|
// iOS attaches partial download bytes via userInfo so the next
|
|
935
1101
|
// attempt can finish from the cut point instead of byte 0. Mixpanel
|
|
@@ -948,14 +1114,16 @@ private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
|
948
1114
|
cont?.resume(returning: (tempURL, response))
|
|
949
1115
|
} else {
|
|
950
1116
|
self.lastResumeData = nil
|
|
951
|
-
cont?.resume(throwing:
|
|
952
|
-
userInfo: [NSLocalizedDescriptionKey: "Download completed without file"]))
|
|
1117
|
+
cont?.resume(throwing: BundleUpdateError(.storage, .download))
|
|
953
1118
|
}
|
|
954
1119
|
}
|
|
955
1120
|
|
|
956
1121
|
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
|
|
957
1122
|
if request.url?.scheme?.lowercased() != "https" {
|
|
958
1123
|
OneKeyLog.error("BundleUpdate", "Blocked redirect to non-HTTPS URL")
|
|
1124
|
+
lock.lock()
|
|
1125
|
+
rejectedInsecureRedirect = true
|
|
1126
|
+
lock.unlock()
|
|
959
1127
|
completionHandler(nil)
|
|
960
1128
|
} else {
|
|
961
1129
|
completionHandler(request)
|
|
@@ -966,8 +1134,12 @@ private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
|
966
1134
|
func reset() {
|
|
967
1135
|
lock.lock()
|
|
968
1136
|
continuation = nil
|
|
1137
|
+
cancelledByClear = false
|
|
1138
|
+
rejectedInsecureRedirect = false
|
|
1139
|
+
runCompleted = false
|
|
969
1140
|
lock.unlock()
|
|
970
1141
|
tempFileURL = nil
|
|
1142
|
+
exceededMaxBytes = false
|
|
971
1143
|
onProgress = nil
|
|
972
1144
|
prevProgress = -1
|
|
973
1145
|
lastResumeData = nil
|
|
@@ -980,13 +1152,101 @@ private struct BundleListener {
|
|
|
980
1152
|
let callback: (BundleDownloadEvent) -> Void
|
|
981
1153
|
}
|
|
982
1154
|
|
|
1155
|
+
private enum BundleUpdateCode: String {
|
|
1156
|
+
case network = "NETWORK_ERROR"
|
|
1157
|
+
case timeout = "TIMEOUT"
|
|
1158
|
+
case http = "HTTP_ERROR"
|
|
1159
|
+
case missing = "PACKAGE_MISSING"
|
|
1160
|
+
case storage = "STORAGE_ERROR"
|
|
1161
|
+
case integrity = "INTEGRITY_FAILED"
|
|
1162
|
+
case incompatible = "INCOMPATIBLE"
|
|
1163
|
+
case install = "INSTALL_FAILED"
|
|
1164
|
+
case cancelled = "CANCELLED"
|
|
1165
|
+
case unknown = "UNKNOWN"
|
|
1166
|
+
case invalidArgument = "INVALID_ARGUMENT"
|
|
1167
|
+
case security = "SECURITY_REJECTED"
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private enum BundleUpdateStep: String {
|
|
1171
|
+
case download, downloadASC, verifyASC, verifyPackage, install, cleanup
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
private struct BundleUpdateError: Error, CustomStringConvertible {
|
|
1175
|
+
let code: BundleUpdateCode
|
|
1176
|
+
let step: BundleUpdateStep
|
|
1177
|
+
let httpStatus: Int?
|
|
1178
|
+
|
|
1179
|
+
init(_ code: BundleUpdateCode, _ step: BundleUpdateStep, httpStatus: Int? = nil) {
|
|
1180
|
+
self.code = code
|
|
1181
|
+
self.step = step
|
|
1182
|
+
self.httpStatus = code == .http && (100...599).contains(httpStatus ?? -1) ? httpStatus : nil
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
var description: String {
|
|
1186
|
+
let status = httpStatus.map { ",\"httpStatus\":\($0)" } ?? ""
|
|
1187
|
+
return "ONEKEY_UPDATE_ERROR:{\"code\":\"\(code.rawValue)\",\"step\":\"\(step.rawValue)\"\(status)}"
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
private func bundleUpdateError(_ error: Error, step: BundleUpdateStep) -> BundleUpdateError {
|
|
1192
|
+
if let typed = error as? BundleUpdateError {
|
|
1193
|
+
return BundleUpdateError(typed.code, step, httpStatus: typed.httpStatus)
|
|
1194
|
+
}
|
|
1195
|
+
if let httpError = error as? RangeDownloader.HTTPStatusError {
|
|
1196
|
+
return BundleUpdateError(.http, step, httpStatus: httpError.statusCode)
|
|
1197
|
+
}
|
|
1198
|
+
let nsError = error as NSError
|
|
1199
|
+
if nsError.domain == NSURLErrorDomain {
|
|
1200
|
+
switch nsError.code {
|
|
1201
|
+
case URLError.timedOut.rawValue:
|
|
1202
|
+
return BundleUpdateError(.timeout, step)
|
|
1203
|
+
case URLError.cannotFindHost.rawValue,
|
|
1204
|
+
URLError.cannotConnectToHost.rawValue,
|
|
1205
|
+
URLError.networkConnectionLost.rawValue,
|
|
1206
|
+
URLError.notConnectedToInternet.rawValue,
|
|
1207
|
+
URLError.dnsLookupFailed.rawValue,
|
|
1208
|
+
URLError.cancelled.rawValue:
|
|
1209
|
+
return BundleUpdateError(.network, step)
|
|
1210
|
+
case URLError.secureConnectionFailed.rawValue,
|
|
1211
|
+
URLError.serverCertificateHasBadDate.rawValue,
|
|
1212
|
+
URLError.serverCertificateNotYetValid.rawValue,
|
|
1213
|
+
URLError.serverCertificateUntrusted.rawValue,
|
|
1214
|
+
URLError.serverCertificateHasUnknownRoot.rawValue,
|
|
1215
|
+
URLError.clientCertificateRejected.rawValue,
|
|
1216
|
+
URLError.clientCertificateRequired.rawValue:
|
|
1217
|
+
return BundleUpdateError(.security, step)
|
|
1218
|
+
case URLError.badURL.rawValue, URLError.unsupportedURL.rawValue:
|
|
1219
|
+
return BundleUpdateError(.invalidArgument, step)
|
|
1220
|
+
default:
|
|
1221
|
+
return BundleUpdateError(.unknown, step)
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if nsError.domain == NSCocoaErrorDomain || nsError.domain == NSPOSIXErrorDomain {
|
|
1225
|
+
return BundleUpdateError(.storage, step)
|
|
1226
|
+
}
|
|
1227
|
+
return BundleUpdateError(.unknown, step)
|
|
1228
|
+
}
|
|
1229
|
+
|
|
983
1230
|
class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
984
1231
|
|
|
1232
|
+
private func updatePromise<T>(step: BundleUpdateStep, work: @escaping () async throws -> T) -> Promise<T> {
|
|
1233
|
+
return Promise.async {
|
|
1234
|
+
do {
|
|
1235
|
+
return try await work()
|
|
1236
|
+
} catch {
|
|
1237
|
+
OneKeyLog.error("BundleUpdate", "\(step.rawValue) failed: \(error)")
|
|
1238
|
+
throw bundleUpdateError(error, step: step)
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
985
1243
|
// Serial queue protects mutable state (listeners, nextListenerId, isDownloading)
|
|
986
1244
|
private let stateQueue = DispatchQueue(label: "so.onekey.bundleupdate.state")
|
|
987
1245
|
private var listeners: [BundleListener] = []
|
|
988
1246
|
private var nextListenerId: Double = 1
|
|
989
1247
|
private var isDownloading = false
|
|
1248
|
+
private var clearingDownloads = false
|
|
1249
|
+
private var activeRunDelegate: DownloadDelegate?
|
|
990
1250
|
private var urlSession: URLSession?
|
|
991
1251
|
private var downloadDelegate: DownloadDelegate?
|
|
992
1252
|
private var downloadFilePath: String?
|
|
@@ -1060,7 +1320,6 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1060
1320
|
}
|
|
1061
1321
|
|
|
1062
1322
|
private func snapshotResumeDataForBackgrounding() {
|
|
1063
|
-
guard let session = self.urlSession else { return }
|
|
1064
1323
|
// Read both isDownloading AND activeDownloadFilePath inside the
|
|
1065
1324
|
// same stateQueue.sync block. A concurrent downloadBundle entry
|
|
1066
1325
|
// flips isDownloading=true at the start of the Promise body but
|
|
@@ -1070,11 +1329,12 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1070
1329
|
// activeDownloadFilePath is still nil from a previous run, or —
|
|
1071
1330
|
// on the unwind via defer — see isDownloading=false alongside a
|
|
1072
1331
|
// not-yet-cleared path.
|
|
1073
|
-
let snapshot: (
|
|
1074
|
-
|
|
1332
|
+
let snapshot: (URLSession?, DownloadDelegate?, String?) = self.stateQueue.sync {
|
|
1333
|
+
guard self.isDownloading, !self.clearingDownloads else { return (nil, nil, nil) }
|
|
1334
|
+
return (self.urlSession, self.activeRunDelegate, self.activeDownloadFilePath)
|
|
1075
1335
|
}
|
|
1076
|
-
let
|
|
1077
|
-
|
|
1336
|
+
guard let session = snapshot.0, let runDelegate = snapshot.1,
|
|
1337
|
+
let filePath = snapshot.2 else { return }
|
|
1078
1338
|
|
|
1079
1339
|
let resumeDataPath = "\(filePath).resume"
|
|
1080
1340
|
let bgTaskName = "BundleUpdateResumeSnapshot"
|
|
@@ -1109,22 +1369,27 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1109
1369
|
bgTask.set(started)
|
|
1110
1370
|
OneKeyLog.info("BundleUpdate", "didEnterBackground: snapshotting resumeData for \(filePath)")
|
|
1111
1371
|
|
|
1112
|
-
session.getAllTasks { tasks in
|
|
1372
|
+
session.getAllTasks { [weak self] tasks in
|
|
1113
1373
|
let group = DispatchGroup()
|
|
1114
1374
|
for task in tasks {
|
|
1115
1375
|
guard let dl = task as? URLSessionDownloadTask, dl.state == .running else { continue }
|
|
1116
1376
|
group.enter()
|
|
1117
1377
|
dl.cancel(byProducingResumeData: { data in
|
|
1118
1378
|
if let data = data, data.count > 0 {
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1379
|
+
self?.stateQueue.sync {
|
|
1380
|
+
guard self?.activeRunDelegate === runDelegate,
|
|
1381
|
+
self?.clearingDownloads == false,
|
|
1382
|
+
!runDelegate.wasCancelledByClear else { return }
|
|
1383
|
+
do {
|
|
1384
|
+
let dir = (resumeDataPath as NSString).deletingLastPathComponent
|
|
1385
|
+
if !FileManager.default.fileExists(atPath: dir) {
|
|
1386
|
+
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
|
|
1387
|
+
}
|
|
1388
|
+
try data.write(to: URL(fileURLWithPath: resumeDataPath), options: .atomic)
|
|
1389
|
+
OneKeyLog.info("BundleUpdate", "didEnterBackground: persisted resumeData (\(data.count) bytes) at \(resumeDataPath)")
|
|
1390
|
+
} catch {
|
|
1391
|
+
OneKeyLog.warn("BundleUpdate", "didEnterBackground: failed to persist resumeData: \(error)")
|
|
1123
1392
|
}
|
|
1124
|
-
try data.write(to: URL(fileURLWithPath: resumeDataPath), options: .atomic)
|
|
1125
|
-
OneKeyLog.info("BundleUpdate", "didEnterBackground: persisted resumeData (\(data.count) bytes) at \(resumeDataPath)")
|
|
1126
|
-
} catch {
|
|
1127
|
-
OneKeyLog.warn("BundleUpdate", "didEnterBackground: failed to persist resumeData: \(error)")
|
|
1128
1393
|
}
|
|
1129
1394
|
} else {
|
|
1130
1395
|
OneKeyLog.info("BundleUpdate", "didEnterBackground: cancel produced no resumeData (task may have just completed)")
|
|
@@ -1168,17 +1433,22 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1168
1433
|
}
|
|
1169
1434
|
|
|
1170
1435
|
func downloadBundle(params: BundleDownloadParams) throws -> Promise<BundleDownloadResult> {
|
|
1171
|
-
return
|
|
1172
|
-
guard let self = self else { throw
|
|
1436
|
+
return updatePromise(step: .download) { [weak self] in
|
|
1437
|
+
guard let self = self else { throw BundleUpdateError(.unknown, .download) }
|
|
1173
1438
|
|
|
1174
|
-
let alreadyDownloading = self.stateQueue.sync { () -> Bool in
|
|
1175
|
-
if self.isDownloading { return true }
|
|
1439
|
+
let (alreadyDownloading, runDelegate, runSession) = self.stateQueue.sync { () -> (Bool, DownloadDelegate?, URLSession?) in
|
|
1440
|
+
if self.isDownloading || self.clearingDownloads { return (true, nil, nil) }
|
|
1176
1441
|
self.isDownloading = true
|
|
1177
|
-
|
|
1442
|
+
self.downloadDelegate?.reset()
|
|
1443
|
+
self.activeRunDelegate = self.downloadDelegate
|
|
1444
|
+
return (false, self.downloadDelegate, self.urlSession)
|
|
1178
1445
|
}
|
|
1179
1446
|
guard !alreadyDownloading else {
|
|
1180
1447
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: rejected, already downloading")
|
|
1181
|
-
throw
|
|
1448
|
+
throw BundleUpdateError(.invalidArgument, .download)
|
|
1449
|
+
}
|
|
1450
|
+
guard let runDelegate, let runSession else {
|
|
1451
|
+
throw BundleUpdateError(.unknown, .download)
|
|
1182
1452
|
}
|
|
1183
1453
|
defer {
|
|
1184
1454
|
// Clear isDownloading + the snapshot anchor under the same
|
|
@@ -1186,8 +1456,12 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1186
1456
|
// a half-cleared state (isDownloading=true while
|
|
1187
1457
|
// activeDownloadFilePath=nil, or vice versa).
|
|
1188
1458
|
self.stateQueue.sync {
|
|
1189
|
-
|
|
1190
|
-
self.
|
|
1459
|
+
runDelegate.finishRun()
|
|
1460
|
+
if self.activeRunDelegate === runDelegate {
|
|
1461
|
+
self.isDownloading = false
|
|
1462
|
+
self.activeDownloadFilePath = nil
|
|
1463
|
+
self.activeRunDelegate = nil
|
|
1464
|
+
}
|
|
1191
1465
|
}
|
|
1192
1466
|
}
|
|
1193
1467
|
|
|
@@ -1200,12 +1474,12 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1200
1474
|
|
|
1201
1475
|
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
1202
1476
|
OneKeyLog.error("BundleUpdate", "downloadBundle: invalid version string format: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
1203
|
-
throw
|
|
1477
|
+
throw BundleUpdateError(.invalidArgument, .download)
|
|
1204
1478
|
}
|
|
1205
1479
|
|
|
1206
1480
|
guard downloadUrl.hasPrefix("https://") else {
|
|
1207
1481
|
OneKeyLog.error("BundleUpdate", "downloadBundle: URL is not HTTPS: \(downloadUrl)")
|
|
1208
|
-
throw
|
|
1482
|
+
throw BundleUpdateError(.security, .download)
|
|
1209
1483
|
}
|
|
1210
1484
|
|
|
1211
1485
|
let fileName = "\(appVersion)-\(bundleVersion).zip"
|
|
@@ -1216,7 +1490,6 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1216
1490
|
let resumeDataPath = "\(filePath).resume"
|
|
1217
1491
|
|
|
1218
1492
|
let result = BundleDownloadResult(
|
|
1219
|
-
downloadedFile: filePath,
|
|
1220
1493
|
downloadUrl: downloadUrl,
|
|
1221
1494
|
latestVersion: appVersion,
|
|
1222
1495
|
bundleVersion: bundleVersion,
|
|
@@ -1233,8 +1506,12 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1233
1506
|
// A valid completed bundle invalidates any stale resume blob.
|
|
1234
1507
|
try? FileManager.default.removeItem(atPath: resumeDataPath)
|
|
1235
1508
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
|
1509
|
+
guard !runDelegate.wasCancelledByClear else { return }
|
|
1236
1510
|
self?.sendEvent(type: "update/complete")
|
|
1237
1511
|
}
|
|
1512
|
+
if runDelegate.wasCancelledByClear {
|
|
1513
|
+
throw BundleUpdateError(.cancelled, .download)
|
|
1514
|
+
}
|
|
1238
1515
|
return result
|
|
1239
1516
|
} else {
|
|
1240
1517
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: existing file SHA256 mismatch, re-downloading")
|
|
@@ -1261,6 +1538,9 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1261
1538
|
self.activeDownloadFilePath = filePath
|
|
1262
1539
|
self.lastReportedProgress = 0
|
|
1263
1540
|
}
|
|
1541
|
+
if runDelegate.wasCancelledByClear {
|
|
1542
|
+
throw BundleUpdateError(.cancelled, .download)
|
|
1543
|
+
}
|
|
1264
1544
|
|
|
1265
1545
|
// Stable, unique task id for this bundle. Same value across retry
|
|
1266
1546
|
// attempts so the range downloader can resume its `.segN` files, and
|
|
@@ -1276,7 +1556,8 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1276
1556
|
guard let self = self,
|
|
1277
1557
|
event.channel.stringValue == DownloadChannel.bundle.stringValue,
|
|
1278
1558
|
event.taskId == rangeTaskId,
|
|
1279
|
-
event.type == "progress"
|
|
1559
|
+
event.type == "progress",
|
|
1560
|
+
!runDelegate.wasCancelledByClear else { return }
|
|
1280
1561
|
// §5.7: record the concurrent floor so a later single-stream
|
|
1281
1562
|
// transient fallback resumes the bar from here, not from 0.
|
|
1282
1563
|
let floored: Int = self.stateQueue.sync {
|
|
@@ -1286,6 +1567,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1286
1567
|
}
|
|
1287
1568
|
self.sendEvent(type: "update/downloading", progress: floored)
|
|
1288
1569
|
}
|
|
1570
|
+
defer { RangeDownloader.shared.removeListener(progressListenerId) }
|
|
1289
1571
|
|
|
1290
1572
|
// A `.fallback` outcome covers two very different situations whose
|
|
1291
1573
|
// recovery must NOT be the same:
|
|
@@ -1311,9 +1593,11 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1311
1593
|
// `download(...)` directly, so we get the precise class here.
|
|
1312
1594
|
var rangeClass: RangeDownloadClass = .fallbackPermanent
|
|
1313
1595
|
var rangeReason: String?
|
|
1596
|
+
var rangeKind: RangeFallbackClass?
|
|
1597
|
+
var rangeError: Error?
|
|
1314
1598
|
while true {
|
|
1315
1599
|
concurrentAttempt += 1
|
|
1316
|
-
(rangeClass, _, rangeReason,
|
|
1600
|
+
(rangeClass, _, rangeReason, rangeKind, rangeError) = await RangeDownloader.shared.download(
|
|
1317
1601
|
channel: .bundle,
|
|
1318
1602
|
taskId: rangeTaskId,
|
|
1319
1603
|
urlString: downloadUrl,
|
|
@@ -1324,9 +1608,24 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1324
1608
|
segmentCount: nil,
|
|
1325
1609
|
minConcurrentBytes: nil
|
|
1326
1610
|
)
|
|
1611
|
+
if runDelegate.wasCancelledByClear {
|
|
1612
|
+
throw BundleUpdateError(.cancelled, .download)
|
|
1613
|
+
}
|
|
1327
1614
|
if rangeClass == .completed {
|
|
1328
1615
|
break // completed
|
|
1329
1616
|
}
|
|
1617
|
+
if let rangeError {
|
|
1618
|
+
let updateError = bundleUpdateError(rangeError, step: .download)
|
|
1619
|
+
if updateError.code != .network && updateError.code != .timeout {
|
|
1620
|
+
self.sendEvent(type: "update/error", message: updateError.description)
|
|
1621
|
+
throw updateError
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
if rangeKind == .redirectRejected || rangeKind == .checksumMismatch {
|
|
1625
|
+
let updateError = BundleUpdateError(rangeKind == .redirectRejected ? .security : .integrity, .download)
|
|
1626
|
+
self.sendEvent(type: "update/error", message: updateError.description)
|
|
1627
|
+
throw updateError
|
|
1628
|
+
}
|
|
1330
1629
|
// §4: a Transient class is resumable — retry the concurrent path.
|
|
1331
1630
|
// The core keeps `.segN`; the next call resumes only what's missing.
|
|
1332
1631
|
if rangeClass == .fallbackTransient,
|
|
@@ -1335,11 +1634,16 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1335
1634
|
// Brief backoff so a flapping network / suspend settles before
|
|
1336
1635
|
// we re-probe and resume the missing segment(s).
|
|
1337
1636
|
try? await Task.sleep(nanoseconds: 1_500_000_000)
|
|
1637
|
+
if runDelegate.wasCancelledByClear {
|
|
1638
|
+
throw BundleUpdateError(.cancelled, .download)
|
|
1639
|
+
}
|
|
1338
1640
|
continue
|
|
1339
1641
|
}
|
|
1340
1642
|
break // permanent, or transient retries exhausted
|
|
1341
1643
|
}
|
|
1342
|
-
|
|
1644
|
+
if runDelegate.wasCancelledByClear {
|
|
1645
|
+
throw BundleUpdateError(.cancelled, .download)
|
|
1646
|
+
}
|
|
1343
1647
|
|
|
1344
1648
|
if rangeClass != .completed {
|
|
1345
1649
|
if rangeClass == .fallbackTransient {
|
|
@@ -1358,13 +1662,19 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1358
1662
|
// fall through to the single-stream path below
|
|
1359
1663
|
} else {
|
|
1360
1664
|
OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent finished, verifying SHA256...")
|
|
1361
|
-
|
|
1665
|
+
guard let downloadedHash = BundleUpdateStore.calculateSHA256(filePath) else {
|
|
1666
|
+
throw BundleUpdateError(.storage, .download)
|
|
1667
|
+
}
|
|
1668
|
+
if !BundleCryptoCore.secureEqualHex(downloadedHash, sha256) {
|
|
1362
1669
|
let reason = BundleUpdateStore.lastSHA256FailureReason() ?? "MISMATCH"
|
|
1363
1670
|
try? FileManager.default.removeItem(atPath: filePath)
|
|
1364
|
-
self.sendEvent(type: "update/error", message:
|
|
1365
|
-
throw
|
|
1671
|
+
self.sendEvent(type: "update/error", message: BundleUpdateError(.integrity, .download).description)
|
|
1672
|
+
throw BundleUpdateError(.integrity, .download)
|
|
1366
1673
|
}
|
|
1367
1674
|
try? FileManager.default.removeItem(atPath: resumeDataPath)
|
|
1675
|
+
if runDelegate.wasCancelledByClear {
|
|
1676
|
+
throw BundleUpdateError(.cancelled, .download)
|
|
1677
|
+
}
|
|
1368
1678
|
self.sendEvent(type: "update/complete")
|
|
1369
1679
|
OneKeyLog.info("BundleUpdate", "downloadBundle: concurrent completed successfully, appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
1370
1680
|
return result
|
|
@@ -1373,13 +1683,10 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1373
1683
|
// Download the file
|
|
1374
1684
|
guard let url = URL(string: downloadUrl) else {
|
|
1375
1685
|
OneKeyLog.error("BundleUpdate", "downloadBundle: invalid URL: \(downloadUrl)")
|
|
1376
|
-
throw
|
|
1686
|
+
throw BundleUpdateError(.invalidArgument, .download)
|
|
1377
1687
|
}
|
|
1378
1688
|
|
|
1379
|
-
|
|
1380
|
-
OneKeyLog.error("BundleUpdate", "downloadBundle: URLSession not initialized")
|
|
1381
|
-
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "URLSession not initialized"])
|
|
1382
|
-
}
|
|
1689
|
+
let session = runSession
|
|
1383
1690
|
|
|
1384
1691
|
// Resume blob from a prior failed attempt; iOS rebuilds the byte
|
|
1385
1692
|
// offset internally so we never need a Range header on this path.
|
|
@@ -1396,11 +1703,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1396
1703
|
let request = URLRequest(url: url)
|
|
1397
1704
|
|
|
1398
1705
|
// Use delegate-based download for real progress reporting
|
|
1399
|
-
|
|
1400
|
-
OneKeyLog.error("BundleUpdate", "downloadBundle: download delegate not initialized")
|
|
1401
|
-
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Download delegate not initialized"])
|
|
1402
|
-
}
|
|
1403
|
-
delegate.reset()
|
|
1706
|
+
let delegate = runDelegate
|
|
1404
1707
|
// OCDS §5.7: reported progress is monotonic non-decreasing within a
|
|
1405
1708
|
// run, and resets only on a GENUINE restart. The single-stream
|
|
1406
1709
|
// delegate emits its own 0→100, which would jump the bar backward at
|
|
@@ -1412,7 +1715,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1412
1715
|
: 0
|
|
1413
1716
|
self.stateQueue.sync { self.lastReportedProgress = progressFloor }
|
|
1414
1717
|
delegate.onProgress = { [weak self] progress in
|
|
1415
|
-
guard let self = self else { return }
|
|
1718
|
+
guard let self = self, !runDelegate.wasCancelledByClear else { return }
|
|
1416
1719
|
let floored: Int = self.stateQueue.sync {
|
|
1417
1720
|
let clamped = max(self.lastReportedProgress, progress)
|
|
1418
1721
|
self.lastReportedProgress = clamped
|
|
@@ -1428,11 +1731,14 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1428
1731
|
// snapshot reader uses, so isDownloading=true and a non-nil
|
|
1429
1732
|
// activeDownloadFilePath always go together.
|
|
1430
1733
|
self.stateQueue.sync { self.activeDownloadFilePath = filePath }
|
|
1734
|
+
if runDelegate.wasCancelledByClear {
|
|
1735
|
+
throw BundleUpdateError(.cancelled, .download)
|
|
1736
|
+
}
|
|
1431
1737
|
|
|
1432
1738
|
let downloadOutcome: Result<(URL, URLResponse), Error>
|
|
1433
1739
|
do {
|
|
1434
1740
|
let value = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(URL, URLResponse), Error>) in
|
|
1435
|
-
delegate.setContinuation(continuation)
|
|
1741
|
+
guard delegate.setContinuation(continuation) else { return }
|
|
1436
1742
|
let task: URLSessionDownloadTask
|
|
1437
1743
|
if let resumeData = persistedResumeData {
|
|
1438
1744
|
task = session.downloadTask(withResumeData: resumeData)
|
|
@@ -1467,20 +1773,16 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1467
1773
|
}
|
|
1468
1774
|
let nsError = error as NSError
|
|
1469
1775
|
OneKeyLog.error("BundleUpdate", "downloadBundle: download failed: \(nsError.domain) \(nsError.code) \(nsError.localizedDescription)")
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
throw
|
|
1480
|
-
domain: nsError.domain,
|
|
1481
|
-
code: nsError.code,
|
|
1482
|
-
userInfo: [NSLocalizedDescriptionKey: "Download failed: \(nsError.domain) \(nsError.code)"]
|
|
1483
|
-
)
|
|
1776
|
+
let intentionallyCancelled = delegate.wasCancelledByClear
|
|
1777
|
+
let updateError = intentionallyCancelled
|
|
1778
|
+
? BundleUpdateError(.cancelled, .download)
|
|
1779
|
+
: (delegate.wasInsecureRedirectRejected
|
|
1780
|
+
? BundleUpdateError(.security, .download)
|
|
1781
|
+
: bundleUpdateError(error, step: .download))
|
|
1782
|
+
if !intentionallyCancelled {
|
|
1783
|
+
self.sendEvent(type: "update/error", message: updateError.description)
|
|
1784
|
+
}
|
|
1785
|
+
throw updateError
|
|
1484
1786
|
|
|
1485
1787
|
case .success(let (tempURL, response)):
|
|
1486
1788
|
// Successful completion ⇒ no longer need the resume blob.
|
|
@@ -1491,7 +1793,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1491
1793
|
let responseUrl = httpResponse.url,
|
|
1492
1794
|
responseUrl.scheme?.lowercased() != "https" {
|
|
1493
1795
|
OneKeyLog.error("BundleUpdate", "downloadBundle: redirected to non-HTTPS URL: \(responseUrl)")
|
|
1494
|
-
throw
|
|
1796
|
+
throw BundleUpdateError(.security, .download)
|
|
1495
1797
|
}
|
|
1496
1798
|
|
|
1497
1799
|
// 206 is acceptable when the OS finished a Range-resumed task on
|
|
@@ -1500,8 +1802,9 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1500
1802
|
httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else {
|
|
1501
1803
|
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
|
|
1502
1804
|
OneKeyLog.error("BundleUpdate", "downloadBundle: HTTP error, statusCode=\(statusCode)")
|
|
1503
|
-
|
|
1504
|
-
|
|
1805
|
+
let updateError = BundleUpdateError(.http, .download, httpStatus: statusCode)
|
|
1806
|
+
self.sendEvent(type: "update/error", message: updateError.description)
|
|
1807
|
+
throw updateError
|
|
1505
1808
|
}
|
|
1506
1809
|
|
|
1507
1810
|
OneKeyLog.info("BundleUpdate", "downloadBundle: download finished, HTTP \(httpResponse.statusCode), moving to destination...")
|
|
@@ -1518,12 +1821,15 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1518
1821
|
|
|
1519
1822
|
// Verify SHA256
|
|
1520
1823
|
OneKeyLog.info("BundleUpdate", "downloadBundle: verifying SHA256...")
|
|
1521
|
-
|
|
1824
|
+
guard let downloadedHash = BundleUpdateStore.calculateSHA256(filePath) else {
|
|
1825
|
+
throw BundleUpdateError(.storage, .download)
|
|
1826
|
+
}
|
|
1827
|
+
if !BundleCryptoCore.secureEqualHex(downloadedHash, sha256) {
|
|
1522
1828
|
let reason = BundleUpdateStore.lastSHA256FailureReason() ?? "MISMATCH"
|
|
1523
1829
|
try? FileManager.default.removeItem(atPath: filePath)
|
|
1524
1830
|
OneKeyLog.error("BundleUpdate", "downloadBundle: SHA256 verification failed after download, reason=\(reason)")
|
|
1525
|
-
self.sendEvent(type: "update/error", message:
|
|
1526
|
-
throw
|
|
1831
|
+
self.sendEvent(type: "update/error", message: BundleUpdateError(.integrity, .download).description)
|
|
1832
|
+
throw BundleUpdateError(.integrity, .download)
|
|
1527
1833
|
}
|
|
1528
1834
|
|
|
1529
1835
|
// A verified single-stream file is authoritative — drop any
|
|
@@ -1531,6 +1837,9 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1531
1837
|
// transient fallback so they don't linger as orphans.
|
|
1532
1838
|
RangeDownloader.shared.discardArtifacts(filePath: filePath)
|
|
1533
1839
|
|
|
1840
|
+
if runDelegate.wasCancelledByClear {
|
|
1841
|
+
throw BundleUpdateError(.cancelled, .download)
|
|
1842
|
+
}
|
|
1534
1843
|
self.sendEvent(type: "update/complete")
|
|
1535
1844
|
OneKeyLog.info("BundleUpdate", "downloadBundle: completed successfully, appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
1536
1845
|
return result
|
|
@@ -1549,30 +1858,71 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1549
1858
|
}
|
|
1550
1859
|
|
|
1551
1860
|
func downloadBundleASC(params: BundleDownloadASCParams) throws -> Promise<Void> {
|
|
1552
|
-
return
|
|
1861
|
+
return updatePromise(step: .downloadASC) {
|
|
1553
1862
|
let appVersion = params.latestVersion
|
|
1554
1863
|
let bundleVersion = params.bundleVersion
|
|
1555
1864
|
let signature = params.signature
|
|
1556
1865
|
|
|
1557
1866
|
OneKeyLog.info("BundleUpdate", "downloadBundleASC: appVersion=\(appVersion), bundleVersion=\(bundleVersion), signatureLength=\(signature.count)")
|
|
1558
1867
|
|
|
1868
|
+
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
1869
|
+
OneKeyLog.error("BundleUpdate", "downloadBundleASC: invalid version string format: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
1870
|
+
throw BundleUpdateError(.invalidArgument, .downloadASC)
|
|
1871
|
+
}
|
|
1872
|
+
guard signature.utf8.count <= 64 * 1024 else {
|
|
1873
|
+
OneKeyLog.error("BundleUpdate", "downloadBundleASC: signature exceeds maximum allowed size (\(signature.utf8.count))")
|
|
1874
|
+
throw BundleUpdateError(.invalidArgument, .downloadASC)
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1559
1877
|
let storageKey = "\(appVersion)-\(bundleVersion)"
|
|
1560
|
-
BundleUpdateStore.
|
|
1878
|
+
let sigPath = (BundleUpdateStore.signatureFilePath(storageKey) as NSString).resolvingSymlinksInPath
|
|
1879
|
+
let baseAscDir = (BundleUpdateStore.ascDir() as NSString).resolvingSymlinksInPath
|
|
1880
|
+
guard sigPath.hasPrefix(baseAscDir + "/") else {
|
|
1881
|
+
OneKeyLog.error("BundleUpdate", "downloadBundleASC: signature path outside asc directory: \(sigPath)")
|
|
1882
|
+
throw BundleUpdateError(.security, .downloadASC)
|
|
1883
|
+
}
|
|
1561
1884
|
|
|
1562
|
-
OneKeyLog.info("BundleUpdate", "downloadBundleASC: stored signature for key=\(storageKey)")
|
|
1563
1885
|
}
|
|
1564
1886
|
}
|
|
1565
1887
|
|
|
1566
1888
|
func verifyBundleASC(params: BundleVerifyASCParams) throws -> Promise<Void> {
|
|
1567
|
-
return
|
|
1568
|
-
let filePath = params.downloadedFile
|
|
1889
|
+
return updatePromise(step: .verifyASC) {
|
|
1569
1890
|
let sha256 = params.sha256
|
|
1570
1891
|
let appVersion = params.latestVersion
|
|
1571
1892
|
let bundleVersion = params.bundleVersion
|
|
1572
1893
|
let signature = params.signature
|
|
1573
1894
|
|
|
1895
|
+
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
1896
|
+
OneKeyLog.error("BundleUpdate", "verifyBundleASC: invalid version string format: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
1897
|
+
throw BundleUpdateError(.invalidArgument, .verifyASC)
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
let filePath = (BundleUpdateStore.downloadBundleDir() as NSString)
|
|
1901
|
+
.appendingPathComponent("\(appVersion)-\(bundleVersion).zip")
|
|
1902
|
+
let resolvedFilePath = (filePath as NSString).resolvingSymlinksInPath
|
|
1903
|
+
let baseDownloadDir = (BundleUpdateStore.downloadBundleDir() as NSString).resolvingSymlinksInPath
|
|
1904
|
+
guard resolvedFilePath.hasPrefix(baseDownloadDir + "/") else {
|
|
1905
|
+
OneKeyLog.error("BundleUpdate", "verifyBundleASC: ZIP outside download directory: \(resolvedFilePath)")
|
|
1906
|
+
throw BundleUpdateError(.security, .verifyASC)
|
|
1907
|
+
}
|
|
1908
|
+
guard FileManager.default.fileExists(atPath: resolvedFilePath) else {
|
|
1909
|
+
throw BundleUpdateError(.missing, .verifyASC)
|
|
1910
|
+
}
|
|
1574
1911
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: appVersion=\(appVersion), bundleVersion=\(bundleVersion), file=\(filePath), signatureLength=\(signature.count)")
|
|
1575
1912
|
|
|
1913
|
+
let folderName = "\(appVersion)-\(bundleVersion)"
|
|
1914
|
+
let isInstalled = BundleUpdateStore.currentBundleVersion() == folderName ||
|
|
1915
|
+
BundleUpdateStore.readFallbackUpdateBundleDataFile().contains {
|
|
1916
|
+
$0["appVersion"] == appVersion && $0["bundleVersion"] == bundleVersion
|
|
1917
|
+
}
|
|
1918
|
+
let destination = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
|
|
1919
|
+
let resolvedDestination = (destination as NSString).resolvingSymlinksInPath
|
|
1920
|
+
let baseBundleDir = (BundleUpdateStore.bundleDir() as NSString).resolvingSymlinksInPath
|
|
1921
|
+
guard resolvedDestination.hasPrefix(baseBundleDir + "/") else {
|
|
1922
|
+
OneKeyLog.error("BundleUpdate", "verifyBundleASC: destination outside bundle directory: \(resolvedDestination)")
|
|
1923
|
+
throw BundleUpdateError(.security, .verifyASC)
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1576
1926
|
// GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled
|
|
1577
1927
|
#if ALLOW_SKIP_GPG_VERIFICATION
|
|
1578
1928
|
let skipGPG = BundleUpdateStore.isDevSettingsEnabled() && BundleUpdateStore.isSkipGPGEnabled()
|
|
@@ -1582,56 +1932,80 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1582
1932
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG check: skipGPG=\(skipGPG)")
|
|
1583
1933
|
|
|
1584
1934
|
if !skipGPG {
|
|
1935
|
+
OneKeyLog.info("BundleUpdate", "verifyBundleASC: pre-verifying GPG signature before extraction...")
|
|
1936
|
+
guard let preVerifiedSha256 = BundleUpdateStore.readMetadataFileSha256(
|
|
1937
|
+
signature,
|
|
1938
|
+
expectedAppVersion: appVersion,
|
|
1939
|
+
expectedBundleVersion: bundleVersion,
|
|
1940
|
+
expectedAppType: "ios"
|
|
1941
|
+
), !preVerifiedSha256.isEmpty else {
|
|
1942
|
+
OneKeyLog.error("BundleUpdate", "verifyBundleASC: pre-extraction GPG signature verification failed")
|
|
1943
|
+
throw BundleUpdateError(.integrity, .verifyASC)
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1585
1946
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: verifying SHA256 of downloaded file...")
|
|
1586
|
-
let calculated = BundleUpdateStore.calculateSHA256(filePath)
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
//
|
|
1591
|
-
// analytics' extractUpdateErrorCode can split this
|
|
1592
|
-
// bucket the same way the download stage already does.
|
|
1947
|
+
guard let calculated = BundleUpdateStore.calculateSHA256(filePath) else {
|
|
1948
|
+
throw BundleUpdateError(.storage, .verifyASC)
|
|
1949
|
+
}
|
|
1950
|
+
if !BundleCryptoCore.secureEqualHex(calculated, sha256) {
|
|
1951
|
+
// Keep the detailed hash reason in the local log only.
|
|
1593
1952
|
let reason = BundleUpdateStore.lastSHA256FailureReason() ?? "MISMATCH"
|
|
1594
1953
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: SHA256 verification failed for file=\(filePath), reason=\(reason)")
|
|
1595
|
-
throw
|
|
1954
|
+
throw BundleUpdateError(.integrity, .verifyASC)
|
|
1596
1955
|
}
|
|
1597
1956
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: SHA256 verified OK")
|
|
1598
1957
|
} else {
|
|
1599
1958
|
OneKeyLog.warn("BundleUpdate", "verifyBundleASC: SHA256 + GPG verification skipped (DevSettings enabled)")
|
|
1600
1959
|
}
|
|
1601
1960
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1961
|
+
if isInstalled {
|
|
1962
|
+
if !skipGPG && !BundleUpdateStore.validateMetadataFileSha256(folderName, signature: signature, expectedAppVersion: appVersion, expectedBundleVersion: bundleVersion) {
|
|
1963
|
+
throw BundleUpdateError(.integrity, .verifyASC)
|
|
1964
|
+
}
|
|
1965
|
+
guard let metadata = BundleUpdateStore.getMetadataFileContent(folderName),
|
|
1966
|
+
BundleCryptoCore.validateExtractedPathSafety(destination),
|
|
1967
|
+
BundleUpdateStore.validateAllFilesInDir(destination, metadata: metadata, appVersion: appVersion, bundleVersion: bundleVersion),
|
|
1968
|
+
BundleUpdateStore.validateBundlePairCompatibility(bundleDirPath: destination, metadata: metadata) else {
|
|
1969
|
+
throw BundleUpdateError(.integrity, .verifyASC)
|
|
1970
|
+
}
|
|
1971
|
+
return
|
|
1972
|
+
}
|
|
1604
1973
|
|
|
1605
|
-
// Check zip file size before extraction (decompression bomb protection)
|
|
1974
|
+
// Check zip file size and uncompressed payload size before extraction (decompression bomb protection)
|
|
1606
1975
|
let maxZipFileSize: UInt64 = 512 * 1024 * 1024 // 512 MB
|
|
1976
|
+
let maxUncompressedSize: UInt64 = 512 * 1024 * 1024
|
|
1607
1977
|
if let attrs = try? FileManager.default.attributesOfItem(atPath: filePath),
|
|
1608
1978
|
let fileSize = attrs[.size] as? UInt64 {
|
|
1609
1979
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: zip file size=\(fileSize) bytes")
|
|
1610
1980
|
if fileSize > maxZipFileSize {
|
|
1611
1981
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: zip file too large (\(fileSize) > \(maxZipFileSize))")
|
|
1612
|
-
throw
|
|
1982
|
+
throw BundleUpdateError(.security, .verifyASC)
|
|
1613
1983
|
}
|
|
1614
1984
|
}
|
|
1615
1985
|
|
|
1986
|
+
var payloadError: NSError?
|
|
1987
|
+
let payloadSize = SSZipArchive.payloadSizeForArchive(atPath: filePath, error: &payloadError)
|
|
1988
|
+
if payloadError != nil || payloadSize.uint64Value == 0 || payloadSize.uint64Value > maxUncompressedSize {
|
|
1989
|
+
OneKeyLog.error("BundleUpdate", "verifyBundleASC: zip payload size invalid or exceeds limit (payloadSize=\(payloadSize), error=\(String(describing: payloadError)))")
|
|
1990
|
+
throw BundleUpdateError(.security, .verifyASC)
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// Clean up any existing destination directory before unzipping so leftover files cannot persist
|
|
1994
|
+
if FileManager.default.fileExists(atPath: destination) {
|
|
1995
|
+
try? FileManager.default.removeItem(atPath: destination)
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1616
1998
|
// Unzip using SSZipArchive
|
|
1617
1999
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: extracting zip to \(destination)...")
|
|
1618
2000
|
do {
|
|
1619
2001
|
try SSZipArchive.unzipFile(atPath: filePath, toDestination: destination, overwrite: true, password: nil)
|
|
1620
2002
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: extraction completed")
|
|
1621
2003
|
} catch {
|
|
1622
|
-
//
|
|
1623
|
-
// failing file path which on iOS includes the install UUID
|
|
1624
|
-
// (/var/mobile/Containers/Data/Application/<UUID>/...). Keep
|
|
1625
|
-
// the rich detail in OneKeyLog (local-only), but expose only
|
|
1626
|
-
// a code-shaped tag in the thrown message so the JS-side
|
|
1627
|
-
// analytics layer can never reflect that path back to the
|
|
1628
|
-
// server. Subtypes intentionally low-cardinality so
|
|
1629
|
-
// extractUpdateErrorCode picks them up cleanly:
|
|
1630
|
-
// IO_<NSError code>
|
|
2004
|
+
// Keep the file path in the local log only.
|
|
1631
2005
|
let nsErr = error as NSError
|
|
1632
2006
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: unzip failed: domain=\(nsErr.domain) code=\(nsErr.code) desc=\(nsErr.localizedDescription)")
|
|
1633
2007
|
try? FileManager.default.removeItem(atPath: destination)
|
|
1634
|
-
throw
|
|
2008
|
+
throw BundleUpdateError(.storage, .verifyASC)
|
|
1635
2009
|
}
|
|
1636
2010
|
|
|
1637
2011
|
// Validate extracted paths (symlinks, path traversal)
|
|
@@ -1639,23 +2013,48 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1639
2013
|
if !BundleCryptoCore.validateExtractedPathSafety(destination) {
|
|
1640
2014
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: path traversal or symlink attack detected")
|
|
1641
2015
|
try? FileManager.default.removeItem(atPath: destination)
|
|
1642
|
-
throw
|
|
2016
|
+
throw BundleUpdateError(.security, .verifyASC)
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Enforce extracted entry count and total uncompressed size limits
|
|
2020
|
+
let maxZipEntries = 4096
|
|
2021
|
+
if let enumerator = FileManager.default.enumerator(atPath: destination) {
|
|
2022
|
+
var entryCount = 0
|
|
2023
|
+
var totalUncompressedBytes: UInt64 = 0
|
|
2024
|
+
while let relPath = enumerator.nextObject() as? String {
|
|
2025
|
+
entryCount += 1
|
|
2026
|
+
if entryCount > maxZipEntries {
|
|
2027
|
+
OneKeyLog.error("BundleUpdate", "verifyBundleASC: extracted entry count exceeds maximum allowed (\(maxZipEntries))")
|
|
2028
|
+
try? FileManager.default.removeItem(atPath: destination)
|
|
2029
|
+
throw BundleUpdateError(.security, .verifyASC)
|
|
2030
|
+
}
|
|
2031
|
+
let fullPath = (destination as NSString).appendingPathComponent(relPath)
|
|
2032
|
+
if let attrs = try? FileManager.default.attributesOfItem(atPath: fullPath),
|
|
2033
|
+
let fileSize = attrs[.size] as? UInt64 {
|
|
2034
|
+
totalUncompressedBytes += fileSize
|
|
2035
|
+
if totalUncompressedBytes > maxUncompressedSize {
|
|
2036
|
+
OneKeyLog.error("BundleUpdate", "verifyBundleASC: extracted size exceeds maximum allowed (\(maxUncompressedSize))")
|
|
2037
|
+
try? FileManager.default.removeItem(atPath: destination)
|
|
2038
|
+
throw BundleUpdateError(.security, .verifyASC)
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1643
2042
|
}
|
|
1644
2043
|
|
|
1645
2044
|
let metadataJsonPath = (destination as NSString).appendingPathComponent("metadata.json")
|
|
1646
2045
|
guard FileManager.default.fileExists(atPath: metadataJsonPath) else {
|
|
1647
2046
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: metadata.json not found after extraction")
|
|
1648
2047
|
try? FileManager.default.removeItem(atPath: destination)
|
|
1649
|
-
throw
|
|
2048
|
+
throw BundleUpdateError(.missing, .verifyASC)
|
|
1650
2049
|
}
|
|
1651
2050
|
|
|
1652
2051
|
let currentBundleVersion = "\(appVersion)-\(bundleVersion)"
|
|
1653
2052
|
if !skipGPG {
|
|
1654
2053
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating GPG signature for metadata...")
|
|
1655
|
-
if !BundleUpdateStore.validateMetadataFileSha256(currentBundleVersion, signature: signature) {
|
|
2054
|
+
if !BundleUpdateStore.validateMetadataFileSha256(currentBundleVersion, signature: signature, expectedAppVersion: appVersion, expectedBundleVersion: bundleVersion) {
|
|
1656
2055
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: GPG signature verification failed")
|
|
1657
2056
|
try? FileManager.default.removeItem(atPath: destination)
|
|
1658
|
-
throw
|
|
2057
|
+
throw BundleUpdateError(.integrity, .verifyASC)
|
|
1659
2058
|
}
|
|
1660
2059
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG signature verified OK")
|
|
1661
2060
|
} else {
|
|
@@ -1666,18 +2065,18 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1666
2065
|
guard let metadata = BundleUpdateStore.getMetadataFileContent(currentBundleVersion) else {
|
|
1667
2066
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: failed to read metadata.json content")
|
|
1668
2067
|
try? FileManager.default.removeItem(atPath: destination)
|
|
1669
|
-
throw
|
|
2068
|
+
throw BundleUpdateError(.missing, .verifyASC)
|
|
1670
2069
|
}
|
|
1671
2070
|
|
|
1672
2071
|
if !BundleUpdateStore.validateAllFilesInDir(destination, metadata: metadata, appVersion: appVersion, bundleVersion: bundleVersion) {
|
|
1673
2072
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: file integrity check failed")
|
|
1674
2073
|
try? FileManager.default.removeItem(atPath: destination)
|
|
1675
|
-
throw
|
|
2074
|
+
throw BundleUpdateError(.integrity, .verifyASC)
|
|
1676
2075
|
}
|
|
1677
2076
|
if !BundleUpdateStore.validateBundlePairCompatibility(bundleDirPath: destination, metadata: metadata) {
|
|
1678
2077
|
OneKeyLog.error("BundleUpdate", "verifyBundleASC: bundle pair compatibility check failed")
|
|
1679
2078
|
try? FileManager.default.removeItem(atPath: destination)
|
|
1680
|
-
throw
|
|
2079
|
+
throw BundleUpdateError(.incompatible, .verifyASC)
|
|
1681
2080
|
}
|
|
1682
2081
|
|
|
1683
2082
|
OneKeyLog.info("BundleUpdate", "verifyBundleASC: all verifications passed, appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
@@ -1685,22 +2084,37 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1685
2084
|
}
|
|
1686
2085
|
|
|
1687
2086
|
func verifyBundle(params: BundleVerifyParams) throws -> Promise<Void> {
|
|
1688
|
-
return
|
|
1689
|
-
let filePath = params.downloadedFile
|
|
2087
|
+
return updatePromise(step: .verifyPackage) {
|
|
1690
2088
|
let sha256 = params.sha256
|
|
1691
2089
|
let appVersion = params.latestVersion
|
|
1692
2090
|
let bundleVersion = params.bundleVersion
|
|
1693
2091
|
|
|
2092
|
+
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
2093
|
+
OneKeyLog.error("BundleUpdate", "verifyBundle: invalid version string format: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
2094
|
+
throw BundleUpdateError(.invalidArgument, .verifyPackage)
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
let filePath = (BundleUpdateStore.downloadBundleDir() as NSString)
|
|
2098
|
+
.appendingPathComponent("\(appVersion)-\(bundleVersion).zip")
|
|
2099
|
+
let resolvedFilePath = (filePath as NSString).resolvingSymlinksInPath
|
|
2100
|
+
let baseDownloadDir = (BundleUpdateStore.downloadBundleDir() as NSString).resolvingSymlinksInPath
|
|
2101
|
+
guard resolvedFilePath.hasPrefix(baseDownloadDir + "/") else {
|
|
2102
|
+
OneKeyLog.error("BundleUpdate", "verifyBundle: ZIP outside download directory: \(resolvedFilePath)")
|
|
2103
|
+
throw BundleUpdateError(.security, .verifyPackage)
|
|
2104
|
+
}
|
|
2105
|
+
guard FileManager.default.fileExists(atPath: resolvedFilePath) else {
|
|
2106
|
+
throw BundleUpdateError(.missing, .verifyPackage)
|
|
2107
|
+
}
|
|
1694
2108
|
OneKeyLog.info("BundleUpdate", "verifyBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion), file=\(filePath)")
|
|
1695
2109
|
|
|
1696
2110
|
// Verify SHA256 of the downloaded file
|
|
1697
2111
|
guard let calculated = BundleUpdateStore.calculateSHA256(filePath) else {
|
|
1698
2112
|
OneKeyLog.error("BundleUpdate", "verifyBundle: failed to calculate SHA256 for file=\(filePath)")
|
|
1699
|
-
throw
|
|
2113
|
+
throw BundleUpdateError(.storage, .verifyPackage)
|
|
1700
2114
|
}
|
|
1701
2115
|
guard BundleCryptoCore.secureEqualHex(calculated, sha256) else {
|
|
1702
2116
|
OneKeyLog.error("BundleUpdate", "verifyBundle: SHA256 mismatch, expected=\(sha256.prefix(16))..., got=\(calculated.prefix(16))...")
|
|
1703
|
-
throw
|
|
2117
|
+
throw BundleUpdateError(.integrity, .verifyPackage)
|
|
1704
2118
|
}
|
|
1705
2119
|
|
|
1706
2120
|
OneKeyLog.info("BundleUpdate", "verifyBundle: SHA256 verified OK for appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
@@ -1713,13 +2127,18 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1713
2127
|
// commits so a same-version reinstall (or any reader that re-cached
|
|
1714
2128
|
// pre-commit data inside the race window) can't leave stale entries.
|
|
1715
2129
|
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
1716
|
-
return
|
|
2130
|
+
return updatePromise(step: .install) {
|
|
1717
2131
|
let appVersion = params.latestVersion
|
|
1718
2132
|
let bundleVersion = params.bundleVersion
|
|
1719
2133
|
let signature = params.signature
|
|
1720
2134
|
|
|
1721
2135
|
OneKeyLog.info("BundleUpdate", "installBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion), signatureLength=\(signature.count)")
|
|
1722
2136
|
|
|
2137
|
+
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
2138
|
+
OneKeyLog.error("BundleUpdate", "installBundle: invalid version string format: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
2139
|
+
throw BundleUpdateError(.invalidArgument, .install)
|
|
2140
|
+
}
|
|
2141
|
+
|
|
1723
2142
|
// GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled
|
|
1724
2143
|
#if ALLOW_SKIP_GPG_VERIFICATION
|
|
1725
2144
|
let skipGPG = BundleUpdateStore.isDevSettingsEnabled() && BundleUpdateStore.isSkipGPGEnabled()
|
|
@@ -1730,20 +2149,63 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1730
2149
|
|
|
1731
2150
|
let folderName = "\(appVersion)-\(bundleVersion)"
|
|
1732
2151
|
let currentFolderName = BundleUpdateStore.currentBundleVersion()
|
|
2152
|
+
let previousActiveBundleVersion = currentFolderName
|
|
1733
2153
|
OneKeyLog.info("BundleUpdate", "installBundle: target=\(folderName), current=\(currentFolderName ?? "nil")")
|
|
1734
2154
|
|
|
1735
|
-
// Verify bundle directory
|
|
2155
|
+
// Verify bundle directory confinement and existence
|
|
1736
2156
|
let bundleDirPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
|
|
2157
|
+
let resolvedBundleDir = (bundleDirPath as NSString).resolvingSymlinksInPath
|
|
2158
|
+
let baseBundleDir = (BundleUpdateStore.bundleDir() as NSString).resolvingSymlinksInPath
|
|
2159
|
+
guard resolvedBundleDir.hasPrefix(baseBundleDir + "/") else {
|
|
2160
|
+
OneKeyLog.error("BundleUpdate", "installBundle: bundle path outside bundle directory: \(resolvedBundleDir)")
|
|
2161
|
+
throw BundleUpdateError(.security, .install)
|
|
2162
|
+
}
|
|
2163
|
+
let sigPath = (BundleUpdateStore.signatureFilePath(folderName) as NSString).resolvingSymlinksInPath
|
|
2164
|
+
let baseAscDir = (BundleUpdateStore.ascDir() as NSString).resolvingSymlinksInPath
|
|
2165
|
+
guard sigPath.hasPrefix(baseAscDir + "/") else {
|
|
2166
|
+
OneKeyLog.error("BundleUpdate", "installBundle: signature path outside asc directory: \(sigPath)")
|
|
2167
|
+
throw BundleUpdateError(.security, .install)
|
|
2168
|
+
}
|
|
2169
|
+
|
|
1737
2170
|
guard FileManager.default.fileExists(atPath: bundleDirPath) else {
|
|
1738
2171
|
OneKeyLog.error("BundleUpdate", "installBundle: bundle directory not found: \(bundleDirPath)")
|
|
1739
|
-
throw
|
|
2172
|
+
throw BundleUpdateError(.missing, .install)
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
let mainBundlePath = (bundleDirPath as NSString).appendingPathComponent("main.jsbundle.hbc")
|
|
2176
|
+
let metadataJsonPath = (bundleDirPath as NSString).appendingPathComponent("metadata.json")
|
|
2177
|
+
guard FileManager.default.fileExists(atPath: mainBundlePath),
|
|
2178
|
+
FileManager.default.fileExists(atPath: metadataJsonPath) else {
|
|
2179
|
+
OneKeyLog.error("BundleUpdate", "installBundle: required bundle files missing in \(bundleDirPath)")
|
|
2180
|
+
throw BundleUpdateError(.missing, .install)
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
if !skipGPG {
|
|
2184
|
+
guard !signature.isEmpty,
|
|
2185
|
+
BundleUpdateStore.validateMetadataFileSha256(folderName, signature: signature, expectedAppVersion: appVersion, expectedBundleVersion: bundleVersion) else {
|
|
2186
|
+
OneKeyLog.error("BundleUpdate", "installBundle: GPG signature verification failed for \(folderName)")
|
|
2187
|
+
throw BundleUpdateError(.integrity, .install)
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
guard let metadata = BundleUpdateStore.getMetadataFileContent(folderName) else {
|
|
2192
|
+
OneKeyLog.error("BundleUpdate", "installBundle: failed to read metadata.json content for \(folderName)")
|
|
2193
|
+
throw BundleUpdateError(.missing, .install)
|
|
2194
|
+
}
|
|
2195
|
+
if !BundleUpdateStore.validateAllFilesInDir(bundleDirPath, metadata: metadata, appVersion: appVersion, bundleVersion: bundleVersion) {
|
|
2196
|
+
OneKeyLog.error("BundleUpdate", "installBundle: extracted file verification failed for \(folderName)")
|
|
2197
|
+
throw BundleUpdateError(.integrity, .install)
|
|
2198
|
+
}
|
|
2199
|
+
if !BundleUpdateStore.validateBundlePairCompatibility(bundleDirPath: bundleDirPath, metadata: metadata) {
|
|
2200
|
+
OneKeyLog.error("BundleUpdate", "installBundle: bundle pair compatibility check failed for \(folderName)")
|
|
2201
|
+
throw BundleUpdateError(.incompatible, .install)
|
|
1740
2202
|
}
|
|
1741
2203
|
|
|
1742
|
-
let ud = UserDefaults.standard
|
|
1743
|
-
ud.set(folderName, forKey: "currentBundleVersion")
|
|
1744
2204
|
if !signature.isEmpty {
|
|
1745
2205
|
BundleUpdateStore.writeSignatureFile(folderName, signature: signature)
|
|
1746
2206
|
}
|
|
2207
|
+
let ud = UserDefaults.standard
|
|
2208
|
+
ud.set(folderName, forKey: "currentBundleVersion")
|
|
1747
2209
|
let currentNativeVersion = BundleUpdateStore.getCurrentNativeVersion()
|
|
1748
2210
|
BundleUpdateStore.setNativeVersion(currentNativeVersion)
|
|
1749
2211
|
let currentBuildNumber = BundleUpdateStore.getCurrentNativeBuildNumber()
|
|
@@ -1754,13 +2216,11 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1754
2216
|
var fallbackData = BundleUpdateStore.readFallbackUpdateBundleDataFile()
|
|
1755
2217
|
|
|
1756
2218
|
if let current = currentFolderName,
|
|
1757
|
-
let
|
|
1758
|
-
let curAppVersion = String(current[current.startIndex..<dashRange.lowerBound])
|
|
1759
|
-
let curBundleVersion = String(current[dashRange.upperBound...])
|
|
2219
|
+
let curBundleVersion = BundleUpdateStore.bundleVersion(inFolder: current, appVersion: currentNativeVersion) {
|
|
1760
2220
|
let curSignature = BundleUpdateStore.readSignatureFile(current)
|
|
1761
2221
|
if !curSignature.isEmpty {
|
|
1762
2222
|
fallbackData.append([
|
|
1763
|
-
"appVersion":
|
|
2223
|
+
"appVersion": currentNativeVersion,
|
|
1764
2224
|
"bundleVersion": curBundleVersion,
|
|
1765
2225
|
"signature": curSignature
|
|
1766
2226
|
])
|
|
@@ -1772,10 +2232,16 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1772
2232
|
let shifted = fallbackData.removeFirst()
|
|
1773
2233
|
if let shiftApp = shifted["appVersion"], let shiftBundle = shifted["bundleVersion"] {
|
|
1774
2234
|
let dirName = "\(shiftApp)-\(shiftBundle)"
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2235
|
+
if dirName != previousActiveBundleVersion &&
|
|
2236
|
+
shiftBundle != previousActiveBundleVersion &&
|
|
2237
|
+
dirName != folderName {
|
|
2238
|
+
BundleUpdateStore.deleteSignatureFile(dirName)
|
|
2239
|
+
let oldPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(dirName)
|
|
2240
|
+
if FileManager.default.fileExists(atPath: oldPath) {
|
|
2241
|
+
try? FileManager.default.removeItem(atPath: oldPath)
|
|
2242
|
+
}
|
|
2243
|
+
} else {
|
|
2244
|
+
OneKeyLog.info("BundleUpdate", "installBundle: skipping disk deletion of evicted fallback \(dirName) because it is still in use")
|
|
1779
2245
|
}
|
|
1780
2246
|
}
|
|
1781
2247
|
}
|
|
@@ -1793,53 +2259,57 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1793
2259
|
}
|
|
1794
2260
|
}
|
|
1795
2261
|
|
|
1796
|
-
func
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
OneKeyLog.info("BundleUpdate", "clearDownload: download directory deleted")
|
|
1803
|
-
} else {
|
|
1804
|
-
OneKeyLog.info("BundleUpdate", "clearDownload: download directory does not exist, skipping")
|
|
1805
|
-
}
|
|
1806
|
-
// Cancel all in-flight downloads by invalidating the session
|
|
1807
|
-
self?.urlSession?.invalidateAndCancel()
|
|
1808
|
-
self?.urlSession = self?.createURLSession()
|
|
1809
|
-
self?.stateQueue.sync { self?.isDownloading = false }
|
|
1810
|
-
OneKeyLog.info("BundleUpdate", "clearDownload: completed")
|
|
2262
|
+
private func clearManagedFiles(includeInstalled: Bool) async throws {
|
|
2263
|
+
let (alreadyClearing, oldSession, activeDelegate, activePath) = stateQueue.sync { () -> (Bool, URLSession?, DownloadDelegate?, String?) in
|
|
2264
|
+
if clearingDownloads { return (true, nil, nil, nil) }
|
|
2265
|
+
clearingDownloads = true
|
|
2266
|
+
activeRunDelegate?.markCancelledByClear()
|
|
2267
|
+
return (false, urlSession, activeRunDelegate, activeDownloadFilePath)
|
|
1811
2268
|
}
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
OneKeyLog.info("BundleUpdate", "clearBundle: clearing download and bundle directories...")
|
|
1818
|
-
// Clear download directory
|
|
1819
|
-
let downloadDir = BundleUpdateStore.downloadBundleDir()
|
|
1820
|
-
if FileManager.default.fileExists(atPath: downloadDir) {
|
|
1821
|
-
try FileManager.default.removeItem(atPath: downloadDir)
|
|
1822
|
-
OneKeyLog.info("BundleUpdate", "clearBundle: download directory deleted")
|
|
2269
|
+
guard !alreadyClearing else { throw BundleUpdateError(.invalidArgument, .cleanup) }
|
|
2270
|
+
defer {
|
|
2271
|
+
stateQueue.sync {
|
|
2272
|
+
urlSession = createURLSession()
|
|
2273
|
+
clearingDownloads = false
|
|
1823
2274
|
}
|
|
1824
|
-
|
|
2275
|
+
}
|
|
2276
|
+
oldSession?.invalidateAndCancel()
|
|
2277
|
+
if let activePath {
|
|
2278
|
+
let taskId = URL(fileURLWithPath: activePath).deletingPathExtension().lastPathComponent
|
|
2279
|
+
await RangeDownloader.shared.cancel(channel: .bundle, taskId: taskId, filePath: activePath)
|
|
2280
|
+
}
|
|
2281
|
+
if let activeDelegate, !(await activeDelegate.waitForRunCompletion(timeout: 65)) {
|
|
2282
|
+
throw BundleUpdateError(.unknown, .cleanup)
|
|
2283
|
+
}
|
|
2284
|
+
let downloadDir = BundleUpdateStore.downloadBundleDir()
|
|
2285
|
+
if FileManager.default.fileExists(atPath: downloadDir) {
|
|
2286
|
+
try FileManager.default.removeItem(atPath: downloadDir)
|
|
2287
|
+
}
|
|
2288
|
+
if includeInstalled {
|
|
1825
2289
|
let bundleDir = BundleUpdateStore.bundleDir()
|
|
1826
2290
|
if FileManager.default.fileExists(atPath: bundleDir) {
|
|
1827
2291
|
try FileManager.default.removeItem(atPath: bundleDir)
|
|
1828
|
-
OneKeyLog.info("BundleUpdate", "clearBundle: bundle directory deleted")
|
|
1829
2292
|
}
|
|
1830
|
-
// Cancel all in-flight downloads by invalidating the session
|
|
1831
|
-
self?.urlSession?.invalidateAndCancel()
|
|
1832
|
-
self?.urlSession = self?.createURLSession()
|
|
1833
|
-
self?.stateQueue.sync { self?.isDownloading = false }
|
|
1834
|
-
OneKeyLog.info("BundleUpdate", "clearBundle: completed")
|
|
1835
2293
|
}
|
|
1836
2294
|
}
|
|
1837
2295
|
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2296
|
+
func clearDownload() throws -> Promise<Void> {
|
|
2297
|
+
return updatePromise(step: .cleanup) { [weak self] in
|
|
2298
|
+
guard let self else { return }
|
|
2299
|
+
try await self.clearManagedFiles(includeInstalled: false)
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
func clearBundle() throws -> Promise<Void> {
|
|
2304
|
+
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
2305
|
+
return updatePromise(step: .cleanup) { [weak self] in
|
|
2306
|
+
guard let self else { return }
|
|
2307
|
+
try await self.clearManagedFiles(includeInstalled: true)
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/// Classifies only non-current stems for best-effort stale cleanup.
|
|
2312
|
+
/// Current-version stems are matched by the exact native-version prefix.
|
|
1843
2313
|
private static func appVersionFromStem(_ stem: String) -> String? {
|
|
1844
2314
|
guard let lastDash = stem.range(of: "-", options: .backwards),
|
|
1845
2315
|
lastDash.lowerBound > stem.startIndex else { return nil }
|
|
@@ -1874,6 +2344,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1874
2344
|
/// is unparseable (leave foreign names alone).
|
|
1875
2345
|
func shouldKeep(stem: String) -> Bool {
|
|
1876
2346
|
if let active = currentBundleVersion, stem == active { return true }
|
|
2347
|
+
if BundleUpdateStore.bundleVersion(inFolder: stem, appVersion: currentAppV) != nil { return true }
|
|
1877
2348
|
guard let appV = Self.appVersionFromStem(stem) else { return true }
|
|
1878
2349
|
return appV == currentAppV
|
|
1879
2350
|
}
|
|
@@ -1961,7 +2432,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1961
2432
|
|
|
1962
2433
|
func resetToBuiltInBundle() throws -> Promise<Void> {
|
|
1963
2434
|
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
1964
|
-
return
|
|
2435
|
+
return updatePromise(step: .cleanup) {
|
|
1965
2436
|
OneKeyLog.info("BundleUpdate", "resetToBuiltInBundle: clearing currentBundleVersion preference...")
|
|
1966
2437
|
let ud = UserDefaults.standard
|
|
1967
2438
|
if let cbv = ud.string(forKey: "currentBundleVersion") {
|
|
@@ -1978,7 +2449,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1978
2449
|
|
|
1979
2450
|
func clearAllJSBundleData() throws -> Promise<TestResult> {
|
|
1980
2451
|
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
1981
|
-
return
|
|
2452
|
+
return updatePromise(step: .cleanup) {
|
|
1982
2453
|
OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: starting...")
|
|
1983
2454
|
let bundleDir = BundleUpdateStore.bundleDir()
|
|
1984
2455
|
if FileManager.default.fileExists(atPath: bundleDir) {
|
|
@@ -2015,15 +2486,34 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
2015
2486
|
|
|
2016
2487
|
func setCurrentUpdateBundleData(params: BundleSwitchParams) throws -> Promise<Void> {
|
|
2017
2488
|
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
2018
|
-
return
|
|
2489
|
+
return updatePromise(step: .install) {
|
|
2490
|
+
guard params.appVersion.isSafeVersionString, params.bundleVersion.isSafeVersionString else {
|
|
2491
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: invalid version string format: appVersion=\(params.appVersion), bundleVersion=\(params.bundleVersion)")
|
|
2492
|
+
throw BundleUpdateError(.invalidArgument, .install)
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2019
2495
|
let bundleVersion = "\(params.appVersion)-\(params.bundleVersion)"
|
|
2020
2496
|
OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switching to \(bundleVersion)")
|
|
2021
2497
|
|
|
2022
|
-
// Verify
|
|
2498
|
+
// Verify bundle directory and signature confinement
|
|
2023
2499
|
let bundleDirPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(bundleVersion)
|
|
2500
|
+
let resolvedBundleDir = (bundleDirPath as NSString).resolvingSymlinksInPath
|
|
2501
|
+
let baseBundleDir = (BundleUpdateStore.bundleDir() as NSString).resolvingSymlinksInPath
|
|
2502
|
+
guard resolvedBundleDir.hasPrefix(baseBundleDir + "/") else {
|
|
2503
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: bundle path outside bundle directory: \(resolvedBundleDir)")
|
|
2504
|
+
throw BundleUpdateError(.security, .install)
|
|
2505
|
+
}
|
|
2506
|
+
let sigPath = (BundleUpdateStore.signatureFilePath(bundleVersion) as NSString).resolvingSymlinksInPath
|
|
2507
|
+
let baseAscDir = (BundleUpdateStore.ascDir() as NSString).resolvingSymlinksInPath
|
|
2508
|
+
guard sigPath.hasPrefix(baseAscDir + "/") else {
|
|
2509
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: signature path outside asc directory: \(sigPath)")
|
|
2510
|
+
throw BundleUpdateError(.security, .install)
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// Verify the bundle directory actually exists
|
|
2024
2514
|
guard FileManager.default.fileExists(atPath: bundleDirPath) else {
|
|
2025
2515
|
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: bundle directory not found: \(bundleDirPath)")
|
|
2026
|
-
throw
|
|
2516
|
+
throw BundleUpdateError(.missing, .install)
|
|
2027
2517
|
}
|
|
2028
2518
|
|
|
2029
2519
|
// Verify GPG signature is valid (skipped when both DevSettings and skip-GPG toggle are enabled)
|
|
@@ -2035,21 +2525,39 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
2035
2525
|
OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG check: skipGPG=\(skipGPGSwitch)")
|
|
2036
2526
|
if !skipGPGSwitch {
|
|
2037
2527
|
guard !params.signature.isEmpty,
|
|
2038
|
-
BundleUpdateStore.validateMetadataFileSha256(bundleVersion, signature: params.signature) else {
|
|
2528
|
+
BundleUpdateStore.validateMetadataFileSha256(bundleVersion, signature: params.signature, expectedAppVersion: params.appVersion, expectedBundleVersion: params.bundleVersion) else {
|
|
2039
2529
|
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification failed")
|
|
2040
|
-
throw
|
|
2530
|
+
throw BundleUpdateError(.integrity, .install)
|
|
2041
2531
|
}
|
|
2042
2532
|
OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verified OK")
|
|
2043
2533
|
} else {
|
|
2044
2534
|
OneKeyLog.warn("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification skipped (DevSettings + skip-GPG enabled)")
|
|
2045
2535
|
}
|
|
2046
2536
|
|
|
2047
|
-
let
|
|
2048
|
-
|
|
2537
|
+
guard let metadata = BundleUpdateStore.getMetadataFileContent(bundleVersion) else {
|
|
2538
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: failed to read metadata.json content")
|
|
2539
|
+
throw BundleUpdateError(.missing, .install)
|
|
2540
|
+
}
|
|
2541
|
+
if !BundleUpdateStore.validateAllFilesInDir(bundleDirPath, metadata: metadata, appVersion: params.appVersion, bundleVersion: params.bundleVersion) {
|
|
2542
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: extracted file verification failed")
|
|
2543
|
+
throw BundleUpdateError(.integrity, .install)
|
|
2544
|
+
}
|
|
2545
|
+
if !BundleUpdateStore.validateBundlePairCompatibility(bundleDirPath: bundleDirPath, metadata: metadata) {
|
|
2546
|
+
OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: bundle pair compatibility check failed")
|
|
2547
|
+
throw BundleUpdateError(.incompatible, .install)
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2049
2550
|
if !params.signature.isEmpty {
|
|
2050
2551
|
BundleUpdateStore.writeSignatureFile(bundleVersion, signature: params.signature)
|
|
2051
2552
|
}
|
|
2553
|
+
let ud = UserDefaults.standard
|
|
2554
|
+
ud.set(bundleVersion, forKey: "currentBundleVersion")
|
|
2555
|
+
let currentNativeVersion = BundleUpdateStore.getCurrentNativeVersion()
|
|
2556
|
+
BundleUpdateStore.setNativeVersion(currentNativeVersion)
|
|
2557
|
+
let currentBuildNumber = BundleUpdateStore.getCurrentNativeBuildNumber()
|
|
2558
|
+
BundleUpdateStore.setNativeBuildNumber(currentBuildNumber)
|
|
2052
2559
|
ud.synchronize()
|
|
2560
|
+
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
2053
2561
|
OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switched to \(bundleVersion)")
|
|
2054
2562
|
}
|
|
2055
2563
|
}
|
|
@@ -2182,8 +2690,17 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
2182
2690
|
|
|
2183
2691
|
func isBundleExists(appVersion: String, bundleVersion: String) throws -> Promise<Bool> {
|
|
2184
2692
|
return Promise.async {
|
|
2693
|
+
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
2694
|
+
OneKeyLog.error("BundleUpdate", "isBundleExists: invalid version string format: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
2695
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid version string format"])
|
|
2696
|
+
}
|
|
2185
2697
|
let folderName = "\(appVersion)-\(bundleVersion)"
|
|
2186
2698
|
let path = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
|
|
2699
|
+
let resolvedPath = (path as NSString).resolvingSymlinksInPath
|
|
2700
|
+
let baseBundleDir = (BundleUpdateStore.bundleDir() as NSString).resolvingSymlinksInPath
|
|
2701
|
+
guard resolvedPath.hasPrefix(baseBundleDir + "/") else {
|
|
2702
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle path outside bundle directory"])
|
|
2703
|
+
}
|
|
2187
2704
|
let exists = FileManager.default.fileExists(atPath: path)
|
|
2188
2705
|
OneKeyLog.info("BundleUpdate", "isBundleExists: appVersion=\(appVersion), bundleVersion=\(bundleVersion), exists=\(exists)")
|
|
2189
2706
|
return exists
|
|
@@ -2191,32 +2708,41 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
2191
2708
|
}
|
|
2192
2709
|
|
|
2193
2710
|
func verifyExtractedBundle(appVersion: String, bundleVersion: String) throws -> Promise<Void> {
|
|
2194
|
-
return
|
|
2711
|
+
return updatePromise(step: .verifyPackage) {
|
|
2195
2712
|
OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
2713
|
+
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
2714
|
+
OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: invalid version string format: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
2715
|
+
throw BundleUpdateError(.invalidArgument, .verifyPackage)
|
|
2716
|
+
}
|
|
2196
2717
|
let folderName = "\(appVersion)-\(bundleVersion)"
|
|
2197
2718
|
let bundlePath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
|
|
2719
|
+
let resolvedPath = (bundlePath as NSString).resolvingSymlinksInPath
|
|
2720
|
+
let baseBundleDir = (BundleUpdateStore.bundleDir() as NSString).resolvingSymlinksInPath
|
|
2721
|
+
guard resolvedPath.hasPrefix(baseBundleDir + "/") else {
|
|
2722
|
+
throw BundleUpdateError(.security, .verifyPackage)
|
|
2723
|
+
}
|
|
2198
2724
|
guard FileManager.default.fileExists(atPath: bundlePath) else {
|
|
2199
2725
|
OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: bundle directory not found: \(bundlePath)")
|
|
2200
|
-
throw
|
|
2726
|
+
throw BundleUpdateError(.missing, .verifyPackage)
|
|
2201
2727
|
}
|
|
2202
2728
|
let metadataJsonPath = (bundlePath as NSString).appendingPathComponent("metadata.json")
|
|
2203
2729
|
guard FileManager.default.fileExists(atPath: metadataJsonPath) else {
|
|
2204
2730
|
OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: metadata.json not found in \(bundlePath)")
|
|
2205
|
-
throw
|
|
2731
|
+
throw BundleUpdateError(.missing, .verifyPackage)
|
|
2206
2732
|
}
|
|
2207
2733
|
guard let data = FileManager.default.contents(atPath: metadataJsonPath),
|
|
2208
2734
|
let metadata = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
2209
2735
|
OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: failed to parse metadata.json")
|
|
2210
|
-
throw
|
|
2736
|
+
throw BundleUpdateError(.integrity, .verifyPackage)
|
|
2211
2737
|
}
|
|
2212
2738
|
OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: parsing metadata and validating files...")
|
|
2213
2739
|
if !BundleUpdateStore.validateAllFilesInDir(bundlePath, metadata: metadata, appVersion: appVersion, bundleVersion: bundleVersion) {
|
|
2214
2740
|
OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: file integrity check failed")
|
|
2215
|
-
throw
|
|
2741
|
+
throw BundleUpdateError(.integrity, .verifyPackage)
|
|
2216
2742
|
}
|
|
2217
2743
|
if !BundleUpdateStore.validateBundlePairCompatibility(bundleDirPath: bundlePath, metadata: metadata) {
|
|
2218
2744
|
OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: bundle pair compatibility check failed")
|
|
2219
|
-
throw
|
|
2745
|
+
throw BundleUpdateError(.incompatible, .verifyPackage)
|
|
2220
2746
|
}
|
|
2221
2747
|
OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: all files verified OK, fileCount=\(metadata.count)")
|
|
2222
2748
|
}
|
|
@@ -2225,6 +2751,8 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
2225
2751
|
func listLocalBundles() throws -> Promise<[LocalBundleInfo]> {
|
|
2226
2752
|
return Promise.async {
|
|
2227
2753
|
let bundleDir = BundleUpdateStore.bundleDir()
|
|
2754
|
+
let appVersion = BundleUpdateStore.getCurrentNativeVersion()
|
|
2755
|
+
guard !appVersion.isEmpty else { return [] }
|
|
2228
2756
|
let fm = FileManager.default
|
|
2229
2757
|
guard let contents = try? fm.contentsOfDirectory(atPath: bundleDir) else {
|
|
2230
2758
|
OneKeyLog.info("BundleUpdate", "listLocalBundles: bundle directory empty or not found")
|
|
@@ -2235,13 +2763,8 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
2235
2763
|
let fullPath = (bundleDir as NSString).appendingPathComponent(name)
|
|
2236
2764
|
var isDir: ObjCBool = false
|
|
2237
2765
|
guard fm.fileExists(atPath: fullPath, isDirectory: &isDir), isDir.boolValue else { continue }
|
|
2238
|
-
guard let
|
|
2239
|
-
|
|
2240
|
-
let appVersion = String(name[name.startIndex..<lastDash.lowerBound])
|
|
2241
|
-
let bundleVersion = String(name[lastDash.upperBound...])
|
|
2242
|
-
if !appVersion.isEmpty && !bundleVersion.isEmpty {
|
|
2243
|
-
results.append(LocalBundleInfo(appVersion: appVersion, bundleVersion: bundleVersion))
|
|
2244
|
-
}
|
|
2766
|
+
guard let bundleVersion = BundleUpdateStore.bundleVersion(inFolder: name, appVersion: appVersion) else { continue }
|
|
2767
|
+
results.append(LocalBundleInfo(appVersion: appVersion, bundleVersion: bundleVersion))
|
|
2245
2768
|
}
|
|
2246
2769
|
OneKeyLog.info("BundleUpdate", "listLocalBundles: found \(results.count) bundles")
|
|
2247
2770
|
return results
|
|
@@ -2300,11 +2823,24 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
2300
2823
|
|
|
2301
2824
|
func testDeleteJsBundle(appVersion: String, bundleVersion: String) throws -> Promise<TestResult> {
|
|
2302
2825
|
return Promise.async {
|
|
2826
|
+
#if DEBUG
|
|
2827
|
+
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
2828
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid version string"])
|
|
2829
|
+
}
|
|
2303
2830
|
OneKeyLog.info("BundleUpdate", "testDeleteJsBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
2304
2831
|
let folderName = "\(appVersion)-\(bundleVersion)"
|
|
2305
2832
|
let jsBundlePath = (BundleUpdateStore.bundleDir() as NSString)
|
|
2306
2833
|
.appendingPathComponent(folderName)
|
|
2834
|
+
let resolvedDir = (jsBundlePath as NSString).resolvingSymlinksInPath
|
|
2835
|
+
let baseBundleDir = (BundleUpdateStore.bundleDir() as NSString).resolvingSymlinksInPath
|
|
2836
|
+
guard resolvedDir.hasPrefix(baseBundleDir + "/") else {
|
|
2837
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle path outside bundle directory"])
|
|
2838
|
+
}
|
|
2307
2839
|
let path = (jsBundlePath as NSString).appendingPathComponent("main.jsbundle.hbc")
|
|
2840
|
+
let resolvedPath = (path as NSString).resolvingSymlinksInPath
|
|
2841
|
+
guard resolvedPath.hasPrefix(baseBundleDir + "/") else {
|
|
2842
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle path outside bundle directory"])
|
|
2843
|
+
}
|
|
2308
2844
|
|
|
2309
2845
|
if FileManager.default.fileExists(atPath: path) {
|
|
2310
2846
|
try FileManager.default.removeItem(atPath: path)
|
|
@@ -2313,14 +2849,26 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
2313
2849
|
}
|
|
2314
2850
|
OneKeyLog.warn("BundleUpdate", "testDeleteJsBundle: file not found: \(path)")
|
|
2315
2851
|
return TestResult(success: false, message: "jsBundle not found: \(path)")
|
|
2852
|
+
#else
|
|
2853
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Test fault-injection methods are disabled in release builds"])
|
|
2854
|
+
#endif
|
|
2316
2855
|
}
|
|
2317
2856
|
}
|
|
2318
2857
|
|
|
2319
2858
|
func testDeleteJsRuntimeDir(appVersion: String, bundleVersion: String) throws -> Promise<TestResult> {
|
|
2320
2859
|
return Promise.async {
|
|
2860
|
+
#if DEBUG
|
|
2861
|
+
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
2862
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid version string"])
|
|
2863
|
+
}
|
|
2321
2864
|
OneKeyLog.info("BundleUpdate", "testDeleteJsRuntimeDir: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
2322
2865
|
let folderName = "\(appVersion)-\(bundleVersion)"
|
|
2323
2866
|
let dirPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
|
|
2867
|
+
let resolvedDir = (dirPath as NSString).resolvingSymlinksInPath
|
|
2868
|
+
let baseBundleDir = (BundleUpdateStore.bundleDir() as NSString).resolvingSymlinksInPath
|
|
2869
|
+
guard resolvedDir.hasPrefix(baseBundleDir + "/") else {
|
|
2870
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle path outside bundle directory"])
|
|
2871
|
+
}
|
|
2324
2872
|
|
|
2325
2873
|
if FileManager.default.fileExists(atPath: dirPath) {
|
|
2326
2874
|
try FileManager.default.removeItem(atPath: dirPath)
|
|
@@ -2329,16 +2877,32 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
2329
2877
|
}
|
|
2330
2878
|
OneKeyLog.warn("BundleUpdate", "testDeleteJsRuntimeDir: directory not found: \(dirPath)")
|
|
2331
2879
|
return TestResult(success: false, message: "js runtime directory not found: \(dirPath)")
|
|
2880
|
+
#else
|
|
2881
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Test fault-injection methods are disabled in release builds"])
|
|
2882
|
+
#endif
|
|
2332
2883
|
}
|
|
2333
2884
|
}
|
|
2334
2885
|
|
|
2335
2886
|
func testDeleteMetadataJson(appVersion: String, bundleVersion: String) throws -> Promise<TestResult> {
|
|
2336
2887
|
return Promise.async {
|
|
2888
|
+
#if DEBUG
|
|
2889
|
+
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
2890
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid version string"])
|
|
2891
|
+
}
|
|
2337
2892
|
OneKeyLog.info("BundleUpdate", "testDeleteMetadataJson: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
2338
2893
|
let folderName = "\(appVersion)-\(bundleVersion)"
|
|
2339
2894
|
let metadataPath = (BundleUpdateStore.bundleDir() as NSString)
|
|
2340
2895
|
.appendingPathComponent(folderName)
|
|
2896
|
+
let resolvedDir = (metadataPath as NSString).resolvingSymlinksInPath
|
|
2897
|
+
let baseBundleDir = (BundleUpdateStore.bundleDir() as NSString).resolvingSymlinksInPath
|
|
2898
|
+
guard resolvedDir.hasPrefix(baseBundleDir + "/") else {
|
|
2899
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle path outside bundle directory"])
|
|
2900
|
+
}
|
|
2341
2901
|
let path = (metadataPath as NSString).appendingPathComponent("metadata.json")
|
|
2902
|
+
let resolvedPath = (path as NSString).resolvingSymlinksInPath
|
|
2903
|
+
guard resolvedPath.hasPrefix(baseBundleDir + "/") else {
|
|
2904
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle path outside bundle directory"])
|
|
2905
|
+
}
|
|
2342
2906
|
|
|
2343
2907
|
if FileManager.default.fileExists(atPath: path) {
|
|
2344
2908
|
try FileManager.default.removeItem(atPath: path)
|
|
@@ -2347,15 +2911,31 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
2347
2911
|
}
|
|
2348
2912
|
OneKeyLog.warn("BundleUpdate", "testDeleteMetadataJson: file not found: \(path)")
|
|
2349
2913
|
return TestResult(success: false, message: "metadata.json not found: \(path)")
|
|
2914
|
+
#else
|
|
2915
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Test fault-injection methods are disabled in release builds"])
|
|
2916
|
+
#endif
|
|
2350
2917
|
}
|
|
2351
2918
|
}
|
|
2352
2919
|
|
|
2353
2920
|
func testWriteEmptyMetadataJson(appVersion: String, bundleVersion: String) throws -> Promise<TestResult> {
|
|
2354
2921
|
return Promise.async {
|
|
2922
|
+
#if DEBUG
|
|
2923
|
+
guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else {
|
|
2924
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid version string"])
|
|
2925
|
+
}
|
|
2355
2926
|
OneKeyLog.info("BundleUpdate", "testWriteEmptyMetadataJson: appVersion=\(appVersion), bundleVersion=\(bundleVersion)")
|
|
2356
2927
|
let folderName = "\(appVersion)-\(bundleVersion)"
|
|
2357
2928
|
let jsRuntimeDir = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName)
|
|
2929
|
+
let resolvedDir = (jsRuntimeDir as NSString).resolvingSymlinksInPath
|
|
2930
|
+
let baseBundleDir = (BundleUpdateStore.bundleDir() as NSString).resolvingSymlinksInPath
|
|
2931
|
+
guard resolvedDir.hasPrefix(baseBundleDir + "/") else {
|
|
2932
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle path outside bundle directory"])
|
|
2933
|
+
}
|
|
2358
2934
|
let metadataPath = (jsRuntimeDir as NSString).appendingPathComponent("metadata.json")
|
|
2935
|
+
let resolvedPath = (metadataPath as NSString).resolvingSymlinksInPath
|
|
2936
|
+
guard resolvedPath.hasPrefix(baseBundleDir + "/") else {
|
|
2937
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle path outside bundle directory"])
|
|
2938
|
+
}
|
|
2359
2939
|
|
|
2360
2940
|
if !FileManager.default.fileExists(atPath: jsRuntimeDir) {
|
|
2361
2941
|
try FileManager.default.createDirectory(atPath: jsRuntimeDir, withIntermediateDirectories: true)
|
|
@@ -2367,6 +2947,10 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
2367
2947
|
|
|
2368
2948
|
OneKeyLog.info("BundleUpdate", "testWriteEmptyMetadataJson: created \(metadataPath)")
|
|
2369
2949
|
return TestResult(success: true, message: "Created empty metadata.json: \(metadataPath)")
|
|
2950
|
+
#else
|
|
2951
|
+
throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Test fault-injection methods are disabled in release builds"])
|
|
2952
|
+
#endif
|
|
2370
2953
|
}
|
|
2371
2954
|
}
|
|
2372
2955
|
}
|
|
2956
|
+
|