@hot-updater/react-native 1.0.5 → 1.0.7
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/HotUpdater.podspec +24 -3
- package/Package.swift +100 -0
- package/android/build.gradle +3 -4
- package/android/hot-updater-builtin-assets.gradle +67 -0
- package/android/hot-updater-min-bundle-id.gradle +69 -0
- package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +85 -0
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +116 -11
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +117 -12
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +5 -52
- package/android/src/newarch/HotUpdaterModule.kt +129 -34
- package/android/src/oldarch/HotUpdaterModule.kt +130 -34
- package/android/src/oldarch/HotUpdaterSpec.kt +5 -0
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +69 -0
- package/ios/HotUpdater/Interface/HotUpdaterInterface.m +3 -0
- package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +87 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +88 -13
- package/ios/HotUpdater/Internal/HotUpdater.mm +90 -90
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +154 -52
- package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +26 -0
- package/ios/HotUpdater/Internal/HotUpdaterMinBundleId.m +10 -0
- package/ios/HotUpdater/Package.swift +88 -0
- package/ios/Plugins/HotUpdaterMinBundleIdPlugin/HotUpdaterMinBundleIdPlugin.swift +28 -0
- package/ios/scripts/generate-min-bundle-id.zsh +49 -0
- package/lib/commonjs/checkForUpdate.js +17 -2
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/fetchUpdateResponse.js +5 -2
- package/lib/commonjs/fetchUpdateResponse.js.map +1 -1
- package/lib/commonjs/httpClient.js +58 -5
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/native.js +42 -7
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js +5 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +18 -3
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/fetchUpdateResponse.js +4 -1
- package/lib/module/fetchUpdateResponse.js.map +1 -1
- package/lib/module/httpClient.js +61 -7
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/native.js +41 -7
- package/lib/module/native.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js +7 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +2 -0
- package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +26 -2
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +17 -3
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +37 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/fetchUpdateResponse.d.ts +2 -0
- package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +26 -2
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +17 -3
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +37 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +5 -4
- package/react-native.config.js +7 -0
- package/src/checkForUpdate.ts +19 -3
- package/src/fetchUpdateResponse.ts +4 -1
- package/src/httpClient.ts +88 -9
- package/src/native.ts +53 -9
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +39 -1
|
@@ -124,6 +124,7 @@ interface BundleStorageService {
|
|
|
124
124
|
manifestFileHash: String,
|
|
125
125
|
assets: Map<String, ChangedAssetDescriptor>,
|
|
126
126
|
archiveUrl: String? = null,
|
|
127
|
+
fileResolver: ArtifactFileResolver? = null,
|
|
127
128
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
128
129
|
): UpdateBundleResult
|
|
129
130
|
|
|
@@ -239,6 +240,16 @@ class BundleFileStorageService(
|
|
|
239
240
|
private const val MAX_SAFE_INTEGER = 9_007_199_254_740_991L
|
|
240
241
|
private val SHA256_PATTERN = Regex("^[0-9a-f]{64}$")
|
|
241
242
|
|
|
243
|
+
/** Files downloaded at once when the archive is not used. */
|
|
244
|
+
private const val PARALLEL_FILE_DOWNLOADS = 16
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* What one request costs beyond the first [PARALLEL_FILE_DOWNLOADS], in
|
|
248
|
+
* bytes: each waits a round trip for a free slot, about 100 ms, which
|
|
249
|
+
* at 5 MB/s over 16 slots is the time of 32 KiB.
|
|
250
|
+
*/
|
|
251
|
+
private const val REQUEST_OVERHEAD_BYTES = 32L * 1024
|
|
252
|
+
|
|
242
253
|
/** A patch produced a file that does not match the target, or none at all. */
|
|
243
254
|
private val PATCH_FAILURE =
|
|
244
255
|
UpdateFailure(UpdateFailureStage.INSTALL, UpdateFailureReason.PATCH, resource = UpdateFailureResource.PATCH)
|
|
@@ -718,10 +729,35 @@ class BundleFileStorageService(
|
|
|
718
729
|
if (resolveBundleFile(bundleDir, bundleId) == null) return false
|
|
719
730
|
return manifest.assets.all { (assetPath, asset) ->
|
|
720
731
|
val file = RelativePathResolver.resolveInside(bundleDir, assetPath)
|
|
721
|
-
file?.isFile == true &&
|
|
732
|
+
file?.isFile == true &&
|
|
733
|
+
(
|
|
734
|
+
runCatching { verifyManifestAssetFileOrThrow(file, asset) }.isSuccess ||
|
|
735
|
+
isBuiltInEquivalent(assetPath, file, asset)
|
|
736
|
+
)
|
|
722
737
|
}
|
|
723
738
|
}
|
|
724
739
|
|
|
740
|
+
/**
|
|
741
|
+
* Whether [file] holds the packaged bytes of the built-in asset the native
|
|
742
|
+
* build recorded with the manifest's source hash, as AAPT2 re-encodes a
|
|
743
|
+
* PNG. Both come from the APK, never from app storage. The manifest's
|
|
744
|
+
* signature still covers that source hash.
|
|
745
|
+
*/
|
|
746
|
+
private fun isBuiltInEquivalent(
|
|
747
|
+
assetPath: String,
|
|
748
|
+
file: File,
|
|
749
|
+
asset: ParsedManifestAsset,
|
|
750
|
+
): Boolean {
|
|
751
|
+
val packagedHash = builtInAssetResolver.packagedHashIfSourceMatches(assetPath, asset.fileHash) ?: return false
|
|
752
|
+
return HashUtils.calculateSHA256(file).equals(packagedHash, ignoreCase = true) &&
|
|
753
|
+
runCatching {
|
|
754
|
+
if (SignatureVerifier.isSigningEnabled(context)) {
|
|
755
|
+
val signature = asset.signature ?: throw SignatureVerificationException.InvalidSignatureFormat()
|
|
756
|
+
SignatureVerifier.verifyHashSignature(context, asset.fileHash, signature)
|
|
757
|
+
}
|
|
758
|
+
}.isSuccess
|
|
759
|
+
}
|
|
760
|
+
|
|
725
761
|
private fun recoverInterruptedPromotions(): Boolean {
|
|
726
762
|
val bundleStoreDir = getBundleStoreDir()
|
|
727
763
|
val backups =
|
|
@@ -1610,6 +1646,7 @@ class BundleFileStorageService(
|
|
|
1610
1646
|
manifestFileHash: String,
|
|
1611
1647
|
assets: Map<String, ChangedAssetDescriptor>,
|
|
1612
1648
|
archiveUrl: String?,
|
|
1649
|
+
fileResolver: ArtifactFileResolver?,
|
|
1613
1650
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1614
1651
|
): UpdateBundleResult =
|
|
1615
1652
|
updateMutex.withLock {
|
|
@@ -1619,6 +1656,7 @@ class BundleFileStorageService(
|
|
|
1619
1656
|
manifestFileHash = manifestFileHash,
|
|
1620
1657
|
changedAssets = assets,
|
|
1621
1658
|
archiveUrl = archiveUrl,
|
|
1659
|
+
fileResolver = fileResolver,
|
|
1622
1660
|
progressCallback = progressCallback,
|
|
1623
1661
|
)
|
|
1624
1662
|
}
|
|
@@ -1629,6 +1667,7 @@ class BundleFileStorageService(
|
|
|
1629
1667
|
manifestFileHash: String,
|
|
1630
1668
|
changedAssets: Map<String, ChangedAssetDescriptor>,
|
|
1631
1669
|
archiveUrl: String?,
|
|
1670
|
+
fileResolver: ArtifactFileResolver?,
|
|
1632
1671
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1633
1672
|
): UpdateBundleResult {
|
|
1634
1673
|
Log.d(
|
|
@@ -1660,6 +1699,7 @@ class BundleFileStorageService(
|
|
|
1660
1699
|
manifestFileHash = manifestFileHash,
|
|
1661
1700
|
changedAssets = changedAssets,
|
|
1662
1701
|
archiveUrl = archiveUrl,
|
|
1702
|
+
fileResolver = fileResolver,
|
|
1663
1703
|
bundleStoreDir = bundleStoreDir,
|
|
1664
1704
|
finalBundleDir = finalBundleDir,
|
|
1665
1705
|
progressCallback = progressCallback,
|
|
@@ -1710,6 +1750,35 @@ class BundleFileStorageService(
|
|
|
1710
1750
|
}
|
|
1711
1751
|
}
|
|
1712
1752
|
|
|
1753
|
+
/**
|
|
1754
|
+
* Asks [fileResolver] for the URLs of [downloads], in requests of at most
|
|
1755
|
+
* [ArtifactFileResolver.ARTIFACT_FILES_REQUEST_LIMIT] paths.
|
|
1756
|
+
*/
|
|
1757
|
+
private suspend fun resolveFileUrls(
|
|
1758
|
+
fileResolver: ArtifactFileResolver,
|
|
1759
|
+
downloads: MutableMap<String, ChangedAssetDescriptor>,
|
|
1760
|
+
) {
|
|
1761
|
+
for (paths in downloads.keys.sorted().chunked(ArtifactFileResolver.ARTIFACT_FILES_REQUEST_LIMIT)) {
|
|
1762
|
+
val files =
|
|
1763
|
+
try {
|
|
1764
|
+
fileResolver.resolve(paths)
|
|
1765
|
+
} catch (error: CancellationException) {
|
|
1766
|
+
throw error
|
|
1767
|
+
} catch (error: Exception) {
|
|
1768
|
+
throw downloadError(error)
|
|
1769
|
+
}
|
|
1770
|
+
for (path in paths) {
|
|
1771
|
+
val file = files[path] ?: throw HotUpdaterException.invalidBundle()
|
|
1772
|
+
try {
|
|
1773
|
+
URL(file.url)
|
|
1774
|
+
} catch (error: java.net.MalformedURLException) {
|
|
1775
|
+
throw HotUpdaterException.invalidFileUrl(path, file.url, UpdateFailureResource.FILE)
|
|
1776
|
+
}
|
|
1777
|
+
downloads[path] = downloads.getValue(path).copy(fileUrl = file.url, fileCompression = file.compression)
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1713
1782
|
/** The rejection for a download that returned [error]. */
|
|
1714
1783
|
private fun downloadError(error: Exception): HotUpdaterException =
|
|
1715
1784
|
if (error is IncompleteDownloadException) {
|
|
@@ -1945,10 +2014,15 @@ class BundleFileStorageService(
|
|
|
1945
2014
|
}
|
|
1946
2015
|
if (archive.tarByteSize < logicalBytes) return null
|
|
1947
2016
|
val tarFramingBytes = archive.tarByteSize - logicalBytes
|
|
1948
|
-
|
|
1949
|
-
val
|
|
1950
|
-
|
|
1951
|
-
if (
|
|
2017
|
+
// Many small files cost more in requests than in bytes.
|
|
2018
|
+
val requestOverheadBytes =
|
|
2019
|
+
(downloads.size - PARALLEL_FILE_DOWNLOADS).coerceAtLeast(0) * REQUEST_OVERHEAD_BYTES
|
|
2020
|
+
if (individualBytes > MAX_SAFE_INTEGER - requestOverheadBytes) return null
|
|
2021
|
+
val individualCost = individualBytes + requestOverheadBytes
|
|
2022
|
+
if (individualCost > MAX_SAFE_INTEGER - tarFramingBytes) return null
|
|
2023
|
+
val framedByteLimit = individualCost + tarFramingBytes
|
|
2024
|
+
|
|
2025
|
+
if (archive.downloadByteSize <= individualCost) return archive
|
|
1952
2026
|
if (downloads.size != manifest.assets.size || downloads.values.any { it.patch != null }) return null
|
|
1953
2027
|
return archive.takeIf { it.downloadByteSize <= framedByteLimit }
|
|
1954
2028
|
}
|
|
@@ -2074,6 +2148,7 @@ class BundleFileStorageService(
|
|
|
2074
2148
|
manifestFileHash: String,
|
|
2075
2149
|
changedAssets: Map<String, ChangedAssetDescriptor>,
|
|
2076
2150
|
archiveUrl: String?,
|
|
2151
|
+
fileResolver: ArtifactFileResolver?,
|
|
2077
2152
|
bundleStoreDir: File,
|
|
2078
2153
|
finalBundleDir: File,
|
|
2079
2154
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
@@ -2135,14 +2210,20 @@ class BundleFileStorageService(
|
|
|
2135
2210
|
if (manifest.bundleId != bundleId) {
|
|
2136
2211
|
throw HotUpdaterException.invalidBundle()
|
|
2137
2212
|
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2213
|
+
if (fileResolver == null) {
|
|
2214
|
+
// Legacy: artifact protocol v1, where every file comes with its URL.
|
|
2215
|
+
// Reuse never bypasses the complete artifact contract.
|
|
2216
|
+
if (manifest.assets.keys != changedAssets.keys) throw HotUpdaterException.invalidBundle()
|
|
2217
|
+
} else if (!manifest.assets.keys.containsAll(changedAssets.keys)) {
|
|
2218
|
+
// In v2, `assets` holds only the Hermes patch.
|
|
2219
|
+
throw HotUpdaterException.invalidBundle()
|
|
2220
|
+
}
|
|
2140
2221
|
manifest.assets.forEach { (path, asset) ->
|
|
2141
|
-
val descriptor = changedAssets
|
|
2222
|
+
val descriptor = changedAssets[path]
|
|
2142
2223
|
if (RelativePathResolver.normalizeRelativePath(path) != path ||
|
|
2143
2224
|
path == "manifest.json" ||
|
|
2144
|
-
descriptor
|
|
2145
|
-
!descriptor
|
|
2225
|
+
(fileResolver == null && descriptor?.fileUrl.isNullOrBlank()) ||
|
|
2226
|
+
(fileResolver == null && !descriptor!!.fileHash.equals(asset.fileHash, ignoreCase = true)) ||
|
|
2146
2227
|
RelativePathResolver.resolveInside(tmpDir, path) == null
|
|
2147
2228
|
) {
|
|
2148
2229
|
throw HotUpdaterException.invalidBundle()
|
|
@@ -2150,6 +2231,20 @@ class BundleFileStorageService(
|
|
|
2150
2231
|
}
|
|
2151
2232
|
manifest
|
|
2152
2233
|
}
|
|
2234
|
+
// In v2, the manifest names every file; URLs come once the install
|
|
2235
|
+
// knows which files it downloads.
|
|
2236
|
+
val plannedAssets =
|
|
2237
|
+
if (fileResolver == null) {
|
|
2238
|
+
changedAssets
|
|
2239
|
+
} else {
|
|
2240
|
+
targetManifest.assets.mapValues { (path, asset) ->
|
|
2241
|
+
ChangedAssetDescriptor(
|
|
2242
|
+
fileUrl = null,
|
|
2243
|
+
fileHash = asset.fileHash,
|
|
2244
|
+
patch = changedAssets[path]?.patch,
|
|
2245
|
+
)
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2153
2248
|
updateStep(UpdateFailureStage.INSTALL, resource = null) {
|
|
2154
2249
|
if (!tmpDir.isDirectory && !tmpDir.mkdirs()) throw HotUpdaterException.directoryCreationFailed()
|
|
2155
2250
|
pruneStagingDirectory(tmpDir, targetManifest.assets.keys)
|
|
@@ -2194,7 +2289,12 @@ class BundleFileStorageService(
|
|
|
2194
2289
|
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
2195
2290
|
return@forEachIndexed
|
|
2196
2291
|
}
|
|
2197
|
-
|
|
2292
|
+
val packagedHash = builtInAssetResolver.copyIfSourceMatches(assetPath, expectedAsset.fileHash, targetFile)
|
|
2293
|
+
if (packagedHash != null && isBuiltInEquivalent(assetPath, targetFile, expectedAsset)) {
|
|
2294
|
+
return@forEachIndexed
|
|
2295
|
+
}
|
|
2296
|
+
if (packagedHash != null) targetFile.delete()
|
|
2297
|
+
downloads[assetPath] = plannedAssets.getValue(assetPath)
|
|
2198
2298
|
}
|
|
2199
2299
|
downloads
|
|
2200
2300
|
}
|
|
@@ -2222,11 +2322,16 @@ class BundleFileStorageService(
|
|
|
2222
2322
|
diffFiles = archiveProgress
|
|
2223
2323
|
emptyList()
|
|
2224
2324
|
} else {
|
|
2325
|
+
if (fileResolver != null) {
|
|
2326
|
+
updateStep(UpdateFailureStage.DOWNLOAD, UpdateFailureResource.FILE) {
|
|
2327
|
+
resolveFileUrls(fileResolver, downloads)
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2225
2330
|
diffFiles = createDiffProgressFiles(downloads)
|
|
2226
2331
|
emitDiffProgress(progressCallback, "downloading", diffFiles)
|
|
2227
2332
|
val progressLock = Any()
|
|
2228
2333
|
coroutineScope {
|
|
2229
|
-
val permits = Semaphore(
|
|
2334
|
+
val permits = Semaphore(PARALLEL_FILE_DOWNLOADS)
|
|
2230
2335
|
diffFiles
|
|
2231
2336
|
.toList()
|
|
2232
2337
|
.map { progressFile ->
|
|
@@ -162,65 +162,16 @@ class HotUpdaterImpl {
|
|
|
162
162
|
) ?: DEFAULT_CHANNEL
|
|
163
163
|
|
|
164
164
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
165
|
+
* The bundle ID of the bundle this build ships (MIN_BUNDLE_ID),
|
|
166
|
+
* written by the generateHotUpdaterMinBundleId build task
|
|
167
167
|
*/
|
|
168
168
|
fun getMinBundleId(): String {
|
|
169
169
|
if (BuildConfig.DEBUG) {
|
|
170
170
|
return "00000000-0000-0000-0000-000000000000"
|
|
171
171
|
}
|
|
172
|
-
return
|
|
172
|
+
return HotUpdaterMinBundleId.get()
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
/**
|
|
176
|
-
* Generates a bundle ID based on build timestamp
|
|
177
|
-
* @return The generated minimum bundle ID string
|
|
178
|
-
*/
|
|
179
|
-
private fun generateMinBundleIdFromBuildTimestamp(): String =
|
|
180
|
-
try {
|
|
181
|
-
val buildTimestampMs = BuildConfig.BUILD_TIMESTAMP
|
|
182
|
-
val bytes =
|
|
183
|
-
ByteArray(16).apply {
|
|
184
|
-
this[0] = ((buildTimestampMs shr 40) and 0xFF).toByte()
|
|
185
|
-
this[1] = ((buildTimestampMs shr 32) and 0xFF).toByte()
|
|
186
|
-
this[2] = ((buildTimestampMs shr 24) and 0xFF).toByte()
|
|
187
|
-
this[3] = ((buildTimestampMs shr 16) and 0xFF).toByte()
|
|
188
|
-
this[4] = ((buildTimestampMs shr 8) and 0xFF).toByte()
|
|
189
|
-
this[5] = (buildTimestampMs and 0xFF).toByte()
|
|
190
|
-
this[6] = 0x70.toByte()
|
|
191
|
-
this[7] = 0x00.toByte()
|
|
192
|
-
this[8] = 0x80.toByte()
|
|
193
|
-
this[9] = 0x00.toByte()
|
|
194
|
-
this[10] = 0x00.toByte()
|
|
195
|
-
this[11] = 0x00.toByte()
|
|
196
|
-
this[12] = 0x00.toByte()
|
|
197
|
-
this[13] = 0x00.toByte()
|
|
198
|
-
this[14] = 0x00.toByte()
|
|
199
|
-
this[15] = 0x00.toByte()
|
|
200
|
-
}
|
|
201
|
-
String.format(
|
|
202
|
-
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
|
203
|
-
bytes[0].toInt() and 0xFF,
|
|
204
|
-
bytes[1].toInt() and 0xFF,
|
|
205
|
-
bytes[2].toInt() and 0xFF,
|
|
206
|
-
bytes[3].toInt() and 0xFF,
|
|
207
|
-
bytes[4].toInt() and 0xFF,
|
|
208
|
-
bytes[5].toInt() and 0xFF,
|
|
209
|
-
bytes[6].toInt() and 0xFF,
|
|
210
|
-
bytes[7].toInt() and 0xFF,
|
|
211
|
-
bytes[8].toInt() and 0xFF,
|
|
212
|
-
bytes[9].toInt() and 0xFF,
|
|
213
|
-
bytes[10].toInt() and 0xFF,
|
|
214
|
-
bytes[11].toInt() and 0xFF,
|
|
215
|
-
bytes[12].toInt() and 0xFF,
|
|
216
|
-
bytes[13].toInt() and 0xFF,
|
|
217
|
-
bytes[14].toInt() and 0xFF,
|
|
218
|
-
bytes[15].toInt() and 0xFF,
|
|
219
|
-
)
|
|
220
|
-
} catch (e: Exception) {
|
|
221
|
-
"00000000-0000-0000-0000-000000000000"
|
|
222
|
-
}
|
|
223
|
-
|
|
224
175
|
/**
|
|
225
176
|
* Gets the current fingerprint hash
|
|
226
177
|
* @param context Application context
|
|
@@ -290,6 +241,7 @@ class HotUpdaterImpl {
|
|
|
290
241
|
channel: String?,
|
|
291
242
|
selection: PersistedSelection? = null,
|
|
292
243
|
archiveUrl: String? = null,
|
|
244
|
+
fileResolver: ArtifactFileResolver? = null,
|
|
293
245
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
294
246
|
): UpdateBundleResult {
|
|
295
247
|
if (selection != null && !bundleStorage.stageReleaseSelection(selection)) {
|
|
@@ -302,6 +254,7 @@ class HotUpdaterImpl {
|
|
|
302
254
|
manifestFileHash,
|
|
303
255
|
assets,
|
|
304
256
|
archiveUrl,
|
|
257
|
+
fileResolver,
|
|
305
258
|
progressCallback,
|
|
306
259
|
)
|
|
307
260
|
|
|
@@ -47,7 +47,14 @@ class HotUpdaterModule internal constructor(
|
|
|
47
47
|
*/
|
|
48
48
|
private fun getInstance(): HotUpdaterImpl = HotUpdater.getInstance(mReactApplicationContext)
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Every changed file with its hash and URL or Hermes patch; with
|
|
52
|
+
* [requireFileHash] false, also entries that hold only the Hermes patch.
|
|
53
|
+
*/
|
|
54
|
+
private fun parseAssets(
|
|
55
|
+
params: ReadableMap,
|
|
56
|
+
requireFileHash: Boolean,
|
|
57
|
+
): Map<String, ChangedAssetDescriptor>? {
|
|
51
58
|
if (!params.hasKey("assets") || params.isNull("assets")) {
|
|
52
59
|
return null
|
|
53
60
|
}
|
|
@@ -59,38 +66,10 @@ class HotUpdaterModule internal constructor(
|
|
|
59
66
|
while (iterator.hasNextKey()) {
|
|
60
67
|
val assetPath = iterator.nextKey()
|
|
61
68
|
val assetMap = changedAssetsMap.getMap(assetPath) ?: continue
|
|
62
|
-
val assetHash = assetMap.getString("fileHash")
|
|
69
|
+
val assetHash = if (assetMap.hasKey("fileHash")) assetMap.getString("fileHash") else null
|
|
70
|
+
if (assetHash == null && requireFileHash) continue
|
|
63
71
|
val patchMap = assetMap.getMap("patch")
|
|
64
|
-
val patch =
|
|
65
|
-
if (patchMap != null) {
|
|
66
|
-
val algorithm = patchMap.getString("algorithm")
|
|
67
|
-
val baseBundleId = patchMap.getString("baseBundleId")
|
|
68
|
-
val baseFileHash = patchMap.getString("baseFileHash")
|
|
69
|
-
val patchFileHash = patchMap.getString("patchFileHash")
|
|
70
|
-
val patchUrl = patchMap.getString("patchUrl")
|
|
71
|
-
val byteSize = readSafeInteger(patchMap, "byteSize")
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
algorithm != null &&
|
|
75
|
-
baseBundleId != null &&
|
|
76
|
-
baseFileHash != null &&
|
|
77
|
-
patchFileHash != null &&
|
|
78
|
-
patchUrl != null
|
|
79
|
-
) {
|
|
80
|
-
BsdiffPatchDescriptor(
|
|
81
|
-
algorithm = algorithm,
|
|
82
|
-
baseBundleId = baseBundleId,
|
|
83
|
-
baseFileHash = baseFileHash,
|
|
84
|
-
patchFileHash = patchFileHash,
|
|
85
|
-
patchUrl = patchUrl,
|
|
86
|
-
byteSize = byteSize,
|
|
87
|
-
)
|
|
88
|
-
} else {
|
|
89
|
-
null
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
null
|
|
93
|
-
}
|
|
72
|
+
val patch = parsePatch(patchMap)
|
|
94
73
|
val fileMap =
|
|
95
74
|
if (assetMap.hasKey("file") && !assetMap.isNull("file")) {
|
|
96
75
|
assetMap.getMap("file")
|
|
@@ -107,10 +86,13 @@ class HotUpdaterModule internal constructor(
|
|
|
107
86
|
if (assetUrl == null && patch == null) {
|
|
108
87
|
continue
|
|
109
88
|
}
|
|
89
|
+
if (assetHash == null && assetUrl != null) {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
110
92
|
parsedAssets[assetPath] =
|
|
111
93
|
ChangedAssetDescriptor(
|
|
112
94
|
fileUrl = assetUrl,
|
|
113
|
-
fileHash = assetHash,
|
|
95
|
+
fileHash = assetHash ?: "",
|
|
114
96
|
fileCompression = fileCompression,
|
|
115
97
|
patch = patch,
|
|
116
98
|
)
|
|
@@ -119,6 +101,94 @@ class HotUpdaterModule internal constructor(
|
|
|
119
101
|
return parsedAssets
|
|
120
102
|
}
|
|
121
103
|
|
|
104
|
+
/** The update check's request headers, which the artifact files request repeats. */
|
|
105
|
+
private fun parseRequestHeaders(params: ReadableMap): Map<String, String> {
|
|
106
|
+
if (!params.hasKey("requestHeaders") || params.isNull("requestHeaders")) return emptyMap()
|
|
107
|
+
val headers = params.getMap("requestHeaders") ?: return emptyMap()
|
|
108
|
+
val parsed = linkedMapOf<String, String>()
|
|
109
|
+
val iterator = headers.keySetIterator()
|
|
110
|
+
while (iterator.hasNextKey()) {
|
|
111
|
+
val name = iterator.nextKey()
|
|
112
|
+
if (headers.getType(name) == com.facebook.react.bridge.ReadableType.String) {
|
|
113
|
+
headers.getString(name)?.let { parsed[name] = it }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return parsed
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private fun parsePatch(patchMap: ReadableMap?): BsdiffPatchDescriptor? {
|
|
120
|
+
if (patchMap == null) return null
|
|
121
|
+
val algorithm = patchMap.getString("algorithm")
|
|
122
|
+
val baseBundleId = patchMap.getString("baseBundleId")
|
|
123
|
+
val baseFileHash = patchMap.getString("baseFileHash")
|
|
124
|
+
val patchFileHash = patchMap.getString("patchFileHash")
|
|
125
|
+
val patchUrl = patchMap.getString("patchUrl")
|
|
126
|
+
val byteSize = readSafeInteger(patchMap, "byteSize")
|
|
127
|
+
|
|
128
|
+
return if (
|
|
129
|
+
algorithm != null &&
|
|
130
|
+
baseBundleId != null &&
|
|
131
|
+
baseFileHash != null &&
|
|
132
|
+
patchFileHash != null &&
|
|
133
|
+
patchUrl != null
|
|
134
|
+
) {
|
|
135
|
+
BsdiffPatchDescriptor(
|
|
136
|
+
algorithm = algorithm,
|
|
137
|
+
baseBundleId = baseBundleId,
|
|
138
|
+
baseFileHash = baseFileHash,
|
|
139
|
+
patchFileHash = patchFileHash,
|
|
140
|
+
patchUrl = patchUrl,
|
|
141
|
+
byteSize = byteSize,
|
|
142
|
+
)
|
|
143
|
+
} else {
|
|
144
|
+
null
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The resolver that asks `filesUrl` for the files native downloads one by
|
|
150
|
+
* one; null when `filesUrl` is missing.
|
|
151
|
+
*/
|
|
152
|
+
private fun parseFileResolver(params: ReadableMap): ArtifactFileResolver? {
|
|
153
|
+
val filesUrl =
|
|
154
|
+
if (params.hasKey("filesUrl") && !params.isNull("filesUrl")) {
|
|
155
|
+
params.getString("filesUrl")?.takeIf { it.isNotEmpty() }
|
|
156
|
+
} else {
|
|
157
|
+
null
|
|
158
|
+
} ?: return null
|
|
159
|
+
try {
|
|
160
|
+
java.net.URL(filesUrl)
|
|
161
|
+
} catch (e: java.net.MalformedURLException) {
|
|
162
|
+
throw HotUpdaterException.invalidFileUrl("filesUrl", filesUrl, UpdateFailureResource.FILE)
|
|
163
|
+
}
|
|
164
|
+
val requestTimeoutMs =
|
|
165
|
+
if (params.hasKey("requestTimeout") && !params.isNull("requestTimeout")) {
|
|
166
|
+
params.getDouble("requestTimeout").toLong().takeIf { it > 0 }
|
|
167
|
+
} else {
|
|
168
|
+
null
|
|
169
|
+
} ?: HttpArtifactFileResolver.DEFAULT_TIMEOUT_MS
|
|
170
|
+
return HttpArtifactFileResolver(filesUrl, parseRequestHeaders(params), requestTimeoutMs)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Artifact protocol v2: the Hermes patch as the only planned asset, and
|
|
175
|
+
* the resolver that asks `filesUrl` for the files native downloads one by
|
|
176
|
+
* one; null when `filesUrl` is missing.
|
|
177
|
+
*/
|
|
178
|
+
private fun parseArtifactInstall(params: ReadableMap): Pair<Map<String, ChangedAssetDescriptor>, ArtifactFileResolver?>? {
|
|
179
|
+
val fileResolver = parseFileResolver(params) ?: return null
|
|
180
|
+
val patchMap = if (params.hasKey("patch") && !params.isNull("patch")) params.getMap("patch") else null
|
|
181
|
+
val assetPath = patchMap?.getString("assetPath")
|
|
182
|
+
val patch = parsePatch(patchMap)
|
|
183
|
+
val assets =
|
|
184
|
+
if (assetPath != null && patch != null) {
|
|
185
|
+
mapOf(assetPath to ChangedAssetDescriptor(fileUrl = null, fileHash = "", patch = patch))
|
|
186
|
+
} else {
|
|
187
|
+
emptyMap()
|
|
188
|
+
}
|
|
189
|
+
return assets to fileResolver
|
|
190
|
+
}
|
|
191
|
+
|
|
122
192
|
private fun parseSelection(params: ReadableMap): PersistedSelection? {
|
|
123
193
|
if (!params.hasKey("selection") || params.isNull("selection")) return null
|
|
124
194
|
val selection = params.getMap("selection") ?: return null
|
|
@@ -169,6 +239,28 @@ class HotUpdaterModule internal constructor(
|
|
|
169
239
|
override fun updateBundle(
|
|
170
240
|
params: ReadableMap,
|
|
171
241
|
promise: Promise,
|
|
242
|
+
) {
|
|
243
|
+
// Legacy: artifact protocol v1, where every file comes with its URL.
|
|
244
|
+
// JavaScript from the first protocol v2 SDK also sends `filesUrl`
|
|
245
|
+
// here, with only the Hermes patch in `assets`; its OTA Bundles must
|
|
246
|
+
// still install.
|
|
247
|
+
stage(params, promise) {
|
|
248
|
+
val fileResolver = parseFileResolver(params)
|
|
249
|
+
parseAssets(params, requireFileHash = fileResolver == null)?.let { it to fileResolver }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
override fun installArtifact(
|
|
254
|
+
params: ReadableMap,
|
|
255
|
+
promise: Promise,
|
|
256
|
+
) {
|
|
257
|
+
stage(params, promise) { parseArtifactInstall(params) }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private fun stage(
|
|
261
|
+
params: ReadableMap,
|
|
262
|
+
promise: Promise,
|
|
263
|
+
plan: () -> Pair<Map<String, ChangedAssetDescriptor>, ArtifactFileResolver?>?,
|
|
172
264
|
) {
|
|
173
265
|
moduleScope.launch {
|
|
174
266
|
try {
|
|
@@ -180,7 +272,8 @@ class HotUpdaterModule internal constructor(
|
|
|
180
272
|
|
|
181
273
|
val manifestUrl = params.getString("manifestUrl")
|
|
182
274
|
val manifestFileHash = params.getString("manifestFileHash")
|
|
183
|
-
val
|
|
275
|
+
val planned = plan()
|
|
276
|
+
val assets = planned?.first
|
|
184
277
|
if (manifestUrl.isNullOrEmpty() || manifestFileHash.isNullOrEmpty() || assets == null) {
|
|
185
278
|
promise.rejectUpdateBundle(HotUpdaterException.invalidManifestParams())
|
|
186
279
|
return@launch
|
|
@@ -211,6 +304,7 @@ class HotUpdaterModule internal constructor(
|
|
|
211
304
|
}
|
|
212
305
|
}
|
|
213
306
|
val selection = parseSelection(params)
|
|
307
|
+
val fileResolver = planned.second
|
|
214
308
|
|
|
215
309
|
val impl = getInstance()
|
|
216
310
|
|
|
@@ -223,6 +317,7 @@ class HotUpdaterModule internal constructor(
|
|
|
223
317
|
channel,
|
|
224
318
|
selection,
|
|
225
319
|
archiveUrl,
|
|
320
|
+
fileResolver,
|
|
226
321
|
) { progress ->
|
|
227
322
|
// Post to Main thread for React Native event emission
|
|
228
323
|
Handler(Looper.getMainLooper()).post {
|