react-native-update 10.48.2 → 10.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BUNDLEHASH_MIGRATION.md +335 -0
- package/NATIVE_CHECKUPDATE_DESIGN.md +192 -0
- package/README-CN.md +40 -4
- package/README.md +40 -4
- package/android/jni/Android.mk +1 -0
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +89 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +13 -4
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +11 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
- package/biome.json +59 -0
- package/cpp/patch_core/digest.cpp +177 -0
- package/cpp/patch_core/digest.h +43 -0
- package/cpp/patch_core/tests/patch_core_test.cpp +70 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +1 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +47 -0
- package/harmony/pushy/src/main/ets/DownloadTask.ts +2 -2
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +2 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +54 -0
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +55 -0
- package/package.json +5 -6
- package/react-native-update.podspec +1 -0
- package/src/NativePushy.ts +7 -1
- package/src/client.ts +83 -57
- package/src/context.ts +7 -7
- package/src/core.ts +78 -51
- package/src/endpoint.ts +7 -7
- package/src/error.ts +3 -3
- package/src/i18n.ts +2 -2
- package/src/index.ts +4 -4
- package/src/isInRollout.ts +10 -1
- package/src/locales/en.ts +2 -0
- package/src/locales/zh.ts +2 -0
- package/src/provider.tsx +46 -56
- package/src/resolveCheckResult.ts +1 -1
- package/src/telemetry.ts +1 -1
- package/src/type.ts +31 -5
- package/src/utils.ts +30 -30
- package/.eslintrc.js +0 -4
- package/.prettierrc.js +0 -6
package/README.md
CHANGED
|
@@ -17,13 +17,45 @@ See the docs:
|
|
|
17
17
|
## Advantages
|
|
18
18
|
|
|
19
19
|
1. react-native-update provides a dedicated global service with fast and reliable worldwide delivery.
|
|
20
|
-
2. **Tiny update packages** generated with bsdiff/hdiff are typically only tens to hundreds of KB, instead of the tens of MB usually required by full-bundle update systems.
|
|
20
|
+
2. **Tiny update packages** generated with bsdiff/hdiff are typically only tens to hundreds of KB, instead of the tens of MB usually required by full-bundle update systems. On top of that, the diff algorithm is **specifically optimized for Hermes bytecode** (see the [comparison below](#diff-algorithm-comparison)), shrinking patches even further.
|
|
21
21
|
3. The library tracks new React Native stable releases closely, supports Hermes bytecode, and supports the new architecture. Note: Android RN 0.73.0 to 0.76.0 new architecture is unavailable because of upstream issues; versions below 0.73 or above 0.76.1 are supported.
|
|
22
22
|
4. When updating across multiple versions, clients only need to download **one update package** instead of applying every intermediate version in sequence.
|
|
23
23
|
5. Command-line tools and a web dashboard are both available, making release workflows simple and CI-friendly.
|
|
24
|
-
6.
|
|
25
|
-
7.
|
|
26
|
-
8.
|
|
24
|
+
6. The dashboard ships with **built-in analytics** (version distribution, update success rate), **staged (gray) rollouts** by percentage, and **release health monitoring** — no need to build your own data pipeline to keep every release under control.
|
|
25
|
+
7. Built-in crash rollback keeps updates safe and reliable, and health monitoring helps you catch and stop a bad release early.
|
|
26
|
+
8. Meta information and open APIs make the system more extensible.
|
|
27
|
+
9. Paid technical support is available.
|
|
28
|
+
|
|
29
|
+
## Diff Algorithm Comparison
|
|
30
|
+
|
|
31
|
+
Traditional diff algorithms operate on raw bytes. Hermes bytecode, however, is full of offset tables — a small JS change shifts every subsequent offset, which dramatically amplifies the binary difference. Before running hdiff, we apply a **delta-friendly reversible transform tailored to HBC (Hermes bytecode)**: offset bitfields are delta-encoded so the offset-shift amplification disappears at the source. The layout description table is shipped with each patch, so when Hermes evolves, **clients need zero upgrades** — compatibility is automatic.
|
|
32
|
+
|
|
33
|
+
The numbers below are measured on real release bundles of a React Native 0.86 app (Hermes HBC v98, ~4.4 MB bytecode); every patch is verified by an actual round-trip before its size is recorded. Full methodology, fixtures, and runnable code: **[hbc-diff-benchmark](https://github.com/sunnylqm/hbc-diff-benchmark)**.
|
|
34
|
+
|
|
35
|
+
**Hermes bytecode (.hbc) — what ships in production Hermes apps:**
|
|
36
|
+
|
|
37
|
+
| Scenario | Full OTA (gzip) | bsdiff | bsdiff+lzma | hdiff | hdiff + HBC transform |
|
|
38
|
+
|---|---|---|---|---|---|
|
|
39
|
+
| One-line text change | 1901.5 KB | 93.7 KB | 87.7 KB | 89.0 KB | **63.5 KB** (−28% / −29%) |
|
|
40
|
+
| Small feature (~60 LOC) | 1913.9 KB | 411.6 KB | 330.9 KB | 328.0 KB | **285.5 KB** (−14% / −13%) |
|
|
41
|
+
| Medium feature (~300 LOC) | 1973.7 KB | 551.6 KB | 430.6 KB | 431.6 KB | **398.4 KB** (−7% / −8%) |
|
|
42
|
+
|
|
43
|
+
**Plain-text JS bundle — non-Hermes apps (same scenarios, same app):**
|
|
44
|
+
|
|
45
|
+
| Scenario | Full OTA (gzip) | bsdiff | bsdiff+lzma | hdiff |
|
|
46
|
+
|---|---|---|---|---|
|
|
47
|
+
| One-line text change | 807.2 KB | 0.3 KB | 0.7 KB | **0.1 KB** |
|
|
48
|
+
| Small feature (~60 LOC) | 813.1 KB | 7.4 KB | 7.4 KB | **5.8 KB** |
|
|
49
|
+
| Medium feature (~300 LOC) | 837.7 KB | 38.4 KB | 37.4 KB | **28.7 KB** |
|
|
50
|
+
|
|
51
|
+
Text bundles have no offset tables, so they diff cleanly without any transform — patches are hundreds of bytes to tens of KB, and hdiff still beats bsdiff by ~20–25% on the larger changes. The offset-shift amplification is a Hermes-bytecode-specific problem, which is exactly why the HBC transform exists.
|
|
52
|
+
|
|
53
|
+
Highlights:
|
|
54
|
+
|
|
55
|
+
- Incremental updates save **5–30×** bandwidth compared to full-bundle OTA (63.5 KB vs 1.9 MB for a one-line change).
|
|
56
|
+
- The bsdiff+lzma control group (same bsdiff delta, lzma compression) shows the compressor accounts for most of the bsdiff-vs-hdiff gap — the further **7–29%** cut from the HBC-aware transform is **pure algorithmic gain**, largest on the small, frequent updates that dominate real OTA traffic.
|
|
57
|
+
- Patch generation with hdiff is **2–4× faster** than bsdiff; the transform itself costs single-digit milliseconds.
|
|
58
|
+
- Safety first: section bounds and bitfield ranges are fully validated before any byte is touched, and the pipeline automatically falls back to plain hdiff on any mismatch.
|
|
27
59
|
|
|
28
60
|
## Comparison With Other OTA Libraries
|
|
29
61
|
|
|
@@ -31,6 +63,10 @@ See the docs:
|
|
|
31
63
|
|---------|---------------------|-------------|------------------------|
|
|
32
64
|
| **Price / Cost** | Free tier with multiple paid plans, bandwidth included | Free tier with multiple paid plans, extra bandwidth charges apply | ❌ **Discontinued** (Microsoft App Center shut down on March 31, 2025) |
|
|
33
65
|
| **Package Size** | ⭐⭐⭐⭐⭐ Tens to hundreds of KB (incremental) | ⭐⭐⭐ Full bundle updates (usually tens of MB) | ❌ **Discontinued** |
|
|
66
|
+
| **Hermes-specific diff** | ✅ HBC-aware transform, smallest patches | ❌ Full bundle | ❌ **Discontinued** |
|
|
67
|
+
| **Staged (gray) rollout** | ✅ Built-in, percentage-based | ✅ Supported | ❌ **Discontinued** |
|
|
68
|
+
| **Analytics** | ✅ Built-in (version distribution, success rate) | ⚠️ Limited | ❌ **Discontinued** |
|
|
69
|
+
| **Release health monitoring** | ✅ Built-in | ❌ Not available | ❌ **Discontinued** |
|
|
34
70
|
| **Technical Support** | ✅ Paid dedicated support | ⚠️ Community support | ❌ **Discontinued** |
|
|
35
71
|
| **Server Deployment** | ✅ Hosted service or paid private deployment | ✅ Hosted by Expo (EAS Update) | ❌ **Discontinued** |
|
|
36
72
|
| **Bandwidth Usage** | ⭐⭐⭐⭐⭐ Very low (incremental) | ⭐⭐⭐ Higher (full bundle) | ❌ **Discontinued** |
|
package/android/jni/Android.mk
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -9,6 +9,10 @@ import android.os.Environment;
|
|
|
9
9
|
import android.util.Log;
|
|
10
10
|
import com.facebook.react.ReactInstanceManager;
|
|
11
11
|
import java.io.File;
|
|
12
|
+
import java.io.IOException;
|
|
13
|
+
import java.io.InputStream;
|
|
14
|
+
import java.security.MessageDigest;
|
|
15
|
+
import java.security.NoSuchAlgorithmException;
|
|
12
16
|
import java.util.concurrent.Executor;
|
|
13
17
|
import java.util.concurrent.Executors;
|
|
14
18
|
|
|
@@ -118,6 +122,91 @@ public class UpdateContext {
|
|
|
118
122
|
return isUsingBundleUrl;
|
|
119
123
|
}
|
|
120
124
|
|
|
125
|
+
// bundleHash cache: "<packageVersion>|<lastUpdateTime>|<sha256hex>". The
|
|
126
|
+
// key identifies the installed binary; every (re)install changes
|
|
127
|
+
// lastUpdateTime, so the hash is recomputed once per install.
|
|
128
|
+
private static final String KEY_BUNDLE_HASH_CACHE = "bundleHashCache";
|
|
129
|
+
|
|
130
|
+
public interface BundleHashListener {
|
|
131
|
+
void onBundleHash(String hash);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* bundleHash = sha256 of the JS bundle embedded in the binary — the
|
|
136
|
+
* identity of the binary itself, not of whatever hot update is currently
|
|
137
|
+
* running. Hashes exactly the bytes pdiff patches from: the hardcoded
|
|
138
|
+
* "index.android.bundle" asset read via AssetManager, same as
|
|
139
|
+
* DownloadTask.copyBundledAssetToFile. Runs on the download executor;
|
|
140
|
+
* never fails — an empty string means "unknown" and the server falls back
|
|
141
|
+
* to the buildTime heuristic.
|
|
142
|
+
*
|
|
143
|
+
* Deliberately java.security.MessageDigest instead of the C++
|
|
144
|
+
* pushy::digest: librnupdate.so is a prebuilt artifact and this must not
|
|
145
|
+
* force a rebuild. The NIST vectors in the patch_core tests anchor both
|
|
146
|
+
* implementations to the same standard.
|
|
147
|
+
*/
|
|
148
|
+
public void getBundleHash(final BundleHashListener listener) {
|
|
149
|
+
if (DEBUG) {
|
|
150
|
+
// Metro serves the bundle in debug; mirror the dev buildTime behaviour.
|
|
151
|
+
listener.onBundleHash("");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
executor.execute(new Runnable() {
|
|
155
|
+
@Override
|
|
156
|
+
public void run() {
|
|
157
|
+
listener.onBundleHash(computeBundleHash());
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private String computeBundleHash() {
|
|
163
|
+
String cachePrefix = getPackageVersion() + "|" + getPackageLastUpdateTime() + "|";
|
|
164
|
+
String cached = sp.getString(KEY_BUNDLE_HASH_CACHE, null);
|
|
165
|
+
if (cached != null && cached.startsWith(cachePrefix)) {
|
|
166
|
+
return cached.substring(cachePrefix.length());
|
|
167
|
+
}
|
|
168
|
+
String hash = sha256OfBundledAsset("index.android.bundle");
|
|
169
|
+
if (!hash.isEmpty()) {
|
|
170
|
+
SharedPreferences.Editor editor = sp.edit();
|
|
171
|
+
editor.putString(KEY_BUNDLE_HASH_CACHE, cachePrefix + hash);
|
|
172
|
+
persistEditor(editor, "cache bundle hash");
|
|
173
|
+
}
|
|
174
|
+
return hash;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private long getPackageLastUpdateTime() {
|
|
178
|
+
try {
|
|
179
|
+
PackageInfo pi = context.getPackageManager()
|
|
180
|
+
.getPackageInfo(context.getPackageName(), 0);
|
|
181
|
+
return pi.lastUpdateTime;
|
|
182
|
+
} catch (PackageManager.NameNotFoundException e) {
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private String sha256OfBundledAsset(String assetName) {
|
|
188
|
+
try (InputStream in = context.getAssets().open(assetName)) {
|
|
189
|
+
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
190
|
+
byte[] buffer = new byte[64 * 1024];
|
|
191
|
+
int read;
|
|
192
|
+
while ((read = in.read(buffer)) != -1) {
|
|
193
|
+
md.update(buffer, 0, read);
|
|
194
|
+
}
|
|
195
|
+
byte[] digest = md.digest();
|
|
196
|
+
StringBuilder hex = new StringBuilder(digest.length * 2);
|
|
197
|
+
for (byte b : digest) {
|
|
198
|
+
hex.append(Character.forDigit((b >> 4) & 0xf, 16));
|
|
199
|
+
hex.append(Character.forDigit(b & 0xf, 16));
|
|
200
|
+
}
|
|
201
|
+
return hex.toString();
|
|
202
|
+
} catch (IOException | NoSuchAlgorithmException e) {
|
|
203
|
+
// Expected when there is no embedded bundle (custom name, no
|
|
204
|
+
// release bundle); "unknown" is a valid answer, not an error.
|
|
205
|
+
Log.i(TAG, "Cannot hash bundled asset " + assetName + ": " + e);
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
121
210
|
private void enqueue(DownloadTaskParams params) {
|
|
122
211
|
executor.execute(new DownloadTask(context, params));
|
|
123
212
|
}
|
|
@@ -6,6 +6,7 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
|
|
6
6
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
7
7
|
import com.facebook.react.bridge.WritableMap;
|
|
8
8
|
import java.lang.ref.WeakReference;
|
|
9
|
+
import java.lang.reflect.Method;
|
|
9
10
|
|
|
10
11
|
final class UpdateEventEmitter {
|
|
11
12
|
private static WeakReference<ReactApplicationContext> reactContextRef =
|
|
@@ -45,12 +46,20 @@ final class UpdateEventEmitter {
|
|
|
45
46
|
@SuppressWarnings("deprecation")
|
|
46
47
|
private static boolean hasActiveInstance(ReactApplicationContext reactContext) {
|
|
47
48
|
try {
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// Use reflection for hasActiveReactInstance() because older RN versions (<0.68)
|
|
50
|
+
// don't have this method in the class signature at compile time.
|
|
51
|
+
Method method = reactContext.getClass().getMethod("hasActiveReactInstance");
|
|
52
|
+
Object result = method.invoke(reactContext);
|
|
53
|
+
if (result instanceof Boolean) {
|
|
54
|
+
return (Boolean) result;
|
|
55
|
+
}
|
|
56
|
+
} catch (Throwable ignored) {
|
|
52
57
|
// RN < 0.68 has no hasActiveReactInstance(); fall back for old peers.
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
53
60
|
return reactContext.hasActiveCatalystInstance();
|
|
61
|
+
} catch (Throwable ignored) {
|
|
62
|
+
return false;
|
|
54
63
|
}
|
|
55
64
|
}
|
|
56
65
|
}
|
|
@@ -191,6 +191,17 @@ public class UpdateModuleImpl {
|
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
public static void getBundleHash(final UpdateContext updateContext, final Promise promise) {
|
|
195
|
+
// Threading lives in UpdateContext (download executor); resolve is
|
|
196
|
+
// thread-safe. Never rejects — empty string means "unknown".
|
|
197
|
+
updateContext.getBundleHash(new UpdateContext.BundleHashListener() {
|
|
198
|
+
@Override
|
|
199
|
+
public void onBundleHash(String hash) {
|
|
200
|
+
promise.resolve(hash);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
194
205
|
public static void resetToPackagedBundle(
|
|
195
206
|
final UpdateContext updateContext,
|
|
196
207
|
final Promise promise
|
|
@@ -73,6 +73,11 @@ public class UpdateModule extends NativePushySpec {
|
|
|
73
73
|
UpdateModuleImpl.markSuccess(updateContext, promise);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
@Override
|
|
77
|
+
public void getBundleHash(Promise promise) {
|
|
78
|
+
UpdateModuleImpl.getBundleHash(updateContext, promise);
|
|
79
|
+
}
|
|
80
|
+
|
|
76
81
|
@Override
|
|
77
82
|
public void resetToPackagedBundle(Promise promise) {
|
|
78
83
|
UpdateModuleImpl.resetToPackagedBundle(updateContext, promise);
|
|
@@ -75,6 +75,11 @@ public class UpdateModule extends ReactContextBaseJavaModule {
|
|
|
75
75
|
UpdateModuleImpl.markSuccess(updateContext);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
@ReactMethod
|
|
79
|
+
public void getBundleHash(Promise promise) {
|
|
80
|
+
UpdateModuleImpl.getBundleHash(updateContext, promise);
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
@ReactMethod
|
|
79
84
|
public void resetToPackagedBundle(Promise promise) {
|
|
80
85
|
UpdateModuleImpl.resetToPackagedBundle(updateContext, promise);
|
package/biome.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.5/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": true,
|
|
10
|
+
"includes": [
|
|
11
|
+
"**/*.js",
|
|
12
|
+
"**/*.jsx",
|
|
13
|
+
"**/*.ts",
|
|
14
|
+
"**/*.tsx",
|
|
15
|
+
"**/*.json",
|
|
16
|
+
"!**/node_modules",
|
|
17
|
+
"!**/android",
|
|
18
|
+
"!**/ios",
|
|
19
|
+
"!**/dist",
|
|
20
|
+
"!**/harmony",
|
|
21
|
+
"!**/.tmp"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"formatter": {
|
|
25
|
+
"enabled": true,
|
|
26
|
+
"indentStyle": "space",
|
|
27
|
+
"indentWidth": 2,
|
|
28
|
+
"lineWidth": 80
|
|
29
|
+
},
|
|
30
|
+
"assist": {
|
|
31
|
+
"actions": {
|
|
32
|
+
"source": {
|
|
33
|
+
"organizeImports": "on"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"linter": {
|
|
38
|
+
"enabled": true,
|
|
39
|
+
"rules": {
|
|
40
|
+
"preset": "recommended",
|
|
41
|
+
"suspicious": {
|
|
42
|
+
"noExplicitAny": "off"
|
|
43
|
+
},
|
|
44
|
+
"style": {
|
|
45
|
+
"noNonNullAssertion": "off"
|
|
46
|
+
},
|
|
47
|
+
"complexity": {
|
|
48
|
+
"noUselessFragments": "off"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"javascript": {
|
|
53
|
+
"formatter": {
|
|
54
|
+
"quoteStyle": "single",
|
|
55
|
+
"jsxQuoteStyle": "double",
|
|
56
|
+
"trailingCommas": "es5"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#include "digest.h"
|
|
2
|
+
|
|
3
|
+
#include <cstdio>
|
|
4
|
+
#include <cstring>
|
|
5
|
+
|
|
6
|
+
namespace pushy {
|
|
7
|
+
namespace digest {
|
|
8
|
+
|
|
9
|
+
namespace {
|
|
10
|
+
|
|
11
|
+
// FIPS 180-4 SHA-256 round constants.
|
|
12
|
+
constexpr uint32_t kRoundConstants[64] = {
|
|
13
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
|
|
14
|
+
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
15
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
16
|
+
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
17
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
|
18
|
+
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
19
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
20
|
+
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
21
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
22
|
+
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
23
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
inline uint32_t RotateRight(uint32_t value, uint32_t bits) {
|
|
27
|
+
return (value >> bits) | (value << (32 - bits));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
} // namespace
|
|
31
|
+
|
|
32
|
+
Sha256::Sha256() {
|
|
33
|
+
state_[0] = 0x6a09e667;
|
|
34
|
+
state_[1] = 0xbb67ae85;
|
|
35
|
+
state_[2] = 0x3c6ef372;
|
|
36
|
+
state_[3] = 0xa54ff53a;
|
|
37
|
+
state_[4] = 0x510e527f;
|
|
38
|
+
state_[5] = 0x9b05688c;
|
|
39
|
+
state_[6] = 0x1f83d9ab;
|
|
40
|
+
state_[7] = 0x5be0cd19;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void Sha256::ProcessBlock(const uint8_t* block) {
|
|
44
|
+
uint32_t w[64];
|
|
45
|
+
for (int i = 0; i < 16; i++) {
|
|
46
|
+
w[i] = (static_cast<uint32_t>(block[i * 4]) << 24) |
|
|
47
|
+
(static_cast<uint32_t>(block[i * 4 + 1]) << 16) |
|
|
48
|
+
(static_cast<uint32_t>(block[i * 4 + 2]) << 8) |
|
|
49
|
+
static_cast<uint32_t>(block[i * 4 + 3]);
|
|
50
|
+
}
|
|
51
|
+
for (int i = 16; i < 64; i++) {
|
|
52
|
+
const uint32_t s0 = RotateRight(w[i - 15], 7) ^ RotateRight(w[i - 15], 18) ^
|
|
53
|
+
(w[i - 15] >> 3);
|
|
54
|
+
const uint32_t s1 = RotateRight(w[i - 2], 17) ^ RotateRight(w[i - 2], 19) ^
|
|
55
|
+
(w[i - 2] >> 10);
|
|
56
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
uint32_t a = state_[0];
|
|
60
|
+
uint32_t b = state_[1];
|
|
61
|
+
uint32_t c = state_[2];
|
|
62
|
+
uint32_t d = state_[3];
|
|
63
|
+
uint32_t e = state_[4];
|
|
64
|
+
uint32_t f = state_[5];
|
|
65
|
+
uint32_t g = state_[6];
|
|
66
|
+
uint32_t h = state_[7];
|
|
67
|
+
|
|
68
|
+
for (int i = 0; i < 64; i++) {
|
|
69
|
+
const uint32_t s1 =
|
|
70
|
+
RotateRight(e, 6) ^ RotateRight(e, 11) ^ RotateRight(e, 25);
|
|
71
|
+
const uint32_t ch = (e & f) ^ (~e & g);
|
|
72
|
+
const uint32_t temp1 = h + s1 + ch + kRoundConstants[i] + w[i];
|
|
73
|
+
const uint32_t s0 =
|
|
74
|
+
RotateRight(a, 2) ^ RotateRight(a, 13) ^ RotateRight(a, 22);
|
|
75
|
+
const uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
|
|
76
|
+
const uint32_t temp2 = s0 + maj;
|
|
77
|
+
|
|
78
|
+
h = g;
|
|
79
|
+
g = f;
|
|
80
|
+
f = e;
|
|
81
|
+
e = d + temp1;
|
|
82
|
+
d = c;
|
|
83
|
+
c = b;
|
|
84
|
+
b = a;
|
|
85
|
+
a = temp1 + temp2;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
state_[0] += a;
|
|
89
|
+
state_[1] += b;
|
|
90
|
+
state_[2] += c;
|
|
91
|
+
state_[3] += d;
|
|
92
|
+
state_[4] += e;
|
|
93
|
+
state_[5] += f;
|
|
94
|
+
state_[6] += g;
|
|
95
|
+
state_[7] += h;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
void Sha256::Update(const uint8_t* data, size_t length) {
|
|
99
|
+
total_length_ += length;
|
|
100
|
+
|
|
101
|
+
if (buffer_length_ > 0) {
|
|
102
|
+
const size_t fill = 64 - buffer_length_;
|
|
103
|
+
const size_t take = length < fill ? length : fill;
|
|
104
|
+
std::memcpy(buffer_ + buffer_length_, data, take);
|
|
105
|
+
buffer_length_ += take;
|
|
106
|
+
data += take;
|
|
107
|
+
length -= take;
|
|
108
|
+
if (buffer_length_ < 64) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
ProcessBlock(buffer_);
|
|
112
|
+
buffer_length_ = 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
while (length >= 64) {
|
|
116
|
+
ProcessBlock(data);
|
|
117
|
+
data += 64;
|
|
118
|
+
length -= 64;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (length > 0) {
|
|
122
|
+
std::memcpy(buffer_, data, length);
|
|
123
|
+
buffer_length_ = length;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
std::string Sha256::HexDigest() {
|
|
128
|
+
// Padding: 0x80, zeros, then the 64-bit big-endian bit length.
|
|
129
|
+
const uint64_t bit_length = total_length_ * 8;
|
|
130
|
+
const uint8_t pad_byte = 0x80;
|
|
131
|
+
Update(&pad_byte, 1);
|
|
132
|
+
const uint8_t zero = 0x00;
|
|
133
|
+
// total_length_ is mutated by the padding updates; the final length field is
|
|
134
|
+
// captured above, so this loop only needs to reach the 56-byte boundary.
|
|
135
|
+
while (buffer_length_ != 56) {
|
|
136
|
+
Update(&zero, 1);
|
|
137
|
+
}
|
|
138
|
+
uint8_t length_bytes[8];
|
|
139
|
+
for (int i = 0; i < 8; i++) {
|
|
140
|
+
length_bytes[i] = static_cast<uint8_t>(bit_length >> (56 - i * 8));
|
|
141
|
+
}
|
|
142
|
+
Update(length_bytes, 8);
|
|
143
|
+
|
|
144
|
+
static const char kHex[] = "0123456789abcdef";
|
|
145
|
+
std::string out;
|
|
146
|
+
out.reserve(64);
|
|
147
|
+
for (int i = 0; i < 8; i++) {
|
|
148
|
+
for (int shift = 24; shift >= 0; shift -= 8) {
|
|
149
|
+
const uint8_t byte = static_cast<uint8_t>(state_[i] >> shift);
|
|
150
|
+
out.push_back(kHex[byte >> 4]);
|
|
151
|
+
out.push_back(kHex[byte & 0x0f]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
std::string Sha256File(const std::string& path) {
|
|
158
|
+
std::FILE* file = std::fopen(path.c_str(), "rb");
|
|
159
|
+
if (file == nullptr) {
|
|
160
|
+
return std::string();
|
|
161
|
+
}
|
|
162
|
+
Sha256 hasher;
|
|
163
|
+
uint8_t buffer[64 * 1024];
|
|
164
|
+
size_t read = 0;
|
|
165
|
+
while ((read = std::fread(buffer, 1, sizeof(buffer), file)) > 0) {
|
|
166
|
+
hasher.Update(buffer, read);
|
|
167
|
+
}
|
|
168
|
+
const bool failed = std::ferror(file) != 0;
|
|
169
|
+
std::fclose(file);
|
|
170
|
+
if (failed) {
|
|
171
|
+
return std::string();
|
|
172
|
+
}
|
|
173
|
+
return hasher.HexDigest();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
} // namespace digest
|
|
177
|
+
} // namespace pushy
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstddef>
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
#include <string>
|
|
6
|
+
|
|
7
|
+
namespace pushy {
|
|
8
|
+
namespace digest {
|
|
9
|
+
|
|
10
|
+
// Streaming SHA-256 (FIPS 180-4). Used for bundleHash (the identity of the
|
|
11
|
+
// bundle embedded in the binary) and, later, download artifact verification
|
|
12
|
+
// (IO-6). Streaming because Android/Harmony read the bundle from the package
|
|
13
|
+
// (AssetManager / rawfile) rather than from a plain file.
|
|
14
|
+
//
|
|
15
|
+
// Android intentionally does NOT use this implementation: librnupdate.so is a
|
|
16
|
+
// prebuilt artifact, so the Java layer hashes with java.security.MessageDigest
|
|
17
|
+
// instead. The NIST vectors in the tests anchor both implementations to the
|
|
18
|
+
// same standard.
|
|
19
|
+
class Sha256 {
|
|
20
|
+
public:
|
|
21
|
+
Sha256();
|
|
22
|
+
|
|
23
|
+
void Update(const uint8_t* data, size_t length);
|
|
24
|
+
|
|
25
|
+
// Finalizes and returns the lowercase hex digest. The instance must not be
|
|
26
|
+
// reused afterwards.
|
|
27
|
+
std::string HexDigest();
|
|
28
|
+
|
|
29
|
+
private:
|
|
30
|
+
void ProcessBlock(const uint8_t* block);
|
|
31
|
+
|
|
32
|
+
uint32_t state_[8];
|
|
33
|
+
uint64_t total_length_ = 0;
|
|
34
|
+
uint8_t buffer_[64];
|
|
35
|
+
size_t buffer_length_ = 0;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Hashes a file on disk (streaming, bounded memory). Returns the lowercase hex
|
|
39
|
+
// digest, or an empty string if the file cannot be read.
|
|
40
|
+
std::string Sha256File(const std::string& path);
|
|
41
|
+
|
|
42
|
+
} // namespace digest
|
|
43
|
+
} // namespace pushy
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "../archive_patch_core.h"
|
|
2
|
+
#include "../digest.h"
|
|
2
3
|
#include "../patch_core.h"
|
|
3
4
|
#include "../state_core.h"
|
|
4
5
|
|
|
@@ -747,6 +748,72 @@ void TestStateCoreSwitchToSameVersion() {
|
|
|
747
748
|
Expect(!switched.first_time_ok, "first_time_ok should be false");
|
|
748
749
|
}
|
|
749
750
|
|
|
751
|
+
// NIST FIPS 180-4 vectors. These anchor the C++ implementation (iOS/Harmony)
|
|
752
|
+
// and, by transitivity, Android's java.security.MessageDigest and the CLI's
|
|
753
|
+
// node:crypto to the same standard — the bundleHash produced on any of them
|
|
754
|
+
// must be byte-identical for the same input.
|
|
755
|
+
void TestSha256KnownVectors() {
|
|
756
|
+
{
|
|
757
|
+
pushy::digest::Sha256 hasher;
|
|
758
|
+
ExpectEq(
|
|
759
|
+
hasher.HexDigest(),
|
|
760
|
+
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
761
|
+
"sha256 of empty input");
|
|
762
|
+
}
|
|
763
|
+
{
|
|
764
|
+
pushy::digest::Sha256 hasher;
|
|
765
|
+
const char* input = "abc";
|
|
766
|
+
hasher.Update(reinterpret_cast<const uint8_t*>(input), 3);
|
|
767
|
+
ExpectEq(
|
|
768
|
+
hasher.HexDigest(),
|
|
769
|
+
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
|
770
|
+
"sha256 of \"abc\"");
|
|
771
|
+
}
|
|
772
|
+
{
|
|
773
|
+
pushy::digest::Sha256 hasher;
|
|
774
|
+
const std::string input =
|
|
775
|
+
"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
|
|
776
|
+
hasher.Update(
|
|
777
|
+
reinterpret_cast<const uint8_t*>(input.data()), input.size());
|
|
778
|
+
ExpectEq(
|
|
779
|
+
hasher.HexDigest(),
|
|
780
|
+
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1",
|
|
781
|
+
"sha256 of NIST two-block message");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
void TestSha256StreamingMatchesOneShot() {
|
|
786
|
+
// One million 'a' (NIST long vector), fed in deliberately awkward chunk
|
|
787
|
+
// sizes to cross every buffer boundary case in Update.
|
|
788
|
+
const std::string chunk(4096 + 37, 'a');
|
|
789
|
+
size_t remaining = 1000000;
|
|
790
|
+
pushy::digest::Sha256 hasher;
|
|
791
|
+
while (remaining > 0) {
|
|
792
|
+
const size_t take = remaining < chunk.size() ? remaining : chunk.size();
|
|
793
|
+
hasher.Update(reinterpret_cast<const uint8_t*>(chunk.data()), take);
|
|
794
|
+
remaining -= take;
|
|
795
|
+
}
|
|
796
|
+
ExpectEq(
|
|
797
|
+
hasher.HexDigest(),
|
|
798
|
+
"cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0",
|
|
799
|
+
"sha256 of one million 'a' fed in odd-sized chunks");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
void TestSha256File() {
|
|
803
|
+
TempDir temp;
|
|
804
|
+
const std::string dir = temp.path;
|
|
805
|
+
const std::string path = dir + "/input.bin";
|
|
806
|
+
WriteFile(path, "abc");
|
|
807
|
+
ExpectEq(
|
|
808
|
+
pushy::digest::Sha256File(path),
|
|
809
|
+
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
|
810
|
+
"Sha256File over a small file");
|
|
811
|
+
ExpectEq(
|
|
812
|
+
pushy::digest::Sha256File(dir + "/missing.bin"),
|
|
813
|
+
"",
|
|
814
|
+
"Sha256File over a missing file returns empty");
|
|
815
|
+
}
|
|
816
|
+
|
|
750
817
|
} // namespace
|
|
751
818
|
|
|
752
819
|
int main(int argc, char** argv) {
|
|
@@ -778,6 +845,9 @@ int main(int argc, char** argv) {
|
|
|
778
845
|
{"StateCoreRollbackToEmptyVersion", TestStateCoreRollbackToEmptyVersion},
|
|
779
846
|
{"StateCoreResolveLaunchNoCurrentVersion", TestStateCoreResolveLaunchNoCurrentVersion},
|
|
780
847
|
{"StateCoreSwitchToSameVersion", TestStateCoreSwitchToSameVersion},
|
|
848
|
+
{"Sha256KnownVectors", TestSha256KnownVectors},
|
|
849
|
+
{"Sha256StreamingMatchesOneShot", TestSha256StreamingMatchesOneShot},
|
|
850
|
+
{"Sha256File", TestSha256File},
|
|
781
851
|
};
|
|
782
852
|
|
|
783
853
|
int failures = 0;
|
|
@@ -27,6 +27,7 @@ set(LZMA_DIR ${ANDROID_JNI_DIR}/lzma)
|
|
|
27
27
|
set(HDP_SOURCES
|
|
28
28
|
${CMAKE_CURRENT_SOURCE_DIR}/pushy.cpp
|
|
29
29
|
${PATCH_CORE_DIR}/archive_patch_core.cpp
|
|
30
|
+
${PATCH_CORE_DIR}/digest.cpp
|
|
30
31
|
${PATCH_CORE_DIR}/hbc_transform.cpp
|
|
31
32
|
${PATCH_CORE_DIR}/hbc_transform_wire.cpp
|
|
32
33
|
${PATCH_CORE_DIR}/patch_core.cpp
|