react-native-update 10.59.1 → 10.60.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 (125) hide show
  1. package/README-CN.md +7 -7
  2. package/README.md +7 -6
  3. package/android/build.gradle +25 -33
  4. package/android/jni/Android.mk +16 -24
  5. package/android/lib/arm64-v8a/librnpushy.so +0 -0
  6. package/android/lib/armeabi-v7a/librnpushy.so +0 -0
  7. package/android/lib/x86/librnpushy.so +0 -0
  8. package/android/lib/x86_64/librnpushy.so +0 -0
  9. package/android/proguard.pro +9 -7
  10. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +11 -12
  11. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
  12. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
  13. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
  14. package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
  17. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
  18. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
  21. package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
  22. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
  23. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
  24. package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
  25. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
  26. package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
  27. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
  28. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
  29. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
  30. package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
  31. package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
  32. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
  33. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
  34. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
  35. package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
  36. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
  37. package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
  38. package/cpp/android_jni/archive_limits_jni.cpp +83 -0
  39. package/cpp/android_jni/install_record_jni.cpp +89 -0
  40. package/cpp/android_jni/jni_log.h +31 -0
  41. package/cpp/android_jni/jni_natives.h +40 -0
  42. package/cpp/android_jni/jni_registration.cpp +133 -0
  43. package/cpp/android_jni/jni_util.h +160 -0
  44. package/cpp/android_jni/update_core_android.cpp +450 -0
  45. package/cpp/android_jni/update_flow_jni.cpp +133 -0
  46. package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
  47. package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
  48. package/cpp/patch_core/archive_limits.cpp +194 -0
  49. package/cpp/patch_core/archive_limits.h +99 -10
  50. package/cpp/patch_core/archive_patch_core.cpp +96 -12
  51. package/cpp/patch_core/archive_patch_core.h +49 -0
  52. package/cpp/patch_core/digest.h +5 -4
  53. package/cpp/patch_core/error_codes.h +5 -3
  54. package/cpp/patch_core/install_record.cpp +430 -0
  55. package/cpp/patch_core/install_record.h +163 -9
  56. package/cpp/patch_core/patch_core.cpp +62 -16
  57. package/cpp/patch_core/patch_core.h +27 -7
  58. package/cpp/patch_core/patch_manifest.cpp +201 -0
  59. package/cpp/patch_core/patch_manifest.h +74 -0
  60. package/cpp/patch_core/state_ops.h +3 -3
  61. package/cpp/update_flow_core/flow_json.cpp +82 -8
  62. package/cpp/update_flow_core/flow_json.h +21 -0
  63. package/cpp/update_flow_core/native_config.cpp +456 -0
  64. package/cpp/update_flow_core/native_config.h +84 -0
  65. package/cpp/update_flow_core/native_round.cpp +326 -0
  66. package/cpp/update_flow_core/native_round.h +90 -0
  67. package/cpp/update_flow_core/update_flow_core.h +2 -2
  68. package/harmony/hvigor-plugin.ts +11 -19
  69. package/harmony/pushy/src/main/cpp/CMakeLists.txt +19 -26
  70. package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
  71. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
  72. package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
  73. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
  74. package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
  75. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
  76. package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
  77. package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
  78. package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
  79. package/harmony/pushy/src/main/ets/Logger.ts +20 -34
  80. package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
  81. package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
  82. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
  83. package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
  84. package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
  85. package/harmony/pushy/src/main/ets/Texts.ts +0 -4
  86. package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
  87. package/harmony/pushy.har +0 -0
  88. package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
  89. package/ios/RCTPushy/RCTPushy.h +12 -0
  90. package/ios/RCTPushy/RCTPushy.mm +87 -2628
  91. package/ios/RCTPushy/RCTPushyCommon.h +34 -0
  92. package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
  93. package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
  94. package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
  95. package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
  96. package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
  97. package/ios/RCTPushy/RCTPushyInternal.h +144 -0
  98. package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
  99. package/ios/RCTPushy/RCTPushyState.mm +314 -0
  100. package/package.json +25 -23
  101. package/react-native-update.podspec +74 -83
  102. package/src/client.ts +361 -451
  103. package/src/context.ts +1 -1
  104. package/src/core.ts +35 -8
  105. package/src/endpoint.ts +0 -2
  106. package/src/error.ts +13 -32
  107. package/src/i18n.ts +18 -83
  108. package/src/locales/en.ts +0 -4
  109. package/src/locales/zh.ts +0 -4
  110. package/src/provider.tsx +60 -53
  111. package/src/telemetry.ts +0 -13
  112. package/src/type.ts +0 -15
  113. package/src/updateFlowCore.ts +116 -10
  114. package/src/utils.ts +49 -42
  115. package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
  116. package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
  117. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
  118. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
  119. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
  120. package/cpp/patch_core/jni_natives.h +0 -27
  121. package/cpp/patch_core/jni_registration.cpp +0 -103
  122. package/cpp/patch_core/jni_util.h +0 -60
  123. package/cpp/patch_core/patch_core_android.cpp +0 -97
  124. package/cpp/patch_core/update_core_android.cpp +0 -485
  125. package/cpp/update_flow_core/update_flow_jni.cpp +0 -150
