react-native-update 10.48.3 → 10.50.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 +355 -0
- package/NATIVE_CHECKUPDATE_DESIGN.md +192 -0
- 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/BundledResourceCopier.java +42 -9
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +22 -1
- package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +16 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +125 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +23 -3
- 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/cpp/patch_core/digest.cpp +230 -0
- package/cpp/patch_core/digest.h +61 -0
- package/cpp/patch_core/patch_core.cpp +17 -1
- package/cpp/patch_core/patch_core.h +8 -0
- package/cpp/patch_core/tests/patch_core_test.cpp +169 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +1 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +94 -0
- package/harmony/pushy/src/main/ets/DownloadTask.ts +85 -0
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +4 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +79 -4
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +116 -7
- package/package.json +1 -1
- package/react-native-update.podspec +1 -0
- package/src/NativePushy.ts +6 -0
- package/src/client.ts +119 -7
- package/src/core.ts +27 -0
- package/src/endpoint.ts +98 -51
- package/src/error.ts +4 -0
- package/src/isInRollout.ts +2 -1
- package/src/locales/en.ts +6 -0
- package/src/locales/zh.ts +6 -0
- package/src/provider.tsx +36 -7
- package/src/telemetry.ts +8 -0
- package/src/type.ts +34 -3
- package/src/utils.ts +13 -0
|
@@ -77,6 +77,9 @@ class DownloadTask implements Runnable {
|
|
|
77
77
|
private final byte[] buffer = new byte[DOWNLOAD_CHUNK_SIZE];
|
|
78
78
|
private final BundledResourceCopier bundledResourceCopier;
|
|
79
79
|
private String hash;
|
|
80
|
+
// Set once downloadFile() returns: failures after this point in a patch
|
|
81
|
+
// task are patch-application failures, not download failures.
|
|
82
|
+
private boolean downloadPhaseCompleted = false;
|
|
80
83
|
|
|
81
84
|
DownloadTask(Context context, DownloadTaskParams params) {
|
|
82
85
|
this.context = context.getApplicationContext();
|
|
@@ -160,6 +163,7 @@ class DownloadTask implements Runnable {
|
|
|
160
163
|
}
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
downloadPhaseCompleted = true;
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
private byte[] readBytes(InputStream input) throws IOException {
|
|
@@ -408,6 +412,12 @@ class DownloadTask implements Runnable {
|
|
|
408
412
|
}
|
|
409
413
|
}
|
|
410
414
|
|
|
415
|
+
private boolean isPatchTask(int taskType) {
|
|
416
|
+
return taskType == DownloadTaskParams.TASK_TYPE_PATCH_FULL
|
|
417
|
+
|| taskType == DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK
|
|
418
|
+
|| taskType == DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
|
|
419
|
+
}
|
|
420
|
+
|
|
411
421
|
@Override
|
|
412
422
|
public void run() {
|
|
413
423
|
int taskType = params.type;
|
|
@@ -436,7 +446,18 @@ class DownloadTask implements Runnable {
|
|
|
436
446
|
cleanUpAfterFailure(taskType);
|
|
437
447
|
|
|
438
448
|
if (params.listener != null) {
|
|
439
|
-
|
|
449
|
+
// A patch task that failed after its artifact was fully
|
|
450
|
+
// downloaded (unzip / hdiff / resource copy, incl. copiesCrc
|
|
451
|
+
// verification) is a patch failure, not a download failure —
|
|
452
|
+
// the module maps the exception type to PATCH_FAILED.
|
|
453
|
+
Throwable classified = error;
|
|
454
|
+
if (downloadPhaseCompleted
|
|
455
|
+
&& isPatchTask(taskType)
|
|
456
|
+
&& !(error instanceof PatchFailedException)) {
|
|
457
|
+
classified = new PatchFailedException(
|
|
458
|
+
String.valueOf(error.getMessage()), error);
|
|
459
|
+
}
|
|
460
|
+
params.listener.onDownloadFailed(classified);
|
|
440
461
|
}
|
|
441
462
|
return;
|
|
442
463
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
import java.io.IOException;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A download task failure that happened after the artifact was fully
|
|
7
|
+
* downloaded — unzip, hdiff apply, or bundled-resource copy (including the
|
|
8
|
+
* copiesCrc content verification). UpdateModuleImpl rejects these with
|
|
9
|
+
* PATCH_FAILED instead of DOWNLOAD_FAILED so the JS layer and server-side
|
|
10
|
+
* telemetry can separate patch health from network health.
|
|
11
|
+
*/
|
|
12
|
+
class PatchFailedException extends IOException {
|
|
13
|
+
PatchFailedException(String message, Throwable cause) {
|
|
14
|
+
super(message, cause);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -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,16 +122,125 @@ 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
|
}
|
|
124
213
|
|
|
214
|
+
// Server-provided identifiers (hash/originHash/fileName) become child
|
|
215
|
+
// names under rootDir; anything that could resolve outside of it (path
|
|
216
|
+
// separators, "..", ".") must be rejected before touching the filesystem.
|
|
217
|
+
static boolean isSafePathComponent(String name) {
|
|
218
|
+
return name != null
|
|
219
|
+
&& !name.isEmpty()
|
|
220
|
+
&& !name.equals(".")
|
|
221
|
+
&& !name.equals("..")
|
|
222
|
+
&& !name.contains("/")
|
|
223
|
+
&& !name.contains("\\")
|
|
224
|
+
&& name.indexOf('\0') < 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private static boolean rejectUnsafeComponent(String name, DownloadFileListener listener) {
|
|
228
|
+
if (isSafePathComponent(name)) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
listener.onDownloadFailed(new IllegalArgumentException("Invalid path component: " + name));
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
125
235
|
public interface DownloadFileListener {
|
|
126
236
|
void onDownloadCompleted(DownloadTaskParams params);
|
|
127
237
|
void onDownloadFailed(Throwable error);
|
|
128
238
|
}
|
|
129
239
|
|
|
130
240
|
public void downloadFullUpdate(String url, String hash, DownloadFileListener listener) {
|
|
241
|
+
if (rejectUnsafeComponent(hash, listener)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
131
244
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
132
245
|
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FULL;
|
|
133
246
|
params.url = url;
|
|
@@ -139,6 +252,9 @@ public class UpdateContext {
|
|
|
139
252
|
}
|
|
140
253
|
|
|
141
254
|
public void downloadFile(String url, String hash, String fileName, DownloadFileListener listener) {
|
|
255
|
+
if (rejectUnsafeComponent(fileName, listener)) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
142
258
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
143
259
|
params.type = DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD;
|
|
144
260
|
params.url = url;
|
|
@@ -157,6 +273,9 @@ public class UpdateContext {
|
|
|
157
273
|
}
|
|
158
274
|
|
|
159
275
|
public void downloadPatchFromApk(String url, String hash, DownloadFileListener listener) {
|
|
276
|
+
if (rejectUnsafeComponent(hash, listener)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
160
279
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
161
280
|
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK;
|
|
162
281
|
params.url = url;
|
|
@@ -168,6 +287,9 @@ public class UpdateContext {
|
|
|
168
287
|
}
|
|
169
288
|
|
|
170
289
|
public void downloadPatchFromPpk(String url, String hash, String originHash, DownloadFileListener listener) {
|
|
290
|
+
if (rejectUnsafeComponent(hash, listener) || rejectUnsafeComponent(originHash, listener)) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
171
293
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
172
294
|
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
|
|
173
295
|
params.url = url;
|
|
@@ -223,6 +345,9 @@ public class UpdateContext {
|
|
|
223
345
|
}
|
|
224
346
|
|
|
225
347
|
public void switchVersion(String hash) {
|
|
348
|
+
if (!isSafePathComponent(hash)) {
|
|
349
|
+
throw new IllegalArgumentException("Invalid hash: " + hash);
|
|
350
|
+
}
|
|
226
351
|
if (!new File(rootDir, hash+"/index.bundlejs").exists()) {
|
|
227
352
|
throw new IllegalStateException("Bundle version " + hash + " not found.");
|
|
228
353
|
}
|
|
@@ -41,11 +41,20 @@ public class UpdateModuleImpl {
|
|
|
41
41
|
|
|
42
42
|
@Override
|
|
43
43
|
public void onDownloadFailed(Throwable error) {
|
|
44
|
-
promise.reject(
|
|
44
|
+
promise.reject(downloadErrorCode(error), error);
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Post-download failures (unzip / hdiff / resource copy, incl. copiesCrc
|
|
50
|
+
// verification) reject as PATCH_FAILED so JS-side telemetry can separate
|
|
51
|
+
// patch health from network health. DownloadTask wraps them.
|
|
52
|
+
private static String downloadErrorCode(Throwable error) {
|
|
53
|
+
return error instanceof PatchFailedException
|
|
54
|
+
? ErrorCodes.PATCH_FAILED
|
|
55
|
+
: ErrorCodes.DOWNLOAD_FAILED;
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
public static void downloadAndInstallApk(
|
|
50
59
|
final ReactApplicationContext reactContext,
|
|
51
60
|
UpdateContext updateContext,
|
|
@@ -84,7 +93,7 @@ public class UpdateModuleImpl {
|
|
|
84
93
|
|
|
85
94
|
@Override
|
|
86
95
|
public void onDownloadFailed(Throwable error) {
|
|
87
|
-
promise.reject(
|
|
96
|
+
promise.reject(downloadErrorCode(error), error);
|
|
88
97
|
}
|
|
89
98
|
});
|
|
90
99
|
}
|
|
@@ -107,7 +116,7 @@ public class UpdateModuleImpl {
|
|
|
107
116
|
|
|
108
117
|
@Override
|
|
109
118
|
public void onDownloadFailed(Throwable error) {
|
|
110
|
-
promise.reject(
|
|
119
|
+
promise.reject(downloadErrorCode(error), error);
|
|
111
120
|
}
|
|
112
121
|
});
|
|
113
122
|
} catch (Exception e) {
|
|
@@ -191,6 +200,17 @@ public class UpdateModuleImpl {
|
|
|
191
200
|
});
|
|
192
201
|
}
|
|
193
202
|
|
|
203
|
+
public static void getBundleHash(final UpdateContext updateContext, final Promise promise) {
|
|
204
|
+
// Threading lives in UpdateContext (download executor); resolve is
|
|
205
|
+
// thread-safe. Never rejects — empty string means "unknown".
|
|
206
|
+
updateContext.getBundleHash(new UpdateContext.BundleHashListener() {
|
|
207
|
+
@Override
|
|
208
|
+
public void onBundleHash(String hash) {
|
|
209
|
+
promise.resolve(hash);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
194
214
|
public static void resetToPackagedBundle(
|
|
195
215
|
final UpdateContext updateContext,
|
|
196
216
|
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);
|
|
@@ -0,0 +1,230 @@
|
|
|
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 {
|
|
177
|
+
|
|
178
|
+
struct Crc32TableHolder {
|
|
179
|
+
uint32_t table[256];
|
|
180
|
+
|
|
181
|
+
Crc32TableHolder() {
|
|
182
|
+
for (uint32_t i = 0; i < 256; i++) {
|
|
183
|
+
uint32_t value = i;
|
|
184
|
+
for (int bit = 0; bit < 8; bit++) {
|
|
185
|
+
value = (value & 1u) ? (value >> 1) ^ 0xEDB88320u : value >> 1;
|
|
186
|
+
}
|
|
187
|
+
table[i] = value;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const uint32_t* Crc32Table() {
|
|
193
|
+
// Function-local static: thread-safe one-time construction (C++11).
|
|
194
|
+
static const Crc32TableHolder holder;
|
|
195
|
+
return holder.table;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
} // namespace
|
|
199
|
+
|
|
200
|
+
void Crc32::Update(const uint8_t* data, size_t length) {
|
|
201
|
+
const uint32_t* table = Crc32Table();
|
|
202
|
+
uint32_t state = state_;
|
|
203
|
+
for (size_t i = 0; i < length; i++) {
|
|
204
|
+
state = table[(state ^ data[i]) & 0xFFu] ^ (state >> 8);
|
|
205
|
+
}
|
|
206
|
+
state_ = state;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
bool Crc32File(const std::string& path, uint32_t* out) {
|
|
210
|
+
std::FILE* file = std::fopen(path.c_str(), "rb");
|
|
211
|
+
if (file == nullptr) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
Crc32 crc;
|
|
215
|
+
uint8_t buffer[64 * 1024];
|
|
216
|
+
size_t read = 0;
|
|
217
|
+
while ((read = std::fread(buffer, 1, sizeof(buffer), file)) > 0) {
|
|
218
|
+
crc.Update(buffer, read);
|
|
219
|
+
}
|
|
220
|
+
const bool failed = std::ferror(file) != 0;
|
|
221
|
+
std::fclose(file);
|
|
222
|
+
if (failed) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
*out = crc.Value();
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
} // namespace digest
|
|
230
|
+
} // namespace pushy
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
// Streaming CRC32 (IEEE 802.3, reflected polynomial 0xEDB88320 — the zip /
|
|
43
|
+
// zlib checksum). Used to verify pdiff copy sources against the CRCs the CLI
|
|
44
|
+
// records from the origin package's zip entries (copiesCrc in __diff.json):
|
|
45
|
+
// the values must be byte-compatible with what any zip tool would report.
|
|
46
|
+
class Crc32 {
|
|
47
|
+
public:
|
|
48
|
+
void Update(const uint8_t* data, size_t length);
|
|
49
|
+
|
|
50
|
+
uint32_t Value() const { return state_ ^ 0xFFFFFFFFu; }
|
|
51
|
+
|
|
52
|
+
private:
|
|
53
|
+
uint32_t state_ = 0xFFFFFFFFu;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// CRC32 of a file on disk (streaming, bounded memory). Returns false if the
|
|
57
|
+
// file cannot be read.
|
|
58
|
+
bool Crc32File(const std::string& path, uint32_t* out);
|
|
59
|
+
|
|
60
|
+
} // namespace digest
|
|
61
|
+
} // namespace pushy
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#include "patch_core.h"
|
|
2
2
|
|
|
3
|
+
#include "digest.h"
|
|
4
|
+
|
|
3
5
|
#include <cerrno>
|
|
4
6
|
#include <cstdio>
|
|
5
7
|
#include <cstring>
|
|
@@ -532,8 +534,22 @@ Status ApplyPatchFromFileSource(
|
|
|
532
534
|
}
|
|
533
535
|
|
|
534
536
|
for (const CopyOperation& copy : options.manifest.copies) {
|
|
537
|
+
const std::string source_path = JoinPath(options.source_root, copy.from);
|
|
538
|
+
if (copy.has_expected_crc) {
|
|
539
|
+
uint32_t actual_crc = 0;
|
|
540
|
+
if (!digest::Crc32File(source_path, &actual_crc)) {
|
|
541
|
+
return Status::Error("Failed to read copy source " + source_path);
|
|
542
|
+
}
|
|
543
|
+
if (actual_crc != copy.expected_crc) {
|
|
544
|
+
// Rebuilt binary with drifted content at an unchanged path. Fail the
|
|
545
|
+
// patch — the caller falls back to the full package; never install a
|
|
546
|
+
// resource whose bytes differ from what the diff was generated against.
|
|
547
|
+
return Status::Error(
|
|
548
|
+
"Copy source content mismatch (crc32): " + copy.from);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
535
551
|
Status copy_status = CopyFile(
|
|
536
|
-
|
|
552
|
+
source_path,
|
|
537
553
|
JoinPath(options.target_root, copy.to),
|
|
538
554
|
true);
|
|
539
555
|
if (!copy_status) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
+
#include <cstdint>
|
|
3
4
|
#include <ctime>
|
|
4
5
|
#include <string>
|
|
5
6
|
#include <vector>
|
|
@@ -20,6 +21,13 @@ struct Status {
|
|
|
20
21
|
struct CopyOperation {
|
|
21
22
|
std::string from;
|
|
22
23
|
std::string to;
|
|
24
|
+
// Manifest-declared CRC32 of the source content (__diff.json copiesCrc,
|
|
25
|
+
// present in pdiff manifests from CLI >= 2.21.2). When set, the copy source
|
|
26
|
+
// is verified before copying; a mismatch fails the whole patch so the JS
|
|
27
|
+
// strategy chain falls back to the full package instead of installing a
|
|
28
|
+
// wrong resource from a rebuilt binary.
|
|
29
|
+
bool has_expected_crc = false;
|
|
30
|
+
uint32_t expected_crc = 0;
|
|
23
31
|
};
|
|
24
32
|
|
|
25
33
|
struct PatchManifest {
|