react-native-bspatch 1.1.2 → 1.1.4

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 (59) hide show
  1. package/README.md +145 -42
  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 +3 -0
  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 +3 -0
  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 +5 -0
  53. package/src/ota/storage/GitHubReleaseStorage.ts +5 -0
  54. package/src/ota/types.ts +6 -2
  55. package/tools/build_ota.sh +366 -18
  56. package/tools/bump_version.js +133 -0
  57. package/tools/generate_keys.js +75 -0
  58. package/tools/setup_project.js +69 -4
  59. 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
 
@@ -341,6 +363,7 @@ Add these to your `package.json` (or use the existing ones in this project):
341
363
  "react-native-bspatch": "bash ./src/native_modules/react-native-bspatch/tools/build_ota.sh",
342
364
  "ota:setup": "yarn react-native-bspatch setup",
343
365
  "ota:init": "yarn react-native-bspatch base",
366
+ "ota:seed": "yarn react-native-bspatch seed",
344
367
  "ota:build": "yarn react-native-bspatch",
345
368
  "ota:clean": "yarn react-native-bspatch reset"
346
369
  }
@@ -350,22 +373,19 @@ Add these to your `package.json` (or use the existing ones in this project):
350
373
  ### Workflow
351
374
 
352
375
  ```
353
- ota:setup ──► ota:init ──► ota:build ──► (ota:build ◄── repeat for each release)
354
- │ │ │
355
- │ │ └── 1. Bundle JS → Hermes bytecode (.hbc)
356
- │ │ 2. Compare assets → generate asset-manifest-{ver}.json
357
- │ │ 3. bsdiff against previous bundle → .patch files
358
- │ │ 4. Upload to server + update ota_registry.json
359
- │ │ 5. Write otaPatches.generated.ts for bundled patches fallback
360
- │ │
361
- │ └── First build only: creates base bundle from APK/IPA.
362
- │ Stores base asset manifest so subsequent builds know
363
- │ which assets are already in the APK and skip them.
376
+ ota:setup ──► (Build Release APK/IPA) ──► ota:build ──► (repeat for each OTA update)
377
+ │ │ │
378
+ │ │ └── Incremental patch from Base Bundle v1
379
+ │ │
380
+ │ └── Automatic Base Capture (Gradle & Xcode):
381
+ │ Each release build automatically saves `bundle_{appVersion}_v1_{platform}.hbc`.
382
+ │ No manual `ota:init` is required!
364
383
  │
365
384
  └── Patches native integration into project files:
366
385
  • iOS: AppDelegate.swift — adds bundleURL() override
367
- • iOS: project.pbxproj — adds build-phase script to sync bundle → ios/output/
386
+ • iOS: project.pbxproj — syncs and auto-captures base bundle on Release archive
368
387
  • Android: MainApplication.kt — adds OtaBundleResolver.resolve() call
388
+ • Android: app/build.gradle — auto-captures base bundle upon assembleRelease / bundleRelease
369
389
  Idempotent: safe to run multiple times; skips already-patched files.
370
390
  ```
371
391
 
