@onekeyfe/react-native-bundle-update 3.0.19 → 3.0.21
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
CHANGED
|
@@ -114,8 +114,43 @@ object BundleUpdateStoreAndroid {
|
|
|
114
114
|
private const val COMMON_BUNDLE_FILE_NAME = "common.bundle"
|
|
115
115
|
private const val METADATA_REQUIRES_BACKGROUND_BUNDLE_KEY = "requiresBackgroundBundle"
|
|
116
116
|
private const val METADATA_BACKGROUND_PROTOCOL_VERSION_KEY = "backgroundProtocolVersion"
|
|
117
|
+
private const val METADATA_REQUIRES_COMMON_BUNDLE_KEY = "requiresCommonBundle"
|
|
118
|
+
private const val METADATA_BUNDLE_FORMAT_KEY = "bundleFormat"
|
|
119
|
+
private const val BUNDLE_FORMAT_THREE_BUNDLE = "three-bundle"
|
|
117
120
|
private const val SUPPORTED_BACKGROUND_PROTOCOL_VERSION = "1"
|
|
118
121
|
|
|
122
|
+
// In-memory cache for getValidatedCurrentBundleInfo. Without this, every
|
|
123
|
+
// bundleURL / common / main / background path getter on startup re-runs
|
|
124
|
+
// the whole validation pipeline. Cache key is currentBundleVersion;
|
|
125
|
+
// invalidated on any mutation of the current bundle.
|
|
126
|
+
@Volatile private var cachedValidatedBundleInfo: ValidatedBundleInfo? = null
|
|
127
|
+
|
|
128
|
+
// Lazy per-version cache for the web-embed subtree. getWebEmbedPath is
|
|
129
|
+
// called every time a WebView is created, but the subtree contents are
|
|
130
|
+
// immutable for a given currentBundleVersion. Stores the version that
|
|
131
|
+
// most recently passed full sha256 sweep over web-embed/**; invalidated
|
|
132
|
+
// alongside cachedValidatedBundleInfo on any bundle mutation.
|
|
133
|
+
@Volatile private var cachedWebEmbedVerifiedVersion: String? = null
|
|
134
|
+
|
|
135
|
+
@Synchronized
|
|
136
|
+
fun invalidateValidatedBundleInfoCache() {
|
|
137
|
+
cachedValidatedBundleInfo = null
|
|
138
|
+
cachedWebEmbedVerifiedVersion = null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* True when the current bundle's metadata declares the three-bundle /
|
|
143
|
+
* split-thread layout (or explicitly opts in via requiresCommonBundle).
|
|
144
|
+
* SplitBundleLoader queries this reflectively to decide whether an empty
|
|
145
|
+
* per-segment sha256 is a back-compat skip (older formats) or a hard
|
|
146
|
+
* fail (three-bundle, which always ships per-segment hashes).
|
|
147
|
+
*/
|
|
148
|
+
@JvmStatic
|
|
149
|
+
fun currentBundleRequiresPerSegmentHash(context: Context): Boolean {
|
|
150
|
+
val info = getValidatedCurrentBundleInfo(context) ?: return false
|
|
151
|
+
return metadataRequiresCommonBundle(info.metadata)
|
|
152
|
+
}
|
|
153
|
+
|
|
119
154
|
fun getDownloadBundleDir(context: Context): String {
|
|
120
155
|
val dir = File(context.filesDir, "onekey-bundle-download")
|
|
121
156
|
if (!dir.exists()) dir.mkdirs()
|
|
@@ -190,6 +225,7 @@ object BundleUpdateStoreAndroid {
|
|
|
190
225
|
}
|
|
191
226
|
|
|
192
227
|
fun clearUpdateBundleData(context: Context) {
|
|
228
|
+
invalidateValidatedBundleInfoCache()
|
|
193
229
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
194
230
|
prefs.edit().clear().commit()
|
|
195
231
|
// Clear all signature files
|
|
@@ -407,7 +443,16 @@ object BundleUpdateStoreAndroid {
|
|
|
407
443
|
val keys = obj.keys()
|
|
408
444
|
while (keys.hasNext()) {
|
|
409
445
|
val key = keys.next()
|
|
410
|
-
|
|
446
|
+
// Only accept string-valued entries. Any nested object / array
|
|
447
|
+
// / numeric value is silently skipped so the metadata can carry
|
|
448
|
+
// non-string scalars (e.g. runtimeGraphVersion) or future V2
|
|
449
|
+
// descriptors without breaking file-level SHA verification.
|
|
450
|
+
val value = obj.opt(key)
|
|
451
|
+
if (value is String) {
|
|
452
|
+
metadata[key] = value
|
|
453
|
+
} else if (value is Boolean || value is Number) {
|
|
454
|
+
metadata[key] = value.toString()
|
|
455
|
+
}
|
|
411
456
|
}
|
|
412
457
|
} catch (e: Exception) {
|
|
413
458
|
OneKeyLog.error("BundleUpdate", "Error parsing metadata JSON: ${e.message}")
|
|
@@ -421,7 +466,9 @@ object BundleUpdateStoreAndroid {
|
|
|
421
466
|
|
|
422
467
|
private fun isReservedMetadataKey(key: String): Boolean {
|
|
423
468
|
return key == METADATA_REQUIRES_BACKGROUND_BUNDLE_KEY ||
|
|
424
|
-
key == METADATA_BACKGROUND_PROTOCOL_VERSION_KEY
|
|
469
|
+
key == METADATA_BACKGROUND_PROTOCOL_VERSION_KEY ||
|
|
470
|
+
key == METADATA_REQUIRES_COMMON_BUNDLE_KEY ||
|
|
471
|
+
key == METADATA_BUNDLE_FORMAT_KEY
|
|
425
472
|
}
|
|
426
473
|
|
|
427
474
|
private fun getFileMetadataEntries(metadata: Map<String, String>): Map<String, String> {
|
|
@@ -435,6 +482,18 @@ object BundleUpdateStoreAndroid {
|
|
|
435
482
|
?: false
|
|
436
483
|
}
|
|
437
484
|
|
|
485
|
+
private fun metadataRequiresCommonBundle(metadata: Map<String, String>): Boolean {
|
|
486
|
+
// OR semantics matching iOS: an explicit `requiresCommonBundle=false`
|
|
487
|
+
// does NOT suppress the `bundleFormat=three-bundle` signal. Both
|
|
488
|
+
// signals are treated as opt-ins; either one being true is enough.
|
|
489
|
+
val explicit = metadata[METADATA_REQUIRES_COMMON_BUNDLE_KEY]
|
|
490
|
+
?.lowercase()
|
|
491
|
+
?.let { value -> value == "1" || value == "true" || value == "yes" }
|
|
492
|
+
?: false
|
|
493
|
+
if (explicit) return true
|
|
494
|
+
return metadata[METADATA_BUNDLE_FORMAT_KEY]?.lowercase() == BUNDLE_FORMAT_THREE_BUNDLE
|
|
495
|
+
}
|
|
496
|
+
|
|
438
497
|
private fun metadataBackgroundProtocolVersion(metadata: Map<String, String>): String {
|
|
439
498
|
return metadata[METADATA_BACKGROUND_PROTOCOL_VERSION_KEY] ?: ""
|
|
440
499
|
}
|
|
@@ -686,6 +745,21 @@ object BundleUpdateStoreAndroid {
|
|
|
686
745
|
return false
|
|
687
746
|
}
|
|
688
747
|
|
|
748
|
+
// Three-bundle (union / split thread) mode requires a common.bundle
|
|
749
|
+
// shipped alongside main.jsbundle.hbc. Without it, entry-only main
|
|
750
|
+
// bundle references moduleIds that live in the common bundle and the
|
|
751
|
+
// runtime crashes on first require().
|
|
752
|
+
if (metadataRequiresCommonBundle(metadata)) {
|
|
753
|
+
val commonBundleFile = File(bundleDir, COMMON_BUNDLE_FILE_NAME)
|
|
754
|
+
if (!commonBundleFile.exists()) {
|
|
755
|
+
OneKeyLog.error(
|
|
756
|
+
"BundleUpdate",
|
|
757
|
+
"requiresCommonBundle is true but common.bundle is missing at ${commonBundleFile.absolutePath}",
|
|
758
|
+
)
|
|
759
|
+
return false
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
689
763
|
if (!metadataRequiresBackgroundBundle(metadata)) {
|
|
690
764
|
return true
|
|
691
765
|
}
|
|
@@ -723,9 +797,19 @@ object BundleUpdateStoreAndroid {
|
|
|
723
797
|
val currentAppVersion = getAppVersion(context)
|
|
724
798
|
val currentBundleVersion = getCurrentBundleVersion(context) ?: run {
|
|
725
799
|
OneKeyLog.warn("BundleUpdate", "getJsBundlePath: no currentBundleVersion stored")
|
|
800
|
+
invalidateValidatedBundleInfoCache()
|
|
726
801
|
return null
|
|
727
802
|
}
|
|
728
803
|
|
|
804
|
+
// Memo cache: avoid re-running signature + entry-bundle sha256 for
|
|
805
|
+
// every bundleURL / common / main / background path getter on
|
|
806
|
+
// startup.
|
|
807
|
+
cachedValidatedBundleInfo?.let { cached ->
|
|
808
|
+
if (cached.currentBundleVersion == currentBundleVersion) {
|
|
809
|
+
return cached
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
729
813
|
OneKeyLog.info("BundleUpdate", "currentAppVersion: $currentAppVersion, currentBundleVersion: $currentBundleVersion")
|
|
730
814
|
|
|
731
815
|
val prevNativeVersion = getNativeVersion(context)
|
|
@@ -777,31 +861,78 @@ object BundleUpdateStoreAndroid {
|
|
|
777
861
|
}
|
|
778
862
|
val metadata = parseMetadataJson(metadataContent)
|
|
779
863
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
864
|
+
// Startup hot path: only verify entry-bundle SHA-256
|
|
865
|
+
// (main + common + background as required by metadata flags). The
|
|
866
|
+
// full-tree sha256 sweep already runs at install time
|
|
867
|
+
// (validateAllFilesInDir in installBundle), so re-doing it on every
|
|
868
|
+
// launch costs hundreds of ms per startup with no security gain
|
|
869
|
+
// (sandboxed app data + signed metadata.json bind every file's
|
|
870
|
+
// expected hash). Per-segment integrity is checked at loadSegment
|
|
871
|
+
// time by SplitBundleLoader.
|
|
872
|
+
if (!validateEntryBundlesSha256(bundleDir, metadata)) {
|
|
873
|
+
OneKeyLog.info("BundleUpdate", "validateEntryBundlesSha256 failed on startup")
|
|
874
|
+
return null
|
|
788
875
|
}
|
|
789
876
|
|
|
790
877
|
if (!validateBundlePairCompatibility(bundleDir, metadata)) {
|
|
791
878
|
return null
|
|
792
879
|
}
|
|
793
880
|
|
|
794
|
-
ValidatedBundleInfo(
|
|
881
|
+
val info = ValidatedBundleInfo(
|
|
795
882
|
bundleDir = bundleDir,
|
|
796
883
|
currentBundleVersion = currentBundleVersion,
|
|
797
884
|
metadata = metadata,
|
|
798
885
|
)
|
|
886
|
+
cachedValidatedBundleInfo = info
|
|
887
|
+
info
|
|
799
888
|
} catch (e: Exception) {
|
|
800
889
|
OneKeyLog.error("BundleUpdate", "Error getting bundle: ${e.message}")
|
|
801
890
|
null
|
|
802
891
|
}
|
|
803
892
|
}
|
|
804
893
|
|
|
894
|
+
/**
|
|
895
|
+
* Verifies SHA-256 of just the entry bundles required to boot the JS
|
|
896
|
+
* runtime (main + common + background, gated by metadata flags). Runs in
|
|
897
|
+
* place of the legacy validateAllFilesInDir on the startup hot path.
|
|
898
|
+
*/
|
|
899
|
+
fun validateEntryBundlesSha256(bundleDir: String, metadata: Map<String, String>): Boolean {
|
|
900
|
+
val fileEntries = getFileMetadataEntries(metadata)
|
|
901
|
+
val entriesToCheck = mutableListOf(MAIN_JS_BUNDLE_FILE_NAME)
|
|
902
|
+
if (metadataRequiresCommonBundle(metadata)) {
|
|
903
|
+
entriesToCheck += COMMON_BUNDLE_FILE_NAME
|
|
904
|
+
}
|
|
905
|
+
if (metadataRequiresBackgroundBundle(metadata)) {
|
|
906
|
+
entriesToCheck += BACKGROUND_BUNDLE_FILE_NAME
|
|
907
|
+
}
|
|
908
|
+
for (entry in entriesToCheck) {
|
|
909
|
+
val file = File(bundleDir, entry)
|
|
910
|
+
if (!file.exists()) {
|
|
911
|
+
OneKeyLog.error("BundleUpdate", "[entry-verify] missing entry file: $entry")
|
|
912
|
+
return false
|
|
913
|
+
}
|
|
914
|
+
val expected = fileEntries[entry]
|
|
915
|
+
if (expected.isNullOrEmpty()) {
|
|
916
|
+
OneKeyLog.error("BundleUpdate", "[entry-verify] no metadata sha256 for entry: $entry")
|
|
917
|
+
return false
|
|
918
|
+
}
|
|
919
|
+
val actual = calculateSHA256(file.absolutePath)
|
|
920
|
+
// secureCompare is byte-wise (case-sensitive); lowercase both
|
|
921
|
+
// sides so an uppercase-hex manifest doesn't false-mismatch
|
|
922
|
+
// against `calculateSHA256`'s lowercase output. Matches the
|
|
923
|
+
// semantic of the previous `equals(actual, ignoreCase = true)`
|
|
924
|
+
// while preserving the constant-time guarantee against timing
|
|
925
|
+
// attacks on hash comparisons (consistent with every other
|
|
926
|
+
// sha256 comparison in this file: validateFilesRecursive,
|
|
927
|
+
// validateWebEmbedRecursive, iOS validateEntryBundlesSha256).
|
|
928
|
+
if (actual == null || !secureCompare(expected.lowercase(), actual.lowercase())) {
|
|
929
|
+
OneKeyLog.error("BundleUpdate", "[entry-verify] sha256 mismatch for entry: $entry")
|
|
930
|
+
return false
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return true
|
|
934
|
+
}
|
|
935
|
+
|
|
805
936
|
fun getCurrentBundleEntryPath(context: Context, entryFileName: String): String? {
|
|
806
937
|
val bundleInfo = getValidatedCurrentBundleInfo(context) ?: return null
|
|
807
938
|
val entryFile = File(bundleInfo.bundleDir, entryFileName)
|
|
@@ -826,9 +957,122 @@ object BundleUpdateStoreAndroid {
|
|
|
826
957
|
|
|
827
958
|
fun getWebEmbedPath(context: Context): String {
|
|
828
959
|
val bundleInfo = getValidatedCurrentBundleInfo(context) ?: return ""
|
|
960
|
+
// Lazy full-tree sha256 sweep for web-embed/**. The startup hot path
|
|
961
|
+
// only validates the JS entry bundles (validateEntryBundlesSha256), so
|
|
962
|
+
// without this, a tampered web-embed asset would slip through. Result
|
|
963
|
+
// is cached per currentBundleVersion and invalidated on any bundle
|
|
964
|
+
// mutation, so cost is paid once per (re)install.
|
|
965
|
+
if (!ensureWebEmbedVerified(
|
|
966
|
+
bundleDir = bundleInfo.bundleDir,
|
|
967
|
+
currentBundleVersion = bundleInfo.currentBundleVersion,
|
|
968
|
+
metadata = bundleInfo.metadata,
|
|
969
|
+
)) {
|
|
970
|
+
return ""
|
|
971
|
+
}
|
|
829
972
|
return File(bundleInfo.bundleDir, "web-embed").absolutePath
|
|
830
973
|
}
|
|
831
974
|
|
|
975
|
+
@Synchronized
|
|
976
|
+
private fun ensureWebEmbedVerified(
|
|
977
|
+
bundleDir: String,
|
|
978
|
+
currentBundleVersion: String,
|
|
979
|
+
metadata: Map<String, String>,
|
|
980
|
+
): Boolean {
|
|
981
|
+
if (cachedWebEmbedVerifiedVersion == currentBundleVersion) {
|
|
982
|
+
return true
|
|
983
|
+
}
|
|
984
|
+
if (!validateWebEmbedSha256(bundleDir, metadata)) {
|
|
985
|
+
OneKeyLog.error("BundleUpdate", "validateWebEmbedSha256 failed for $currentBundleVersion")
|
|
986
|
+
return false
|
|
987
|
+
}
|
|
988
|
+
cachedWebEmbedVerifiedVersion = currentBundleVersion
|
|
989
|
+
return true
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Walks bundleDir/web-embed/** and verifies every file's sha256 against
|
|
994
|
+
* the metadata entry (key is the bundle-relative path). Also rejects
|
|
995
|
+
* files on disk that aren't listed in metadata, and metadata entries
|
|
996
|
+
* whose backing file is missing. Returns true when web-embed is absent
|
|
997
|
+
* from both disk and metadata (bundles without web-embed).
|
|
998
|
+
*/
|
|
999
|
+
fun validateWebEmbedSha256(bundleDir: String, metadata: Map<String, String>): Boolean {
|
|
1000
|
+
val webEmbedRoot = File(bundleDir, "web-embed")
|
|
1001
|
+
val fileEntries = getFileMetadataEntries(metadata)
|
|
1002
|
+
val webEmbedEntries = fileEntries.filterKeys { it.startsWith("web-embed/") }
|
|
1003
|
+
|
|
1004
|
+
if (!webEmbedRoot.exists() || !webEmbedRoot.isDirectory) {
|
|
1005
|
+
if (webEmbedEntries.isNotEmpty()) {
|
|
1006
|
+
OneKeyLog.error(
|
|
1007
|
+
"BundleUpdate",
|
|
1008
|
+
"[web-embed-verify] metadata lists web-embed entries but directory is missing",
|
|
1009
|
+
)
|
|
1010
|
+
return false
|
|
1011
|
+
}
|
|
1012
|
+
return true
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
val bundleDirWithSlash = if (bundleDir.endsWith("/")) bundleDir else "$bundleDir/"
|
|
1016
|
+
if (!validateWebEmbedRecursive(webEmbedRoot, webEmbedEntries, bundleDirWithSlash)) {
|
|
1017
|
+
return false
|
|
1018
|
+
}
|
|
1019
|
+
for ((key, _) in webEmbedEntries) {
|
|
1020
|
+
val expectedFile = File(bundleDirWithSlash + key)
|
|
1021
|
+
if (!expectedFile.exists()) {
|
|
1022
|
+
OneKeyLog.error(
|
|
1023
|
+
"BundleUpdate",
|
|
1024
|
+
"[web-embed-verify] file listed in metadata but missing on disk: $key",
|
|
1025
|
+
)
|
|
1026
|
+
return false
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return true
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
private fun validateWebEmbedRecursive(
|
|
1033
|
+
dir: File,
|
|
1034
|
+
webEmbedEntries: Map<String, String>,
|
|
1035
|
+
bundleDirWithSlash: String,
|
|
1036
|
+
): Boolean {
|
|
1037
|
+
val files = dir.listFiles()
|
|
1038
|
+
if (files == null) {
|
|
1039
|
+
// listFiles() returns null on I/O error or unreadable directory.
|
|
1040
|
+
// Fail closed instead of treating an unlistable subtree as
|
|
1041
|
+
// "nothing to verify" — that would silently allow a tampered
|
|
1042
|
+
// web-embed asset whose containing dir was made unreadable.
|
|
1043
|
+
OneKeyLog.error(
|
|
1044
|
+
"BundleUpdate",
|
|
1045
|
+
"[web-embed-verify] failed to list directory: ${dir.absolutePath}",
|
|
1046
|
+
)
|
|
1047
|
+
return false
|
|
1048
|
+
}
|
|
1049
|
+
for (file in files) {
|
|
1050
|
+
if (file.isDirectory) {
|
|
1051
|
+
if (!validateWebEmbedRecursive(file, webEmbedEntries, bundleDirWithSlash)) return false
|
|
1052
|
+
continue
|
|
1053
|
+
}
|
|
1054
|
+
if (file.name == ".DS_Store") continue
|
|
1055
|
+
val relativePath = file.absolutePath.removePrefix(bundleDirWithSlash)
|
|
1056
|
+
val expected = webEmbedEntries[relativePath]
|
|
1057
|
+
if (expected.isNullOrEmpty()) {
|
|
1058
|
+
OneKeyLog.error(
|
|
1059
|
+
"BundleUpdate",
|
|
1060
|
+
"[web-embed-verify] file on disk not in metadata: $relativePath",
|
|
1061
|
+
)
|
|
1062
|
+
return false
|
|
1063
|
+
}
|
|
1064
|
+
val actual = calculateSHA256(file.absolutePath)
|
|
1065
|
+
if (actual == null || !secureCompare(expected, actual)) {
|
|
1066
|
+
OneKeyLog.error(
|
|
1067
|
+
"BundleUpdate",
|
|
1068
|
+
"[web-embed-verify] sha256 mismatch for $relativePath",
|
|
1069
|
+
)
|
|
1070
|
+
return false
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return true
|
|
1074
|
+
}
|
|
1075
|
+
|
|
832
1076
|
/**
|
|
833
1077
|
* Returns true if the OneKey developer mode (DevSettings) is enabled.
|
|
834
1078
|
* Reads the persisted value from MMKV storage (key: onekey_developer_mode_enabled,
|
|
@@ -1242,6 +1486,11 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1242
1486
|
}
|
|
1243
1487
|
|
|
1244
1488
|
override fun installBundle(params: BundleInstallParams): Promise<Unit> {
|
|
1489
|
+
// Invalidate up front so any concurrent reader misses the cache while
|
|
1490
|
+
// the install is running. We invalidate again after the async body
|
|
1491
|
+
// commits so a same-version reinstall (or any reader that re-cached
|
|
1492
|
+
// pre-commit data inside the race window) can't leave stale entries.
|
|
1493
|
+
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
1245
1494
|
return Promise.async {
|
|
1246
1495
|
val context = getContext()
|
|
1247
1496
|
val appVersion = params.latestVersion
|
|
@@ -1311,6 +1560,11 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1311
1560
|
} catch (e: Exception) {
|
|
1312
1561
|
OneKeyLog.error("BundleUpdate", "installBundle: fallbackUpdateBundleData error: ${e.message}")
|
|
1313
1562
|
}
|
|
1563
|
+
// Second invalidate: closes the race window between the up-front
|
|
1564
|
+
// invalidate and the actual currentBundleVersion write. Without
|
|
1565
|
+
// this, a reader entering getValidatedCurrentBundleInfo() between
|
|
1566
|
+
// the two could re-cache pre-install state and leave it stale.
|
|
1567
|
+
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
1314
1568
|
}
|
|
1315
1569
|
}
|
|
1316
1570
|
|
|
@@ -1331,6 +1585,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1331
1585
|
}
|
|
1332
1586
|
|
|
1333
1587
|
override fun clearBundle(): Promise<Unit> {
|
|
1588
|
+
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
1334
1589
|
return Promise.async {
|
|
1335
1590
|
OneKeyLog.info("BundleUpdate", "clearBundle: clearing download and bundle directories...")
|
|
1336
1591
|
val context = getContext()
|
|
@@ -1352,6 +1607,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1352
1607
|
}
|
|
1353
1608
|
|
|
1354
1609
|
override fun resetToBuiltInBundle(): Promise<Unit> {
|
|
1610
|
+
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
1355
1611
|
return Promise.async {
|
|
1356
1612
|
OneKeyLog.info("BundleUpdate", "resetToBuiltInBundle: clearing currentBundleVersion preference...")
|
|
1357
1613
|
val context = getContext()
|
|
@@ -1368,6 +1624,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1368
1624
|
}
|
|
1369
1625
|
|
|
1370
1626
|
override fun clearAllJSBundleData(): Promise<TestResult> {
|
|
1627
|
+
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
1371
1628
|
return Promise.async {
|
|
1372
1629
|
OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: starting...")
|
|
1373
1630
|
val context = getContext()
|
|
@@ -1405,6 +1662,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1405
1662
|
}
|
|
1406
1663
|
|
|
1407
1664
|
override fun setCurrentUpdateBundleData(params: BundleSwitchParams): Promise<Unit> {
|
|
1665
|
+
BundleUpdateStoreAndroid.invalidateValidatedBundleInfoCache()
|
|
1408
1666
|
return Promise.async {
|
|
1409
1667
|
val context = getContext()
|
|
1410
1668
|
val bundleVersion = "${params.appVersion}-${params.bundleVersion}"
|
|
@@ -82,10 +82,58 @@ public class BundleUpdateStore: NSObject {
|
|
|
82
82
|
private static let nativeBuildNumberKey = "nativeBuildNumber"
|
|
83
83
|
private static let mainBundleEntryFileName = "main.jsbundle.hbc"
|
|
84
84
|
private static let backgroundBundleEntryFileName = "background.bundle"
|
|
85
|
+
private static let commonBundleEntryFileName = "common.bundle"
|
|
85
86
|
private static let metadataRequiresBackgroundBundleKey = "requiresBackgroundBundle"
|
|
86
87
|
private static let metadataBackgroundProtocolVersionKey = "backgroundProtocolVersion"
|
|
88
|
+
private static let metadataRequiresCommonBundleKey = "requiresCommonBundle"
|
|
89
|
+
private static let metadataBundleFormatKey = "bundleFormat"
|
|
90
|
+
private static let bundleFormatThreeBundle = "three-bundle"
|
|
87
91
|
private static let supportedBackgroundProtocolVersion = "1"
|
|
88
92
|
|
|
93
|
+
// In-memory cache for validatedCurrentBundleInfo. Without this, every
|
|
94
|
+
// bundleURL / common / main / background path getter on startup re-runs
|
|
95
|
+
// the whole validation pipeline (signature + sha256 of every entry file).
|
|
96
|
+
// Cache key is currentBundleVersion; invalidated on any mutation of the
|
|
97
|
+
// current bundle (setCurrentUpdateBundleData / clearBundle /
|
|
98
|
+
// resetToBuiltInBundle / clearAllJSBundleData / native version change).
|
|
99
|
+
private static var cachedValidatedBundleInfo: (
|
|
100
|
+
bundleDirPath: String,
|
|
101
|
+
currentBundleVersion: String,
|
|
102
|
+
metadata: [String: Any]
|
|
103
|
+
)?
|
|
104
|
+
private static let cachedValidatedBundleInfoLock = NSLock()
|
|
105
|
+
|
|
106
|
+
// Lazy per-version cache for the web-embed subtree. getWebEmbedPath is
|
|
107
|
+
// called every time a WebView is created, but the subtree contents are
|
|
108
|
+
// immutable for a given currentBundleVersion. Stores the version that
|
|
109
|
+
// most recently passed full sha256 sweep over web-embed/**; invalidated
|
|
110
|
+
// alongside cachedValidatedBundleInfo on any bundle mutation.
|
|
111
|
+
private static var cachedWebEmbedVerifiedVersion: String?
|
|
112
|
+
private static let cachedWebEmbedVerifiedVersionLock = NSLock()
|
|
113
|
+
|
|
114
|
+
public static func invalidateValidatedBundleInfoCache() {
|
|
115
|
+
cachedValidatedBundleInfoLock.lock()
|
|
116
|
+
cachedValidatedBundleInfo = nil
|
|
117
|
+
cachedValidatedBundleInfoLock.unlock()
|
|
118
|
+
cachedWebEmbedVerifiedVersionLock.lock()
|
|
119
|
+
cachedWebEmbedVerifiedVersion = nil
|
|
120
|
+
cachedWebEmbedVerifiedVersionLock.unlock()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// True when the current bundle's metadata declares the three-bundle /
|
|
124
|
+
/// split-thread layout (or explicitly opts in via requiresCommonBundle).
|
|
125
|
+
/// SplitBundleLoader queries this reflectively to decide whether an empty
|
|
126
|
+
/// per-segment sha256 is a back-compat skip (older formats) or a hard
|
|
127
|
+
/// fail (three-bundle, which always ships per-segment hashes).
|
|
128
|
+
public static func currentBundleRequiresPerSegmentHash() -> Bool {
|
|
129
|
+
guard let info = validatedCurrentBundleInfo() else { return false }
|
|
130
|
+
if metadataBoolValue(info.metadata, key: metadataRequiresCommonBundleKey) {
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
let format = metadataStringValue(info.metadata, key: metadataBundleFormatKey) ?? ""
|
|
134
|
+
return format.lowercased() == bundleFormatThreeBundle
|
|
135
|
+
}
|
|
136
|
+
|
|
89
137
|
public static func documentDirectory() -> String {
|
|
90
138
|
NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
|
|
91
139
|
}
|
|
@@ -165,9 +213,105 @@ public class BundleUpdateStore: NSObject {
|
|
|
165
213
|
|
|
166
214
|
public static func getWebEmbedPath() -> String {
|
|
167
215
|
guard let bundleInfo = validatedCurrentBundleInfo() else { return "" }
|
|
216
|
+
// Lazy full-tree sha256 sweep for web-embed/**. The startup hot path
|
|
217
|
+
// only validates the JS entry bundles (validateEntryBundlesSha256), so
|
|
218
|
+
// without this, a tampered web-embed asset would slip through. Result
|
|
219
|
+
// is cached per currentBundleVersion and invalidated on any bundle
|
|
220
|
+
// mutation, so cost is paid once per (re)install.
|
|
221
|
+
if !ensureWebEmbedVerified(
|
|
222
|
+
bundleDirPath: bundleInfo.bundleDirPath,
|
|
223
|
+
currentBundleVersion: bundleInfo.currentBundleVersion,
|
|
224
|
+
metadata: bundleInfo.metadata,
|
|
225
|
+
) {
|
|
226
|
+
return ""
|
|
227
|
+
}
|
|
168
228
|
return (bundleInfo.bundleDirPath as NSString).appendingPathComponent("web-embed")
|
|
169
229
|
}
|
|
170
230
|
|
|
231
|
+
private static func ensureWebEmbedVerified(
|
|
232
|
+
bundleDirPath: String,
|
|
233
|
+
currentBundleVersion: String,
|
|
234
|
+
metadata: [String: Any],
|
|
235
|
+
) -> Bool {
|
|
236
|
+
cachedWebEmbedVerifiedVersionLock.lock()
|
|
237
|
+
if cachedWebEmbedVerifiedVersion == currentBundleVersion {
|
|
238
|
+
cachedWebEmbedVerifiedVersionLock.unlock()
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
cachedWebEmbedVerifiedVersionLock.unlock()
|
|
242
|
+
|
|
243
|
+
if !validateWebEmbedSha256(bundleDirPath: bundleDirPath, metadata: metadata) {
|
|
244
|
+
OneKeyLog.error("BundleUpdate", "validateWebEmbedSha256 failed for \(currentBundleVersion)")
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
cachedWebEmbedVerifiedVersionLock.lock()
|
|
249
|
+
cachedWebEmbedVerifiedVersion = currentBundleVersion
|
|
250
|
+
cachedWebEmbedVerifiedVersionLock.unlock()
|
|
251
|
+
return true
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Walks bundleDirPath/web-embed/** and verifies every file's sha256
|
|
255
|
+
/// against the metadata entry (key is the bundle-relative path). Also
|
|
256
|
+
/// rejects files on disk that aren't listed in metadata, and metadata
|
|
257
|
+
/// entries whose backing file is missing. Returns true when web-embed
|
|
258
|
+
/// is absent from both disk and metadata (bundles without web-embed).
|
|
259
|
+
static func validateWebEmbedSha256(
|
|
260
|
+
bundleDirPath: String,
|
|
261
|
+
metadata: [String: Any],
|
|
262
|
+
) -> Bool {
|
|
263
|
+
let fm = FileManager.default
|
|
264
|
+
let webEmbedDir = (bundleDirPath as NSString).appendingPathComponent("web-embed")
|
|
265
|
+
let allEntries = fileMetadataEntries(from: metadata)
|
|
266
|
+
let webEmbedEntries = allEntries.filter { $0.key.hasPrefix("web-embed/") }
|
|
267
|
+
|
|
268
|
+
var dirExists: ObjCBool = false
|
|
269
|
+
let dirPresent = fm.fileExists(atPath: webEmbedDir, isDirectory: &dirExists) && dirExists.boolValue
|
|
270
|
+
if !dirPresent {
|
|
271
|
+
// No web-embed in this bundle is fine, as long as metadata agrees.
|
|
272
|
+
if !webEmbedEntries.isEmpty {
|
|
273
|
+
OneKeyLog.error("BundleUpdate", "[web-embed-verify] metadata lists web-embed entries but directory is missing")
|
|
274
|
+
return false
|
|
275
|
+
}
|
|
276
|
+
return true
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let bundleDirWithSlash = bundleDirPath.hasSuffix("/") ? bundleDirPath : bundleDirPath + "/"
|
|
280
|
+
guard let enumerator = fm.enumerator(atPath: webEmbedDir) else {
|
|
281
|
+
OneKeyLog.error("BundleUpdate", "[web-embed-verify] failed to enumerate web-embed directory")
|
|
282
|
+
return false
|
|
283
|
+
}
|
|
284
|
+
while let entry = enumerator.nextObject() as? String {
|
|
285
|
+
let fullPath = (webEmbedDir as NSString).appendingPathComponent(entry)
|
|
286
|
+
var isDir: ObjCBool = false
|
|
287
|
+
if fm.fileExists(atPath: fullPath, isDirectory: &isDir), isDir.boolValue { continue }
|
|
288
|
+
if entry.hasSuffix(".DS_Store") { continue }
|
|
289
|
+
|
|
290
|
+
let relativePath = fullPath.replacingOccurrences(of: bundleDirWithSlash, with: "")
|
|
291
|
+
guard let expected = webEmbedEntries[relativePath], !expected.isEmpty else {
|
|
292
|
+
OneKeyLog.error("BundleUpdate", "[web-embed-verify] file on disk not in metadata: \(relativePath)")
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
guard let actual = calculateSHA256(fullPath) else {
|
|
296
|
+
OneKeyLog.error("BundleUpdate", "[web-embed-verify] failed to hash file: \(relativePath)")
|
|
297
|
+
return false
|
|
298
|
+
}
|
|
299
|
+
if !expected.secureCompare(actual) {
|
|
300
|
+
OneKeyLog.error("BundleUpdate", "[web-embed-verify] sha256 mismatch for \(relativePath)")
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for key in webEmbedEntries.keys {
|
|
306
|
+
let expectedFilePath = bundleDirWithSlash + key
|
|
307
|
+
if !fm.fileExists(atPath: expectedFilePath) {
|
|
308
|
+
OneKeyLog.error("BundleUpdate", "[web-embed-verify] file listed in metadata but missing on disk: \(key)")
|
|
309
|
+
return false
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return true
|
|
313
|
+
}
|
|
314
|
+
|
|
171
315
|
public static func calculateSHA256(_ filePath: String) -> String? {
|
|
172
316
|
guard let fileHandle = FileHandle(forReadingAtPath: filePath) else { return nil }
|
|
173
317
|
defer { fileHandle.closeFile() }
|
|
@@ -335,7 +479,10 @@ public class BundleUpdateStore: NSObject {
|
|
|
335
479
|
}
|
|
336
480
|
|
|
337
481
|
private static func isReservedMetadataKey(_ key: String) -> Bool {
|
|
338
|
-
key == metadataRequiresBackgroundBundleKey ||
|
|
482
|
+
key == metadataRequiresBackgroundBundleKey ||
|
|
483
|
+
key == metadataBackgroundProtocolVersionKey ||
|
|
484
|
+
key == metadataRequiresCommonBundleKey ||
|
|
485
|
+
key == metadataBundleFormatKey
|
|
339
486
|
}
|
|
340
487
|
|
|
341
488
|
private static func metadataStringValue(
|
|
@@ -589,6 +736,26 @@ public class BundleUpdateStore: NSObject {
|
|
|
589
736
|
return false
|
|
590
737
|
}
|
|
591
738
|
|
|
739
|
+
// Three-bundle (union build / split thread) mode requires a
|
|
740
|
+
// common.bundle shipped alongside main.jsbundle.hbc. Without it
|
|
741
|
+
// the entry-only main bundle references moduleIds that only exist in
|
|
742
|
+
// the common bundle and the runtime crashes on first require().
|
|
743
|
+
let bundleFormat = metadataStringValue(metadata, key: metadataBundleFormatKey) ?? ""
|
|
744
|
+
let requiresCommonBundle =
|
|
745
|
+
metadataBoolValue(metadata, key: metadataRequiresCommonBundleKey) ||
|
|
746
|
+
bundleFormat.lowercased() == bundleFormatThreeBundle
|
|
747
|
+
if requiresCommonBundle {
|
|
748
|
+
let commonBundlePath = (bundleDirPath as NSString)
|
|
749
|
+
.appendingPathComponent(commonBundleEntryFileName)
|
|
750
|
+
guard FileManager.default.fileExists(atPath: commonBundlePath) else {
|
|
751
|
+
OneKeyLog.error(
|
|
752
|
+
"BundleUpdate",
|
|
753
|
+
"requiresCommonBundle is true but common.bundle is missing at \(commonBundlePath)",
|
|
754
|
+
)
|
|
755
|
+
return false
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
592
759
|
let requiresBackgroundBundle = metadataBoolValue(
|
|
593
760
|
metadata,
|
|
594
761
|
key: metadataRequiresBackgroundBundleKey,
|
|
@@ -630,9 +797,19 @@ public class BundleUpdateStore: NSObject {
|
|
|
630
797
|
processPreLaunchPendingTask()
|
|
631
798
|
guard let currentBundleVer = currentBundleVersion() else {
|
|
632
799
|
OneKeyLog.warn("BundleUpdate", "getJsBundlePath: no currentBundleVersion stored")
|
|
800
|
+
invalidateValidatedBundleInfoCache()
|
|
633
801
|
return nil
|
|
634
802
|
}
|
|
635
803
|
|
|
804
|
+
// Memo cache: avoid re-running signature + entry-bundle sha256 for every
|
|
805
|
+
// bundleURL / common / main / background path getter on startup.
|
|
806
|
+
cachedValidatedBundleInfoLock.lock()
|
|
807
|
+
if let cached = cachedValidatedBundleInfo, cached.currentBundleVersion == currentBundleVer {
|
|
808
|
+
cachedValidatedBundleInfoLock.unlock()
|
|
809
|
+
return cached
|
|
810
|
+
}
|
|
811
|
+
cachedValidatedBundleInfoLock.unlock()
|
|
812
|
+
|
|
636
813
|
let currentAppVersion = getCurrentNativeVersion()
|
|
637
814
|
guard let prevNativeVersion = getNativeVersion() else {
|
|
638
815
|
OneKeyLog.warn("BundleUpdate", "getJsBundlePath: prevNativeVersion is nil")
|
|
@@ -694,20 +871,69 @@ public class BundleUpdateStore: NSObject {
|
|
|
694
871
|
return nil
|
|
695
872
|
}
|
|
696
873
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
874
|
+
// Startup hot path: only verify entry-bundle SHA-256
|
|
875
|
+
// (main + common + background as required by metadata flags). The
|
|
876
|
+
// full-tree sha256 sweep already runs at install time
|
|
877
|
+
// (validateAllFilesInDir in installBundle), so re-doing it on every
|
|
878
|
+
// launch costs hundreds of ms per startup with no security gain
|
|
879
|
+
// (sandboxed app data + signed metadata.json bind every file's
|
|
880
|
+
// expected hash). Per-segment integrity is checked at loadSegment
|
|
881
|
+
// time by SplitBundleLoader.
|
|
882
|
+
if !validateEntryBundlesSha256(bundleDirPath: folderName, metadata: metadata) {
|
|
883
|
+
OneKeyLog.info("BundleUpdate", "validateEntryBundlesSha256 failed on startup")
|
|
884
|
+
return nil
|
|
704
885
|
}
|
|
705
886
|
|
|
706
887
|
if !validateBundlePairCompatibility(bundleDirPath: folderName, metadata: metadata) {
|
|
707
888
|
return nil
|
|
708
889
|
}
|
|
709
890
|
|
|
710
|
-
|
|
891
|
+
let result = (folderName, currentBundleVer, metadata)
|
|
892
|
+
cachedValidatedBundleInfoLock.lock()
|
|
893
|
+
cachedValidatedBundleInfo = result
|
|
894
|
+
cachedValidatedBundleInfoLock.unlock()
|
|
895
|
+
return result
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/// Verifies SHA-256 of just the entry bundles required to boot the JS
|
|
899
|
+
/// runtime (main + common + background, gated by metadata flags). Runs in
|
|
900
|
+
/// place of the legacy validateAllFilesInDir on the startup hot path.
|
|
901
|
+
static func validateEntryBundlesSha256(
|
|
902
|
+
bundleDirPath: String,
|
|
903
|
+
metadata: [String: Any],
|
|
904
|
+
) -> Bool {
|
|
905
|
+
let fileEntries = fileMetadataEntries(from: metadata)
|
|
906
|
+
|
|
907
|
+
var entriesToCheck: [String] = [mainBundleEntryFileName]
|
|
908
|
+
let bundleFormat = metadataStringValue(metadata, key: metadataBundleFormatKey) ?? ""
|
|
909
|
+
if metadataBoolValue(metadata, key: metadataRequiresCommonBundleKey) ||
|
|
910
|
+
bundleFormat.lowercased() == bundleFormatThreeBundle {
|
|
911
|
+
entriesToCheck.append(commonBundleEntryFileName)
|
|
912
|
+
}
|
|
913
|
+
if metadataBoolValue(metadata, key: metadataRequiresBackgroundBundleKey) {
|
|
914
|
+
entriesToCheck.append(backgroundBundleEntryFileName)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
for entry in entriesToCheck {
|
|
918
|
+
let path = (bundleDirPath as NSString).appendingPathComponent(entry)
|
|
919
|
+
guard FileManager.default.fileExists(atPath: path) else {
|
|
920
|
+
OneKeyLog.error("BundleUpdate", "[entry-verify] missing entry file: \(entry)")
|
|
921
|
+
return false
|
|
922
|
+
}
|
|
923
|
+
guard let expected = fileEntries[entry], !expected.isEmpty else {
|
|
924
|
+
OneKeyLog.error("BundleUpdate", "[entry-verify] no metadata sha256 for entry: \(entry)")
|
|
925
|
+
return false
|
|
926
|
+
}
|
|
927
|
+
guard let actual = calculateSHA256(path) else {
|
|
928
|
+
OneKeyLog.error("BundleUpdate", "[entry-verify] failed to hash entry: \(entry)")
|
|
929
|
+
return false
|
|
930
|
+
}
|
|
931
|
+
if !expected.secureCompare(actual) {
|
|
932
|
+
OneKeyLog.error("BundleUpdate", "[entry-verify] sha256 mismatch for entry: \(entry)")
|
|
933
|
+
return false
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return true
|
|
711
937
|
}
|
|
712
938
|
|
|
713
939
|
private static func currentBundleEntryPath(_ entryFileName: String) -> String? {
|
|
@@ -731,6 +957,10 @@ public class BundleUpdateStore: NSObject {
|
|
|
731
957
|
currentBundleEntryPath(backgroundBundleEntryFileName)
|
|
732
958
|
}
|
|
733
959
|
|
|
960
|
+
public static func currentBundleCommonJSBundle() -> String? {
|
|
961
|
+
currentBundleEntryPath(commonBundleEntryFileName)
|
|
962
|
+
}
|
|
963
|
+
|
|
734
964
|
// Fallback data management
|
|
735
965
|
static func getFallbackUpdateBundleDataPath() -> String {
|
|
736
966
|
let path = (bundleDir() as NSString).appendingPathComponent("fallbackUpdateBundleData.json")
|
|
@@ -759,6 +989,7 @@ public class BundleUpdateStore: NSObject {
|
|
|
759
989
|
}
|
|
760
990
|
|
|
761
991
|
public static func clearUpdateBundleData() {
|
|
992
|
+
invalidateValidatedBundleInfoCache()
|
|
762
993
|
let bDir = bundleDir()
|
|
763
994
|
let fm = FileManager.default
|
|
764
995
|
if fm.fileExists(atPath: bDir) {
|
|
@@ -1225,6 +1456,11 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1225
1456
|
}
|
|
1226
1457
|
|
|
1227
1458
|
func installBundle(params: BundleInstallParams) throws -> Promise<Void> {
|
|
1459
|
+
// Invalidate up front so any concurrent reader misses the cache while
|
|
1460
|
+
// the install is running. We invalidate again after the async body
|
|
1461
|
+
// commits so a same-version reinstall (or any reader that re-cached
|
|
1462
|
+
// pre-commit data inside the race window) can't leave stale entries.
|
|
1463
|
+
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
1228
1464
|
return Promise.async {
|
|
1229
1465
|
let appVersion = params.latestVersion
|
|
1230
1466
|
let bundleVersion = params.bundleVersion
|
|
@@ -1295,6 +1531,12 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1295
1531
|
BundleUpdateStore.writeFallbackUpdateBundleDataFile(fallbackData)
|
|
1296
1532
|
ud.synchronize()
|
|
1297
1533
|
|
|
1534
|
+
// Second invalidate: closes the race window between the up-front
|
|
1535
|
+
// invalidate and the actual currentBundleVersion write. Without
|
|
1536
|
+
// this, a reader entering validatedCurrentBundleInfo() between
|
|
1537
|
+
// the two could re-cache pre-install state and leave it stale.
|
|
1538
|
+
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
1539
|
+
|
|
1298
1540
|
OneKeyLog.info("BundleUpdate", "installBundle: completed successfully, installed version=\(folderName), fallbackCount=\(fallbackData.count)")
|
|
1299
1541
|
}
|
|
1300
1542
|
}
|
|
@@ -1318,6 +1560,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1318
1560
|
}
|
|
1319
1561
|
|
|
1320
1562
|
func clearBundle() throws -> Promise<Void> {
|
|
1563
|
+
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
1321
1564
|
return Promise.async { [weak self] in
|
|
1322
1565
|
OneKeyLog.info("BundleUpdate", "clearBundle: clearing download and bundle directories...")
|
|
1323
1566
|
// Clear download directory
|
|
@@ -1341,6 +1584,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1341
1584
|
}
|
|
1342
1585
|
|
|
1343
1586
|
func resetToBuiltInBundle() throws -> Promise<Void> {
|
|
1587
|
+
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
1344
1588
|
return Promise.async {
|
|
1345
1589
|
OneKeyLog.info("BundleUpdate", "resetToBuiltInBundle: clearing currentBundleVersion preference...")
|
|
1346
1590
|
let ud = UserDefaults.standard
|
|
@@ -1357,6 +1601,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1357
1601
|
}
|
|
1358
1602
|
|
|
1359
1603
|
func clearAllJSBundleData() throws -> Promise<TestResult> {
|
|
1604
|
+
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
1360
1605
|
return Promise.async {
|
|
1361
1606
|
OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: starting...")
|
|
1362
1607
|
let bundleDir = BundleUpdateStore.bundleDir()
|
|
@@ -1393,6 +1638,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1393
1638
|
}
|
|
1394
1639
|
|
|
1395
1640
|
func setCurrentUpdateBundleData(params: BundleSwitchParams) throws -> Promise<Void> {
|
|
1641
|
+
BundleUpdateStore.invalidateValidatedBundleInfoCache()
|
|
1396
1642
|
return Promise.async {
|
|
1397
1643
|
let bundleVersion = "\(params.appVersion)-\(params.bundleVersion)"
|
|
1398
1644
|
OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switching to \(bundleVersion)")
|