@@ -21,7 +21,10 @@
21
21
  #include "hbc_transform_wire.h"
22
22
 
23
23
  extern "C" {
24
- #include "hpatch.h"
24
+ // Relative to this file: under use_frameworks! a bare "hpatch.h" resolves
25
+ // to the flattened framework PrivateHeaders copy, whose own HDiffPatch/
26
+ // includes then fail.
27
+ #include "../hpatch/hpatch.h"
25
28
  }
26
29
 
27
30
  namespace pushy {
@@ -487,14 +490,6 @@ Status MergeDirectoryRecursively(
487
490
 
488
491
  } // namespace
489
492
 
490
- Status Status::Ok() {
491
- return Status{true, std::string()};
492
- }
493
-
494
- Status Status::Error(const std::string& message) {
495
- return Status{false, message};
496
- }
497
-
498
493
  Status ValidateManifest(const PatchManifest& manifest) {
499
494
  return ValidateManifestImpl(manifest);
500
495
  }
@@ -580,7 +575,7 @@ Status ApplyBundlePatchWithHbcTransform(
580
575
  const FileSourcePatchOptions& options,
581
576
  const BundleRebuilder& bundle_patcher) {
582
577
  hbc::HbcTransformMeta meta;
583
- if (!hbc::ParseHbcTransformMeta(options.bundle_hbc_transform_meta, &meta)) {
578
+ if (!hbc::ParseHbcTransformMeta(options.manifest.bundle_hbc_transform_meta, &meta)) {
584
579
  return Status::Error("Invalid hbcTransform metadata");
585
580
  }
586
581
  if (meta.v != hbc::kHbcTransformSupportedVersion) {
@@ -654,7 +649,7 @@ Status ApplyPatchFromFileSource(
654
649
  }
655
650
 
656
651
  Status bundle_status =
657
- options.bundle_hbc_transform_meta.empty()
652
+ options.manifest.bundle_hbc_transform_meta.empty()
658
653
  ? bundle_patcher.Apply(
659
654
  options.origin_bundle_path,
660
655
  options.bundle_patch_path,
@@ -727,11 +722,16 @@ Status CleanupOldEntries(
727
722
  max_age_days, now);
728
723
  }
729
724
 
730
- Status CleanupOldEntries(
725
+ namespace {
726
+
727
+ // Lists the non-dot entries of root_dir that are not kept and are at least
728
+ // max_age_days old: the one selection rule shared by delete and trash.
729
+ Status SelectOldEntries(
731
730
  const std::string& root_dir,
732
731
  const std::vector<std::string>& keep_names,
733
732
  int max_age_days,
734
- std::time_t now) {
733
+ std::time_t now,
734
+ std::vector<std::string>* out) {
735
735
  DIR* dir = opendir(root_dir.c_str());
736
736
  if (!dir) {
737
737
  if (errno == ENOENT) {
@@ -765,15 +765,61 @@ Status CleanupOldEntries(
765
765
  if (effective_now - st.st_mtime < max_age_seconds) {
766
766
  continue;
767
767
  }
768
+ out->push_back(name);
769
+ }
768
770
 
769
- Status remove_status = RemovePathRecursively(entry_path);
771
+ closedir(dir);
772
+ return Status::Ok();
773
+ }
774
+
775
+ } // namespace
776
+
777
+ Status CleanupOldEntries(
778
+ const std::string& root_dir,
779
+ const std::vector<std::string>& keep_names,
780
+ int max_age_days,
781
+ std::time_t now) {
782
+ std::vector<std::string> doomed;
783
+ Status select_status =
784
+ SelectOldEntries(root_dir, keep_names, max_age_days, now, &doomed);
785
+ if (!select_status) {
786
+ return select_status;
787
+ }
788
+ for (const std::string& name : doomed) {
789
+ Status remove_status = RemovePathRecursively(JoinPath(root_dir, name));
770
790
  if (!remove_status) {
771
- closedir(dir);
772
791
  return remove_status;
773
792
  }
774
793
  }
794
+ return Status::Ok();
795
+ }
775
796
 
776
- closedir(dir);
797
+ Status MoveOldEntriesToTrash(
798
+ const std::string& root_dir,
799
+ const std::vector<std::string>& keep_names,
800
+ int max_age_days,
801
+ const std::string& trash_dir,
802
+ std::time_t now) {
803
+ std::vector<std::string> doomed;
804
+ Status select_status =
805
+ SelectOldEntries(root_dir, keep_names, max_age_days, now, &doomed);
806
+ if (!select_status || doomed.empty()) {
807
+ return select_status;
808
+ }
809
+ Status dir_status = EnsureDirectory(trash_dir);
810
+ if (!dir_status) {
811
+ return dir_status;
812
+ }
813
+ for (const std::string& name : doomed) {
814
+ std::string target = JoinPath(trash_dir, name);
815
+ for (int suffix = 1; PathExists(target); ++suffix) {
816
+ target = JoinPath(trash_dir, name + "." + IntToString(suffix));
817
+ }
818
+ const std::string source = JoinPath(root_dir, name);
819
+ if (rename(source.c_str(), target.c_str()) != 0) {
820
+ return MakeErrnoStatus("Failed to move " + source + " to trash");
821
+ }
822
+ }
777
823
  return Status::Ok();
778
824
  }
779
825
 
@@ -17,8 +17,12 @@ struct Status {
17
17
 
18
18
  explicit operator bool() const { return ok; }
19
19
 
20
- static Status Ok();
21
- static Status Error(const std::string& message);
20
+ // Inline so a core that only reports a Status (install_record.cpp) links
21
+ // without patch_core.cpp and its hpatch dependencies.
22
+ static Status Ok() { return Status{true, std::string()}; }
23
+ static Status Error(const std::string& message) {
24
+ return Status{false, message};
25
+ }
22
26
  };
23
27
 
24
28
  struct CopyOperation {
@@ -33,9 +37,16 @@ struct CopyOperation {
33
37
  uint32_t expected_crc = 0;
34
38
  };
35
39
 
40
+ // What a patch archive's __diff.json declares (ParsePatchManifest in
41
+ // patch_manifest.h is the one parser every platform uses).
36
42
  struct PatchManifest {
37
43
  std::vector<CopyOperation> copies;
38
44
  std::vector<std::string> deletes;
45
+ // __diff.json 中该 bundle patch 条目的 hbcTransform 元数据(JSON 文本)。
46
+ // 非空时 patch 走变换域:T(origin) → hpatch → T⁻¹。元数据不可解析或
47
+ // 变换规范版本不受支持时返回错误(调用方回退整包),绝不忽略元数据
48
+ // 直接应用——那会产出损坏的 bundle。
49
+ std::string bundle_hbc_transform_meta;
39
50
  };
40
51
 
41
52
  struct FileSourcePatchOptions {
@@ -47,11 +58,6 @@ struct FileSourcePatchOptions {
47
58
  std::string bundle_output_path;
48
59
  std::string merge_source_subdir;
49
60
  bool enable_merge = true;
50
- // __diff.json 中该 bundle patch 条目的 hbcTransform 元数据(原始 JSON)。
51
- // 非空时 patch 走变换域:T(origin) → hpatch → T⁻¹。元数据不可解析或
52
- // 变换规范版本不受支持时返回错误(调用方回退整包),绝不忽略元数据
53
- // 直接应用——那会产出损坏的 bundle。
54
- std::string bundle_hbc_transform_meta;
55
61
  };
56
62
 
57
63
  class BundleRebuilder {
@@ -89,6 +95,20 @@ Status CleanupOldEntries(
89
95
  int max_age_days,
90
96
  std::time_t now = 0);
91
97
 
98
+ // Same selection as CleanupOldEntries, but each doomed entry is renamed into
99
+ // trash_dir (created if needed; must be on the same file system, e.g. a
100
+ // dot-named child of root_dir so the selection itself skips it) instead of
101
+ // being deleted. Renames are cheap, so a caller can hold its version-files
102
+ // lock across this call and delete trash_dir's contents after releasing it
103
+ // (CleanupOldEntries(trash_dir, {}, 0)). A name already present in trash_dir
104
+ // (left over by an interrupted earlier delete) gets a numeric suffix.
105
+ Status MoveOldEntriesToTrash(
106
+ const std::string& root_dir,
107
+ const std::vector<std::string>& keep_names,
108
+ int max_age_days,
109
+ const std::string& trash_dir,
110
+ std::time_t now = 0);
111
+
92
112
  bool IsSafeRelativePath(const std::string& path);
93
113
 
94
114
  namespace internal {
@@ -0,0 +1,201 @@
1
+ #include "patch_manifest.h"
2
+
3
+ #include <cmath>
4
+ #include <cstdint>
5
+ #include <unordered_map>
6
+ #include <utility>
7
+
8
+ // Relative like every other cross-core include (see patch_core.cpp's hpatch.h
9
+ // include): use_frameworks! flattens the pod's headers.
10
+ #include "../update_flow_core/flow_json.h"
11
+
12
+ namespace pushy {
13
+ namespace delta {
14
+ namespace {
15
+
16
+ constexpr const char* kErrorPrefix = "delta manifest: ";
17
+
18
+ Status ManifestError(const std::string& message) {
19
+ return Status::Error(kErrorPrefix + message);
20
+ }
21
+
22
+ // null counts as absent everywhere (JSON writers emit it for empty maps).
23
+ bool IsAbsent(const flowjson::Value& value) {
24
+ return value.IsUndefined() ||
25
+ value.kind() == flowjson::Value::Kind::Null;
26
+ }
27
+
28
+ // An integer in [0, 2^32): the only CRC value a client can compare.
29
+ bool ReadCrc32(const flowjson::Value& value, uint32_t* out) {
30
+ if (!value.IsNumber()) {
31
+ return false;
32
+ }
33
+ const double number = value.AsNumber();
34
+ if (!std::isfinite(number) || number < 0 || number > 4294967295.0 ||
35
+ std::floor(number) != number) {
36
+ return false;
37
+ }
38
+ *out = static_cast<uint32_t>(number);
39
+ return true;
40
+ }
41
+
42
+ Status ParseCopies(
43
+ const flowjson::Value& copies,
44
+ const flowjson::Value& copies_crc,
45
+ const PatchManifestOptions& options,
46
+ PatchManifest* manifest) {
47
+ if (IsAbsent(copies)) {
48
+ return Status::Ok();
49
+ }
50
+ if (!copies.IsObject()) {
51
+ return ManifestError("copies is not an object");
52
+ }
53
+ if (!IsAbsent(copies_crc) && !copies_crc.IsObject()) {
54
+ return ManifestError("copiesCrc is not an object");
55
+ }
56
+ // Target -> declared CRC value; one pass, not a member scan per copy.
57
+ std::unordered_map<std::string, const flowjson::Value*> crc_by_target;
58
+ if (copies_crc.IsObject()) {
59
+ crc_by_target.reserve(copies_crc.members().size());
60
+ for (const auto& member : copies_crc.members()) {
61
+ crc_by_target.emplace(member.first, &member.second);
62
+ }
63
+ }
64
+ const std::string& prefix = options.copy_source_prefix;
65
+ // Source -> the CRC some copy of it declared, to catch contradictions.
66
+ std::unordered_map<std::string, uint32_t> crc_by_source;
67
+ manifest->copies.reserve(copies.members().size());
68
+ for (const auto& member : copies.members()) {
69
+ const std::string& to = member.first;
70
+ const flowjson::Value& from_value = member.second;
71
+ std::string from;
72
+ if (from_value.IsString()) {
73
+ from = from_value.AsString();
74
+ } else if (!IsAbsent(from_value)) {
75
+ return ManifestError("copies entry is not a string: " + to);
76
+ }
77
+ if (!prefix.empty() && from.compare(0, prefix.size(), prefix) == 0) {
78
+ from.erase(0, prefix.size());
79
+ }
80
+ if (from.empty()) {
81
+ from = to;
82
+ }
83
+
84
+ CopyOperation copy;
85
+ copy.from = std::move(from);
86
+ copy.to = to;
87
+ const auto declared = crc_by_target.find(to);
88
+ if (declared != crc_by_target.end()) {
89
+ const flowjson::Value& crc = *declared->second;
90
+ if (!IsAbsent(crc)) {
91
+ if (!ReadCrc32(crc, &copy.expected_crc)) {
92
+ return ManifestError("copiesCrc entry is not a CRC32: " + to);
93
+ }
94
+ copy.has_expected_crc = true;
95
+ const auto inserted =
96
+ crc_by_source.emplace(copy.from, copy.expected_crc);
97
+ if (!inserted.second &&
98
+ inserted.first->second != copy.expected_crc) {
99
+ return ManifestError("conflicting copiesCrc for source " + copy.from);
100
+ }
101
+ }
102
+ }
103
+ manifest->copies.push_back(std::move(copy));
104
+ }
105
+ return Status::Ok();
106
+ }
107
+
108
+ Status ParseDeletes(const flowjson::Value& deletes, PatchManifest* manifest) {
109
+ if (IsAbsent(deletes)) {
110
+ return Status::Ok();
111
+ }
112
+ if (deletes.IsObject()) {
113
+ manifest->deletes.reserve(deletes.members().size());
114
+ for (const auto& member : deletes.members()) {
115
+ manifest->deletes.push_back(member.first);
116
+ }
117
+ return Status::Ok();
118
+ }
119
+ if (deletes.IsArray()) {
120
+ manifest->deletes.reserve(deletes.Size());
121
+ for (const flowjson::Value& path : deletes.elements()) {
122
+ if (!path.IsString()) {
123
+ return ManifestError("deletes entry is not a string");
124
+ }
125
+ manifest->deletes.push_back(path.AsString());
126
+ }
127
+ return Status::Ok();
128
+ }
129
+ return ManifestError("deletes is not an object or array");
130
+ }
131
+
132
+ Status ParseHbcTransform(
133
+ const flowjson::Value& hbc_transform,
134
+ const PatchManifestOptions& options,
135
+ PatchManifest* manifest) {
136
+ if (IsAbsent(hbc_transform)) {
137
+ return Status::Ok();
138
+ }
139
+ if (!hbc_transform.IsObject()) {
140
+ return ManifestError("hbcTransform is not an object");
141
+ }
142
+ const flowjson::Value& meta =
143
+ hbc_transform.Get(options.bundle_patch_entry_name);
144
+ if (IsAbsent(meta)) {
145
+ return Status::Ok();
146
+ }
147
+ if (!meta.IsObject()) {
148
+ return ManifestError(
149
+ "hbcTransform entry is not an object: " +
150
+ options.bundle_patch_entry_name);
151
+ }
152
+ manifest->bundle_hbc_transform_meta = flowjson::Stringify(meta);
153
+ return Status::Ok();
154
+ }
155
+
156
+ } // namespace
157
+
158
+ Status ParsePatchManifest(
159
+ const std::string& json_text,
160
+ const PatchManifestOptions& options,
161
+ PatchManifest* out) {
162
+ if (out == nullptr) {
163
+ return Status::Error("Manifest output is required");
164
+ }
165
+ flowjson::ParseLimits limits;
166
+ limits.max_input_bytes =
167
+ static_cast<size_t>(archive_limits::kMaxManifestBytes);
168
+ limits.max_nodes = kMaxManifestNodes;
169
+ bool ok = false;
170
+ const flowjson::Value root = flowjson::Parse(json_text, limits, &ok);
171
+ if (!ok) {
172
+ return ManifestError("invalid JSON or over the size limits");
173
+ }
174
+ if (!root.IsObject()) {
175
+ return ManifestError("not a JSON object");
176
+ }
177
+
178
+ PatchManifest manifest;
179
+ Status status =
180
+ ParseCopies(root.Get("copies"), root.Get("copiesCrc"), options, &manifest);
181
+ if (!status) {
182
+ return status;
183
+ }
184
+ status = ParseDeletes(root.Get("deletes"), &manifest);
185
+ if (!status) {
186
+ return status;
187
+ }
188
+ status = ParseHbcTransform(root.Get("hbcTransform"), options, &manifest);
189
+ if (!status) {
190
+ return status;
191
+ }
192
+ status = ValidateManifest(manifest);
193
+ if (!status) {
194
+ return status;
195
+ }
196
+ *out = std::move(manifest);
197
+ return Status::Ok();
198
+ }
199
+
200
+ } // namespace delta
201
+ } // namespace pushy
@@ -0,0 +1,74 @@
1
+ #pragma once
2
+
3
+ #include <cstddef>
4
+ #include <string>
5
+
6
+ #include "archive_limits.h"
7
+ #include "patch_core.h"
8
+
9
+ // Keep the core internal to whichever binary links it (see podspec Core).
10
+ #pragma GCC visibility push(hidden)
11
+
12
+ namespace pushy {
13
+ namespace delta {
14
+
15
+ // __diff.json, the manifest inside every patch archive, as the generators
16
+ // emit it (pushy-go internal/taskexecutor/bundle_diff.go and
17
+ // react-native-update-cli src/diff.ts):
18
+ //
19
+ // {"copies": {"<to>": "<from>" | "", ...},
20
+ // "copiesCrc": {"<to>": <crc32>, ...}, package diffs only
21
+ // "deletes": {"<path>": 1, ...}, ppk diffs only
22
+ // "hbcTransform": {"<bundle patch entry>": {...}}} hdiffv2 track only
23
+ //
24
+ // The one parser of that file on Android, iOS and HarmonyOS. The rules are
25
+ // the most permissive form that is still safe; anything else fails the
26
+ // patch, and the JS strategy chain falls back to the full package:
27
+ // - the text is a JSON object (at most archive_limits::kMaxManifestBytes,
28
+ // at most kMaxManifestNodes values); unknown members are ignored, and a
29
+ // member that is null counts as absent;
30
+ // - copies: an object of target -> source strings. An empty (or null)
31
+ // source means "same path as the target". Order follows the document;
32
+ // - copiesCrc: an object keyed by copy *target* (not source). Each value is
33
+ // an integer in [0, 2^32) or null; any other value is an error (a CRC the
34
+ // client cannot honour must not silently skip the content check). Keys
35
+ // naming no copy are ignored. Two copies of one source that declare
36
+ // different CRCs are a self-contradictory manifest and an error;
37
+ // - deletes: an object (its keys) or an array of strings;
38
+ // - hbcTransform: an object; its member named after the bundle patch entry,
39
+ // when present, must be an object and is handed on re-serialized
40
+ // (PatchManifest::bundle_hbc_transform_meta). Non-object metadata is an
41
+ // error, never ignored: applying a transform-domain patch without the
42
+ // transform produces a corrupt bundle;
43
+ // - every path passes ValidateManifest.
44
+ // Values of the wrong type (a numeric source, a non-string delete, copies as
45
+ // an array, ...) are errors on every platform.
46
+ struct PatchManifestOptions {
47
+ // The archive entry whose hbcTransform metadata is extracted
48
+ // (archive_patch::DefaultBundleDeltaEntryName() unless the platform names
49
+ // its bundle differently, e.g. HarmonyOS).
50
+ std::string bundle_patch_entry_name;
51
+ // Stripped once from the start of every copy source that begins with it,
52
+ // before the empty-source rule applies. HarmonyOS package diffs name the
53
+ // origin entries by their HAP path ("resources/rawfile/assets/a.png"),
54
+ // which the resource manager reads back as "assets/a.png".
55
+ std::string copy_source_prefix;
56
+ };
57
+
58
+ // A package holds at most archive_limits::kMaxEntries entries, and the
59
+ // generators emit at most one copy source, one CRC and one delete per entry
60
+ // (plus a few hundred values of hbcTransform metadata). The cap bounds the
61
+ // parsed tree of a hostile 16 MB manifest to about ten MB of heap.
62
+ constexpr size_t kMaxManifestNodes =
63
+ 4 * static_cast<size_t>(archive_limits::kMaxEntries);
64
+
65
+ // Fails without touching *out.
66
+ Status ParsePatchManifest(
67
+ const std::string& json_text,
68
+ const PatchManifestOptions& options,
69
+ PatchManifest* out);
70
+
71
+ } // namespace delta
72
+ } // namespace pushy
73
+
74
+ #pragma GCC visibility pop
@@ -1,9 +1,9 @@
1
1
  #pragma once
2
2
 
3
3
  // Single source of truth for the state-machine operation codes shared across
4
- // the platform glue layers (Android JNI in update_core_android.cpp and
5
- // HarmonyOS NAPI in pushy.cpp). The integer values MUST stay in sync with the
6
- // callers on each platform:
4
+ // the platform glue layers (Android JNI in
5
+ // cpp/android_jni/update_core_android.cpp and HarmonyOS NAPI in pushy.cpp).
6
+ // The integer values MUST stay in sync with the callers on each platform:
7
7
  // - Android: UpdateContext.java (STATE_OP_* constants)
8
8
  // - HarmonyOS: UpdateContext.ts (StateOperation usage)
9
9
  // Do not renumber existing entries; only append new ones.
@@ -3,6 +3,7 @@
3
3
  #include <cmath>
4
4
  #include <cstdint>
5
5
  #include <cstdio>
6
+ #include <unordered_map>
6
7
 
7
8
  namespace flowjson {
8
9
 
@@ -86,6 +87,40 @@ bool Value::StrictEquals(const Value& a, const Value& b) {
86
87
  return false;
87
88
  }
88
89
 
90
+ bool Value::DeepEquals(const Value& a, const Value& b) {
91
+ if (a.kind_ == Kind::Array && b.kind_ == Kind::Array) {
92
+ if (a.elements_.size() != b.elements_.size()) {
93
+ return false;
94
+ }
95
+ for (size_t i = 0; i < a.elements_.size(); i++) {
96
+ if (!DeepEquals(a.elements_[i], b.elements_[i])) {
97
+ return false;
98
+ }
99
+ }
100
+ return true;
101
+ }
102
+ if (a.kind_ == Kind::Object && b.kind_ == Kind::Object) {
103
+ size_t defined = 0;
104
+ for (const auto& member : a.members_) {
105
+ if (member.second.IsUndefined()) {
106
+ continue;
107
+ }
108
+ defined++;
109
+ const Value& other = b.Get(member.first);
110
+ if (other.IsUndefined() || !DeepEquals(member.second, other)) {
111
+ return false;
112
+ }
113
+ }
114
+ for (const auto& member : b.members_) {
115
+ if (!member.second.IsUndefined()) {
116
+ defined--;
117
+ }
118
+ }
119
+ return defined == 0;
120
+ }
121
+ return StrictEquals(a, b);
122
+ }
123
+
89
124
  namespace {
90
125
 
91
126
  void AppendEscaped(const std::string& s, std::string* out) {
@@ -232,6 +267,8 @@ namespace {
232
267
  // stack limit (size and node count are capped in flow_json.h). Real
233
268
  // responses nest ~4 levels.
234
269
  constexpr int kMaxDepth = 64;
270
+ // Objects with more members than this get a hash index while parsing.
271
+ constexpr size_t kIndexedObjectMembers = 16;
235
272
 
236
273
  // Locale-independent decimal -> double. strtod() honours LC_NUMERIC, so a
237
274
  // host process that called setlocale() into a comma-decimal locale would stop
@@ -327,7 +364,8 @@ double DecimalToDouble(const std::string& token) {
327
364
 
328
365
  class Parser {
329
366
  public:
330
- Parser(const std::string& text) : text_(text) {}
367
+ Parser(const std::string& text, size_t max_nodes)
368
+ : text_(text), max_nodes_(max_nodes) {}
331
369
 
332
370
  Value Run(bool* ok) {
333
371
  Value v = ParseValue();
@@ -376,7 +414,7 @@ class Parser {
376
414
  ok_ = false;
377
415
  return Value::Undefined();
378
416
  }
379
- if (++nodes_ > kMaxNodes) {
417
+ if (++nodes_ > max_nodes_) {
380
418
  ok_ = false;
381
419
  return Value::Undefined();
382
420
  }
@@ -409,20 +447,51 @@ class Parser {
409
447
  }
410
448
  }
411
449
 
450
+ // Duplicate keys keep the first position and the last value (Value::Set
451
+ // semantics). Large objects (a patch manifest's copies map has thousands
452
+ // of members) look keys up through a hash index instead of Set's linear
453
+ // scan, which would make parsing quadratic in the member count.
412
454
  Value ParseObject() {
413
- Value obj = Value::Object();
414
455
  Consume('{');
415
456
  SkipWs();
416
457
  if (pos_ < text_.size() && text_[pos_] == '}') {
417
458
  pos_++;
418
- return obj;
459
+ return Value::Object();
419
460
  }
461
+ Members members;
462
+ std::unordered_map<std::string, size_t> index;
420
463
  while (ok_) {
421
464
  SkipWs();
422
465
  std::string key = ParseString();
423
466
  SkipWs();
424
467
  Consume(':');
425
- obj.Set(key, ParseValue());
468
+ Value value = ParseValue();
469
+ if (members.size() == kIndexedObjectMembers && index.empty()) {
470
+ for (size_t i = 0; i < members.size(); i++) {
471
+ index.emplace(members[i].first, i);
472
+ }
473
+ }
474
+ size_t existing = members.size();
475
+ if (index.empty()) {
476
+ for (size_t i = 0; i < members.size(); i++) {
477
+ if (members[i].first == key) {
478
+ existing = i;
479
+ break;
480
+ }
481
+ }
482
+ } else {
483
+ auto found = index.find(key);
484
+ if (found != index.end()) {
485
+ existing = found->second;
486
+ } else {
487
+ index.emplace(key, members.size());
488
+ }
489
+ }
490
+ if (existing < members.size()) {
491
+ members[existing].second = std::move(value);
492
+ } else {
493
+ members.emplace_back(std::move(key), std::move(value));
494
+ }
426
495
  SkipWs();
427
496
  if (pos_ < text_.size() && text_[pos_] == ',') {
428
497
  pos_++;
@@ -431,7 +500,7 @@ class Parser {
431
500
  Consume('}');
432
501
  break;
433
502
  }
434
- return obj;
503
+ return Value::Object(std::move(members));
435
504
  }
436
505
 
437
506
  Value ParseArray() {
@@ -635,6 +704,7 @@ class Parser {
635
704
  }
636
705
 
637
706
  const std::string& text_;
707
+ const size_t max_nodes_;
638
708
  size_t pos_ = 0;
639
709
  int depth_ = 0;
640
710
  size_t nodes_ = 0;
@@ -644,11 +714,15 @@ class Parser {
644
714
  } // namespace
645
715
 
646
716
  Value Parse(const std::string& text, bool* ok) {
647
- if (text.size() > kMaxInputBytes) {
717
+ return Parse(text, ParseLimits(), ok);
718
+ }
719
+
720
+ Value Parse(const std::string& text, const ParseLimits& limits, bool* ok) {
721
+ if (text.size() > limits.max_input_bytes) {
648
722
  *ok = false;
649
723
  return Value::Undefined();
650
724
  }
651
- Parser parser(text);
725
+ Parser parser(text, limits.max_nodes);
652
726
  return parser.Run(ok);
653
727
  }
654
728
 
@@ -51,6 +51,12 @@ class Value {
51
51
  }
52
52
  static Value Array() { return Value(Kind::Array); }
53
53
  static Value Object() { return Value(Kind::Object); }
54
+ // The members must have distinct keys (Set's invariant).
55
+ static Value Object(Members members) {
56
+ Value v(Kind::Object);
57
+ v.members_ = std::move(members);
58
+ return v;
59
+ }
54
60
 
55
61
  Kind kind() const { return kind_; }
56
62
  bool IsUndefined() const { return kind_ == Kind::Undefined; }
@@ -81,6 +87,12 @@ class Value {
81
87
 
82
88
  static bool StrictEquals(const Value& a, const Value& b);
83
89
 
90
+ // Structural equality of JSON values (src/updateFlowCore.ts
91
+ // jsonDeepEqual): object key order is ignored and undefined members count
92
+ // as absent; arrays compare element-wise in order; primitives use ===
93
+ // (so 1 equals 1.0, false is not 0, null equals only null).
94
+ static bool DeepEquals(const Value& a, const Value& b);
95
+
84
96
  private:
85
97
  explicit Value(Kind kind) : kind_(kind) {}
86
98
 
@@ -116,6 +128,15 @@ constexpr size_t kMaxNodes = 65536;
116
128
  // Lone surrogate escapes decode to U+FFFD so the output is always valid UTF-8.
117
129
  Value Parse(const std::string& text, bool* ok);
118
130
 
131
+ // Same parser under caller-chosen caps, for documents whose size is bounded
132
+ // by something other than a checkUpdate response (the patch core's
133
+ // __diff.json manifest, cpp/patch_core/patch_manifest.h).
134
+ struct ParseLimits {
135
+ size_t max_input_bytes = kMaxInputBytes;
136
+ size_t max_nodes = kMaxNodes;
137
+ };
138
+ Value Parse(const std::string& text, const ParseLimits& limits, bool* ok);
139
+
119
140
  } // namespace flowjson
120
141
 
121
142
  #pragma GCC visibility pop