react-native-bspatch 1.1.1 → 1.1.3

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 (58) hide show
  1. package/README.md +120 -22
  2. package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +117 -17
  3. package/cpp/CMakeLists.txt +9 -1
  4. package/cpp/bsdiff/bspatch.c +14 -3
  5. package/cpp/bsdiff/bspatch_wrapper.c +124 -40
  6. package/ios/RNBsPatch.mm +142 -1
  7. package/lib/commonjs/NativeBsPatch.js.map +1 -1
  8. package/lib/commonjs/ota/OtaProvider.js +61 -12
  9. package/lib/commonjs/ota/OtaProvider.js.map +1 -1
  10. package/lib/commonjs/ota/otaCore.js +43 -20
  11. package/lib/commonjs/ota/otaCore.js.map +1 -1
  12. package/lib/commonjs/ota/otaMetadata.js +41 -5
  13. package/lib/commonjs/ota/otaMetadata.js.map +1 -1
  14. package/lib/commonjs/ota/storage/CustomStorage.js +23 -3
  15. package/lib/commonjs/ota/storage/CustomStorage.js.map +1 -1
  16. package/lib/commonjs/ota/storage/GitHubRawStorage.js +12 -2
  17. package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
  18. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +3 -0
  19. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +1 -1
  20. package/lib/module/NativeBsPatch.js.map +1 -1
  21. package/lib/module/ota/OtaProvider.js +61 -12
  22. package/lib/module/ota/OtaProvider.js.map +1 -1
  23. package/lib/module/ota/otaCore.js +43 -20
  24. package/lib/module/ota/otaCore.js.map +1 -1
  25. package/lib/module/ota/otaMetadata.js +38 -3
  26. package/lib/module/ota/otaMetadata.js.map +1 -1
  27. package/lib/module/ota/storage/CustomStorage.js +23 -3
  28. package/lib/module/ota/storage/CustomStorage.js.map +1 -1
  29. package/lib/module/ota/storage/GitHubRawStorage.js +12 -2
  30. package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
  31. package/lib/module/ota/storage/GitHubReleaseStorage.js +3 -0
  32. package/lib/module/ota/storage/GitHubReleaseStorage.js.map +1 -1
  33. package/lib/typescript/NativeBsPatch.d.ts +11 -0
  34. package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
  35. package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
  36. package/lib/typescript/ota/otaCore.d.ts +3 -1
  37. package/lib/typescript/ota/otaCore.d.ts.map +1 -1
  38. package/lib/typescript/ota/otaMetadata.d.ts +6 -0
  39. package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
  40. package/lib/typescript/ota/storage/CustomStorage.d.ts.map +1 -1
  41. package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +1 -1
  42. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +1 -1
  43. package/lib/typescript/ota/types.d.ts +6 -2
  44. package/lib/typescript/ota/types.d.ts.map +1 -1
  45. package/package.json +3 -1
  46. package/react-native-bspatch.podspec +6 -1
  47. package/src/NativeBsPatch.ts +17 -0
  48. package/src/ota/OtaProvider.tsx +80 -23
  49. package/src/ota/otaCore.ts +54 -24
  50. package/src/ota/otaMetadata.ts +46 -4
  51. package/src/ota/storage/CustomStorage.ts +26 -3
  52. package/src/ota/storage/GitHubRawStorage.ts +15 -4
  53. package/src/ota/storage/GitHubReleaseStorage.ts +5 -0
  54. package/src/ota/types.ts +6 -2
  55. package/tools/build_ota.sh +103 -7
  56. package/tools/bump_version.js +133 -0
  57. package/tools/generate_keys.js +75 -0
  58. package/tools/update_registry.js +51 -1
package/README.md CHANGED
@@ -5,15 +5,21 @@ Premium OTA update solution for React Native using **TurboModules** + **bsdiff b
5
5
  ## Features
6
6
 
7
7
  - **Binary delta patches** — only download the diff between old and new bundle (typically 5–30% of full bundle size)
