react-native-update 10.53.1 → 10.55.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.
Files changed (106) hide show
  1. package/README-CN.md +59 -31
  2. package/README.md +62 -32
  3. package/android/jni/HDiffPatch/.travis.yml +33 -0
  4. package/android/jni/HDiffPatch/builds/xcode/bestParams.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  5. package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  6. package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  7. package/android/jni/HDiffPatch/builds/xcode/hpatchz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  8. package/android/jni/HDiffPatch/builds/xcode/lz4.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  9. package/android/jni/HDiffPatch/builds/xcode/lzma.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  10. package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  11. package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  12. package/android/jni/HDiffPatch/builds/xcode/unitTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  13. package/android/jni/HDiffPatch/builds/xcode/zstd.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  14. package/android/jni/lzma/DOC/7zC.txt +187 -0
  15. package/android/jni/lzma/DOC/7zFormat.txt +469 -0
  16. package/android/jni/lzma/DOC/Methods.txt +173 -0
  17. package/android/jni/lzma/DOC/installer.txt +166 -0
  18. package/android/jni/lzma/DOC/lzma-history.txt +446 -0
  19. package/android/jni/lzma/DOC/lzma-sdk.txt +357 -0
  20. package/android/jni/lzma/DOC/lzma-specification.txt +1176 -0
  21. package/android/jni/lzma/DOC/lzma.txt +328 -0
  22. package/android/jni/lzma/bin/7zS2.sfx +0 -0
  23. package/android/jni/lzma/bin/7zS2con.sfx +0 -0
  24. package/android/jni/lzma/bin/7zSD.sfx +0 -0
  25. package/android/jni/lzma/bin/7zdec.exe +0 -0
  26. package/android/jni/lzma/bin/7zr.exe +0 -0
  27. package/android/jni/lzma/bin/installer/config.txt +5 -0
  28. package/android/jni/lzma/bin/installer/cr.bat +5 -0
  29. package/android/jni/lzma/bin/lzma.exe +0 -0
  30. package/android/jni/lzma/bin/x64/7zr.exe +0 -0
  31. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  32. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  33. package/android/lib/x86/librnupdate.so +0 -0
  34. package/android/lib/x86_64/librnupdate.so +0 -0
  35. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +44 -0
  36. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +124 -40
  37. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +125 -0
  38. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +43 -11
  39. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +2 -0
  40. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +90 -0
  41. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +79 -19
  42. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +21 -0
  43. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +33 -2
  44. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +1 -0
  45. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  46. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  47. package/cpp/patch_core/archive_limits.h +38 -0
  48. package/cpp/patch_core/install_record.h +36 -0
  49. package/cpp/update_flow_core/flow_json.h +1 -0
  50. package/cpp/update_flow_core/update_flow_core.cpp +41 -7
  51. package/cpp/update_flow_core/update_flow_core.h +9 -0
  52. package/cpp/update_flow_core/update_flow_jni.cpp +12 -0
  53. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +2 -0
  54. package/harmony/pushy/src/main/cpp/pushy.cpp +37 -0
  55. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +83 -0
  56. package/harmony/pushy/src/main/ets/DownloadTask.ts +126 -30
  57. package/harmony/pushy/src/main/ets/InstallRecord.ts +129 -0
  58. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +24 -2
  59. package/harmony/pushy/src/main/ets/NativePatchCore.ts +5 -0
  60. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
  61. package/harmony/pushy/src/main/ets/UpdateContext.ts +78 -15
  62. package/harmony/pushy.har +0 -0
  63. package/ios/RCTPushy/RCTPushy.mm +384 -25
  64. package/ios/RCTPushy/RCTPushyDownloader.mm +32 -0
  65. package/package.json +25 -2
  66. package/src/NativePushy.ts +13 -0
  67. package/src/client.ts +288 -47
  68. package/src/core.ts +17 -3
  69. package/src/error.ts +3 -0
  70. package/src/errorReporting.ts +136 -0
  71. package/src/index.ts +14 -0
  72. package/src/locales/en.ts +3 -0
  73. package/src/locales/zh.ts +2 -0
  74. package/src/metadata.ts +166 -0
  75. package/src/provider.tsx +52 -17
  76. package/src/type.ts +29 -0
  77. package/src/updateFlowCore.ts +95 -4
  78. package/src/utils.ts +31 -19
  79. package/.gitmodules +0 -6
  80. package/BUNDLEHASH_MIGRATION.md +0 -355
  81. package/NATIVE_CHECKUPDATE_DESIGN.md +0 -539
  82. package/NATIVE_CHECK_FOLLOWUPS.md +0 -514
  83. package/android/jni/HDiffPatch/test/_private_searchBestParams.cpp +0 -301
  84. package/android/jni/HDiffPatch/test/hpatchz_test.cpp +0 -292
  85. package/android/jni/HDiffPatch/test/testHashClash.cpp +0 -436
  86. package/android/jni/HDiffPatch/test/unit_test.cpp +0 -417
  87. package/biome.json +0 -59
  88. package/cpp/patch_core/tests/fixtures/rawstored.lzma2label.patch.bin +0 -0
  89. package/cpp/patch_core/tests/fixtures/rawstored.new.bin +0 -0
  90. package/cpp/patch_core/tests/fixtures/rawstored.old.bin +0 -0
  91. package/cpp/patch_core/tests/fixtures/v96.hbc +0 -0
  92. package/cpp/patch_core/tests/fixtures/v96.meta.json +0 -1
  93. package/cpp/patch_core/tests/fixtures/v96.streampatch.bin +0 -0
  94. package/cpp/patch_core/tests/fixtures/v96.t.hbc +0 -0
  95. package/cpp/patch_core/tests/fixtures/v96.tpatch.bin +0 -0
  96. package/cpp/patch_core/tests/fixtures/v96.tstreampatch.bin +0 -0
  97. package/cpp/patch_core/tests/fixtures/v96b.hbc +0 -0
  98. package/cpp/patch_core/tests/fixtures/v98.hbc +0 -0
  99. package/cpp/patch_core/tests/fixtures/v98.t.hbc +0 -0
  100. package/cpp/patch_core/tests/fixtures/v98b.hbc +0 -0
  101. package/cpp/patch_core/tests/fixtures/v98b.t.hbc +0 -0
  102. package/cpp/patch_core/tests/hbc_transform_test.cpp +0 -384
  103. package/cpp/patch_core/tests/patch_core_test.cpp +0 -970
  104. package/cpp/update_flow_core/tests/flow_vectors.json +0 -1401
  105. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +0 -317
  106. 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 = ".pushy-complete";
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
- private void persistEditor(SharedPreferences.Editor editor, String reason) {
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
- public void switchVersion(String hash) {
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 (!new File(versionDir, VERSION_COMPLETE_FILE).isFile()
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
- StateCoreResult nextState = runStateCore(
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
- persistEditor(editor, "switch version");
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
- persistEditor(editor, "set key " + key);
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
- persistEditor(editor, "remove key " + key);
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
- persistEditor(editor, "mark success");
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
- persistEditor(editor, "clear first time");
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
- persistEditor(editor, "reset to packaged bundle");
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
- setKv("hash_" + hash, hashInfoJson);
722
+ editor.putString("hash_" + hash, hashInfoJson);
681
723
  }
682
- if (activate && hash != null) {
683
- switchVersion(hash);
724
+ boolean switching = activate && hash != null;
725
+ if (switching) {
726
+ applyState(editor, computeSwitchVersion(hash));
684
727
  }
685
728
  if (responseCacheJson != null) {
686
- setKv(NativeCheckOrchestrator.KEY_RESP_CACHE, responseCacheJson);
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
- && new File(versionDir, VERSION_COMPLETE_FILE).isFile();
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
- updateContext.setKv("hash_" + hash, info);
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
- setLocalHashInfoInternal(updateContext, hash, info);
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
- const Value& extra = input.Get("extra");
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.IsObject()) {
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
  }