react-native-update 10.53.1 → 10.54.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/README-CN.md +31 -31
- package/README.md +31 -31
- package/android/jni/HDiffPatch/.travis.yml +33 -0
- package/android/jni/HDiffPatch/builds/xcode/bestParams.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/android/jni/HDiffPatch/builds/xcode/hpatchz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/lz4.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/lzma.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/android/jni/HDiffPatch/builds/xcode/unitTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/HDiffPatch/builds/xcode/zstd.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/android/jni/lzma/DOC/7zC.txt +187 -0
- package/android/jni/lzma/DOC/7zFormat.txt +469 -0
- package/android/jni/lzma/DOC/Methods.txt +173 -0
- package/android/jni/lzma/DOC/installer.txt +166 -0
- package/android/jni/lzma/DOC/lzma-history.txt +446 -0
- package/android/jni/lzma/DOC/lzma-sdk.txt +357 -0
- package/android/jni/lzma/DOC/lzma-specification.txt +1176 -0
- package/android/jni/lzma/DOC/lzma.txt +328 -0
- package/android/jni/lzma/bin/7zS2.sfx +0 -0
- package/android/jni/lzma/bin/7zS2con.sfx +0 -0
- package/android/jni/lzma/bin/7zSD.sfx +0 -0
- package/android/jni/lzma/bin/7zdec.exe +0 -0
- package/android/jni/lzma/bin/7zr.exe +0 -0
- package/android/jni/lzma/bin/installer/config.txt +5 -0
- package/android/jni/lzma/bin/installer/cr.bat +5 -0
- package/android/jni/lzma/bin/lzma.exe +0 -0
- package/android/jni/lzma/bin/x64/7zr.exe +0 -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/ArchiveLimits.java +44 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +124 -40
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +125 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +43 -11
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +2 -0
- package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +90 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +79 -19
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +21 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +33 -2
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +1 -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/cpp/patch_core/archive_limits.h +38 -0
- package/cpp/patch_core/install_record.h +36 -0
- package/cpp/update_flow_core/flow_json.h +1 -0
- package/cpp/update_flow_core/update_flow_core.cpp +41 -7
- package/cpp/update_flow_core/update_flow_core.h +9 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +12 -0
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +2 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +37 -0
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +83 -0
- package/harmony/pushy/src/main/ets/DownloadTask.ts +126 -30
- package/harmony/pushy/src/main/ets/InstallRecord.ts +129 -0
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +24 -2
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +5 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +78 -15
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +384 -25
- package/ios/RCTPushy/RCTPushyDownloader.mm +32 -0
- package/package.json +25 -2
- package/src/NativePushy.ts +13 -0
- package/src/client.ts +160 -43
- package/src/core.ts +17 -3
- package/src/error.ts +3 -0
- package/src/index.ts +8 -0
- package/src/locales/en.ts +3 -0
- package/src/locales/zh.ts +2 -0
- package/src/metadata.ts +166 -0
- package/src/provider.tsx +52 -17
- package/src/type.ts +22 -0
- package/src/updateFlowCore.ts +95 -4
- package/src/utils.ts +31 -19
- package/.gitmodules +0 -6
- package/BUNDLEHASH_MIGRATION.md +0 -355
- package/NATIVE_CHECKUPDATE_DESIGN.md +0 -539
- package/NATIVE_CHECK_FOLLOWUPS.md +0 -514
- package/android/jni/HDiffPatch/test/_private_searchBestParams.cpp +0 -301
- package/android/jni/HDiffPatch/test/hpatchz_test.cpp +0 -292
- package/android/jni/HDiffPatch/test/testHashClash.cpp +0 -436
- package/android/jni/HDiffPatch/test/unit_test.cpp +0 -417
- package/biome.json +0 -59
- package/cpp/patch_core/tests/fixtures/rawstored.lzma2label.patch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/rawstored.new.bin +0 -0
- package/cpp/patch_core/tests/fixtures/rawstored.old.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v96.meta.json +0 -1
- package/cpp/patch_core/tests/fixtures/v96.streampatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.t.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v96.tpatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96.tstreampatch.bin +0 -0
- package/cpp/patch_core/tests/fixtures/v96b.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98.t.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98b.hbc +0 -0
- package/cpp/patch_core/tests/fixtures/v98b.t.hbc +0 -0
- package/cpp/patch_core/tests/hbc_transform_test.cpp +0 -384
- package/cpp/patch_core/tests/patch_core_test.cpp +0 -970
- package/cpp/update_flow_core/tests/flow_vectors.json +0 -1401
- package/cpp/update_flow_core/tests/update_flow_core_test.cpp +0 -317
- package/scripts/check-expo-version.js +0 -92
|
@@ -8,6 +8,7 @@ import android.os.Build;
|
|
|
8
8
|
import android.util.Log;
|
|
9
9
|
import com.facebook.react.ReactInstanceManager;
|
|
10
10
|
import java.io.File;
|
|
11
|
+
import org.json.JSONObject;
|
|
11
12
|
import java.io.IOException;
|
|
12
13
|
import java.io.InputStream;
|
|
13
14
|
import java.security.MessageDigest;
|
|
@@ -46,7 +47,7 @@ public class UpdateContext {
|
|
|
46
47
|
private static final int STATE_OP_CLEAR_ROLLBACK_MARK = 5;
|
|
47
48
|
private static final int STATE_OP_RESOLVE_LAUNCH = 6;
|
|
48
49
|
private static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
|
|
49
|
-
static final String VERSION_COMPLETE_FILE =
|
|
50
|
+
static final String VERSION_COMPLETE_FILE = InstallRecord.FILE_NAME;
|
|
50
51
|
// Bumped by resetToPackagedBundle. The cold-start check runs for minutes
|
|
51
52
|
// and may already hold a decision when the app resets to the packaged
|
|
52
53
|
// bundle; the orchestrator samples this counter and abandons activation
|
|
@@ -367,15 +368,39 @@ public class UpdateContext {
|
|
|
367
368
|
putNullableString(editor, "rolledBackVersion", state.rolledBackVersion);
|
|
368
369
|
}
|
|
369
370
|
|
|
370
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Best-effort persistence for launch-path bookkeeping (binary-version
|
|
373
|
+
* sync, first-load markers, rollback resolution): a failure is logged but
|
|
374
|
+
* must not take the app down while it is resolving which bundle to run.
|
|
375
|
+
*/
|
|
376
|
+
private boolean persistEditor(SharedPreferences.Editor editor, String reason) {
|
|
371
377
|
// A lost state write can mean a missed rollback or a version switch
|
|
372
378
|
// that silently never happens, so this must be visible in release too.
|
|
373
379
|
if (!editor.commit()) {
|
|
374
380
|
Log.e(TAG, "Failed to persist update state for " + reason);
|
|
381
|
+
return false;
|
|
375
382
|
}
|
|
383
|
+
return true;
|
|
376
384
|
}
|
|
377
385
|
|
|
378
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Persistence for operations whose result is promised to a caller
|
|
388
|
+
* (switchVersion / markSuccess / setLocalHashInfo / the cold-start round):
|
|
389
|
+
* a commit that did not reach disk is a failed operation, never a
|
|
390
|
+
* resolved promise whose effect silently evaporates on the next launch.
|
|
391
|
+
*/
|
|
392
|
+
private void persistEditorOrThrow(SharedPreferences.Editor editor, String reason) {
|
|
393
|
+
if (!persistEditor(editor, reason)) {
|
|
394
|
+
throw new IllegalStateException("Failed to persist update state for " + reason);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Validates that {@code hash} is a switchable version and computes the
|
|
400
|
+
* resulting state without persisting it, so callers can fold the switch
|
|
401
|
+
* into a larger single-commit transaction.
|
|
402
|
+
*/
|
|
403
|
+
private StateCoreResult computeSwitchVersion(String hash) {
|
|
379
404
|
if (!isSafePathComponent(hash)) {
|
|
380
405
|
throw new IllegalArgumentException("Invalid hash: " + hash);
|
|
381
406
|
}
|
|
@@ -387,31 +412,42 @@ public class UpdateContext {
|
|
|
387
412
|
StateCoreResult currentState = getStateSnapshot();
|
|
388
413
|
boolean isLegacyActivatedVersion = hash.equals(currentState.currentVersion)
|
|
389
414
|
|| hash.equals(currentState.lastVersion);
|
|
390
|
-
if (!
|
|
391
|
-
&& !isLegacyActivatedVersion) {
|
|
415
|
+
if (!InstallRecord.isComplete(versionDir, hash) && !isLegacyActivatedVersion) {
|
|
392
416
|
// Versions activated before completion markers were introduced are
|
|
393
417
|
// explicitly grandfathered through current/last state. An arbitrary
|
|
394
418
|
// markerless directory may be a crash-left partial install.
|
|
395
419
|
throw new IllegalStateException("Bundle version " + hash + " is incomplete.");
|
|
396
420
|
}
|
|
397
|
-
|
|
421
|
+
if (!isLegacyActivatedVersion) {
|
|
422
|
+
// The record's bundle digest must match the bytes on disk before
|
|
423
|
+
// the next launch is pointed at them.
|
|
424
|
+
try {
|
|
425
|
+
InstallRecord.verifyForActivation(versionDir, hash, bundleFile);
|
|
426
|
+
} catch (IOException e) {
|
|
427
|
+
throw new IllegalStateException(e.getMessage(), e);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return runStateCore(
|
|
398
431
|
STATE_OP_SWITCH_VERSION,
|
|
399
432
|
currentState,
|
|
400
|
-
hash
|
|
401
|
-
,
|
|
433
|
+
hash,
|
|
402
434
|
false,
|
|
403
435
|
false
|
|
404
436
|
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
public void switchVersion(String hash) {
|
|
440
|
+
StateCoreResult nextState = computeSwitchVersion(hash);
|
|
405
441
|
SharedPreferences.Editor editor = sp.edit();
|
|
406
442
|
applyState(editor, nextState);
|
|
407
|
-
|
|
443
|
+
persistEditorOrThrow(editor, "switch version");
|
|
408
444
|
ignoreRollback = false;
|
|
409
445
|
}
|
|
410
446
|
|
|
411
447
|
public void setKv(String key, String value) {
|
|
412
448
|
SharedPreferences.Editor editor = sp.edit();
|
|
413
449
|
editor.putString(key, value);
|
|
414
|
-
|
|
450
|
+
persistEditorOrThrow(editor, "set key " + key);
|
|
415
451
|
}
|
|
416
452
|
|
|
417
453
|
public String getKv(String key) {
|
|
@@ -421,7 +457,7 @@ public class UpdateContext {
|
|
|
421
457
|
void removeKv(String key) {
|
|
422
458
|
SharedPreferences.Editor editor = sp.edit();
|
|
423
459
|
editor.remove(key);
|
|
424
|
-
|
|
460
|
+
persistEditorOrThrow(editor, "remove key " + key);
|
|
425
461
|
}
|
|
426
462
|
|
|
427
463
|
public String getCurrentVersion() {
|
|
@@ -461,7 +497,7 @@ public class UpdateContext {
|
|
|
461
497
|
if (nextState.staleVersionToDelete != null) {
|
|
462
498
|
editor.remove("hash_" + nextState.staleVersionToDelete);
|
|
463
499
|
}
|
|
464
|
-
|
|
500
|
+
persistEditorOrThrow(editor, "mark success");
|
|
465
501
|
|
|
466
502
|
this.cleanUp();
|
|
467
503
|
}
|
|
@@ -479,7 +515,7 @@ public class UpdateContext {
|
|
|
479
515
|
SharedPreferences.Editor editor = sp.edit();
|
|
480
516
|
applyState(editor, nextState);
|
|
481
517
|
editor.remove(KEY_FIRST_LOAD_MARKED);
|
|
482
|
-
|
|
518
|
+
persistEditorOrThrow(editor, "clear first time");
|
|
483
519
|
|
|
484
520
|
this.cleanUp();
|
|
485
521
|
}
|
|
@@ -515,7 +551,7 @@ public class UpdateContext {
|
|
|
515
551
|
if (uuid != null) {
|
|
516
552
|
editor.putString("uuid", uuid);
|
|
517
553
|
}
|
|
518
|
-
|
|
554
|
+
persistEditorOrThrow(editor, "reset to packaged bundle");
|
|
519
555
|
ignoreRollback = false;
|
|
520
556
|
// editor.clear() above already dropped the cached check response; it
|
|
521
557
|
// still advertised the version this reset removed.
|
|
@@ -543,6 +579,7 @@ public class UpdateContext {
|
|
|
543
579
|
);
|
|
544
580
|
SharedPreferences.Editor editor = sp.edit();
|
|
545
581
|
applyState(editor, nextState);
|
|
582
|
+
// Runs from getConstants on the launch path: best-effort only.
|
|
546
583
|
persistEditor(editor, "clear rollback mark");
|
|
547
584
|
|
|
548
585
|
this.cleanUp();
|
|
@@ -676,26 +713,49 @@ public class UpdateContext {
|
|
|
676
713
|
if (resetGeneration.get() != expectedGeneration) {
|
|
677
714
|
return false;
|
|
678
715
|
}
|
|
716
|
+
// One editor, one commit: the version info, the activation and
|
|
717
|
+
// the response cache land together or not at all. Three separate
|
|
718
|
+
// commits under the same lock only prevented interleaving, not a
|
|
719
|
+
// half-written round after a failed or interrupted write.
|
|
720
|
+
SharedPreferences.Editor editor = sp.edit();
|
|
679
721
|
if (hash != null && hashInfoJson != null) {
|
|
680
|
-
|
|
722
|
+
editor.putString("hash_" + hash, hashInfoJson);
|
|
681
723
|
}
|
|
682
|
-
|
|
683
|
-
|
|
724
|
+
boolean switching = activate && hash != null;
|
|
725
|
+
if (switching) {
|
|
726
|
+
applyState(editor, computeSwitchVersion(hash));
|
|
684
727
|
}
|
|
685
728
|
if (responseCacheJson != null) {
|
|
686
|
-
|
|
729
|
+
editor.putString(NativeCheckOrchestrator.KEY_RESP_CACHE, responseCacheJson);
|
|
730
|
+
}
|
|
731
|
+
persistEditorOrThrow(editor, "commit native check result");
|
|
732
|
+
if (switching) {
|
|
733
|
+
ignoreRollback = false;
|
|
687
734
|
}
|
|
688
735
|
return true;
|
|
689
736
|
}
|
|
690
737
|
}
|
|
691
738
|
|
|
739
|
+
/**
|
|
740
|
+
* bundleSha256 from the running version's install record (see
|
|
741
|
+
* InstallRecord); "" for the embedded bundle, a legacy install or an
|
|
742
|
+
* unreadable record. Exposed to JS for crash-report attribution.
|
|
743
|
+
*/
|
|
744
|
+
String currentBundleSha256(String hash) {
|
|
745
|
+
if (hash == null || hash.isEmpty() || !isSafePathComponent(hash)) {
|
|
746
|
+
return "";
|
|
747
|
+
}
|
|
748
|
+
JSONObject record = InstallRecord.read(new File(rootDir, hash));
|
|
749
|
+
return record == null ? "" : record.optString("bundleSha256", "");
|
|
750
|
+
}
|
|
751
|
+
|
|
692
752
|
boolean hasCompletedVersion(String hash) {
|
|
693
753
|
if (!isSafePathComponent(hash)) {
|
|
694
754
|
return false;
|
|
695
755
|
}
|
|
696
756
|
File versionDir = new File(rootDir, hash);
|
|
697
757
|
return new File(versionDir, "index.bundlejs").isFile()
|
|
698
|
-
&&
|
|
758
|
+
&& InstallRecord.isComplete(versionDir, hash);
|
|
699
759
|
}
|
|
700
760
|
|
|
701
761
|
private String rollBack() {
|
|
@@ -43,6 +43,27 @@ final class UpdateFileUtils {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** Streaming SHA-256 of a file, lowercase hex. */
|
|
47
|
+
static String sha256Hex(File file) throws IOException {
|
|
48
|
+
try (InputStream in = new FileInputStream(file)) {
|
|
49
|
+
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
|
50
|
+
byte[] buffer = new byte[64 * 1024];
|
|
51
|
+
int read;
|
|
52
|
+
while ((read = in.read(buffer)) != -1) {
|
|
53
|
+
md.update(buffer, 0, read);
|
|
54
|
+
}
|
|
55
|
+
byte[] digest = md.digest();
|
|
56
|
+
StringBuilder hex = new StringBuilder(digest.length * 2);
|
|
57
|
+
for (byte b : digest) {
|
|
58
|
+
hex.append(Character.forDigit((b >> 4) & 0xf, 16));
|
|
59
|
+
hex.append(Character.forDigit(b & 0xf, 16));
|
|
60
|
+
}
|
|
61
|
+
return hex.toString();
|
|
62
|
+
} catch (java.security.NoSuchAlgorithmException e) {
|
|
63
|
+
throw new IOException("SHA-256 unavailable", e);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
static void copyFile(File from, File to) throws IOException {
|
|
47
68
|
ensureParentDirectory(to);
|
|
48
69
|
try (
|
|
@@ -258,6 +258,19 @@ public class UpdateModuleImpl {
|
|
|
258
258
|
* Validated at write time — a corrupt config would otherwise silently
|
|
259
259
|
* disable the native check forever with no signal.
|
|
260
260
|
*/
|
|
261
|
+
public static void markJsCheckCompleted(
|
|
262
|
+
final UpdateContext updateContext,
|
|
263
|
+
final String config,
|
|
264
|
+
final Promise promise
|
|
265
|
+
) {
|
|
266
|
+
if (config == null || config.isEmpty()) {
|
|
267
|
+
promise.reject(ErrorCodes.INVALID_OPTIONS, "config must be a non-empty string");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
NativeCheckOrchestrator.markJsCheckCompleted(config);
|
|
271
|
+
promise.resolve(true);
|
|
272
|
+
}
|
|
273
|
+
|
|
261
274
|
public static void syncNativeConfig(
|
|
262
275
|
final UpdateContext updateContext,
|
|
263
276
|
final String config,
|
|
@@ -313,7 +326,20 @@ public class UpdateModuleImpl {
|
|
|
313
326
|
if (!isValidHashInfo(info)) {
|
|
314
327
|
throw new IllegalArgumentException("invalid json string");
|
|
315
328
|
}
|
|
316
|
-
|
|
329
|
+
try {
|
|
330
|
+
updateContext.setKv("hash_" + hash, info);
|
|
331
|
+
} catch (IllegalStateException e) {
|
|
332
|
+
// Distinguish "could not persist" from "malformed input": JS
|
|
333
|
+
// classifies the former as FILE_OPERATION_FAILED.
|
|
334
|
+
throw new PersistenceException(e);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** A state write that did not reach disk (SharedPreferences commit false). */
|
|
339
|
+
static final class PersistenceException extends RuntimeException {
|
|
340
|
+
PersistenceException(Throwable cause) {
|
|
341
|
+
super(String.valueOf(cause.getMessage()), cause);
|
|
342
|
+
}
|
|
317
343
|
}
|
|
318
344
|
|
|
319
345
|
public static void setLocalHashInfo(
|
|
@@ -325,7 +351,12 @@ public class UpdateModuleImpl {
|
|
|
325
351
|
StateSerialRunner.run(promise, ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
326
352
|
@Override
|
|
327
353
|
public void run() {
|
|
328
|
-
|
|
354
|
+
try {
|
|
355
|
+
setLocalHashInfoInternal(updateContext, hash, info);
|
|
356
|
+
} catch (PersistenceException e) {
|
|
357
|
+
promise.reject(ErrorCodes.FILE_OPERATION_FAILED, "setLocalHashInfo failed", e);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
329
360
|
promise.resolve(true);
|
|
330
361
|
}
|
|
331
362
|
});
|
|
@@ -16,6 +16,7 @@ final class UpdateModuleSupport {
|
|
|
16
16
|
String currentVersion = updateContext.getCurrentVersion();
|
|
17
17
|
constants.put("currentVersion", currentVersion);
|
|
18
18
|
constants.put("currentVersionInfo", updateContext.getKv("hash_" + currentVersion));
|
|
19
|
+
constants.put("currentBundleSha256", updateContext.currentBundleSha256(currentVersion));
|
|
19
20
|
constants.put("buildTime", updateContext.getBuildTime());
|
|
20
21
|
constants.put("isUsingBundleUrl", updateContext.getIsUsingBundleUrl());
|
|
21
22
|
|
|
@@ -98,6 +98,11 @@ public class UpdateModule extends NativePushySpec {
|
|
|
98
98
|
UpdateModuleImpl.getNativeCheckCache(updateContext, promise);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
@Override
|
|
102
|
+
public void markJsCheckCompleted(String config, Promise promise) {
|
|
103
|
+
UpdateModuleImpl.markJsCheckCompleted(updateContext, config, promise);
|
|
104
|
+
}
|
|
105
|
+
|
|
101
106
|
@Override
|
|
102
107
|
public void setLocalHashInfo(String hash, String info, Promise promise) {
|
|
103
108
|
UpdateModuleImpl.setLocalHashInfo(updateContext, hash, info, promise);
|
|
@@ -100,6 +100,11 @@ public class UpdateModule extends ReactContextBaseJavaModule {
|
|
|
100
100
|
UpdateModuleImpl.getNativeCheckCache(updateContext, promise);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
@ReactMethod
|
|
104
|
+
public void markJsCheckCompleted(String config, Promise promise) {
|
|
105
|
+
UpdateModuleImpl.markJsCheckCompleted(updateContext, config, promise);
|
|
106
|
+
}
|
|
107
|
+
|
|
103
108
|
@ReactMethod
|
|
104
109
|
public void setLocalHashInfo(String hash, String info) {
|
|
105
110
|
UpdateModuleImpl.setLocalHashInfo(updateContext, hash, info);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#ifndef PUSHY_PATCH_CORE_ARCHIVE_LIMITS_H_
|
|
2
|
+
#define PUSHY_PATCH_CORE_ARCHIVE_LIMITS_H_
|
|
3
|
+
|
|
4
|
+
// Single source of truth for the resource caps applied to an update package
|
|
5
|
+
// before and while it is unpacked. A corrupt or hostile archive must be able
|
|
6
|
+
// to cost at most a bounded amount of disk, memory and time — never a full
|
|
7
|
+
// disk or a zip bomb. The caps are generous for real packages (a large full
|
|
8
|
+
// bundle with assets is tens of MB) and exist purely as damage bounds.
|
|
9
|
+
//
|
|
10
|
+
// Mirrors that cannot include this header MUST stay in sync by hand:
|
|
11
|
+
// - android/.../ArchiveLimits.java
|
|
12
|
+
// - harmony/pushy/src/main/ets/ArchiveLimits.ts
|
|
13
|
+
// iOS (RCTPushy.mm) includes this header directly.
|
|
14
|
+
|
|
15
|
+
namespace pushy {
|
|
16
|
+
namespace archive_limits {
|
|
17
|
+
|
|
18
|
+
// Downloaded archive (Content-Length up front, streamed bytes as backstop).
|
|
19
|
+
constexpr long long kMaxArchiveBytes = 512LL * 1024 * 1024;
|
|
20
|
+
// Sum of every entry's uncompressed size.
|
|
21
|
+
constexpr long long kMaxTotalUncompressedBytes = 2048LL * 1024 * 1024;
|
|
22
|
+
// A single entry's uncompressed size.
|
|
23
|
+
constexpr long long kMaxEntryBytes = 512LL * 1024 * 1024;
|
|
24
|
+
// Number of entries in one archive.
|
|
25
|
+
constexpr long long kMaxEntries = 20000;
|
|
26
|
+
// Per-entry uncompressed/compressed ratio, checked only above
|
|
27
|
+
// kRatioCheckMinBytes (tiny highly-compressible files are legitimate).
|
|
28
|
+
constexpr long long kMaxCompressionRatio = 100;
|
|
29
|
+
constexpr long long kRatioCheckMinBytes = 1LL * 1024 * 1024;
|
|
30
|
+
// __diff.json is parsed fully in memory.
|
|
31
|
+
constexpr long long kMaxManifestBytes = 16LL * 1024 * 1024;
|
|
32
|
+
// Free disk required beyond the bytes about to be written.
|
|
33
|
+
constexpr long long kFreeDiskMarginBytes = 64LL * 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
} // namespace archive_limits
|
|
36
|
+
} // namespace pushy
|
|
37
|
+
|
|
38
|
+
#endif // PUSHY_PATCH_CORE_ARCHIVE_LIMITS_H_
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#ifndef PUSHY_PATCH_CORE_INSTALL_RECORD_H_
|
|
2
|
+
#define PUSHY_PATCH_CORE_INSTALL_RECORD_H_
|
|
3
|
+
|
|
4
|
+
// The completion record every platform writes into a version directory once
|
|
5
|
+
// an install fully succeeded, and reads back before trusting that directory.
|
|
6
|
+
//
|
|
7
|
+
// <root>/<hash>/.pushy-complete
|
|
8
|
+
// {"schema":1,"versionHash":"<hash>","bundleSha256":"<hex>","artifactSha256":"<hex>"}
|
|
9
|
+
//
|
|
10
|
+
// - Written by the SDK only (archives may not ship a `.pushy-*` entry), as
|
|
11
|
+
// the LAST step of a two-phase install: unpack/patch into
|
|
12
|
+
// <root>/<hash>.staging, write the record there, then atomically rename
|
|
13
|
+
// the staging directory to <root>/<hash>.
|
|
14
|
+
// - versionHash must equal the directory name; bundleSha256 is the digest of
|
|
15
|
+
// the final index.bundlejs (bundle.harmony.js on Harmony) and is
|
|
16
|
+
// re-verified at switchVersion time; artifactSha256 is the digest of the
|
|
17
|
+
// downloaded archive (diagnostics / CDN corruption attribution).
|
|
18
|
+
// - Legacy: an EMPTY file is a completed install written by SDK < 10.53
|
|
19
|
+
// (no digests); it stays trusted for presence, nothing to verify.
|
|
20
|
+
//
|
|
21
|
+
// Mirrors that cannot include this header MUST stay in sync by hand:
|
|
22
|
+
// - android/.../InstallRecord.java
|
|
23
|
+
// - harmony/pushy/src/main/ets/InstallRecord.ts
|
|
24
|
+
// iOS (RCTPushy.mm) includes this header directly.
|
|
25
|
+
|
|
26
|
+
namespace pushy {
|
|
27
|
+
namespace install_record {
|
|
28
|
+
|
|
29
|
+
constexpr int kSchema = 1;
|
|
30
|
+
constexpr const char* kFileName = ".pushy-complete";
|
|
31
|
+
constexpr const char* kStagingSuffix = ".staging";
|
|
32
|
+
|
|
33
|
+
} // namespace install_record
|
|
34
|
+
} // namespace pushy
|
|
35
|
+
|
|
36
|
+
#endif // PUSHY_PATCH_CORE_INSTALL_RECORD_H_
|
|
@@ -53,6 +53,7 @@ class Value {
|
|
|
53
53
|
bool IsUndefined() const { return kind_ == Kind::Undefined; }
|
|
54
54
|
bool IsArray() const { return kind_ == Kind::Array; }
|
|
55
55
|
bool IsObject() const { return kind_ == Kind::Object; }
|
|
56
|
+
bool IsBool() const { return kind_ == Kind::Bool; }
|
|
56
57
|
bool IsNumber() const { return kind_ == Kind::Number; }
|
|
57
58
|
bool IsString() const { return kind_ == Kind::String; }
|
|
58
59
|
|
|
@@ -148,6 +148,15 @@ Value OrderEndpointCandidates(const Value& endpoints, double randomSample) {
|
|
|
148
148
|
|
|
149
149
|
Value BuildCheckRequestBody(const Value& input) {
|
|
150
150
|
Value body = Value::Object();
|
|
151
|
+
// Caller extras go in FIRST: they may add fields but never override the
|
|
152
|
+
// identity fields the server keys its decision on (mirrors the TS
|
|
153
|
+
// `{ ...extra, packageVersion, hash, ... }` spread, including key order).
|
|
154
|
+
const Value& extra = input.Get("extra");
|
|
155
|
+
if (extra.IsObject()) {
|
|
156
|
+
for (const auto& member : extra.members()) {
|
|
157
|
+
body.Set(member.first, member.second);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
151
160
|
body.Set("packageVersion", input.Get("packageVersion"));
|
|
152
161
|
body.Set("hash", input.Get("currentVersion"));
|
|
153
162
|
body.Set("buildTime", input.Get("buildTime"));
|
|
@@ -155,20 +164,27 @@ Value BuildCheckRequestBody(const Value& input) {
|
|
|
155
164
|
const Value& diffV = input.Get("supportedDiffVersion");
|
|
156
165
|
if (diffV.Truthy()) {
|
|
157
166
|
body.Set("diffV", diffV);
|
|
167
|
+
} else {
|
|
168
|
+
body.Remove("diffV");
|
|
158
169
|
}
|
|
159
170
|
const Value& bundleHash = input.Get("bundleHash");
|
|
160
171
|
if (bundleHash.Truthy()) {
|
|
161
172
|
body.Set("bundleHash", bundleHash);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (extra.IsObject()) {
|
|
165
|
-
for (const auto& member : extra.members()) {
|
|
166
|
-
body.Set(member.first, member.second);
|
|
167
|
-
}
|
|
173
|
+
} else {
|
|
174
|
+
body.Remove("bundleHash");
|
|
168
175
|
}
|
|
169
176
|
if (input.Get("isDev").Truthy()) {
|
|
170
177
|
body.Remove("buildTime");
|
|
171
178
|
}
|
|
179
|
+
// Transitional dual form (mirrors the TS side): nested copy of the caller
|
|
180
|
+
// extras next to the legacy root-level spread.
|
|
181
|
+
if (extra.IsObject() && !extra.members().empty()) {
|
|
182
|
+
Value nested = Value::Object();
|
|
183
|
+
for (const auto& member : extra.members()) {
|
|
184
|
+
nested.Set(member.first, member.second);
|
|
185
|
+
}
|
|
186
|
+
body.Set("extra", std::move(nested));
|
|
187
|
+
}
|
|
172
188
|
return body;
|
|
173
189
|
}
|
|
174
190
|
|
|
@@ -280,12 +296,30 @@ bool ShouldActivateAfterDownload(const Value& info,
|
|
|
280
296
|
info.Get("config").Get("forceBoot").Truthy();
|
|
281
297
|
}
|
|
282
298
|
|
|
299
|
+
bool IsValidCheckResult(const Value& root) {
|
|
300
|
+
if (!root.IsObject()) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const Value& upToDate = root.Get("upToDate");
|
|
304
|
+
const Value& update = root.Get("update");
|
|
305
|
+
const Value& expired = root.Get("expired");
|
|
306
|
+
const Value& paused = root.Get("paused");
|
|
307
|
+
return upToDate.IsBool() || update.IsBool() || expired.IsBool() ||
|
|
308
|
+
paused.IsString();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
bool IsValidCheckResponse(const std::string& responseText) {
|
|
312
|
+
bool ok = false;
|
|
313
|
+
Value root = flowjson::Parse(responseText, &ok);
|
|
314
|
+
return ok && IsValidCheckResult(root);
|
|
315
|
+
}
|
|
316
|
+
|
|
283
317
|
Value HandleCheckResponse(const std::string& responseText,
|
|
284
318
|
const Value& identity, bool isDev,
|
|
285
319
|
const std::string& afterDownload) {
|
|
286
320
|
bool ok = false;
|
|
287
321
|
Value root = flowjson::Parse(responseText, &ok);
|
|
288
|
-
if (!ok || !root
|
|
322
|
+
if (!ok || !IsValidCheckResult(root)) {
|
|
289
323
|
return DeclineDownload("invalidResponse");
|
|
290
324
|
}
|
|
291
325
|
Value resolved = ResolveCheckResult(root, identity);
|
|
@@ -56,6 +56,15 @@ flowjson::Value DecideDownload(const flowjson::Value& info,
|
|
|
56
56
|
// JS, so activation cannot wait for it). Native-only; the device-local
|
|
57
57
|
// rolledBackVersion guard in DecideDownload still wins, and the activated
|
|
58
58
|
// version keeps the first_time crash-protection rollback.
|
|
59
|
+
// Schema gate for a checkUpdate response. A valid response is a JSON object
|
|
60
|
+
// carrying at least one verdict field of the expected type: `upToDate`,
|
|
61
|
+
// `update` or `expired` (boolean), or `paused` (string). Anything else — an
|
|
62
|
+
// array, `{"error": "..."}`, an HTML page that happened to parse — is a
|
|
63
|
+
// failed endpoint and must not stop the endpoint fallback. Mirrors
|
|
64
|
+
// src/updateFlowCore.ts isValidCheckResult.
|
|
65
|
+
bool IsValidCheckResult(const flowjson::Value& root);
|
|
66
|
+
bool IsValidCheckResponse(const std::string& responseText);
|
|
67
|
+
|
|
59
68
|
bool ShouldActivateAfterDownload(const flowjson::Value& info,
|
|
60
69
|
const std::string& afterDownload);
|
|
61
70
|
|
|
@@ -104,6 +104,18 @@ Java_cn_reactnative_modules_update_NativeUpdateFlow_orderEndpointCandidates(
|
|
|
104
104
|
endpoints, randomSample)));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
extern "C" JNIEXPORT jboolean JNICALL
|
|
108
|
+
Java_cn_reactnative_modules_update_NativeUpdateFlow_isValidCheckResponse(
|
|
109
|
+
JNIEnv* env, jclass, jstring responseText) {
|
|
110
|
+
if (responseText == nullptr) {
|
|
111
|
+
return JNI_FALSE;
|
|
112
|
+
}
|
|
113
|
+
return updateflow::IsValidCheckResponse(
|
|
114
|
+
pushy::jni_util::JStringToString(env, responseText))
|
|
115
|
+
? JNI_TRUE
|
|
116
|
+
: JNI_FALSE;
|
|
117
|
+
}
|
|
118
|
+
|
|
107
119
|
extern "C" JNIEXPORT jstring JNICALL
|
|
108
120
|
Java_cn_reactnative_modules_update_NativeUpdateFlow_handleCheckResponse(
|
|
109
121
|
JNIEnv* env, jclass, jstring responseText, jstring identityJson,
|
|
@@ -67,6 +67,7 @@ PUSHY_ASYNC_METHOD(downloadAndInstallApk)
|
|
|
67
67
|
// on Harmony. Every new spec method MUST be registered here.
|
|
68
68
|
PUSHY_ASYNC_METHOD(syncNativeConfig)
|
|
69
69
|
PUSHY_ASYNC_METHOD(getNativeCheckCache)
|
|
70
|
+
PUSHY_ASYNC_METHOD(markJsCheckCompleted)
|
|
70
71
|
PUSHY_ASYNC_METHOD(getBundleHash)
|
|
71
72
|
PUSHY_ASYNC_METHOD(resetToPackagedBundle)
|
|
72
73
|
|
|
@@ -104,6 +105,7 @@ PushyTurboModule::PushyTurboModule(
|
|
|
104
105
|
registerMethod("removeListeners", 1, HostFunction_removeListeners);
|
|
105
106
|
registerMethod("syncNativeConfig", 1, HostFunction_syncNativeConfig);
|
|
106
107
|
registerMethod("getNativeCheckCache", 0, HostFunction_getNativeCheckCache);
|
|
108
|
+
registerMethod("markJsCheckCompleted", 1, HostFunction_markJsCheckCompleted);
|
|
107
109
|
registerMethod("getBundleHash", 0, HostFunction_getBundleHash);
|
|
108
110
|
registerMethod(
|
|
109
111
|
"resetToPackagedBundle", 0, HostFunction_resetToPackagedBundle);
|
|
@@ -896,6 +896,25 @@ bool ExportFunction(
|
|
|
896
896
|
|
|
897
897
|
// sha256(Uint8Array | ArrayBuffer) -> 小写 hex。同步:bundleHash 的输入是
|
|
898
898
|
// rawfile 里的 bundle(已整体读入内存,与 pdiff 的读法一致),哈希本身毫秒级。
|
|
899
|
+
static napi_value MakeUtf8String(napi_env env, const std::string& value);
|
|
900
|
+
|
|
901
|
+
// Streaming file digest (bounded memory) for the install record.
|
|
902
|
+
static napi_value Sha256HexFile(napi_env env, napi_callback_info info) {
|
|
903
|
+
size_t argc = 1;
|
|
904
|
+
napi_value args[1] = {nullptr};
|
|
905
|
+
if (!GetArgCount(env, info, &argc, args) || argc < 1) {
|
|
906
|
+
ThrowError(env, "sha256HexFile: missing path argument");
|
|
907
|
+
return nullptr;
|
|
908
|
+
}
|
|
909
|
+
bool ok = false;
|
|
910
|
+
std::string path = GetString(env, args[0], &ok);
|
|
911
|
+
if (!ok) {
|
|
912
|
+
ThrowError(env, "sha256HexFile: path must be a string");
|
|
913
|
+
return nullptr;
|
|
914
|
+
}
|
|
915
|
+
return MakeUtf8String(env, pushy::digest::Sha256File(path));
|
|
916
|
+
}
|
|
917
|
+
|
|
899
918
|
static napi_value Sha256Hex(napi_env env, napi_callback_info info) {
|
|
900
919
|
size_t argc = 1;
|
|
901
920
|
napi_value args[1] = {nullptr};
|
|
@@ -1060,6 +1079,22 @@ static napi_value FlowOrderEndpointCandidates(napi_env env,
|
|
|
1060
1079
|
endpoints, sample)));
|
|
1061
1080
|
}
|
|
1062
1081
|
|
|
1082
|
+
static napi_value FlowIsValidCheckResponse(napi_env env,
|
|
1083
|
+
napi_callback_info info) {
|
|
1084
|
+
size_t argc = 1;
|
|
1085
|
+
napi_value args[1] = {nullptr};
|
|
1086
|
+
if (!GetArgCount(env, info, &argc, args) || argc < 1) {
|
|
1087
|
+
ThrowError(env, "isValidCheckResponse: missing arguments");
|
|
1088
|
+
return nullptr;
|
|
1089
|
+
}
|
|
1090
|
+
bool ok = false;
|
|
1091
|
+
std::string response_text = GetString(env, args[0], &ok);
|
|
1092
|
+
napi_value result = nullptr;
|
|
1093
|
+
napi_get_boolean(env, ok && updateflow::IsValidCheckResponse(response_text),
|
|
1094
|
+
&result);
|
|
1095
|
+
return result;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1063
1098
|
static napi_value FlowHandleCheckResponse(napi_env env,
|
|
1064
1099
|
napi_callback_info info) {
|
|
1065
1100
|
size_t argc = 3;
|
|
@@ -1099,10 +1134,12 @@ napi_value Init(napi_env env, napi_value exports) {
|
|
|
1099
1134
|
!ExportFunction(env, exports, "applyPatchFromFileSource", ApplyPatchFromFileSource) ||
|
|
1100
1135
|
!ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) ||
|
|
1101
1136
|
!ExportFunction(env, exports, "sha256Hex", Sha256Hex) ||
|
|
1137
|
+
!ExportFunction(env, exports, "sha256HexFile", Sha256HexFile) ||
|
|
1102
1138
|
!ExportFunction(env, exports, "crc32", Crc32) ||
|
|
1103
1139
|
!ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion) ||
|
|
1104
1140
|
!ExportFunction(env, exports, "buildCheckRequestBody", FlowBuildCheckRequestBody) ||
|
|
1105
1141
|
!ExportFunction(env, exports, "orderEndpointCandidates", FlowOrderEndpointCandidates) ||
|
|
1142
|
+
!ExportFunction(env, exports, "isValidCheckResponse", FlowIsValidCheckResponse) ||
|
|
1106
1143
|
!ExportFunction(env, exports, "handleCheckResponse", FlowHandleCheckResponse)) {
|
|
1107
1144
|
return nullptr;
|
|
1108
1145
|
}
|