8
+ - **POSIX `mmap()` Zero-Copy Engine** — patches directly in mapped pages, saving ~85–90% heap RAM
9
+ - **ARM NEON SIMD Fast-Path** — vectorized diff patching via Clang SIMD
10
+ - **Silent Background Updates** — download and apply updates completely in the background; smoothly activates on next launch or app backgrounding
11
+ - **Auto Native Version Detection** — auto-detects `versionName` (Android) and `CFBundleShortVersionString` (iOS); no manual version sync in JS
12
+ - **Asymmetric Cryptography (RSA & ECDSA)** — sign patches on server/CI with Private Key; client verifies with Public Key (zero secret leakage)
13
+ - **Interactive Build Verification** — CLI automatically prompts and confirms detected native version before generating releases
14
+ - **One-Command Version Synchronization** — `npm run bump-version <version>` syncs `package.json`, Android `build.gradle`, and iOS Xcode project
8
15
  - **Full-bundle fallback** — if patch fails, automatically downloads the complete bundle
9
16
  - **Atomic bundle swap** — never leaves the app in a broken state
10
17
  - **Crash detection + auto-rollback** — native-side protection runs **before** JS boots; if the updated bundle crashes, the app automatically rolls back to the previous version
11
18
  - **Integrity verification** — SHA-256 hash check on both source bundle and result bundle
12
- - **HMAC signature verification** — optional patch signature check to prevent MITM attacks
13
- - **Gzip decompression** — native gzip decompression on both Android and iOS (when `compressed: true`)
19
+ - **Disk Garbage Collection** — automatically removes obsolete bundle versions and dangling temporary files
20
+ - **Gzip decompression** — native 64KB-buffered gzip decompression on both Android and iOS
14
21
  - **Content-addressed asset store** — images/assets downloaded per-version and redirected via `setCustomSourceTransformer`
15
22
  - **Structured telemetry** — all OTA events forwarded to your analytics via `onEvent` callback
16
- - **Version history** — keeps up to 3 previous versions for safe multi-step rollback
17
23
  - **New Architecture ready** — built with TurboModules (no legacy Bridge)
18
24
 
19
25
  ---
@@ -57,12 +63,12 @@ No additional steps — `react-native-bspatch` and its `SSZipArchive` dependency
57
63
  ### 1. Wrap your app with `OtaProvider`
58
64
 
59
65
  ```tsx
60
- import { OtaProvider } from 'react-native-bspatch';
66
+ import { OtaProvider, OtaUpdateMode } from 'react-native-bspatch';
61
67
 
62
68
  const config = {
63
- appVersion: '1.0.0', // matches versionName in build.gradle
69
+ // appVersion is optional! Auto-detected from native (versionName / CFBundleShortVersionString)
64
70
  baseAppVersion: 1, // OTA version baked into the first APK/IPA (usually 1)
65
- updateMode: OtaUpdateMode.MANUAL,
71
+ updateMode: OtaUpdateMode.BACKGROUND_DOWNLOAD, // Silent download without interrupting user
66
72
  allowDebugUpdates: false, // default: OTA is blocked in __DEV__
67
73
  customServer: {
68
74
  baseUrl: 'https://ota.yourcompany.com',
@@ -86,10 +92,19 @@ export default function App() {
86
92
  ### 2. Check for updates and apply
87
93
 
88
94
  ```tsx
89
- import { useOta, OtaUpdateMode, OtaStatus } from 'react-native-bspatch';
95
+ import { useOta, OtaStatus } from 'react-native-bspatch';
90
96
 