@@ -373,14 +393,22 @@ ota:setup ──► ota:init ──► ota:build ──► (ota:build ◄──
373
393
 
374
394
  | Command | Action |
375
395
  |---|---|
376
- | `yarn ota:setup` | Inject OTA integration into AppDelegate.swift, MainApplication.kt, project.pbxproj. Run once per project. |
377
- | `yarn ota:init` | First release: build base APK/IPA + bundle + base asset manifest. Creates `ota_registry.json`. |
378
- | `yarn ota:build` | Subsequent releases: generate incremental .patch from previous bundle, upload to server, update registry. |
396
+ | `yarn ota:setup` | Inject OTA integration into AppDelegate.swift, MainApplication.kt, project.pbxproj, and app/build.gradle. |
397
+ | `yarn ota:build` | Generate incremental `.patch` from previous bundle, upload to server, update registry. |
398
+ | `yarn ota:seed --apk <path>` | **Zero Mismatch Recovery:** Seed base bundle directly from an existing release APK/AAB. |
399
+ | `yarn ota:seed --ipa <path>` | **Zero Mismatch Recovery:** Seed base bundle directly from an existing release IPA. |
400
+ | `yarn ota:init` | Manual first release: build base APK/IPA + bundle + base asset manifest. Creates `ota_registry.json`. |
379
401
  | `yarn ota:build --platform android` | Build Android only |
380
402
  | `yarn ota:build --platform ios` | Build iOS only |
381
403
  | `yarn ota:build --flavor staging` | Build with flavor "staging" (default: "dev") |
382
404
  | `yarn ota:clean` | Wipe `ota_registry.json` + output files. Preserves base asset manifests so `ota:build` still works after clean. |
383
405
 
406
+ > [!TIP]
407
+ > **Zero Mismatch & Fail-Safe Protection:**
408
+ > 1. **Auto-Capture:** When you build a Release APK/AAB or Archive on Xcode, the bundle is automatically captured into `output/` as Base Bundle v1.
409
+ > 2. **Seed Binary:** If you forgot to capture the base bundle and already submitted your APK/IPA to the Store, run `yarn ota:seed --apk ./app-release.apk`. It extracts the exact byte-for-byte bundle directly from the release binary, guaranteeing 100% hash match.
410
+ > 3. **Fail-Safe Guard:** `ota:build` will refuse to generate an update if no authentic Base Bundle v1 is found, preventing corrupt patches that trigger full-bundle fallback downloads.
411
+
384
412
  ### How It Works
385
413
 
386
414
  1. **Bundle** — `npx react-native bundle` generates Hermes bytecode (`.hbc`). iOS base build uses the Xcode-compiled bundle to guarantee byte-exact match with the IPA.
@@ -395,11 +423,10 @@ ota:setup ──► ota:init ──► ota:build ──► (ota:build ◄──
395
423
  # 1. Add the npm scripts to package.json (or run directly)
396
424
  bash ./src/native_modules/react-native-bspatch/tools/build_ota.sh setup
397
425
 
398
- # 2. Initialize the first release (creates base APK + registry)
399
- yarn ota:init
400
- # Or per platform:
401
- yarn ota:init --platform android # Android only
402
- yarn ota:init --platform ios # iOS only
426
+ # 2. Build your app normally for App Store / Google Play
427
+ # Gradle and Xcode will automatically capture the base bundle (v1)!
428
+ # OR seed from an existing release binary:
429
+ yarn ota:seed --apk ./release.apk
403
430
 
404
431
  # 3. For each subsequent release:
405
432
  yarn ota:build
@@ -440,14 +467,90 @@ yarn ota:build
440
467
 
441
468
  ---
442
469
 
443
- ## Security Notes
470
+ ## Security & Asymmetric Signing
471
+
472
+ In production, enabling patch signature verification is **strongly recommended** to prevent man-in-the-middle (MITM) attacks and malicious patch injection.
473
+
474
+ `react-native-bspatch` supports **bank-grade asymmetric cryptography (RSA-2048 & ECDSA P-256)**:
475
+ - **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.
476
+ - **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.
477
+
478
+ ### 1. Generate Keypair (One-Time)
479
+
480
+ ```bash
481
+ # Generate RSA-2048 keypair
482
+ node ./node_modules/react-native-bspatch/tools/generate_keys.js
483
+ # Or generate ECDSA (P-256) keypair:
484
+ node ./node_modules/react-native-bspatch/tools/generate_keys.js --type ec
485
+ ```
486
+
487
+ This creates:
488
+ - `ota_private_key.pem` ➔ **Add to `.gitignore`!** Keep safe on CI.
489
+ - `ota_public_key.pem` ➔ Copy the public key string into your app config:
490
+
491
+ ```ts
492
+ const otaConfig: OtaConfig = {
493
+ publicKey: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...',
494
+ ...
495
+ };
496
+ ```
497
+
498
+ ### 2. Automated Signing at Build Time
499
+
500
+ 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`.
501
+
502
+ On the client device:
503
+ - **Android**: Uses Android Security Provider (`SHA256withRSA` / `SHA256withECDSA`).
504
+ - **iOS**: Uses Apple Security framework (`SecKeyVerifySignature`).
505
+ - **Backward Compatibility**: Automatically falls back to HMAC-SHA256 if a symmetric key is passed.
506
+
507
+ ---
508
+
509
+ ## Version Management
510
+
511
+ ### 1. Zero Manual Sync in JS
512
+ `config.appVersion` is **optional**. The library automatically queries the native app version at runtime:
513
+ - Android: `PackageInfo.versionName`
514
+ - iOS: `CFBundleShortVersionString`
444
515
 
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.
516
+ > **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.
517
+
518
+ ### 2. One-Touch Version Synchronization Tool
519
+ When releasing a new native binary version (e.g. `1.0.3`), synchronize all project files with a single command:
520
+
521
+ ```bash
522
+ node ./node_modules/react-native-bspatch/tools/bump_version.js 1.0.3
523
+ ```
524
+
525
+ This updates:
526
+ 1. `package.json` ➔ `"version": "1.0.3"`
527
+ 2. `android/app/build.gradle` ➔ `versionName "1.0.3"` and **increments `versionCode`**
528
+ 3. `ios/*.xcodeproj/project.pbxproj` ➔ `MARKETING_VERSION = 1.0.3` and **increments `CURRENT_PROJECT_VERSION`**
529
+ 4. `src/ota/otaConfig.ts` (if present) ➔ `APP_VERSION = '1.0.3'`
530
+
531
+ ### 3. Interactive CLI Confirmation
532
+ 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:
533
+
534
+ ```text
535
+ ────────────────────────────────────────────────────────
536
+ Detected Native App Version: 1.0.2 (android/app/build.gradle)
537
+ Enter App Version to build [Press Enter for '1.0.2']:
538
+ ────────────────────────────────────────────────────────
539
+ ```
540
+
541
+ - **Interactive**: Press **Enter** to proceed with the detected version, or type a custom version.
542
+ - **CI/CD Automation**: Pass `-y` / `--yes` or `--app-version <ver>` to skip the interactive prompt.
448
543
 
449
544
  ---
450
545
 
546
+ ## High-Performance Native Architecture
547
+
548
+ - **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.
549
+ - **SIMD Vectorization**: The inner patching loop includes `#pragma clang loop vectorize(enable)` for Clang ARM NEON 128-bit vectorization.
550
+ - **64KB Streaming Buffers**: File hashing, gzip decompression, and stream copying are upgraded to 64KB chunk sizes across Android and iOS.
551
+ - **Disk Garbage Collection**: Calling `markHealthy()` automatically deletes obsolete bundle versions and dangling `.tmp` files from disk, keeping app storage minimal.
552
+ - **HTTP 304 ETag Caching**: Registry queries send `If-None-Match`. On `304 Not Modified`, cached patches are returned instantly with zero JSON parsing overhead.
553
+
451
554
  ## File Structure
452
555
 
453
556
  ```
@@ -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];