@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.
Files changed (182) hide show
  1. package/README.md +430 -0
  2. package/android/build.gradle +105 -0
  3. package/android/src/main/AndroidManifest.xml +6 -0
  4. package/android/src/main/java/com/swiftpatch/BundleManager.kt +107 -0
  5. package/android/src/main/java/com/swiftpatch/CrashDetector.kt +79 -0
  6. package/android/src/main/java/com/swiftpatch/CryptoVerifier.kt +69 -0
  7. package/android/src/main/java/com/swiftpatch/DownloadManager.kt +120 -0
  8. package/android/src/main/java/com/swiftpatch/EventQueue.kt +86 -0
  9. package/android/src/main/java/com/swiftpatch/FileUtils.kt +60 -0
  10. package/android/src/main/java/com/swiftpatch/PatchApplier.kt +60 -0
  11. package/android/src/main/java/com/swiftpatch/SignalCrashHandler.kt +84 -0
  12. package/android/src/main/java/com/swiftpatch/SlotManager.kt +299 -0
  13. package/android/src/main/java/com/swiftpatch/SwiftPatchModule.kt +630 -0
  14. package/android/src/main/java/com/swiftpatch/SwiftPatchPackage.kt +21 -0
  15. package/android/src/main/jni/CMakeLists.txt +12 -0
  16. package/android/src/main/jni/bspatch.c +188 -0
  17. package/android/src/main/jni/bspatch.h +57 -0
  18. package/android/src/main/jni/bspatch_jni.c +28 -0
  19. package/ios/Libraries/bspatch/bspatch.c +188 -0
  20. package/ios/Libraries/bspatch/bspatch.h +50 -0
  21. package/ios/Libraries/bspatch/module.modulemap +4 -0
  22. package/ios/SwiftPatch/BundleManager.swift +113 -0
  23. package/ios/SwiftPatch/CrashDetector.swift +71 -0
  24. package/ios/SwiftPatch/CryptoVerifier.swift +70 -0
  25. package/ios/SwiftPatch/DownloadManager.swift +125 -0
  26. package/ios/SwiftPatch/EventQueue.swift +116 -0
  27. package/ios/SwiftPatch/FileUtils.swift +38 -0
  28. package/ios/SwiftPatch/PatchApplier.swift +41 -0
  29. package/ios/SwiftPatch/SignalCrashHandler.swift +129 -0
  30. package/ios/SwiftPatch/SlotManager.swift +360 -0
  31. package/ios/SwiftPatch/SwiftPatchModule.m +56 -0
  32. package/ios/SwiftPatch/SwiftPatchModule.swift +621 -0
  33. package/lib/commonjs/SwiftPatchCore.js +140 -0
  34. package/lib/commonjs/SwiftPatchCore.js.map +1 -0
  35. package/lib/commonjs/SwiftPatchProvider.js +617 -0
  36. package/lib/commonjs/SwiftPatchProvider.js.map +1 -0
  37. package/lib/commonjs/constants.js +50 -0
  38. package/lib/commonjs/constants.js.map +1 -0
  39. package/lib/commonjs/core/Downloader.js +63 -0
  40. package/lib/commonjs/core/Downloader.js.map +1 -0
  41. package/lib/commonjs/core/Installer.js +46 -0
  42. package/lib/commonjs/core/Installer.js.map +1 -0
  43. package/lib/commonjs/core/Rollback.js +36 -0
  44. package/lib/commonjs/core/Rollback.js.map +1 -0
  45. package/lib/commonjs/core/UpdateChecker.js +57 -0
  46. package/lib/commonjs/core/UpdateChecker.js.map +1 -0
  47. package/lib/commonjs/core/Verifier.js +82 -0
  48. package/lib/commonjs/core/Verifier.js.map +1 -0
  49. package/lib/commonjs/core/index.js +41 -0
  50. package/lib/commonjs/core/index.js.map +1 -0
  51. package/lib/commonjs/index.js +154 -0
  52. package/lib/commonjs/index.js.map +1 -0
  53. package/lib/commonjs/modal/SwiftPatchModal.js +667 -0
  54. package/lib/commonjs/modal/SwiftPatchModal.js.map +1 -0
  55. package/lib/commonjs/modal/useSwiftPatchModal.js +26 -0
  56. package/lib/commonjs/modal/useSwiftPatchModal.js.map +1 -0
  57. package/lib/commonjs/native/NativeSwiftPatch.js +85 -0
  58. package/lib/commonjs/native/NativeSwiftPatch.js.map +1 -0
  59. package/lib/commonjs/native/NativeSwiftPatchSpec.js +15 -0
  60. package/lib/commonjs/native/NativeSwiftPatchSpec.js.map +1 -0
  61. package/lib/commonjs/package.json +1 -0
  62. package/lib/commonjs/types.js +126 -0
  63. package/lib/commonjs/types.js.map +1 -0
  64. package/lib/commonjs/useSwiftPatch.js +31 -0
  65. package/lib/commonjs/useSwiftPatch.js.map +1 -0
  66. package/lib/commonjs/utils/api.js +206 -0
  67. package/lib/commonjs/utils/api.js.map +1 -0
  68. package/lib/commonjs/utils/device.js +23 -0
  69. package/lib/commonjs/utils/device.js.map +1 -0
  70. package/lib/commonjs/utils/logger.js +30 -0
  71. package/lib/commonjs/utils/logger.js.map +1 -0
  72. package/lib/commonjs/utils/storage.js +31 -0
  73. package/lib/commonjs/utils/storage.js.map +1 -0
  74. package/lib/commonjs/withSwiftPatch.js +42 -0
  75. package/lib/commonjs/withSwiftPatch.js.map +1 -0
  76. package/lib/module/SwiftPatchCore.js +135 -0
  77. package/lib/module/SwiftPatchCore.js.map +1 -0
  78. package/lib/module/SwiftPatchProvider.js +611 -0
  79. package/lib/module/SwiftPatchProvider.js.map +1 -0
  80. package/lib/module/constants.js +46 -0
  81. package/lib/module/constants.js.map +1 -0
  82. package/lib/module/core/Downloader.js +57 -0
  83. package/lib/module/core/Downloader.js.map +1 -0
  84. package/lib/module/core/Installer.js +41 -0
  85. package/lib/module/core/Installer.js.map +1 -0
  86. package/lib/module/core/Rollback.js +31 -0
  87. package/lib/module/core/Rollback.js.map +1 -0
  88. package/lib/module/core/UpdateChecker.js +51 -0
  89. package/lib/module/core/UpdateChecker.js.map +1 -0
  90. package/lib/module/core/Verifier.js +76 -0
  91. package/lib/module/core/Verifier.js.map +1 -0
  92. package/lib/module/core/index.js +8 -0
  93. package/lib/module/core/index.js.map +1 -0
  94. package/lib/module/index.js +34 -0
  95. package/lib/module/index.js.map +1 -0
  96. package/lib/module/modal/SwiftPatchModal.js +661 -0
  97. package/lib/module/modal/SwiftPatchModal.js.map +1 -0
  98. package/lib/module/modal/useSwiftPatchModal.js +22 -0
  99. package/lib/module/modal/useSwiftPatchModal.js.map +1 -0
  100. package/lib/module/native/NativeSwiftPatch.js +78 -0
  101. package/lib/module/native/NativeSwiftPatch.js.map +1 -0
  102. package/lib/module/native/NativeSwiftPatchSpec.js +12 -0
  103. package/lib/module/native/NativeSwiftPatchSpec.js.map +1 -0
  104. package/lib/module/types.js +139 -0
  105. package/lib/module/types.js.map +1 -0
  106. package/lib/module/useSwiftPatch.js +26 -0
  107. package/lib/module/useSwiftPatch.js.map +1 -0
  108. package/lib/module/utils/api.js +197 -0
  109. package/lib/module/utils/api.js.map +1 -0
  110. package/lib/module/utils/device.js +18 -0
  111. package/lib/module/utils/device.js.map +1 -0
  112. package/lib/module/utils/logger.js +26 -0
  113. package/lib/module/utils/logger.js.map +1 -0
  114. package/lib/module/utils/storage.js +24 -0
  115. package/lib/module/utils/storage.js.map +1 -0
  116. package/lib/module/withSwiftPatch.js +37 -0
  117. package/lib/module/withSwiftPatch.js.map +1 -0
  118. package/lib/typescript/SwiftPatchCore.d.ts +64 -0
  119. package/lib/typescript/SwiftPatchCore.d.ts.map +1 -0
  120. package/lib/typescript/SwiftPatchProvider.d.ts +22 -0
  121. package/lib/typescript/SwiftPatchProvider.d.ts.map +1 -0
  122. package/lib/typescript/constants.d.ts +33 -0
  123. package/lib/typescript/constants.d.ts.map +1 -0
  124. package/lib/typescript/core/Downloader.d.ts +34 -0
  125. package/lib/typescript/core/Downloader.d.ts.map +1 -0
  126. package/lib/typescript/core/Installer.d.ts +25 -0
  127. package/lib/typescript/core/Installer.d.ts.map +1 -0
  128. package/lib/typescript/core/Rollback.d.ts +18 -0
  129. package/lib/typescript/core/Rollback.d.ts.map +1 -0
  130. package/lib/typescript/core/UpdateChecker.d.ts +27 -0
  131. package/lib/typescript/core/UpdateChecker.d.ts.map +1 -0
  132. package/lib/typescript/core/Verifier.d.ts +31 -0
  133. package/lib/typescript/core/Verifier.d.ts.map +1 -0
  134. package/lib/typescript/core/index.d.ts +8 -0
  135. package/lib/typescript/core/index.d.ts.map +1 -0
  136. package/lib/typescript/index.d.ts +13 -0
  137. package/lib/typescript/index.d.ts.map +1 -0
  138. package/lib/typescript/modal/SwiftPatchModal.d.ts +11 -0
  139. package/lib/typescript/modal/SwiftPatchModal.d.ts.map +1 -0
  140. package/lib/typescript/modal/useSwiftPatchModal.d.ts +7 -0
  141. package/lib/typescript/modal/useSwiftPatchModal.d.ts.map +1 -0
  142. package/lib/typescript/native/NativeSwiftPatch.d.ts +61 -0
  143. package/lib/typescript/native/NativeSwiftPatch.d.ts.map +1 -0
  144. package/lib/typescript/native/NativeSwiftPatchSpec.d.ts +67 -0
  145. package/lib/typescript/native/NativeSwiftPatchSpec.d.ts.map +1 -0
  146. package/lib/typescript/types.d.ts +266 -0
  147. package/lib/typescript/types.d.ts.map +1 -0
  148. package/lib/typescript/useSwiftPatch.d.ts +12 -0
  149. package/lib/typescript/useSwiftPatch.d.ts.map +1 -0
  150. package/lib/typescript/utils/api.d.ts +87 -0
  151. package/lib/typescript/utils/api.d.ts.map +1 -0
  152. package/lib/typescript/utils/device.d.ts +9 -0
  153. package/lib/typescript/utils/device.d.ts.map +1 -0
  154. package/lib/typescript/utils/logger.d.ts +8 -0
  155. package/lib/typescript/utils/logger.d.ts.map +1 -0
  156. package/lib/typescript/utils/storage.d.ts +14 -0
  157. package/lib/typescript/utils/storage.d.ts.map +1 -0
  158. package/lib/typescript/withSwiftPatch.d.ts +12 -0
  159. package/lib/typescript/withSwiftPatch.d.ts.map +1 -0
  160. package/package.json +99 -0
  161. package/react-native-swiftpatch.podspec +50 -0
  162. package/src/SwiftPatchCore.ts +148 -0
  163. package/src/SwiftPatchProvider.tsx +514 -0
  164. package/src/constants.ts +49 -0
  165. package/src/core/Downloader.ts +74 -0
  166. package/src/core/Installer.ts +38 -0
  167. package/src/core/Rollback.ts +28 -0
  168. package/src/core/UpdateChecker.ts +70 -0
  169. package/src/core/Verifier.ts +92 -0
  170. package/src/core/index.ts +11 -0
  171. package/src/index.ts +64 -0
  172. package/src/modal/SwiftPatchModal.tsx +657 -0
  173. package/src/modal/useSwiftPatchModal.ts +24 -0
  174. package/src/native/NativeSwiftPatch.ts +205 -0
  175. package/src/native/NativeSwiftPatchSpec.ts +139 -0
  176. package/src/types.ts +336 -0
  177. package/src/useSwiftPatch.ts +29 -0
  178. package/src/utils/api.ts +244 -0
  179. package/src/utils/device.ts +15 -0
  180. package/src/utils/logger.ts +29 -0
  181. package/src/utils/storage.ts +23 -0
  182. 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
+ }