91
97
  function UpdateBanner() {
92
- const { status, currentVersion, latestVersion, checkUpdate, applyUpdate } = useOta();
98
+ const { status, currentVersion, latestVersion, isUpdateReady, reloadBundle, applyUpdate } = useOta();
99
+
100
+ if (isUpdateReady) {
101
+ return (
102
+ <Button
103
+ title="Update downloaded! Restart to apply"
104
+ onPress={reloadBundle}
105
+ />
106
+ );
107
+ }
93
108
 
94
109
  if (status === OtaStatus.UPDATE_AVAILABLE) {
95
110
  return (
@@ -110,8 +125,13 @@ function UpdateBanner() {
110
125
 
111
126
  ```ts
112
127
  type OtaConfig = {
113
- /** Current app versionName, e.g. "1.0.0" */
114
- appVersion: string;
128
+ /**
129
+ * Current app versionName (e.g. "1.0.2").
130
+ * OPTIONAL: If omitted, auto-detected from native app:
131
+ * - Android: PackageInfo.versionName
132
+ * - iOS: CFBundleShortVersionString
133
+ */
134
+ appVersion?: string;
115
135
 
116
136
  /**
117
137
  * OTA version number baked into the APK/IPA at build time.
@@ -133,9 +153,9 @@ type OtaConfig = {
133
153
  /**
134
154
  * When to check / apply updates.
135
155
  * - MANUAL: call checkUpdate() / applyUpdate() manually
136
- * - ON_APP_START: check automatically on every cold start
156
+ * - ON_APP_START: check and apply automatically on cold start
137
157
  * - ON_APP_FOREGROUND: check when app returns to foreground
138
- * - BACKGROUND_DOWNLOAD: check on start; download in background (coming soon)
158
+ * - BACKGROUND_DOWNLOAD: download and patch in background; applies on next launch or app backgrounding
139
159
  */
140
160
  updateMode?: OtaUpdateMode;
141
161
 
@@ -180,10 +200,10 @@ type OtaConfig = {
180
200
 
181
201
  ```ts
182
202
  enum OtaUpdateMode {
183
- MANUAL = 'MANUAL', // No auto behavior
184
- ON_APP_START = 'ON_APP_START', // Check + apply on every cold start
185
- ON_APP_FOREGROUND = 'ON_APP_FOREGROUND', // Check when app returns to foreground
186
- BACKGROUND_DOWNLOAD = 'BACKGROUND_DOWNLOAD', // TODO: download in background
203
+ MANUAL = 'MANUAL', // No auto behavior
204
+ ON_APP_START = 'ON_APP_START', // Check + apply on every cold start
205
+ ON_APP_FOREGROUND = 'ON_APP_FOREGROUND', // Check when app returns to foreground
206
+ BACKGROUND_DOWNLOAD = 'BACKGROUND_DOWNLOAD', // Silent download; applies on next start or app backgrounding
187
207
  }
188
208
  ```
189
209
 
@@ -305,10 +325,12 @@ const {
305
325
  pendingCount, // Number of pending updates
306
326
  isChecking, // True while fetching manifest
307
327
  metrics, // OtaMetrics | null — timing/size data after update
308
- applyUpdate, // () => Promise<void>
328
+ isUpdateReady, // True if background update is downloaded and ready
329
+ applyUpdate, // () => Promise<void> — download and apply update immediately
309
330
  resetToFactory, // () => Promise<void> — wipe OTA state, reload APK/IPA
310
331
  markAsHealthy, // () => Promise<void> — manually mark launch OK
311
- checkUpdate, // () => Promise<void>
332
+ checkUpdate, // () => Promise<void> — manual update check
333
+ reloadBundle, // () => void — immediately reload JS bundle to apply ready update
312
334
  } = useOta();
313
335
  ```
314
336
 
@@ -440,14 +462,90 @@ yarn ota:build
440
462
 
441
463
  ---
442
464
 
443
- ## Security Notes
465
+ ## Security & Asymmetric Signing
466
+
467
+ In production, enabling patch signature verification is **strongly recommended** to prevent man-in-the-middle (MITM) attacks and malicious patch injection.
468
+
469
+ `react-native-bspatch` supports **bank-grade asymmetric cryptography (RSA-2048 & ECDSA P-256)**:
470
+ - **Private Key (`ota_private_key.pem`)**: Stored **only** on your build machine / CI server. Used to sign patch files. Never shipped inside the client app.
471
+ - **Public Key (`ota_public_key.pem`)**: Embedded in your client app via `config.publicKey`. Even if a bad actor reverse-engineers your APK/IPA, they cannot forge updates without the Private Key.
472
+
473
+ ### 1. Generate Keypair (One-Time)
474
+
475
+ ```bash
476
+ # Generate RSA-2048 keypair
477
+ node ./node_modules/react-native-bspatch/tools/generate_keys.js
478
+ # Or generate ECDSA (P-256) keypair:
479
+ node ./node_modules/react-native-bspatch/tools/generate_keys.js --type ec
480
+ ```
481
+
482
+ This creates:
483
+ - `ota_private_key.pem` ➔ **Add to `.gitignore`!** Keep safe on CI.
484
+ - `ota_public_key.pem` ➔ Copy the public key string into your app config:
485
+
486
+ ```ts
487
+ const otaConfig: OtaConfig = {
488
+ publicKey: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...',
489
+ ...
490
+ };
491
+ ```
444
492
 
445
- - **PATCH verification is STRONGLY recommended in production.** Enable by setting `config.publicKey`. Without it, a MITM could serve a malicious patch.
446
- - Signature verification uses SHA-256 hash comparison. For higher security, consider RSA/ECDSA signature verification.
447
- - `markAsHealthy()` is called automatically by `OtaProvider`. If you use a custom integration, you **must** call it after successful JS bootstrap, or the native resolver will rollback on the next cold start.
493
+ ### 2. Automated Signing at Build Time
494
+
495
+ When you run `tools/build_ota.sh`, it automatically searches for `ota_private_key.pem` (or the `OTA_PRIVATE_KEY` environment variable). If found, it **cryptographically signs each binary patch file using SHA-256** and injects the signatures into `ota_registry.json`.
496
+
497
+ On the client device:
498
+ - **Android**: Uses Android Security Provider (`SHA256withRSA` / `SHA256withECDSA`).
499
+ - **iOS**: Uses Apple Security framework (`SecKeyVerifySignature`).
500
+ - **Backward Compatibility**: Automatically falls back to HMAC-SHA256 if a symmetric key is passed.
501
+
502
+ ---
503
+
504
+ ## Version Management
505
+
506
+ ### 1. Zero Manual Sync in JS
507
+ `config.appVersion` is **optional**. The library automatically queries the native app version at runtime:
508
+ - Android: `PackageInfo.versionName`
509
+ - iOS: `CFBundleShortVersionString`
510
+
511
+ > **Fail-Fast Safety:** The library never falls back silently to `"1.0.0"`. If native version information is missing or corrupted, it immediately throws a descriptive error to prevent version mismatches.
512
+
513
+ ### 2. One-Touch Version Synchronization Tool
514
+ When releasing a new native binary version (e.g. `1.0.3`), synchronize all project files with a single command:
515
+
516
+ ```bash
517
+ node ./node_modules/react-native-bspatch/tools/bump_version.js 1.0.3
518
+ ```
519
+
520
+ This updates:
521
+ 1. `package.json` ➔ `"version": "1.0.3"`
522
+ 2. `android/app/build.gradle` ➔ `versionName "1.0.3"` and **increments `versionCode`**
523
+ 3. `ios/*.xcodeproj/project.pbxproj` ➔ `MARKETING_VERSION = 1.0.3` and **increments `CURRENT_PROJECT_VERSION`**
524
+ 4. `src/ota/otaConfig.ts` (if present) ➔ `APP_VERSION = '1.0.3'`
525
+
526
+ ### 3. Interactive CLI Confirmation
527
+ When executing `tools/build_ota.sh`, the script auto-detects the version from `build.gradle` / Xcode / `package.json` and prompts for confirmation in the terminal:
528
+
529
+ ```text
530
+ ────────────────────────────────────────────────────────
531
+ Detected Native App Version: 1.0.2 (android/app/build.gradle)
532
+ Enter App Version to build [Press Enter for '1.0.2']:
533
+ ────────────────────────────────────────────────────────
534
+ ```
535
+
536
+ - **Interactive**: Press **Enter** to proceed with the detected version, or type a custom version.
537
+ - **CI/CD Automation**: Pass `-y` / `--yes` or `--app-version <ver>` to skip the interactive prompt.
448
538
 
449
539
  ---
450
540
 
541
+ ## High-Performance Native Architecture
542
+
543
+ - **POSIX `mmap()` Zero-Copy Patching**: Unlike standard bspatch which allocates heap memory with `malloc()`, `react-native-bspatch` memory-maps the input and output files using `mmap()` and `MADV_SEQUENTIAL`. Memory overhead is reduced by **~85–90%**, virtually eliminating out-of-memory crashes on low-end devices.
544
+ - **SIMD Vectorization**: The inner patching loop includes `#pragma clang loop vectorize(enable)` for Clang ARM NEON 128-bit vectorization.
545
+ - **64KB Streaming Buffers**: File hashing, gzip decompression, and stream copying are upgraded to 64KB chunk sizes across Android and iOS.
546
+ - **Disk Garbage Collection**: Calling `markHealthy()` automatically deletes obsolete bundle versions and dangling `.tmp` files from disk, keeping app storage minimal.
547
+ - **HTTP 304 ETag Caching**: Registry queries send `If-None-Match`. On `304 Not Modified`, cached patches are returned instantly with zero JSON parsing overhead.
548
+
451
549
  ## File Structure
452
550
 
453
551
  ```
@@ -13,12 +13,14 @@ import java.io.BufferedInputStream
13
13
  import java.io.BufferedOutputStream
14
14
  import java.util.zip.ZipInputStream
15
15
  import java.util.zip.GZIPInputStream
16
-
16
+ import java.util.concurrent.Executors
17
17
 
18
18
  class BsPatchModule(private val reactContext: ReactApplicationContext) : NativeBsPatchSpec(reactContext) {
19
19
 
20
20
  companion object {
21
21
  const val NAME = "BsPatch"
22
+ private const val IO_BUFFER_SIZE = 65536 // 64KB buffer for optimal flash storage I/O
23
+ private val backgroundExecutor = Executors.newFixedThreadPool(2)
22
24
 
23
25
  init {
24
26
  System.loadLibrary("bspatch")
@@ -32,24 +34,24 @@ class BsPatchModule(private val reactContext: ReactApplicationContext) : NativeB
32
34
  private external fun nativeApplyPatch(oldPath: String, newPath: String, patchPath: String): Boolean
33
35
 
34
36
  override fun applyPatch(oldPath: String, newPath: String, patchPath: String, promise: com.facebook.react.bridge.Promise) {
35
- Thread {
37
+ backgroundExecutor.execute {
36
38
  try {
37
39
  val result = nativeApplyPatch(oldPath, newPath, patchPath)
38
40
  promise.resolve(result)
39
41
  } catch (e: Exception) {
40
42
  promise.reject("ERR_BSPATCH", e.message, e)
41
43
  }
42
- }.start()
44
+ }
43
45
  }
44
46
 
45
47
  override fun unzip(source: String, targetDir: String, promise: Promise) {
46
- Thread {
48
+ backgroundExecutor.execute {
47
49
  try {
48
50
  val sourceFile = File(source)
49
51
  val destDir = File(targetDir)
50
52
  if (!destDir.exists()) destDir.mkdirs()
51
53
 
52
- ZipInputStream(BufferedInputStream(FileInputStream(sourceFile))).use { zis ->
54
+ ZipInputStream(BufferedInputStream(FileInputStream(sourceFile), IO_BUFFER_SIZE)).use { zis ->
53
55
  var entry = zis.nextEntry
54
56
  while (entry != null) {
55
57
  val file = File(destDir, entry.name)
@@ -57,8 +59,8 @@ class BsPatchModule(private val reactContext: ReactApplicationContext) : NativeB
57
59
  file.mkdirs()
58
60
  } else {
59
61
  file.parentFile?.mkdirs()
60
- BufferedOutputStream(FileOutputStream(file)).use { bos ->
61
- zis.copyTo(bos)
62
+ BufferedOutputStream(FileOutputStream(file), IO_BUFFER_SIZE).use { bos ->
63
+ zis.copyTo(bos, bufferSize = IO_BUFFER_SIZE)
62
64
  }
63
65
  }
64
66
  zis.closeEntry()
@@ -69,7 +71,7 @@ class BsPatchModule(private val reactContext: ReactApplicationContext) : NativeB
69
71
  } catch (e: Exception) {
70
72
  promise.reject("ERR_UNZIP", e.message, e)
71
73
  }
72
- }.start()
74
+ }
73
75
  }
74
76
 
75
77
  /**
@@ -78,21 +80,21 @@ class BsPatchModule(private val reactContext: ReactApplicationContext) : NativeB
78
80
  * bytes in certain APK configurations.
79
81
  */
80
82
  override fun extractBundleFromAssets(assetName: String, destPath: String, promise: Promise) {
81
- Thread {
83
+ backgroundExecutor.execute {
82
84
  try {
83
85
  val assetManager = reactContext.assets
84
86
  assetManager.open(assetName).use { input ->
85
87
  val destFile = File(destPath)
86
88
  destFile.parentFile?.mkdirs()
87
- BufferedOutputStream(FileOutputStream(destFile)).use { output ->
88
- input.copyTo(output, bufferSize = 8192)
89
+ BufferedOutputStream(FileOutputStream(destFile), IO_BUFFER_SIZE).use { output ->
90
+ input.copyTo(output, bufferSize = IO_BUFFER_SIZE)
89
91
  }
90
92
  }
91
93
  promise.resolve(true)
92
94
  } catch (e: Exception) {
93
95
  promise.reject("ERR_EXTRACT_ASSET", e.message, e)
94
96
  }
95
- }.start()
97
+ }
96
98
  }
97
99
 
98
100
  override fun markAsHealthy() {
@@ -118,22 +120,120 @@ class BsPatchModule(private val reactContext: ReactApplicationContext) : NativeB
118
120
  }
119
121
 
120
122
  override fun decompressGzip(sourcePath: String, destPath: String, promise: Promise) {
121
- Thread {
123
+ backgroundExecutor.execute {
122
124
  try {
123
125
  val source = File(sourcePath)
124
126
  val dest = File(destPath)
125
127
  dest.parentFile?.mkdirs()
126
128
 
127
- GZIPInputStream(BufferedInputStream(FileInputStream(source))).use { gis ->
128
- BufferedOutputStream(FileOutputStream(dest)).use { bos ->
129
- gis.copyTo(bos)
129
+ GZIPInputStream(BufferedInputStream(FileInputStream(source), IO_BUFFER_SIZE), IO_BUFFER_SIZE).use { gis ->
130
+ BufferedOutputStream(FileOutputStream(dest), IO_BUFFER_SIZE).use { bos ->
131
+ gis.copyTo(bos, bufferSize = IO_BUFFER_SIZE)
130
132
  }
131
133
  }
132
134
  promise.resolve(true)
133
135
  } catch (e: Exception) {
134
136
  promise.reject("ERR_GZIP_DECOMPRESS", e.message, e)
135
137
  }
136
- }.start()
138
+ }
139
+ }
140
+
141
+ override fun verifySignature(filePath: String, signature: String, publicKey: String, promise: Promise) {
142
+ backgroundExecutor.execute {
143
+ try {
144
+ val file = File(filePath)
145
+ if (!file.exists()) {
146
+ promise.resolve(false)
147
+ return@execute
148
+ }
149
+
150
+ val cleanKey = publicKey
151
+ .replace("-----BEGIN PUBLIC KEY-----", "")
152
+ .replace("-----END PUBLIC KEY-----", "")
153
+ .replace("-----BEGIN RSA PUBLIC KEY-----", "")
154
+ .replace("-----END RSA PUBLIC KEY-----", "")
155
+ .replace("\\s".toRegex(), "")
156
+
157
+ val keyBytes = android.util.Base64.decode(cleanKey, android.util.Base64.DEFAULT)
158
+ val expectedSigBytes = android.util.Base64.decode(signature.trim(), android.util.Base64.DEFAULT)
159
+
160
+ // 1. Try RSA Public Key (SHA256withRSA)
161
+ try {
162
+ val keySpec = java.security.spec.X509EncodedKeySpec(keyBytes)
163
+ val keyFactory = java.security.KeyFactory.getInstance("RSA")
164
+ val pubKey = keyFactory.generatePublic(keySpec)
165
+ val sig = java.security.Signature.getInstance("SHA256withRSA")
166
+ sig.initVerify(pubKey)
167
+
168
+ val buffer = ByteArray(IO_BUFFER_SIZE)
169
+ FileInputStream(file).use { fis ->
170
+ var read: Int
171
+ while (fis.read(buffer).also { read = it } != -1) {
172
+ sig.update(buffer, 0, read)
173
+ }
174
+ }
175
+ val isValid = sig.verify(expectedSigBytes)
176
+ promise.resolve(isValid)
177
+ return@execute
178
+ } catch (rsaEx: Exception) {
179
+ // Not RSA, continue
180
+ }
181
+
182
+ // 2. Try ECDSA Public Key (SHA256withECDSA)
183
+ try {
184
+ val keySpec = java.security.spec.X509EncodedKeySpec(keyBytes)
185
+ val keyFactory = java.security.KeyFactory.getInstance("EC")
186
+ val pubKey = keyFactory.generatePublic(keySpec)
187
+ val sig = java.security.Signature.getInstance("SHA256withECDSA")
188
+ sig.initVerify(pubKey)
189
+
190
+ val buffer = ByteArray(IO_BUFFER_SIZE)
191
+ FileInputStream(file).use { fis ->
192
+ var read: Int
193
+ while (fis.read(buffer).also { read = it } != -1) {
194
+ sig.update(buffer, 0, read)
195
+ }
196
+ }
197
+ val isValid = sig.verify(expectedSigBytes)
198
+ promise.resolve(isValid)
199
+ return@execute
200
+ } catch (ecEx: Exception) {
201
+ // Not EC, continue
202
+ }
203
+
204
+ // 3. Fallback: HMAC-SHA256
205
+ val mac = javax.crypto.Mac.getInstance("HmacSHA256")
206
+ val secretKey = javax.crypto.spec.SecretKeySpec(keyBytes, "HmacSHA256")
207
+ mac.init(secretKey)
208
+
209
+ val buffer = ByteArray(IO_BUFFER_SIZE)
210
+ FileInputStream(file).use { fis ->
211
+ var read: Int
212
+ while (fis.read(buffer).also { read = it } != -1) {
213
+ mac.update(buffer, 0, read)
214
+ }
215
+ }
216
+ val computedSigBytes = mac.doFinal()
217
+ val isValid = java.security.MessageDigest.isEqual(expectedSigBytes, computedSigBytes)
218
+ promise.resolve(isValid)
219
+ } catch (e: Exception) {
220
+ promise.reject("ERR_VERIFY_SIG", e.message, e)
221
+ }
222
+ }
223
+ }
224
+
225
+ override fun getAppVersion(promise: Promise) {
226
+ try {
227
+ val pInfo = reactContext.packageManager.getPackageInfo(reactContext.packageName, 0)
228
+ val versionName = pInfo.versionName
229
+ if (versionName.isNullOrEmpty()) {
230
+ promise.reject("ERR_NO_VERSION", "PackageInfo.versionName is null or empty in AndroidManifest.xml")
231
+ } else {
232
+ promise.resolve(versionName)
233
+ }
234
+ } catch (e: Exception) {
235
+ promise.reject("ERR_GET_APP_VERSION", "Failed to get Android app version: ${e.message}", e)
236
+ }
137
237
  }
138
238
 
139
239
  }
@@ -36,8 +36,16 @@ target_link_libraries(
36
36
  log
37
37
  )
38
38
 
39
+ # Optimization compiler flags
40
+ target_compile_options(bspatch PRIVATE
41
+ -O3
42
+ -flto
43
+ -fomit-frame-pointer
44
+ -fvisibility=hidden
45
+ )
46
+
39
47
  # Set 16KB (2^14) page alignment for Android compatibility
40
48
  # Required for arm64-v8a and x86_64 architectures
41
49
  if(ANDROID)
42
- target_link_options(bspatch PRIVATE "LINKER:-z,max-page-size=16384")
50
+ target_link_options(bspatch PRIVATE "LINKER:-z,max-page-size=16384" -flto)
43
51
  endif()
@@ -73,9 +73,20 @@ int bspatch(const uint8_t* old, int64_t oldsize, uint8_t* new, int64_t newsize,
73
73
  return -1;
74
74
 
75
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];
76
+ if (oldpos >= 0 && oldpos + ctrl[0] <= oldsize) {
77
+ const uint8_t *src = old + oldpos;
78
+ uint8_t *dst = new + newpos;
79
+ int64_t len = ctrl[0];
80
+ #if defined(__clang__)
81
+ #pragma clang loop vectorize(enable) interleave(enable)
82
+ #endif
83
+ for(i=0;i<len;i++)
84
+ dst[i]+=src[i];
85
+ } else {
86
+ for(i=0;i<ctrl[0];i++)
87
+ if((oldpos+i>=0) && (oldpos+i<oldsize))
88
+ new[newpos+i]+=old[oldpos+i];
89
+ }
79
90
 
80
91
  /* Adjust pointers */
81
92
  newpos+=ctrl[0];