react-native-bspatch 1.0.0
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/LICENSE +21 -0
- package/README.md +471 -0
- package/android/build.gradle +100 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +139 -0
- package/android/src/main/java/vn/reactnativebspatch/BsPatchPackage.kt +16 -0
- package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +227 -0
- package/cpp/CMakeLists.txt +43 -0
- package/cpp/bsdiff/bspatch.c +194 -0
- package/cpp/bsdiff/bspatch.h +41 -0
- package/cpp/bsdiff/bspatch_wrapper.c +150 -0
- package/cpp/bsdiff/bspatch_wrapper.h +6 -0
- package/cpp/bspatch_jni.cpp +41 -0
- package/cpp/bzip2/LICENSE +42 -0
- package/cpp/bzip2/blocksort.c +1094 -0
- package/cpp/bzip2/bzlib.c +1572 -0
- package/cpp/bzip2/bzlib.h +282 -0
- package/cpp/bzip2/bzlib_private.h +509 -0
- package/cpp/bzip2/compress.c +672 -0
- package/cpp/bzip2/crctable.c +104 -0
- package/cpp/bzip2/decompress.c +652 -0
- package/cpp/bzip2/huffman.c +205 -0
- package/cpp/bzip2/randtable.c +84 -0
- package/ios/OtaBundleResolver.swift +241 -0
- package/ios/RNBsPatch.h +8 -0
- package/ios/RNBsPatch.mm +198 -0
- package/lib/commonjs/NativeBsPatch.js +9 -0
- package/lib/commonjs/NativeBsPatch.js.map +1 -0
- package/lib/commonjs/crypto-js.d.js +2 -0
- package/lib/commonjs/crypto-js.d.js.map +1 -0
- package/lib/commonjs/index.js +90 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/ota/OtaProvider.js +306 -0
- package/lib/commonjs/ota/OtaProvider.js.map +1 -0
- package/lib/commonjs/ota/otaAssetResolver.js +145 -0
- package/lib/commonjs/ota/otaAssetResolver.js.map +1 -0
- package/lib/commonjs/ota/otaCore.js +736 -0
- package/lib/commonjs/ota/otaCore.js.map +1 -0
- package/lib/commonjs/ota/otaLogger.js +64 -0
- package/lib/commonjs/ota/otaLogger.js.map +1 -0
- package/lib/commonjs/ota/otaMetadata.js +138 -0
- package/lib/commonjs/ota/otaMetadata.js.map +1 -0
- package/lib/commonjs/ota/storage/CustomStorage.js +140 -0
- package/lib/commonjs/ota/storage/CustomStorage.js.map +1 -0
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +96 -0
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +1 -0
- package/lib/commonjs/ota/types.js +39 -0
- package/lib/commonjs/ota/types.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/module/NativeBsPatch.js +5 -0
- package/lib/module/NativeBsPatch.js.map +1 -0
- package/lib/module/crypto-js.d.js +2 -0
- package/lib/module/crypto-js.d.js.map +1 -0
- package/lib/module/index.js +23 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/ota/OtaProvider.js +299 -0
- package/lib/module/ota/OtaProvider.js.map +1 -0
- package/lib/module/ota/otaAssetResolver.js +138 -0
- package/lib/module/ota/otaAssetResolver.js.map +1 -0
- package/lib/module/ota/otaCore.js +725 -0
- package/lib/module/ota/otaCore.js.map +1 -0
- package/lib/module/ota/otaLogger.js +59 -0
- package/lib/module/ota/otaLogger.js.map +1 -0
- package/lib/module/ota/otaMetadata.js +127 -0
- package/lib/module/ota/otaMetadata.js.map +1 -0
- package/lib/module/ota/storage/CustomStorage.js +134 -0
- package/lib/module/ota/storage/CustomStorage.js.map +1 -0
- package/lib/module/ota/storage/GitHubReleaseStorage.js +90 -0
- package/lib/module/ota/storage/GitHubReleaseStorage.js.map +1 -0
- package/lib/module/ota/types.js +38 -0
- package/lib/module/ota/types.js.map +1 -0
- package/lib/typescript/NativeBsPatch.d.ts +39 -0
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +15 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/ota/OtaProvider.d.ts +10 -0
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -0
- package/lib/typescript/ota/otaAssetResolver.d.ts +15 -0
- package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -0
- package/lib/typescript/ota/otaCore.d.ts +109 -0
- package/lib/typescript/ota/otaCore.d.ts.map +1 -0
- package/lib/typescript/ota/otaLogger.d.ts +44 -0
- package/lib/typescript/ota/otaLogger.d.ts.map +1 -0
- package/lib/typescript/ota/otaMetadata.d.ts +46 -0
- package/lib/typescript/ota/otaMetadata.d.ts.map +1 -0
- package/lib/typescript/ota/storage/CustomStorage.d.ts +12 -0
- package/lib/typescript/ota/storage/CustomStorage.d.ts.map +1 -0
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts +20 -0
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +1 -0
- package/lib/typescript/ota/types.d.ts +191 -0
- package/lib/typescript/ota/types.d.ts.map +1 -0
- package/package.json +89 -0
- package/react-native-bspatch.podspec +46 -0
- package/src/NativeBsPatch.ts +52 -0
- package/src/crypto-js.d.ts +1 -0
- package/src/index.ts +29 -0
- package/src/ota/OtaProvider.tsx +399 -0
- package/src/ota/otaAssetResolver.ts +182 -0
- package/src/ota/otaCore.ts +963 -0
- package/src/ota/otaLogger.ts +72 -0
- package/src/ota/otaMetadata.ts +149 -0
- package/src/ota/storage/CustomStorage.ts +143 -0
- package/src/ota/storage/GitHubReleaseStorage.ts +98 -0
- package/src/ota/types.ts +211 -0
- package/tools/bsdiff +0 -0
- package/tools/bsdiff.c +445 -0
- package/tools/bsdiff.h +45 -0
- package/tools/build_ota.sh +457 -0
- package/tools/setup_project.js +273 -0
- package/tools/update_registry.js +145 -0
- package/tools/upload_ota_custom.sh +138 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
package vn.reactnativebspatch
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReactMethod
|
|
7
|
+
import com.facebook.react.bridge.Callback
|
|
8
|
+
import com.facebook.react.bridge.Promise
|
|
9
|
+
import java.io.File
|
|
10
|
+
import java.io.FileInputStream
|
|
11
|
+
import java.io.FileOutputStream
|
|
12
|
+
import java.io.BufferedInputStream
|
|
13
|
+
import java.io.BufferedOutputStream
|
|
14
|
+
import java.util.zip.ZipInputStream
|
|
15
|
+
import java.util.zip.GZIPInputStream
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BsPatchModule(private val reactContext: ReactApplicationContext) : NativeBsPatchSpec(reactContext) {
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
const val NAME = "BsPatch"
|
|
22
|
+
|
|
23
|
+
init {
|
|
24
|
+
System.loadLibrary("bspatch")
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun getName(): String {
|
|
29
|
+
return NAME
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private external fun nativeApplyPatch(oldPath: String, newPath: String, patchPath: String): Boolean
|
|
33
|
+
|
|
34
|
+
override fun applyPatch(oldPath: String, newPath: String, patchPath: String, promise: com.facebook.react.bridge.Promise) {
|
|
35
|
+
Thread {
|
|
36
|
+
try {
|
|
37
|
+
val result = nativeApplyPatch(oldPath, newPath, patchPath)
|
|
38
|
+
promise.resolve(result)
|
|
39
|
+
} catch (e: Exception) {
|
|
40
|
+
promise.reject("ERR_BSPATCH", e.message, e)
|
|
41
|
+
}
|
|
42
|
+
}.start()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun unzip(source: String, targetDir: String, promise: Promise) {
|
|
46
|
+
Thread {
|
|
47
|
+
try {
|
|
48
|
+
val sourceFile = File(source)
|
|
49
|
+
val destDir = File(targetDir)
|
|
50
|
+
if (!destDir.exists()) destDir.mkdirs()
|
|
51
|
+
|
|
52
|
+
ZipInputStream(BufferedInputStream(FileInputStream(sourceFile))).use { zis ->
|
|
53
|
+
var entry = zis.nextEntry
|
|
54
|
+
while (entry != null) {
|
|
55
|
+
val file = File(destDir, entry.name)
|
|
56
|
+
if (entry.isDirectory) {
|
|
57
|
+
file.mkdirs()
|
|
58
|
+
} else {
|
|
59
|
+
file.parentFile?.mkdirs()
|
|
60
|
+
BufferedOutputStream(FileOutputStream(file)).use { bos ->
|
|
61
|
+
zis.copyTo(bos)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
zis.closeEntry()
|
|
65
|
+
entry = zis.nextEntry
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
promise.resolve(true)
|
|
69
|
+
} catch (e: Exception) {
|
|
70
|
+
promise.reject("ERR_UNZIP", e.message, e)
|
|
71
|
+
}
|
|
72
|
+
}.start()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extracts the JS bundle from APK assets using Android AssetManager.
|
|
77
|
+
* Guarantees byte-exact copy — RNFS.copyFileAssets can produce different
|
|
78
|
+
* bytes in certain APK configurations.
|
|
79
|
+
*/
|
|
80
|
+
override fun extractBundleFromAssets(assetName: String, destPath: String, promise: Promise) {
|
|
81
|
+
Thread {
|
|
82
|
+
try {
|
|
83
|
+
val assetManager = reactContext.assets
|
|
84
|
+
assetManager.open(assetName).use { input ->
|
|
85
|
+
val destFile = File(destPath)
|
|
86
|
+
destFile.parentFile?.mkdirs()
|
|
87
|
+
BufferedOutputStream(FileOutputStream(destFile)).use { output ->
|
|
88
|
+
input.copyTo(output, bufferSize = 8192)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
promise.resolve(true)
|
|
92
|
+
} catch (e: Exception) {
|
|
93
|
+
promise.reject("ERR_EXTRACT_ASSET", e.message, e)
|
|
94
|
+
}
|
|
95
|
+
}.start()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun markAsHealthy() {
|
|
99
|
+
OtaBundleResolver.markAsHealthy(reactContext)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
override fun reloadBundle() {
|
|
103
|
+
val activity = reactContext.currentActivity
|
|
104
|
+
if (activity != null) {
|
|
105
|
+
Handler(Looper.getMainLooper()).post {
|
|
106
|
+
val intent = activity.packageManager.getLaunchIntentForPackage(activity.packageName)
|
|
107
|
+
if (intent != null) {
|
|
108
|
+
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
109
|
+
activity.startActivity(intent)
|
|
110
|
+
Runtime.getRuntime().exit(0)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
override fun logNative(message: String?) {
|
|
117
|
+
android.util.Log.d("JS-OTA", message ?: "")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
override fun decompressGzip(sourcePath: String, destPath: String, promise: Promise) {
|
|
121
|
+
Thread {
|
|
122
|
+
try {
|
|
123
|
+
val source = File(sourcePath)
|
|
124
|
+
val dest = File(destPath)
|
|
125
|
+
dest.parentFile?.mkdirs()
|
|
126
|
+
|
|
127
|
+
GZIPInputStream(BufferedInputStream(FileInputStream(source))).use { gis ->
|
|
128
|
+
BufferedOutputStream(FileOutputStream(dest)).use { bos ->
|
|
129
|
+
gis.copyTo(bos)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
promise.resolve(true)
|
|
133
|
+
} catch (e: Exception) {
|
|
134
|
+
promise.reject("ERR_GZIP_DECOMPRESS", e.message, e)
|
|
135
|
+
}
|
|
136
|
+
}.start()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package vn.reactnativebspatch
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class BsPatchPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return listOf<NativeModule>(BsPatchModule(reactContext) as NativeModule)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return emptyList()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
package vn.reactnativebspatch
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import org.json.JSONArray
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
import java.io.File
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* OtaBundleResolver — encapsulates OTA bundle path resolution with
|
|
11
|
+
* crash detection and automatic rollback.
|
|
12
|
+
*
|
|
13
|
+
* IMPORTANT: This runs BEFORE any JS code. If the OTA bundle is corrupted,
|
|
14
|
+
* Hermes crashes before React Native boots. This native resolver handles
|
|
15
|
+
* crash detection so the app can recover without JS ever running.
|
|
16
|
+
*
|
|
17
|
+
* Lifecycle:
|
|
18
|
+
* 1. Read metadata.json
|
|
19
|
+
* 2. If lastLaunchSuccessful == false → rollback to previous version
|
|
20
|
+
* 3. Mark current launch as pending (lastLaunchSuccessful = false)
|
|
21
|
+
* 4. Return the bundle path (or null for APK default)
|
|
22
|
+
* 5. JS calls markAsHealthy() → sets lastLaunchSuccessful = true
|
|
23
|
+
* 6. If crash before markAsHealthy → next boot sees false → rollback
|
|
24
|
+
*
|
|
25
|
+
* Usage in MainApplication.kt:
|
|
26
|
+
* ```
|
|
27
|
+
* jsBundleFilePath = OtaBundleResolver.resolve(applicationContext)
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
object OtaBundleResolver {
|
|
31
|
+
|
|
32
|
+
private const val TAG = "OtaBundleResolver"
|
|
33
|
+
private const val OTA_DIR_NAME = "ota"
|
|
34
|
+
private const val METADATA_FILE = "metadata.json"
|
|
35
|
+
private const val DEFAULT_BASE_VERSION = 1
|
|
36
|
+
private const val MAX_ROLLBACK = 3
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the JS bundle path from OTA metadata.
|
|
40
|
+
* Handles crash detection and rollback entirely on the native side.
|
|
41
|
+
*
|
|
42
|
+
* @param context Android Application context
|
|
43
|
+
* @param baseVersion The OTA version baked into the APK (default: 1)
|
|
44
|
+
* @return Absolute path to the OTA bundle, or null to use the APK default.
|
|
45
|
+
*/
|
|
46
|
+
@JvmStatic
|
|
47
|
+
fun resolve(context: Context, baseVersion: Int = DEFAULT_BASE_VERSION): String? {
|
|
48
|
+
return try {
|
|
49
|
+
val otaDir = File(context.filesDir, OTA_DIR_NAME)
|
|
50
|
+
val metadataFile = File(otaDir, METADATA_FILE)
|
|
51
|
+
|
|
52
|
+
if (!metadataFile.exists()) {
|
|
53
|
+
Log.d(TAG, "No OTA metadata → APK bundle")
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
var metadata = JSONObject(metadataFile.readText())
|
|
58
|
+
val currentVersion = metadata.optInt("currentVersion", baseVersion)
|
|
59
|
+
val lastLaunchOk = metadata.optBoolean("lastLaunchSuccessful", true)
|
|
60
|
+
val rollbackCount = metadata.optInt("rollbackCount", 0)
|
|
61
|
+
|
|
62
|
+
// ── Step 1: Crash detection & rollback ──────────────────────────
|
|
63
|
+
if (!lastLaunchOk && currentVersion > baseVersion) {
|
|
64
|
+
Log.w(TAG, "Previous launch failed! Rolling back from V$currentVersion...")
|
|
65
|
+
metadata = performRollback(metadata, baseVersion, rollbackCount, otaDir)
|
|
66
|
+
writeMetadata(metadataFile, metadata)
|
|
67
|
+
|
|
68
|
+
val newVersion = metadata.optInt("currentVersion", baseVersion)
|
|
69
|
+
if (newVersion <= baseVersion) {
|
|
70
|
+
Log.i(TAG, "Rolled back to factory APK bundle")
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if the rollback target bundle exists
|
|
75
|
+
val rollbackVDir = File(otaDir, "v${newVersion}")
|
|
76
|
+
val rollbackBundle = File(rollbackVDir, "index.android.bundle")
|
|
77
|
+
if (!rollbackBundle.exists()) {
|
|
78
|
+
Log.w(TAG, "Rollback target index.android.bundle missing in V$newVersion → factory")
|
|
79
|
+
resetToFactory(metadataFile, baseVersion)
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Mark this rollback attempt as pending
|
|
84
|
+
metadata.put("lastLaunchSuccessful", false)
|
|
85
|
+
writeMetadata(metadataFile, metadata)
|
|
86
|
+
|
|
87
|
+
Log.i(TAG, "Trying rollback bundle: ${rollbackBundle.absolutePath}")
|
|
88
|
+
return rollbackBundle.absolutePath
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Step 2: Normal boot — no OTA bundle needed ──────────────────
|
|
92
|
+
if (currentVersion <= baseVersion) {
|
|
93
|
+
Log.d(TAG, "OTA v$currentVersion ≤ base v$baseVersion → APK bundle")
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Step 3: OTA bundle — mark as pending verification ───────────
|
|
98
|
+
val vDir = File(otaDir, "v${currentVersion}")
|
|
99
|
+
val bundleFile = File(vDir, "index.android.bundle")
|
|
100
|
+
if (!bundleFile.exists()) {
|
|
101
|
+
Log.w(TAG, "index.android.bundle missing in V$currentVersion → APK bundle")
|
|
102
|
+
resetToFactory(metadataFile, baseVersion)
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Mark launch as pending — if Hermes crashes loading this bundle,
|
|
107
|
+
// the next boot will see false and trigger rollback
|
|
108
|
+
metadata.put("lastLaunchSuccessful", false)
|
|
109
|
+
writeMetadata(metadataFile, metadata)
|
|
110
|
+
|
|
111
|
+
Log.i(TAG, "Loading OTA bundle: ${bundleFile.absolutePath}")
|
|
112
|
+
bundleFile.absolutePath
|
|
113
|
+
} catch (e: Exception) {
|
|
114
|
+
Log.e(TAG, "Error resolving OTA bundle: ${e.message}")
|
|
115
|
+
null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Performs rollback by popping from previousVersions.
|
|
121
|
+
* If maxRollback reached, resets to factory.
|
|
122
|
+
*/
|
|
123
|
+
private fun performRollback(
|
|
124
|
+
metadata: JSONObject,
|
|
125
|
+
baseVersion: Int,
|
|
126
|
+
currentRollbackCount: Int,
|
|
127
|
+
otaDir: File
|
|
128
|
+
): JSONObject {
|
|
129
|
+
val newCount = currentRollbackCount + 1
|
|
130
|
+
|
|
131
|
+
// Max rollback reached → factory reset
|
|
132
|
+
if (newCount >= MAX_ROLLBACK) {
|
|
133
|
+
Log.w(TAG, "Max rollback ($MAX_ROLLBACK) reached → factory reset")
|
|
134
|
+
return JSONObject().apply {
|
|
135
|
+
put("currentVersion", baseVersion)
|
|
136
|
+
put("previousVersions", JSONArray())
|
|
137
|
+
put("rollbackCount", 0)
|
|
138
|
+
put("lastLaunchSuccessful", true)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
val prevVersions = metadata.optJSONArray("previousVersions") ?: JSONArray()
|
|
143
|
+
|
|
144
|
+
if (prevVersions.length() == 0) {
|
|
145
|
+
Log.w(TAG, "No previous versions to rollback to → factory reset")
|
|
146
|
+
return JSONObject().apply {
|
|
147
|
+
put("currentVersion", baseVersion)
|
|
148
|
+
put("previousVersions", JSONArray())
|
|
149
|
+
put("rollbackCount", newCount)
|
|
150
|
+
put("lastLaunchSuccessful", true)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Pop the first previous version
|
|
155
|
+
val rollbackTo = prevVersions.optInt(0, baseVersion)
|
|
156
|
+
val remaining = JSONArray()
|
|
157
|
+
for (i in 1 until prevVersions.length()) {
|
|
158
|
+
remaining.put(prevVersions.optInt(i))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
Log.i(TAG, "Rolling back: V${metadata.optInt("currentVersion")} → V$rollbackTo (attempt $newCount)")
|
|
162
|
+
|
|
163
|
+
// Check if this rollback target bundle exists
|
|
164
|
+
val vDir = File(otaDir, "v${rollbackTo}")
|
|
165
|
+
val targetBundle = File(vDir, "index.android.bundle")
|
|
166
|
+
if (!targetBundle.exists() && rollbackTo > baseVersion) {
|
|
167
|
+
Log.w(TAG, "index.android.bundle not found in V$rollbackTo, trying deeper rollback...")
|
|
168
|
+
val intermediate = JSONObject().apply {
|
|
169
|
+
put("currentVersion", rollbackTo)
|
|
170
|
+
put("previousVersions", remaining)
|
|
171
|
+
put("rollbackCount", newCount)
|
|
172
|
+
put("lastLaunchSuccessful", false)
|
|
173
|
+
}
|
|
174
|
+
return performRollback(intermediate, baseVersion, newCount, otaDir)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return JSONObject().apply {
|
|
178
|
+
put("currentVersion", rollbackTo)
|
|
179
|
+
put("previousVersions", remaining)
|
|
180
|
+
put("rollbackCount", newCount)
|
|
181
|
+
put("lastLaunchSuccessful", true) // Will be set to false again in resolve()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resets metadata to factory defaults.
|
|
187
|
+
*/
|
|
188
|
+
private fun resetToFactory(metadataFile: File, baseVersion: Int) {
|
|
189
|
+
val factory = JSONObject().apply {
|
|
190
|
+
put("currentVersion", baseVersion)
|
|
191
|
+
put("previousVersions", JSONArray())
|
|
192
|
+
put("rollbackCount", 0)
|
|
193
|
+
put("lastLaunchSuccessful", true)
|
|
194
|
+
}
|
|
195
|
+
writeMetadata(metadataFile, factory)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@JvmStatic
|
|
199
|
+
fun markAsHealthy(context: Context) {
|
|
200
|
+
try {
|
|
201
|
+
val otaDir = File(context.filesDir, OTA_DIR_NAME)
|
|
202
|
+
val metadataFile = File(otaDir, METADATA_FILE)
|
|
203
|
+
if (!metadataFile.exists()) return
|
|
204
|
+
|
|
205
|
+
val metadata = JSONObject(metadataFile.readText())
|
|
206
|
+
metadata.put("lastLaunchSuccessful", true)
|
|
207
|
+
metadata.put("rollbackCount", 0)
|
|
208
|
+
|
|
209
|
+
writeMetadata(metadataFile, metadata)
|
|
210
|
+
Log.i(TAG, "App marked as healthy")
|
|
211
|
+
} catch (e: Exception) {
|
|
212
|
+
Log.e(TAG, "Failed to mark as healthy: ${e.message}")
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Writes metadata JSON to disk.
|
|
218
|
+
*/
|
|
219
|
+
private fun writeMetadata(file: File, metadata: JSONObject) {
|
|
220
|
+
try {
|
|
221
|
+
file.parentFile?.mkdirs()
|
|
222
|
+
file.writeText(metadata.toString(2))
|
|
223
|
+
} catch (e: Exception) {
|
|
224
|
+
Log.e(TAG, "Failed to write metadata: ${e.message}")
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.18.1)
|
|
2
|
+
|
|
3
|
+
project("bspatch")
|
|
4
|
+
|
|
5
|
+
# Define bzip2 sources explicitly to avoid including executables
|
|
6
|
+
set(BZIP2_SOURCES
|
|
7
|
+
"bzip2/blocksort.c"
|
|
8
|
+
"bzip2/huffman.c"
|
|
9
|
+
"bzip2/crctable.c"
|
|
10
|
+
"bzip2/randtable.c"
|
|
11
|
+
"bzip2/compress.c"
|
|
12
|
+
"bzip2/decompress.c"
|
|
13
|
+
"bzip2/bzlib.c"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# React Native includes
|
|
17
|
+
set(REACT_NATIVE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../node_modules/react-native)
|
|
18
|
+
|
|
19
|
+
# Add our custom library
|
|
20
|
+
add_library(
|
|
21
|
+
bspatch
|
|
22
|
+
SHARED
|
|
23
|
+
|
|
24
|
+
bspatch_jni.cpp
|
|
25
|
+
bsdiff/bspatch_wrapper.c
|
|
26
|
+
bsdiff/bspatch.c
|
|
27
|
+
${BZIP2_SOURCES}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Include directories
|
|
31
|
+
target_include_directories(bspatch PRIVATE bzip2 bsdiff)
|
|
32
|
+
|
|
33
|
+
# Link against Android standard libraries
|
|
34
|
+
target_link_libraries(
|
|
35
|
+
bspatch
|
|
36
|
+
log
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Set 16KB (2^14) page alignment for Android compatibility
|
|
40
|
+
# Required for arm64-v8a and x86_64 architectures
|
|
41
|
+
if(ANDROID)
|
|
42
|
+
target_link_options(bspatch PRIVATE "LINKER:-z,max-page-size=16384")
|
|
43
|
+
endif()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/*-
|
|
2
|
+
* Copyright 2003-2005 Colin Percival
|
|
3
|
+
* Copyright 2012 Matthew Endsley
|
|
4
|
+
* All rights reserved
|
|
5
|
+
*
|
|
6
|
+
* Redistribution and use in source and binary forms, with or without
|
|
7
|
+
* modification, are permitted providing that the following conditions
|
|
8
|
+
* are met:
|
|
9
|
+
* 1. Redistributions of source code must retain the above copyright
|
|
10
|
+
* notice, this list of conditions and the following disclaimer.
|
|
11
|
+
* 2. Redistributions in binary form must reproduce the above copyright
|
|
12
|
+
* notice, this list of conditions and the following disclaimer in the
|
|
13
|
+
* documentation and/or other materials provided with the distribution.
|
|
14
|
+
*
|
|
15
|
+
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
16
|
+
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
17
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
18
|
+
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
19
|
+
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
21
|
+
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
22
|
+
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
23
|
+
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
|
24
|
+
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
25
|
+
* POSSIBILITY OF SUCH DAMAGE.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
#include <limits.h>
|
|
29
|
+
#include "bspatch.h"
|
|
30
|
+
|
|
31
|
+
static int64_t offtin(uint8_t *buf)
|
|
32
|
+
{
|
|
33
|
+
int64_t y;
|
|
34
|
+
|
|
35
|
+
y=buf[7]&0x7F;
|
|
36
|
+
y=y*256;y+=buf[6];
|
|
37
|
+
y=y*256;y+=buf[5];
|
|
38
|
+
y=y*256;y+=buf[4];
|
|
39
|
+
y=y*256;y+=buf[3];
|
|
40
|
+
y=y*256;y+=buf[2];
|
|
41
|
+
y=y*256;y+=buf[1];
|
|
42
|
+
y=y*256;y+=buf[0];
|
|
43
|
+
|
|
44
|
+
if(buf[7]&0x80) y=-y;
|
|
45
|
+
|
|
46
|
+
return y;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
int bspatch(const uint8_t* old, int64_t oldsize, uint8_t* new, int64_t newsize, struct bspatch_stream* stream)
|
|
50
|
+
{
|
|
51
|
+
uint8_t buf[8];
|
|
52
|
+
int64_t oldpos,newpos;
|
|
53
|
+
int64_t ctrl[3];
|
|
54
|
+
int64_t i;
|
|
55
|
+
|
|
56
|
+
oldpos=0;newpos=0;
|
|
57
|
+
while(newpos<newsize) {
|
|
58
|
+
/* Read control data */
|
|
59
|
+
for(i=0;i<=2;i++) {
|
|
60
|
+
if (stream->read(stream, buf, 8))
|
|
61
|
+
return -1;
|
|
62
|
+
ctrl[i]=offtin(buf);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/* Sanity-check */
|
|
66
|
+
if (ctrl[0]<0 || ctrl[0]>INT_MAX ||
|
|
67
|
+
ctrl[1]<0 || ctrl[1]>INT_MAX ||
|
|
68
|
+
newpos+ctrl[0]>newsize)
|
|
69
|
+
return -1;
|
|
70
|
+
|
|
71
|
+
/* Read diff string */
|
|
72
|
+
if (stream->read(stream, new + newpos, ctrl[0]))
|
|
73
|
+
return -1;
|
|
74
|
+
|
|
75
|
+
/* Add old data to diff string */
|
|
76
|
+
for(i=0;i<ctrl[0];i++)
|
|
77
|
+
if((oldpos+i>=0) && (oldpos+i<oldsize))
|
|
78
|
+
new[newpos+i]+=old[oldpos+i];
|
|
79
|
+
|
|
80
|
+
/* Adjust pointers */
|
|
81
|
+
newpos+=ctrl[0];
|
|
82
|
+
oldpos+=ctrl[0];
|
|
83
|
+
|
|
84
|
+
/* Sanity-check */
|
|
85
|
+
if(newpos+ctrl[1]>newsize)
|
|
86
|
+
return -1;
|
|
87
|
+
|
|
88
|
+
/* Read extra string */
|
|
89
|
+
if (stream->read(stream, new + newpos, ctrl[1]))
|
|
90
|
+
return -1;
|
|
91
|
+
|
|
92
|
+
/* Adjust pointers */
|
|
93
|
+
newpos+=ctrl[1];
|
|
94
|
+
oldpos+=ctrl[2];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#if defined(BSPATCH_EXECUTABLE)
|
|
101
|
+
|
|
102
|
+
#include <bzlib.h>
|
|
103
|
+
#include <stdlib.h>
|
|
104
|
+
#include <stdint.h>
|
|
105
|
+
#include <stdio.h>
|
|
106
|
+
#include <string.h>
|
|
107
|
+
#include <err.h>
|
|
108
|
+
#include <sys/types.h>
|
|
109
|
+
#include <sys/stat.h>
|
|
110
|
+
#include <unistd.h>
|
|
111
|
+
#include <fcntl.h>
|
|
112
|
+
|
|
113
|
+
static int bz2_read(const struct bspatch_stream* stream, void* buffer, int length)
|
|
114
|
+
{
|
|
115
|
+
int n;
|
|
116
|
+
int bz2err;
|
|
117
|
+
BZFILE* bz2;
|
|
118
|
+
|
|
119
|
+
bz2 = (BZFILE*)stream->opaque;
|
|
120
|
+
n = BZ2_bzRead(&bz2err, bz2, buffer, length);
|
|
121
|
+
if (n != length)
|
|
122
|
+
return -1;
|
|
123
|
+
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
int main(int argc,char * argv[])
|
|
128
|
+
{
|
|
129
|
+
FILE * f;
|
|
130
|
+
int fd;
|
|
131
|
+
int bz2err;
|
|
132
|
+
uint8_t header[24];
|
|
133
|
+
uint8_t *old, *new;
|
|
134
|
+
int64_t oldsize, newsize;
|
|
135
|
+
BZFILE* bz2;
|
|
136
|
+
struct bspatch_stream stream;
|
|
137
|
+
struct stat sb;
|
|
138
|
+
|
|
139
|
+
if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);
|
|
140
|
+
|
|
141
|
+
/* Open patch file */
|
|
142
|
+
if ((f = fopen(argv[3], "r")) == NULL)
|
|
143
|
+
err(1, "fopen(%s)", argv[3]);
|
|
144
|
+
|
|
145
|
+
/* Read header */
|
|
146
|
+
if (fread(header, 1, 24, f) != 24) {
|
|
147
|
+
if (feof(f))
|
|
148
|
+
errx(1, "Corrupt patch\n");
|
|
149
|
+
err(1, "fread(%s)", argv[3]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Check for appropriate magic */
|
|
153
|
+
if (memcmp(header, "ENDSLEY/BSDIFF43", 16) != 0)
|
|
154
|
+
errx(1, "Corrupt patch\n");
|
|
155
|
+
|
|
156
|
+
/* Read lengths from header */
|
|
157
|
+
newsize=offtin(header+16);
|
|
158
|
+
if(newsize<0)
|
|
159
|
+
errx(1,"Corrupt patch\n");
|
|
160
|
+
|
|
161
|
+
/* Close patch file and re-open it via libbzip2 at the right places */
|
|
162
|
+
if(((fd=open(argv[1],O_RDONLY,0))<0) ||
|
|
163
|
+
((oldsize=lseek(fd,0,SEEK_END))==-1) ||
|
|
164
|
+
((old=malloc(oldsize+1))==NULL) ||
|
|
165
|
+
(lseek(fd,0,SEEK_SET)!=0) ||
|
|
166
|
+
(read(fd,old,oldsize)!=oldsize) ||
|
|
167
|
+
(fstat(fd, &sb)) ||
|
|
168
|
+
(close(fd)==-1)) err(1,"%s",argv[1]);
|
|
169
|
+
if((new=malloc(newsize+1))==NULL) err(1,NULL);
|
|
170
|
+
|
|
171
|
+
if (NULL == (bz2 = BZ2_bzReadOpen(&bz2err, f, 0, 0, NULL, 0)))
|
|
172
|
+
errx(1, "BZ2_bzReadOpen, bz2err=%d", bz2err);
|
|
173
|
+
|
|
174
|
+
stream.read = bz2_read;
|
|
175
|
+
stream.opaque = bz2;
|
|
176
|
+
if (bspatch(old, oldsize, new, newsize, &stream))
|
|
177
|
+
errx(1, "bspatch");
|
|
178
|
+
|
|
179
|
+
/* Clean up the bzip2 reads */
|
|
180
|
+
BZ2_bzReadClose(&bz2err, bz2);
|
|
181
|
+
fclose(f);
|
|
182
|
+
|
|
183
|
+
/* Write the new file */
|
|
184
|
+
if(((fd=open(argv[2],O_CREAT|O_TRUNC|O_WRONLY,sb.st_mode))<0) ||
|
|
185
|
+
(write(fd,new,newsize)!=newsize) || (close(fd)==-1))
|
|
186
|
+
err(1,"%s",argv[2]);
|
|
187
|
+
|
|
188
|
+
free(new);
|
|
189
|
+
free(old);
|
|
190
|
+
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#endif
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*-
|
|
2
|
+
* Copyright 2003-2005 Colin Percival
|
|
3
|
+
* Copyright 2012 Matthew Endsley
|
|
4
|
+
* All rights reserved
|
|
5
|
+
*
|
|
6
|
+
* Redistribution and use in source and binary forms, with or without
|
|
7
|
+
* modification, are permitted providing that the following conditions
|
|
8
|
+
* are met:
|
|
9
|
+
* 1. Redistributions of source code must retain the above copyright
|
|
10
|
+
* notice, this list of conditions and the following disclaimer.
|
|
11
|
+
* 2. Redistributions in binary form must reproduce the above copyright
|
|
12
|
+
* notice, this list of conditions and the following disclaimer in the
|
|
13
|
+
* documentation and/or other materials provided with the distribution.
|
|
14
|
+
*
|
|
15
|
+
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
16
|
+
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
17
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
18
|
+
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
19
|
+
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
21
|
+
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
22
|
+
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
23
|
+
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
|
24
|
+
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
25
|
+
* POSSIBILITY OF SUCH DAMAGE.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
#ifndef BSPATCH_H
|
|
29
|
+
# define BSPATCH_H
|
|
30
|
+
|
|
31
|
+
# include <stdint.h>
|
|
32
|
+
|
|
33
|
+
struct bspatch_stream
|
|
34
|
+
{
|
|
35
|
+
void* opaque;
|
|
36
|
+
int (*read)(const struct bspatch_stream* stream, void* buffer, int length);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
int bspatch(const uint8_t* old, int64_t oldsize, uint8_t* new, int64_t newsize, struct bspatch_stream* stream);
|
|
40
|
+
|
|
41
|
+
#endif
|