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.
- package/README.md +120 -22
- 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 +12 -2
- 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 +12 -2
- 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 +15 -4
- package/src/ota/storage/GitHubReleaseStorage.ts +5 -0
- package/src/ota/types.ts +6 -2
- package/tools/build_ota.sh +103 -7
- package/tools/bump_version.js +133 -0
- package/tools/generate_keys.js +75 -0
- 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
|
|
|
@@ -440,14 +462,90 @@ yarn ota:build
|
|
|
440
462
|
|
|
441
463
|
---
|
|
442
464
|
|
|
443
|
-
## Security
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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];
|