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.
- package/README.md +145 -42
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +117 -17
- package/cpp/CMakeLists.txt +9 -1
- package/cpp/bsdiff/bspatch.c +14 -3
- package/cpp/bsdiff/bspatch_wrapper.c +124 -40
- package/ios/RNBsPatch.mm +142 -1
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +61 -12
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/otaCore.js +43 -20
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/otaMetadata.js +41 -5
- package/lib/commonjs/ota/otaMetadata.js.map +1 -1
- package/lib/commonjs/ota/storage/CustomStorage.js +23 -3
- package/lib/commonjs/ota/storage/CustomStorage.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js +3 -0
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +3 -0
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +61 -12
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/otaCore.js +43 -20
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/otaMetadata.js +38 -3
- package/lib/module/ota/otaMetadata.js.map +1 -1
- package/lib/module/ota/storage/CustomStorage.js +23 -3
- package/lib/module/ota/storage/CustomStorage.js.map +1 -1
- package/lib/module/ota/storage/GitHubRawStorage.js +3 -0
- package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/module/ota/storage/GitHubReleaseStorage.js +3 -0
- package/lib/module/ota/storage/GitHubReleaseStorage.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +11 -0
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/otaCore.d.ts +3 -1
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/otaMetadata.d.ts +6 -0
- package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
- package/lib/typescript/ota/storage/CustomStorage.d.ts.map +1 -1
- package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +1 -1
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +1 -1
- package/lib/typescript/ota/types.d.ts +6 -2
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/react-native-bspatch.podspec +6 -1
- package/src/NativeBsPatch.ts +17 -0
- package/src/ota/OtaProvider.tsx +80 -23
- package/src/ota/otaCore.ts +54 -24
- package/src/ota/otaMetadata.ts +46 -4
- package/src/ota/storage/CustomStorage.ts +26 -3
- package/src/ota/storage/GitHubRawStorage.ts +5 -0
- package/src/ota/storage/GitHubReleaseStorage.ts +5 -0
- package/src/ota/types.ts +6 -2
- package/tools/build_ota.sh +366 -18
- package/tools/bump_version.js +133 -0
- package/tools/generate_keys.js +75 -0
- package/tools/setup_project.js +69 -4
- 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
|
-
- **
|
|
13
|
-
- **Gzip decompression** — native gzip decompression on both Android and iOS
|
|
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
|
|
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.
|
|
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,
|
|
95
|
+
import { useOta, OtaStatus } from 'react-native-bspatch';
|
|
90
96
|
|
|
91
97
|
function UpdateBanner() {
|
|
92
|
-
const { status, currentVersion, latestVersion,
|
|
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
|
-
/**
|
|
114
|
-
|
|
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
|
|
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:
|
|
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
|
|
184
|
-
ON_APP_START
|
|
185
|
-
ON_APP_FOREGROUND
|
|
186
|
-
BACKGROUND_DOWNLOAD = 'BACKGROUND_DOWNLOAD', //
|
|
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
|
-
|
|
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 ──►
|
|
354
|
-
│
|
|
355
|
-
│
|
|
356
|
-
│
|
|
357
|
-
│
|
|
358
|
-
│
|
|
359
|
-
│
|
|
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 —
|
|
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
|
|
377
|
-
| `yarn ota:
|
|
378
|
-
| `yarn ota:
|
|
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.
|
|
399
|
-
|
|
400
|
-
#
|
|
401
|
-
yarn ota:
|
|
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
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
44
|
+
}
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
override fun unzip(source: String, targetDir: String, promise: Promise) {
|
|
46
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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 =
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
}
|
package/cpp/CMakeLists.txt
CHANGED
|
@@ -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()
|
package/cpp/bsdiff/bspatch.c
CHANGED
|
@@ -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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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];
|