react-native-update 10.49.0 → 10.51.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 +22 -2
- package/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
- package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
- package/README-CN.md +14 -1
- package/README.md +14 -1
- package/android/jni/Android.mk +5 -1
- 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 +98 -22
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
- package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +16 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +196 -42
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +56 -3
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/cpp/patch_core/digest.cpp +53 -0
- package/cpp/patch_core/digest.h +18 -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 +99 -0
- package/cpp/update_flow_core/flow_json.cpp +522 -0
- package/cpp/update_flow_core/flow_json.h +103 -0
- package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
- package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
- package/cpp/update_flow_core/update_flow_core.cpp +301 -0
- package/cpp/update_flow_core/update_flow_core.h +77 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +150 -1
- package/harmony/pushy/src/main/ets/DownloadTask.ts +133 -9
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
- package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +16 -0
- package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +152 -52
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +840 -68
- package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
- package/package.json +3 -1
- package/react-native-update.podspec +2 -0
- package/src/NativePushy.ts +14 -0
- package/src/client.ts +439 -122
- package/src/endpoint.ts +105 -80
- package/src/error.ts +4 -0
- package/src/locales/en.ts +4 -0
- package/src/locales/zh.ts +4 -0
- package/src/provider.tsx +74 -9
- package/src/telemetry.ts +8 -0
- package/src/type.ts +31 -0
- package/src/updateFlowCore.ts +320 -0
- package/src/utils.ts +14 -12
- package/src/isInRollout.ts +0 -86
- package/src/resolveCheckResult.ts +0 -30
|
@@ -47,6 +47,17 @@ public class UpdateContext {
|
|
|
47
47
|
private static final int STATE_OP_CLEAR_ROLLBACK_MARK = 5;
|
|
48
48
|
private static final int STATE_OP_RESOLVE_LAUNCH = 6;
|
|
49
49
|
private static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
|
|
50
|
+
static final String VERSION_COMPLETE_FILE = ".pushy-complete";
|
|
51
|
+
// Bumped by resetToPackagedBundle. The cold-start check runs for minutes
|
|
52
|
+
// and may already hold a decision when the app resets to the packaged
|
|
53
|
+
// bundle; the orchestrator samples this counter and abandons activation
|
|
54
|
+
// (and its response cache) when the value moved, so an in-flight rescue
|
|
55
|
+
// can never resurrect the version the app just reset away from.
|
|
56
|
+
private static final java.util.concurrent.atomic.AtomicLong resetGeneration =
|
|
57
|
+
new java.util.concurrent.atomic.AtomicLong(0);
|
|
58
|
+
// Held by resetToPackagedBundle and by the cold-start check's commit, so
|
|
59
|
+
// the generation check and the writes it guards are one atomic step.
|
|
60
|
+
private static final Object commitLock = new Object();
|
|
50
61
|
|
|
51
62
|
// Singleton instance
|
|
52
63
|
private static volatile UpdateContext sInstance;
|
|
@@ -159,7 +170,9 @@ public class UpdateContext {
|
|
|
159
170
|
});
|
|
160
171
|
}
|
|
161
172
|
|
|
162
|
-
private
|
|
173
|
+
// Package-private: also the native cold-start check's request input
|
|
174
|
+
// (NativeCheckOrchestrator). Blocking — call off the main thread.
|
|
175
|
+
String computeBundleHash() {
|
|
163
176
|
String cachePrefix = getPackageVersion() + "|" + getPackageLastUpdateTime() + "|";
|
|
164
177
|
String cached = sp.getString(KEY_BUNDLE_HASH_CACHE, null);
|
|
165
178
|
if (cached != null && cached.startsWith(cachePrefix)) {
|
|
@@ -211,23 +224,57 @@ public class UpdateContext {
|
|
|
211
224
|
executor.execute(new DownloadTask(context, params));
|
|
212
225
|
}
|
|
213
226
|
|
|
227
|
+
// Server-provided identifiers (hash/originHash/fileName) become child
|
|
228
|
+
// names under rootDir; anything that could resolve outside of it (path
|
|
229
|
+
// separators, "..", ".") must be rejected before touching the filesystem.
|
|
230
|
+
static boolean isSafePathComponent(String name) {
|
|
231
|
+
return name != null
|
|
232
|
+
&& !name.isEmpty()
|
|
233
|
+
&& !name.equals(".")
|
|
234
|
+
&& !name.equals("..")
|
|
235
|
+
&& !name.contains("/")
|
|
236
|
+
&& !name.contains("\\")
|
|
237
|
+
&& name.indexOf('\0') < 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private static boolean rejectUnsafeComponent(String name, DownloadFileListener listener) {
|
|
241
|
+
if (isSafePathComponent(name)) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
listener.onDownloadFailed(new IllegalArgumentException("Invalid path component: " + name));
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
214
248
|
public interface DownloadFileListener {
|
|
215
249
|
void onDownloadCompleted(DownloadTaskParams params);
|
|
216
250
|
void onDownloadFailed(Throwable error);
|
|
217
251
|
}
|
|
218
252
|
|
|
219
253
|
public void downloadFullUpdate(String url, String hash, DownloadFileListener listener) {
|
|
254
|
+
downloadFullUpdate(url, hash, listener, 0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
void downloadFullUpdate(
|
|
258
|
+
String url, String hash, DownloadFileListener listener, long deadlineNanos
|
|
259
|
+
) {
|
|
260
|
+
if (rejectUnsafeComponent(hash, listener)) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
220
263
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
221
264
|
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FULL;
|
|
222
265
|
params.url = url;
|
|
223
266
|
params.hash = hash;
|
|
224
267
|
params.listener = listener;
|
|
268
|
+
params.deadlineNanos = deadlineNanos;
|
|
225
269
|
params.targetFile = new File(rootDir, hash + ".ppk");
|
|
226
270
|
params.unzipDirectory = new File(rootDir, hash);
|
|
227
271
|
enqueue(params);
|
|
228
272
|
}
|
|
229
273
|
|
|
230
274
|
public void downloadFile(String url, String hash, String fileName, DownloadFileListener listener) {
|
|
275
|
+
if (rejectUnsafeComponent(fileName, listener)) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
231
278
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
232
279
|
params.type = DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD;
|
|
233
280
|
params.url = url;
|
|
@@ -246,23 +293,47 @@ public class UpdateContext {
|
|
|
246
293
|
}
|
|
247
294
|
|
|
248
295
|
public void downloadPatchFromApk(String url, String hash, DownloadFileListener listener) {
|
|
296
|
+
downloadPatchFromApk(url, hash, listener, 0);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
void downloadPatchFromApk(
|
|
300
|
+
String url, String hash, DownloadFileListener listener, long deadlineNanos
|
|
301
|
+
) {
|
|
302
|
+
if (rejectUnsafeComponent(hash, listener)) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
249
305
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
250
306
|
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK;
|
|
251
307
|
params.url = url;
|
|
252
308
|
params.hash = hash;
|
|
253
309
|
params.listener = listener;
|
|
310
|
+
params.deadlineNanos = deadlineNanos;
|
|
254
311
|
params.targetFile = new File(rootDir, hash + ".apk.patch");
|
|
255
312
|
params.unzipDirectory = new File(rootDir, hash);
|
|
256
313
|
enqueue(params);
|
|
257
314
|
}
|
|
258
315
|
|
|
259
316
|
public void downloadPatchFromPpk(String url, String hash, String originHash, DownloadFileListener listener) {
|
|
317
|
+
downloadPatchFromPpk(url, hash, originHash, listener, 0);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
void downloadPatchFromPpk(
|
|
321
|
+
String url,
|
|
322
|
+
String hash,
|
|
323
|
+
String originHash,
|
|
324
|
+
DownloadFileListener listener,
|
|
325
|
+
long deadlineNanos
|
|
326
|
+
) {
|
|
327
|
+
if (rejectUnsafeComponent(hash, listener) || rejectUnsafeComponent(originHash, listener)) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
260
330
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
261
331
|
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
|
|
262
332
|
params.url = url;
|
|
263
333
|
params.hash = hash;
|
|
264
334
|
params.originHash = originHash;
|
|
265
335
|
params.listener = listener;
|
|
336
|
+
params.deadlineNanos = deadlineNanos;
|
|
266
337
|
params.targetFile = new File(rootDir, originHash + "-" + hash + ".ppk.patch");
|
|
267
338
|
params.unzipDirectory = new File(rootDir, hash);
|
|
268
339
|
params.originDirectory = new File(rootDir, originHash);
|
|
@@ -312,10 +383,24 @@ public class UpdateContext {
|
|
|
312
383
|
}
|
|
313
384
|
|
|
314
385
|
public void switchVersion(String hash) {
|
|
315
|
-
if (!
|
|
386
|
+
if (!isSafePathComponent(hash)) {
|
|
387
|
+
throw new IllegalArgumentException("Invalid hash: " + hash);
|
|
388
|
+
}
|
|
389
|
+
File versionDir = new File(rootDir, hash);
|
|
390
|
+
File bundleFile = new File(versionDir, "index.bundlejs");
|
|
391
|
+
if (!bundleFile.isFile()) {
|
|
316
392
|
throw new IllegalStateException("Bundle version " + hash + " not found.");
|
|
317
393
|
}
|
|
318
394
|
StateCoreResult currentState = getStateSnapshot();
|
|
395
|
+
boolean isLegacyActivatedVersion = hash.equals(currentState.currentVersion)
|
|
396
|
+
|| hash.equals(currentState.lastVersion);
|
|
397
|
+
if (!new File(versionDir, VERSION_COMPLETE_FILE).isFile()
|
|
398
|
+
&& !isLegacyActivatedVersion) {
|
|
399
|
+
// Versions activated before completion markers were introduced are
|
|
400
|
+
// explicitly grandfathered through current/last state. An arbitrary
|
|
401
|
+
// markerless directory may be a crash-left partial install.
|
|
402
|
+
throw new IllegalStateException("Bundle version " + hash + " is incomplete.");
|
|
403
|
+
}
|
|
319
404
|
StateCoreResult nextState = runStateCore(
|
|
320
405
|
STATE_OP_SWITCH_VERSION,
|
|
321
406
|
currentState,
|
|
@@ -409,6 +494,16 @@ public class UpdateContext {
|
|
|
409
494
|
* for gray release bucketing and must not change on reset.
|
|
410
495
|
*/
|
|
411
496
|
public void resetToPackagedBundle() {
|
|
497
|
+
synchronized (commitLock) {
|
|
498
|
+
resetToPackagedBundleLocked();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private void resetToPackagedBundleLocked() {
|
|
503
|
+
// Invalidate any in-flight cold-start round before clearing state: a
|
|
504
|
+
// round committing under the same lock afterwards sees the new
|
|
505
|
+
// generation and drops its result.
|
|
506
|
+
resetGeneration.incrementAndGet();
|
|
412
507
|
StateCoreResult resetState = new StateCoreResult();
|
|
413
508
|
resetState.packageVersion = getPackageVersion();
|
|
414
509
|
resetState.buildTime = getBuildTime();
|
|
@@ -423,6 +518,8 @@ public class UpdateContext {
|
|
|
423
518
|
}
|
|
424
519
|
persistEditor(editor, "reset to packaged bundle");
|
|
425
520
|
ignoreRollback = false;
|
|
521
|
+
// editor.clear() above already dropped the cached check response; it
|
|
522
|
+
// still advertised the version this reset removed.
|
|
426
523
|
Log.i(TAG, "Reset to packaged bundle");
|
|
427
524
|
|
|
428
525
|
DownloadTaskParams params = new DownloadTaskParams();
|
|
@@ -494,55 +591,112 @@ public class UpdateContext {
|
|
|
494
591
|
|
|
495
592
|
public String getBundleUrl(String defaultAssetsUrl) {
|
|
496
593
|
isUsingBundleUrl = true;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
594
|
+
String nativeCheckRolledBackVersion = null;
|
|
595
|
+
try {
|
|
596
|
+
StateCoreResult currentState = getStateSnapshot();
|
|
597
|
+
StateCoreResult launchState = runStateCore(
|
|
598
|
+
STATE_OP_RESOLVE_LAUNCH,
|
|
599
|
+
currentState,
|
|
600
|
+
null,
|
|
601
|
+
ignoreRollback,
|
|
602
|
+
true
|
|
603
|
+
);
|
|
604
|
+
nativeCheckRolledBackVersion = launchState.rolledBackVersion;
|
|
605
|
+
if (launchState.didRollback) {
|
|
606
|
+
// The crash-protection rollback: the new version never called
|
|
607
|
+
// markSuccess. Keep this visible in release logs.
|
|
608
|
+
Log.e(TAG, "Version " + currentState.currentVersion
|
|
609
|
+
+ " was not marked as successful, rolling back to "
|
|
610
|
+
+ launchState.currentVersion);
|
|
611
|
+
}
|
|
612
|
+
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
613
|
+
SharedPreferences.Editor editor = sp.edit();
|
|
614
|
+
applyState(editor, launchState);
|
|
615
|
+
if (launchState.consumedFirstTime) {
|
|
616
|
+
editor.putBoolean(KEY_FIRST_LOAD_MARKED, true);
|
|
617
|
+
}
|
|
618
|
+
persistEditor(editor, "resolve launch");
|
|
619
|
+
}
|
|
515
620
|
if (launchState.consumedFirstTime) {
|
|
516
|
-
|
|
621
|
+
// bundleURL may be resolved multiple times in one process.
|
|
622
|
+
ignoreRollback = true;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
String currentVersion = launchState.loadVersion;
|
|
626
|
+
if (currentVersion == null) {
|
|
627
|
+
return defaultAssetsUrl;
|
|
517
628
|
}
|
|
518
|
-
persistEditor(editor, "resolve launch");
|
|
519
|
-
}
|
|
520
|
-
if (launchState.consumedFirstTime) {
|
|
521
|
-
// bundleURL may be resolved multiple times in one process.
|
|
522
|
-
ignoreRollback = true;
|
|
523
|
-
}
|
|
524
629
|
|
|
525
|
-
|
|
526
|
-
|
|
630
|
+
// Guard the rollback chain against cycles: a corrupted state returning
|
|
631
|
+
// an already-visited version would otherwise spin this loop forever on
|
|
632
|
+
// the main thread.
|
|
633
|
+
java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
|
|
634
|
+
while (currentVersion != null && visitedVersions.add(currentVersion)) {
|
|
635
|
+
File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
|
|
636
|
+
if (!bundleFile.exists()) {
|
|
637
|
+
Log.e(TAG, "Bundle version " + currentVersion + " not found.");
|
|
638
|
+
currentVersion = this.rollBack();
|
|
639
|
+
nativeCheckRolledBackVersion = rolledBackVersion();
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
launchVersion = currentVersion;
|
|
643
|
+
nativeCheckRolledBackVersion = rolledBackVersion();
|
|
644
|
+
return bundleFile.toString();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
nativeCheckRolledBackVersion = rolledBackVersion();
|
|
527
648
|
return defaultAssetsUrl;
|
|
649
|
+
} finally {
|
|
650
|
+
// Even corrupted state or a state-core exception must not disable
|
|
651
|
+
// the next-launch rescue check. A null snapshot simply omits the
|
|
652
|
+
// rollback guard for this exceptional launch.
|
|
653
|
+
NativeCheckOrchestrator.schedule(this, nativeCheckRolledBackVersion);
|
|
528
654
|
}
|
|
655
|
+
}
|
|
529
656
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
657
|
+
/** Sampled/compared by the native check orchestrator; see resetGeneration. */
|
|
658
|
+
static long getResetGeneration() {
|
|
659
|
+
return resetGeneration.get();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Commit everything a cold-start round persists — version info, the
|
|
664
|
+
* activation, the response cache — under one lock that first re-checks the
|
|
665
|
+
* reset generation. resetToPackagedBundle takes the same lock, so there is
|
|
666
|
+
* no compare-and-act window: either the whole round lands, or the reset
|
|
667
|
+
* wins and none of it does. Returns whether the round was committed.
|
|
668
|
+
*/
|
|
669
|
+
boolean commitNativeCheckResult(
|
|
670
|
+
long expectedGeneration,
|
|
671
|
+
String hash,
|
|
672
|
+
String hashInfoJson,
|
|
673
|
+
boolean activate,
|
|
674
|
+
String responseCacheJson
|
|
675
|
+
) {
|
|
676
|
+
synchronized (commitLock) {
|
|
677
|
+
if (resetGeneration.get() != expectedGeneration) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
if (hash != null && hashInfoJson != null) {
|
|
681
|
+
setKv("hash_" + hash, hashInfoJson);
|
|
682
|
+
}
|
|
683
|
+
if (activate && hash != null) {
|
|
684
|
+
switchVersion(hash);
|
|
540
685
|
}
|
|
541
|
-
|
|
542
|
-
|
|
686
|
+
if (responseCacheJson != null) {
|
|
687
|
+
setKv(NativeCheckOrchestrator.KEY_RESP_CACHE, responseCacheJson);
|
|
688
|
+
}
|
|
689
|
+
return true;
|
|
543
690
|
}
|
|
691
|
+
}
|
|
544
692
|
|
|
545
|
-
|
|
693
|
+
boolean hasCompletedVersion(String hash) {
|
|
694
|
+
if (!isSafePathComponent(hash)) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
File versionDir = new File(rootDir, hash);
|
|
698
|
+
return new File(versionDir, "index.bundlejs").isFile()
|
|
699
|
+
&& new File(versionDir, VERSION_COMPLETE_FILE).isFile();
|
|
546
700
|
}
|
|
547
701
|
|
|
548
702
|
private String rollBack() {
|
|
@@ -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) {
|
|
@@ -215,10 +224,54 @@ public class UpdateModuleImpl {
|
|
|
215
224
|
});
|
|
216
225
|
}
|
|
217
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Raw response cached by the native cold-start check, for the JS side to
|
|
229
|
+
* reuse instead of re-checking (§10.3). Empty string when absent; never
|
|
230
|
+
* rejects.
|
|
231
|
+
*/
|
|
232
|
+
public static void getNativeCheckCache(
|
|
233
|
+
final UpdateContext updateContext,
|
|
234
|
+
final Promise promise
|
|
235
|
+
) {
|
|
236
|
+
String cached = updateContext.getKv(NativeCheckOrchestrator.KEY_RESP_CACHE);
|
|
237
|
+
promise.resolve(cached == null ? "" : cached);
|
|
238
|
+
}
|
|
239
|
+
|
|
218
240
|
private static void setUuidInternal(UpdateContext updateContext, String uuid) {
|
|
219
241
|
updateContext.setKv("uuid", uuid);
|
|
220
242
|
}
|
|
221
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Provisioning for the native cold-start update check
|
|
246
|
+
* (NATIVE_CHECKUPDATE_DESIGN §10.1): the raw JSON persists as-is and is
|
|
247
|
+
* parsed on read by the orchestrator; absent config = check disabled.
|
|
248
|
+
* Validated at write time — a corrupt config would otherwise silently
|
|
249
|
+
* disable the native check forever with no signal.
|
|
250
|
+
*/
|
|
251
|
+
public static void syncNativeConfig(
|
|
252
|
+
final UpdateContext updateContext,
|
|
253
|
+
final String config,
|
|
254
|
+
final Promise promise
|
|
255
|
+
) {
|
|
256
|
+
if (config == null || config.isEmpty()) {
|
|
257
|
+
promise.reject(ErrorCodes.INVALID_OPTIONS, "config must be a JSON object string");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
new JSONObject(config);
|
|
262
|
+
} catch (JSONException e) {
|
|
263
|
+
promise.reject(ErrorCodes.INVALID_OPTIONS, "config must be a JSON object string", e);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
StateSerialRunner.run(promise, ErrorCodes.FILE_OPERATION_FAILED, "syncNativeConfig", new StateSerialRunner.Operation() {
|
|
267
|
+
@Override
|
|
268
|
+
public void run() {
|
|
269
|
+
updateContext.setKv(NativeCheckOrchestrator.KEY_CONFIG, config);
|
|
270
|
+
promise.resolve(true);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
222
275
|
public static void setUuid(
|
|
223
276
|
final UpdateContext updateContext,
|
|
224
277
|
final String uuid,
|
|
@@ -88,6 +88,16 @@ public class UpdateModule extends NativePushySpec {
|
|
|
88
88
|
UpdateModuleImpl.setUuid(updateContext, uuid, promise);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
@Override
|
|
92
|
+
public void syncNativeConfig(String config, Promise promise) {
|
|
93
|
+
UpdateModuleImpl.syncNativeConfig(updateContext, config, promise);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Override
|
|
97
|
+
public void getNativeCheckCache(Promise promise) {
|
|
98
|
+
UpdateModuleImpl.getNativeCheckCache(updateContext, promise);
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
@Override
|
|
92
102
|
public void setLocalHashInfo(String hash, String info, Promise promise) {
|
|
93
103
|
UpdateModuleImpl.setLocalHashInfo(updateContext, hash, info, promise);
|
|
@@ -90,6 +90,16 @@ public class UpdateModule extends ReactContextBaseJavaModule {
|
|
|
90
90
|
UpdateModuleImpl.setUuid(updateContext, uuid);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
@ReactMethod
|
|
94
|
+
public void syncNativeConfig(String config, Promise promise) {
|
|
95
|
+
UpdateModuleImpl.syncNativeConfig(updateContext, config, promise);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@ReactMethod
|
|
99
|
+
public void getNativeCheckCache(Promise promise) {
|
|
100
|
+
UpdateModuleImpl.getNativeCheckCache(updateContext, promise);
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
@ReactMethod
|
|
94
104
|
public void setLocalHashInfo(String hash, String info) {
|
|
95
105
|
UpdateModuleImpl.setLocalHashInfo(updateContext, hash, info);
|
|
@@ -173,5 +173,58 @@ std::string Sha256File(const std::string& path) {
|
|
|
173
173
|
return hasher.HexDigest();
|
|
174
174
|
}
|
|
175
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
|
+
|
|
176
229
|
} // namespace digest
|
|
177
230
|
} // namespace pushy
|
package/cpp/patch_core/digest.h
CHANGED
|
@@ -39,5 +39,23 @@ class Sha256 {
|
|
|
39
39
|
// digest, or an empty string if the file cannot be read.
|
|
40
40
|
std::string Sha256File(const std::string& path);
|
|
41
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
|
+
|
|
42
60
|
} // namespace digest
|
|
43
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 {
|