@swiftpatch/react-native 2.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/README.md +430 -0
- package/android/build.gradle +105 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/com/swiftpatch/BundleManager.kt +107 -0
- package/android/src/main/java/com/swiftpatch/CrashDetector.kt +79 -0
- package/android/src/main/java/com/swiftpatch/CryptoVerifier.kt +69 -0
- package/android/src/main/java/com/swiftpatch/DownloadManager.kt +120 -0
- package/android/src/main/java/com/swiftpatch/EventQueue.kt +86 -0
- package/android/src/main/java/com/swiftpatch/FileUtils.kt +60 -0
- package/android/src/main/java/com/swiftpatch/PatchApplier.kt +60 -0
- package/android/src/main/java/com/swiftpatch/SignalCrashHandler.kt +84 -0
- package/android/src/main/java/com/swiftpatch/SlotManager.kt +299 -0
- package/android/src/main/java/com/swiftpatch/SwiftPatchModule.kt +630 -0
- package/android/src/main/java/com/swiftpatch/SwiftPatchPackage.kt +21 -0
- package/android/src/main/jni/CMakeLists.txt +12 -0
- package/android/src/main/jni/bspatch.c +188 -0
- package/android/src/main/jni/bspatch.h +57 -0
- package/android/src/main/jni/bspatch_jni.c +28 -0
- package/ios/Libraries/bspatch/bspatch.c +188 -0
- package/ios/Libraries/bspatch/bspatch.h +50 -0
- package/ios/Libraries/bspatch/module.modulemap +4 -0
- package/ios/SwiftPatch/BundleManager.swift +113 -0
- package/ios/SwiftPatch/CrashDetector.swift +71 -0
- package/ios/SwiftPatch/CryptoVerifier.swift +70 -0
- package/ios/SwiftPatch/DownloadManager.swift +125 -0
- package/ios/SwiftPatch/EventQueue.swift +116 -0
- package/ios/SwiftPatch/FileUtils.swift +38 -0
- package/ios/SwiftPatch/PatchApplier.swift +41 -0
- package/ios/SwiftPatch/SignalCrashHandler.swift +129 -0
- package/ios/SwiftPatch/SlotManager.swift +360 -0
- package/ios/SwiftPatch/SwiftPatchModule.m +56 -0
- package/ios/SwiftPatch/SwiftPatchModule.swift +621 -0
- package/lib/commonjs/SwiftPatchCore.js +140 -0
- package/lib/commonjs/SwiftPatchCore.js.map +1 -0
- package/lib/commonjs/SwiftPatchProvider.js +617 -0
- package/lib/commonjs/SwiftPatchProvider.js.map +1 -0
- package/lib/commonjs/constants.js +50 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/core/Downloader.js +63 -0
- package/lib/commonjs/core/Downloader.js.map +1 -0
- package/lib/commonjs/core/Installer.js +46 -0
- package/lib/commonjs/core/Installer.js.map +1 -0
- package/lib/commonjs/core/Rollback.js +36 -0
- package/lib/commonjs/core/Rollback.js.map +1 -0
- package/lib/commonjs/core/UpdateChecker.js +57 -0
- package/lib/commonjs/core/UpdateChecker.js.map +1 -0
- package/lib/commonjs/core/Verifier.js +82 -0
- package/lib/commonjs/core/Verifier.js.map +1 -0
- package/lib/commonjs/core/index.js +41 -0
- package/lib/commonjs/core/index.js.map +1 -0
- package/lib/commonjs/index.js +154 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/modal/SwiftPatchModal.js +667 -0
- package/lib/commonjs/modal/SwiftPatchModal.js.map +1 -0
- package/lib/commonjs/modal/useSwiftPatchModal.js +26 -0
- package/lib/commonjs/modal/useSwiftPatchModal.js.map +1 -0
- package/lib/commonjs/native/NativeSwiftPatch.js +85 -0
- package/lib/commonjs/native/NativeSwiftPatch.js.map +1 -0
- package/lib/commonjs/native/NativeSwiftPatchSpec.js +15 -0
- package/lib/commonjs/native/NativeSwiftPatchSpec.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +126 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useSwiftPatch.js +31 -0
- package/lib/commonjs/useSwiftPatch.js.map +1 -0
- package/lib/commonjs/utils/api.js +206 -0
- package/lib/commonjs/utils/api.js.map +1 -0
- package/lib/commonjs/utils/device.js +23 -0
- package/lib/commonjs/utils/device.js.map +1 -0
- package/lib/commonjs/utils/logger.js +30 -0
- package/lib/commonjs/utils/logger.js.map +1 -0
- package/lib/commonjs/utils/storage.js +31 -0
- package/lib/commonjs/utils/storage.js.map +1 -0
- package/lib/commonjs/withSwiftPatch.js +42 -0
- package/lib/commonjs/withSwiftPatch.js.map +1 -0
- package/lib/module/SwiftPatchCore.js +135 -0
- package/lib/module/SwiftPatchCore.js.map +1 -0
- package/lib/module/SwiftPatchProvider.js +611 -0
- package/lib/module/SwiftPatchProvider.js.map +1 -0
- package/lib/module/constants.js +46 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/core/Downloader.js +57 -0
- package/lib/module/core/Downloader.js.map +1 -0
- package/lib/module/core/Installer.js +41 -0
- package/lib/module/core/Installer.js.map +1 -0
- package/lib/module/core/Rollback.js +31 -0
- package/lib/module/core/Rollback.js.map +1 -0
- package/lib/module/core/UpdateChecker.js +51 -0
- package/lib/module/core/UpdateChecker.js.map +1 -0
- package/lib/module/core/Verifier.js +76 -0
- package/lib/module/core/Verifier.js.map +1 -0
- package/lib/module/core/index.js +8 -0
- package/lib/module/core/index.js.map +1 -0
- package/lib/module/index.js +34 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/modal/SwiftPatchModal.js +661 -0
- package/lib/module/modal/SwiftPatchModal.js.map +1 -0
- package/lib/module/modal/useSwiftPatchModal.js +22 -0
- package/lib/module/modal/useSwiftPatchModal.js.map +1 -0
- package/lib/module/native/NativeSwiftPatch.js +78 -0
- package/lib/module/native/NativeSwiftPatch.js.map +1 -0
- package/lib/module/native/NativeSwiftPatchSpec.js +12 -0
- package/lib/module/native/NativeSwiftPatchSpec.js.map +1 -0
- package/lib/module/types.js +139 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useSwiftPatch.js +26 -0
- package/lib/module/useSwiftPatch.js.map +1 -0
- package/lib/module/utils/api.js +197 -0
- package/lib/module/utils/api.js.map +1 -0
- package/lib/module/utils/device.js +18 -0
- package/lib/module/utils/device.js.map +1 -0
- package/lib/module/utils/logger.js +26 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/module/utils/storage.js +24 -0
- package/lib/module/utils/storage.js.map +1 -0
- package/lib/module/withSwiftPatch.js +37 -0
- package/lib/module/withSwiftPatch.js.map +1 -0
- package/lib/typescript/SwiftPatchCore.d.ts +64 -0
- package/lib/typescript/SwiftPatchCore.d.ts.map +1 -0
- package/lib/typescript/SwiftPatchProvider.d.ts +22 -0
- package/lib/typescript/SwiftPatchProvider.d.ts.map +1 -0
- package/lib/typescript/constants.d.ts +33 -0
- package/lib/typescript/constants.d.ts.map +1 -0
- package/lib/typescript/core/Downloader.d.ts +34 -0
- package/lib/typescript/core/Downloader.d.ts.map +1 -0
- package/lib/typescript/core/Installer.d.ts +25 -0
- package/lib/typescript/core/Installer.d.ts.map +1 -0
- package/lib/typescript/core/Rollback.d.ts +18 -0
- package/lib/typescript/core/Rollback.d.ts.map +1 -0
- package/lib/typescript/core/UpdateChecker.d.ts +27 -0
- package/lib/typescript/core/UpdateChecker.d.ts.map +1 -0
- package/lib/typescript/core/Verifier.d.ts +31 -0
- package/lib/typescript/core/Verifier.d.ts.map +1 -0
- package/lib/typescript/core/index.d.ts +8 -0
- package/lib/typescript/core/index.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +13 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/modal/SwiftPatchModal.d.ts +11 -0
- package/lib/typescript/modal/SwiftPatchModal.d.ts.map +1 -0
- package/lib/typescript/modal/useSwiftPatchModal.d.ts +7 -0
- package/lib/typescript/modal/useSwiftPatchModal.d.ts.map +1 -0
- package/lib/typescript/native/NativeSwiftPatch.d.ts +61 -0
- package/lib/typescript/native/NativeSwiftPatch.d.ts.map +1 -0
- package/lib/typescript/native/NativeSwiftPatchSpec.d.ts +67 -0
- package/lib/typescript/native/NativeSwiftPatchSpec.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +266 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/useSwiftPatch.d.ts +12 -0
- package/lib/typescript/useSwiftPatch.d.ts.map +1 -0
- package/lib/typescript/utils/api.d.ts +87 -0
- package/lib/typescript/utils/api.d.ts.map +1 -0
- package/lib/typescript/utils/device.d.ts +9 -0
- package/lib/typescript/utils/device.d.ts.map +1 -0
- package/lib/typescript/utils/logger.d.ts +8 -0
- package/lib/typescript/utils/logger.d.ts.map +1 -0
- package/lib/typescript/utils/storage.d.ts +14 -0
- package/lib/typescript/utils/storage.d.ts.map +1 -0
- package/lib/typescript/withSwiftPatch.d.ts +12 -0
- package/lib/typescript/withSwiftPatch.d.ts.map +1 -0
- package/package.json +99 -0
- package/react-native-swiftpatch.podspec +50 -0
- package/src/SwiftPatchCore.ts +148 -0
- package/src/SwiftPatchProvider.tsx +514 -0
- package/src/constants.ts +49 -0
- package/src/core/Downloader.ts +74 -0
- package/src/core/Installer.ts +38 -0
- package/src/core/Rollback.ts +28 -0
- package/src/core/UpdateChecker.ts +70 -0
- package/src/core/Verifier.ts +92 -0
- package/src/core/index.ts +11 -0
- package/src/index.ts +64 -0
- package/src/modal/SwiftPatchModal.tsx +657 -0
- package/src/modal/useSwiftPatchModal.ts +24 -0
- package/src/native/NativeSwiftPatch.ts +205 -0
- package/src/native/NativeSwiftPatchSpec.ts +139 -0
- package/src/types.ts +336 -0
- package/src/useSwiftPatch.ts +29 -0
- package/src/utils/api.ts +244 -0
- package/src/utils/device.ts +15 -0
- package/src/utils/logger.ts +29 -0
- package/src/utils/storage.ts +23 -0
- package/src/withSwiftPatch.tsx +41 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package com.swiftpatch
|
|
2
|
+
|
|
3
|
+
import android.content.SharedPreferences
|
|
4
|
+
|
|
5
|
+
class CrashDetector(
|
|
6
|
+
private val prefs: SharedPreferences
|
|
7
|
+
) {
|
|
8
|
+
companion object {
|
|
9
|
+
private const val CRASH_DETECTION_WINDOW_MS = 10_000L // 10 seconds
|
|
10
|
+
private const val MAX_CRASHES_BEFORE_ROLLBACK = 2
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if we need to rollback due to a crash after install.
|
|
15
|
+
* Called during native module initialization.
|
|
16
|
+
*/
|
|
17
|
+
fun checkForPendingRollback(onRollback: (String) -> Unit) {
|
|
18
|
+
val pendingConfirmation =
|
|
19
|
+
prefs.getBoolean("pending_install_confirmation", false)
|
|
20
|
+
if (!pendingConfirmation) return
|
|
21
|
+
|
|
22
|
+
val installTimestamp = prefs.getLong("install_timestamp", 0)
|
|
23
|
+
val currentTime = System.currentTimeMillis()
|
|
24
|
+
|
|
25
|
+
if (currentTime - installTimestamp < CRASH_DETECTION_WINDOW_MS) {
|
|
26
|
+
// Within crash detection window - this might be a crash-restart cycle
|
|
27
|
+
val crashCount = prefs.getInt("crash_count", 0) + 1
|
|
28
|
+
|
|
29
|
+
if (crashCount >= MAX_CRASHES_BEFORE_ROLLBACK) {
|
|
30
|
+
// Too many crashes, rollback
|
|
31
|
+
val reason = "Automatic rollback after $crashCount crashes"
|
|
32
|
+
performRollback(reason)
|
|
33
|
+
onRollback("Crashed $crashCount times within detection window")
|
|
34
|
+
} else {
|
|
35
|
+
prefs.edit().putInt("crash_count", crashCount).apply()
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
// We survived the crash detection window, mark as stable
|
|
39
|
+
confirmStable()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Mark the current installation as stable.
|
|
45
|
+
* Called from JS after the app has been running stably.
|
|
46
|
+
*/
|
|
47
|
+
fun confirmStable() {
|
|
48
|
+
prefs.edit()
|
|
49
|
+
.putBoolean("pending_install_confirmation", false)
|
|
50
|
+
.putInt("crash_count", 0)
|
|
51
|
+
.apply()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private fun performRollback(reason: String) {
|
|
55
|
+
val previousHash = prefs.getString("previous_bundle_hash", null)
|
|
56
|
+
val previousPath = prefs.getString("previous_bundle_path", null)
|
|
57
|
+
|
|
58
|
+
if (previousHash != null && previousPath != null) {
|
|
59
|
+
prefs.edit()
|
|
60
|
+
.putString("current_bundle_hash", previousHash)
|
|
61
|
+
.putString("current_bundle_path", previousPath)
|
|
62
|
+
.remove("previous_bundle_hash")
|
|
63
|
+
.remove("previous_bundle_path")
|
|
64
|
+
.putBoolean("pending_install_confirmation", false)
|
|
65
|
+
.putInt("crash_count", 0)
|
|
66
|
+
.putString("last_rollback_reason", reason)
|
|
67
|
+
.apply()
|
|
68
|
+
} else {
|
|
69
|
+
// Rollback to original bundled version
|
|
70
|
+
prefs.edit()
|
|
71
|
+
.remove("current_bundle_hash")
|
|
72
|
+
.remove("current_bundle_path")
|
|
73
|
+
.putBoolean("pending_install_confirmation", false)
|
|
74
|
+
.putInt("crash_count", 0)
|
|
75
|
+
.putString("last_rollback_reason", reason)
|
|
76
|
+
.apply()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package com.swiftpatch
|
|
2
|
+
|
|
3
|
+
import android.util.Base64
|
|
4
|
+
import java.io.File
|
|
5
|
+
import java.io.FileInputStream
|
|
6
|
+
import java.security.KeyFactory
|
|
7
|
+
import java.security.MessageDigest
|
|
8
|
+
import java.security.Signature
|
|
9
|
+
import java.security.spec.X509EncodedKeySpec
|
|
10
|
+
|
|
11
|
+
class CryptoVerifier {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Calculate SHA-256 hash of a file
|
|
15
|
+
*/
|
|
16
|
+
fun sha256(file: File): String {
|
|
17
|
+
val digest = MessageDigest.getInstance("SHA-256")
|
|
18
|
+
|
|
19
|
+
FileInputStream(file).use { fis ->
|
|
20
|
+
val buffer = ByteArray(8192)
|
|
21
|
+
var bytesRead: Int
|
|
22
|
+
while (fis.read(buffer).also { bytesRead = it } != -1) {
|
|
23
|
+
digest.update(buffer, 0, bytesRead)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return digest.digest().joinToString("") { "%02x".format(it) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Verify RSA signature
|
|
32
|
+
*
|
|
33
|
+
* @param data The data that was signed (usually the bundle hash)
|
|
34
|
+
* @param signatureBase64 The signature in base64 format
|
|
35
|
+
* @param publicKeyPem The public key in PEM format
|
|
36
|
+
*/
|
|
37
|
+
fun verifySignature(
|
|
38
|
+
data: String,
|
|
39
|
+
signatureBase64: String,
|
|
40
|
+
publicKeyPem: String
|
|
41
|
+
): Boolean {
|
|
42
|
+
return try {
|
|
43
|
+
// Parse public key from PEM
|
|
44
|
+
val publicKeyBytes = Base64.decode(
|
|
45
|
+
publicKeyPem
|
|
46
|
+
.replace("-----BEGIN PUBLIC KEY-----", "")
|
|
47
|
+
.replace("-----END PUBLIC KEY-----", "")
|
|
48
|
+
.replace("\n", "")
|
|
49
|
+
.replace("\r", "")
|
|
50
|
+
.trim(),
|
|
51
|
+
Base64.DEFAULT
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
val keySpec = X509EncodedKeySpec(publicKeyBytes)
|
|
55
|
+
val keyFactory = KeyFactory.getInstance("RSA")
|
|
56
|
+
val publicKey = keyFactory.generatePublic(keySpec)
|
|
57
|
+
|
|
58
|
+
// Verify signature
|
|
59
|
+
val signature = Signature.getInstance("SHA256withRSA")
|
|
60
|
+
signature.initVerify(publicKey)
|
|
61
|
+
signature.update(data.toByteArray(Charsets.UTF_8))
|
|
62
|
+
|
|
63
|
+
val signatureBytes = Base64.decode(signatureBase64, Base64.DEFAULT)
|
|
64
|
+
signature.verify(signatureBytes)
|
|
65
|
+
} catch (_: Exception) {
|
|
66
|
+
false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
package com.swiftpatch
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.io.FileOutputStream
|
|
5
|
+
import java.io.RandomAccessFile
|
|
6
|
+
import java.net.HttpURLConnection
|
|
7
|
+
import java.net.URL
|
|
8
|
+
|
|
9
|
+
data class DownloadProgress(
|
|
10
|
+
val downloadedBytes: Long,
|
|
11
|
+
val totalBytes: Long,
|
|
12
|
+
val percentage: Int
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
class DownloadManager {
|
|
16
|
+
|
|
17
|
+
private var lastProgressTime = 0L
|
|
18
|
+
private val progressThrottleMs = 300L // Throttle progress callbacks
|
|
19
|
+
|
|
20
|
+
fun download(
|
|
21
|
+
urlString: String,
|
|
22
|
+
outputFile: File,
|
|
23
|
+
onProgress: (DownloadProgress) -> Unit
|
|
24
|
+
) {
|
|
25
|
+
val url = URL(urlString)
|
|
26
|
+
val connection = url.openConnection() as HttpURLConnection
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
connection.requestMethod = "GET"
|
|
30
|
+
connection.connectTimeout = 30_000
|
|
31
|
+
connection.readTimeout = 60_000
|
|
32
|
+
|
|
33
|
+
// Resume download support
|
|
34
|
+
var downloadedBytes = 0L
|
|
35
|
+
if (outputFile.exists() && outputFile.length() > 0) {
|
|
36
|
+
downloadedBytes = outputFile.length()
|
|
37
|
+
connection.setRequestProperty("Range", "bytes=$downloadedBytes-")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
connection.connect()
|
|
41
|
+
|
|
42
|
+
val responseCode = connection.responseCode
|
|
43
|
+
|
|
44
|
+
// Handle resume response
|
|
45
|
+
val isResume = responseCode == 206
|
|
46
|
+
if (responseCode !in listOf(200, 206)) {
|
|
47
|
+
throw RuntimeException("Download failed with HTTP $responseCode")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
val contentLength = connection.contentLengthLong
|
|
51
|
+
val totalBytes = if (isResume) {
|
|
52
|
+
downloadedBytes + contentLength
|
|
53
|
+
} else {
|
|
54
|
+
contentLength
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Ensure parent directory exists
|
|
58
|
+
outputFile.parentFile?.mkdirs()
|
|
59
|
+
|
|
60
|
+
connection.inputStream.use { input ->
|
|
61
|
+
val outputStream = if (isResume) {
|
|
62
|
+
// Append to existing file
|
|
63
|
+
FileOutputStream(outputFile, true)
|
|
64
|
+
} else {
|
|
65
|
+
downloadedBytes = 0
|
|
66
|
+
FileOutputStream(outputFile)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
outputStream.use { output ->
|
|
70
|
+
val buffer = ByteArray(8192)
|
|
71
|
+
var bytesRead: Int
|
|
72
|
+
|
|
73
|
+
while (input.read(buffer).also { bytesRead = it } != -1) {
|
|
74
|
+
output.write(buffer, 0, bytesRead)
|
|
75
|
+
downloadedBytes += bytesRead
|
|
76
|
+
|
|
77
|
+
// Throttle progress callbacks
|
|
78
|
+
val now = System.currentTimeMillis()
|
|
79
|
+
if (now - lastProgressTime >= progressThrottleMs) {
|
|
80
|
+
lastProgressTime = now
|
|
81
|
+
val percentage = if (totalBytes > 0) {
|
|
82
|
+
((downloadedBytes * 100) / totalBytes).toInt()
|
|
83
|
+
} else {
|
|
84
|
+
0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onProgress(
|
|
88
|
+
DownloadProgress(
|
|
89
|
+
downloadedBytes = downloadedBytes,
|
|
90
|
+
totalBytes = if (totalBytes > 0) totalBytes else downloadedBytes,
|
|
91
|
+
percentage = percentage.coerceIn(0, 100)
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Final progress callback
|
|
100
|
+
val finalPercentage = if (totalBytes > 0) {
|
|
101
|
+
((downloadedBytes * 100) / totalBytes).toInt()
|
|
102
|
+
} else { 100 }
|
|
103
|
+
|
|
104
|
+
onProgress(
|
|
105
|
+
DownloadProgress(
|
|
106
|
+
downloadedBytes = downloadedBytes,
|
|
107
|
+
totalBytes = if (totalBytes > 0) totalBytes else downloadedBytes,
|
|
108
|
+
percentage = finalPercentage.coerceIn(0, 100)
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (!outputFile.exists() || outputFile.length() == 0L) {
|
|
113
|
+
throw RuntimeException("Downloaded file is empty")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
} finally {
|
|
117
|
+
connection.disconnect()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package com.swiftpatch
|
|
2
|
+
|
|
3
|
+
import android.content.SharedPreferences
|
|
4
|
+
import org.json.JSONArray
|
|
5
|
+
import org.json.JSONObject
|
|
6
|
+
import java.util.UUID
|
|
7
|
+
import java.util.concurrent.locks.ReentrantReadWriteLock
|
|
8
|
+
import kotlin.concurrent.read
|
|
9
|
+
import kotlin.concurrent.write
|
|
10
|
+
|
|
11
|
+
enum class SwiftPatchEventType(val value: String) {
|
|
12
|
+
DOWNLOAD_PROD_STARTED("DOWNLOAD_PROD_STARTED"),
|
|
13
|
+
DOWNLOAD_PROD_COMPLETED("DOWNLOAD_PROD_COMPLETED"),
|
|
14
|
+
DOWNLOAD_PROD_FAILED("DOWNLOAD_PROD_FAILED"),
|
|
15
|
+
INSTALLED_PROD("INSTALLED_PROD"),
|
|
16
|
+
ROLLBACK_PROD("ROLLBACK_PROD"),
|
|
17
|
+
STABILIZE_PROD("STABILIZE_PROD"),
|
|
18
|
+
CORRUPTION_PROD("CORRUPTION_PROD"),
|
|
19
|
+
DOWNLOAD_STAGE_STARTED("DOWNLOAD_STAGE_STARTED"),
|
|
20
|
+
DOWNLOAD_STAGE_COMPLETED("DOWNLOAD_STAGE_COMPLETED"),
|
|
21
|
+
DOWNLOAD_STAGE_FAILED("DOWNLOAD_STAGE_FAILED"),
|
|
22
|
+
INSTALLED_STAGE("INSTALLED_STAGE"),
|
|
23
|
+
SYNC_ERROR("SYNC_ERROR"),
|
|
24
|
+
VERSION_CHANGED("VERSION_CHANGED"),
|
|
25
|
+
CRASH_DETECTED("CRASH_DETECTED"),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class EventQueue(private val prefs: SharedPreferences) {
|
|
29
|
+
|
|
30
|
+
private val lock = ReentrantReadWriteLock()
|
|
31
|
+
private val eventsKey = "event_queue"
|
|
32
|
+
|
|
33
|
+
fun pushEvent(
|
|
34
|
+
type: SwiftPatchEventType,
|
|
35
|
+
releaseHash: String? = null,
|
|
36
|
+
errorMessage: String? = null,
|
|
37
|
+
metadata: Map<String, Any>? = null
|
|
38
|
+
) {
|
|
39
|
+
lock.write {
|
|
40
|
+
val events = loadEvents()
|
|
41
|
+
val event = JSONObject().apply {
|
|
42
|
+
put("id", UUID.randomUUID().toString())
|
|
43
|
+
put("eventType", type.value)
|
|
44
|
+
put("timestamp", System.currentTimeMillis().toDouble() / 1000.0)
|
|
45
|
+
releaseHash?.let { put("releaseHash", it) }
|
|
46
|
+
errorMessage?.let { put("errorMessage", it) }
|
|
47
|
+
metadata?.let { meta ->
|
|
48
|
+
val metaObj = JSONObject()
|
|
49
|
+
meta.forEach { (key, value) -> metaObj.put(key, value) }
|
|
50
|
+
put("metadata", metaObj)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
events.put(event)
|
|
54
|
+
saveEvents(events)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fun popEvents(): JSONArray {
|
|
59
|
+
return lock.read {
|
|
60
|
+
loadEvents()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fun acknowledgeEvents(eventIds: List<String>) {
|
|
65
|
+
lock.write {
|
|
66
|
+
val events = loadEvents()
|
|
67
|
+
val filtered = JSONArray()
|
|
68
|
+
for (i in 0 until events.length()) {
|
|
69
|
+
val event = events.getJSONObject(i)
|
|
70
|
+
if (event.optString("id") !in eventIds) {
|
|
71
|
+
filtered.put(event)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
saveEvents(filtered)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun loadEvents(): JSONArray {
|
|
79
|
+
val raw = prefs.getString(eventsKey, null) ?: return JSONArray()
|
|
80
|
+
return try { JSONArray(raw) } catch (_: Exception) { JSONArray() }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private fun saveEvents(events: JSONArray) {
|
|
84
|
+
prefs.edit().putString(eventsKey, events.toString()).apply()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package com.swiftpatch
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.io.FileInputStream
|
|
5
|
+
import java.io.FileOutputStream
|
|
6
|
+
import java.util.zip.GZIPInputStream
|
|
7
|
+
|
|
8
|
+
object FileUtils {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Decompress a brotli-compressed file.
|
|
12
|
+
*
|
|
13
|
+
* Note: For production, integrate a proper Brotli decoder (e.g., org.brotli:dec).
|
|
14
|
+
* This implementation handles gzip as a fallback.
|
|
15
|
+
*/
|
|
16
|
+
fun decompressBrotli(input: File, output: File) {
|
|
17
|
+
// Attempt gzip decompression as a practical fallback.
|
|
18
|
+
// For true brotli support, add org.brotli:dec dependency.
|
|
19
|
+
try {
|
|
20
|
+
GZIPInputStream(FileInputStream(input)).use { gzis ->
|
|
21
|
+
FileOutputStream(output).use { fos ->
|
|
22
|
+
gzis.copyTo(fos)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch (_: Exception) {
|
|
26
|
+
// If not gzip, just copy directly (assuming uncompressed or
|
|
27
|
+
// pre-decompressed by the server)
|
|
28
|
+
input.copyTo(output, overwrite = true)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Safely delete a file, ignoring errors
|
|
34
|
+
*/
|
|
35
|
+
fun safeDelete(file: File) {
|
|
36
|
+
try {
|
|
37
|
+
if (file.exists()) {
|
|
38
|
+
file.delete()
|
|
39
|
+
}
|
|
40
|
+
} catch (_: Exception) {
|
|
41
|
+
// Ignore deletion errors
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the size of a file in a human-readable format
|
|
47
|
+
*/
|
|
48
|
+
fun humanReadableSize(bytes: Long): String {
|
|
49
|
+
val units = arrayOf("B", "KB", "MB", "GB")
|
|
50
|
+
var size = bytes.toDouble()
|
|
51
|
+
var unitIndex = 0
|
|
52
|
+
|
|
53
|
+
while (size >= 1024 && unitIndex < units.size - 1) {
|
|
54
|
+
size /= 1024
|
|
55
|
+
unitIndex++
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return "%.1f %s".format(size, units[unitIndex])
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package com.swiftpatch
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
|
|
5
|
+
class PatchApplier {
|
|
6
|
+
|
|
7
|
+
companion object {
|
|
8
|
+
init {
|
|
9
|
+
System.loadLibrary("bspatch")
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Apply a bsdiff patch to create a new file
|
|
15
|
+
*
|
|
16
|
+
* @param oldFile The original file
|
|
17
|
+
* @param patchFile The patch file
|
|
18
|
+
* @param newFile The output file (will be created)
|
|
19
|
+
*/
|
|
20
|
+
fun applyPatch(oldFile: File, patchFile: File, newFile: File) {
|
|
21
|
+
if (!oldFile.exists()) {
|
|
22
|
+
throw IllegalArgumentException(
|
|
23
|
+
"Old file does not exist: ${oldFile.absolutePath}"
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
if (!patchFile.exists()) {
|
|
27
|
+
throw IllegalArgumentException(
|
|
28
|
+
"Patch file does not exist: ${patchFile.absolutePath}"
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Ensure parent directory exists
|
|
33
|
+
newFile.parentFile?.mkdirs()
|
|
34
|
+
|
|
35
|
+
// Call native bspatch
|
|
36
|
+
val result = nativeApplyPatch(
|
|
37
|
+
oldFile.absolutePath,
|
|
38
|
+
patchFile.absolutePath,
|
|
39
|
+
newFile.absolutePath
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if (result != 0) {
|
|
43
|
+
newFile.delete()
|
|
44
|
+
throw RuntimeException("bspatch failed with code: $result")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!newFile.exists()) {
|
|
48
|
+
throw RuntimeException("bspatch did not create output file")
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Native JNI method for applying bspatch
|
|
54
|
+
*/
|
|
55
|
+
private external fun nativeApplyPatch(
|
|
56
|
+
oldPath: String,
|
|
57
|
+
patchPath: String,
|
|
58
|
+
newPath: String
|
|
59
|
+
): Int
|
|
60
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package com.swiftpatch
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
import java.io.File
|
|
6
|
+
|
|
7
|
+
class SignalCrashHandler private constructor(private val context: Context) {
|
|
8
|
+
|
|
9
|
+
companion object {
|
|
10
|
+
@Volatile
|
|
11
|
+
private var instance: SignalCrashHandler? = null
|
|
12
|
+
|
|
13
|
+
fun init(context: Context): SignalCrashHandler {
|
|
14
|
+
return instance ?: synchronized(this) {
|
|
15
|
+
instance ?: SignalCrashHandler(context.applicationContext).also { instance = it }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fun getInstance(): SignalCrashHandler? = instance
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private val crashMarkerFile: File
|
|
23
|
+
get() = File(context.filesDir, "swiftpatch/crash_marker.json")
|
|
24
|
+
|
|
25
|
+
@Volatile
|
|
26
|
+
var isMounted = false
|
|
27
|
+
private set
|
|
28
|
+
|
|
29
|
+
fun markMounted() { isMounted = true }
|
|
30
|
+
fun markUnmounted() { isMounted = false }
|
|
31
|
+
|
|
32
|
+
fun installHandler() {
|
|
33
|
+
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
|
34
|
+
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
|
35
|
+
writeCrashMarker(throwable)
|
|
36
|
+
defaultHandler?.uncaughtException(thread, throwable)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private fun writeCrashMarker(throwable: Throwable) {
|
|
41
|
+
try {
|
|
42
|
+
val crashInfo = JSONObject().apply {
|
|
43
|
+
put("exceptionType", throwable.javaClass.name)
|
|
44
|
+
put("message", throwable.message ?: "")
|
|
45
|
+
put("timestamp", System.currentTimeMillis().toDouble() / 1000.0)
|
|
46
|
+
put("wasMounted", isMounted)
|
|
47
|
+
}
|
|
48
|
+
crashMarkerFile.parentFile?.mkdirs()
|
|
49
|
+
crashMarkerFile.writeText(crashInfo.toString())
|
|
50
|
+
} catch (_: Exception) {
|
|
51
|
+
// Best effort
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
data class CrashMarkerInfo(
|
|
56
|
+
val exceptionType: String,
|
|
57
|
+
val message: String,
|
|
58
|
+
val timestamp: Double,
|
|
59
|
+
val wasMounted: Boolean
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
fun checkForCrashMarker(): CrashMarkerInfo? {
|
|
63
|
+
if (!crashMarkerFile.exists()) return null
|
|
64
|
+
|
|
65
|
+
return try {
|
|
66
|
+
val json = JSONObject(crashMarkerFile.readText())
|
|
67
|
+
crashMarkerFile.delete()
|
|
68
|
+
|
|
69
|
+
CrashMarkerInfo(
|
|
70
|
+
exceptionType = json.optString("exceptionType", "Unknown"),
|
|
71
|
+
message = json.optString("message", ""),
|
|
72
|
+
timestamp = json.optDouble("timestamp", 0.0),
|
|
73
|
+
wasMounted = json.optBoolean("wasMounted", false)
|
|
74
|
+
)
|
|
75
|
+
} catch (_: Exception) {
|
|
76
|
+
crashMarkerFile.delete()
|
|
77
|
+
null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun shouldAutoRollback(crashInfo: CrashMarkerInfo): Boolean {
|
|
82
|
+
return !crashInfo.wasMounted
|
|
83
|
+
}
|
|
84
|
+
}
|