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
@@ -0,0 +1,456 @@
1
+ #include "native_config.h"
2
+
3
+ #include <unordered_set>
4
+ #include <utility>
5
+ #include <vector>
6
+
7
+ #include "../patch_core/obscured_text.h"
8
+
9
+ namespace flowcore {
10
+
11
+ using flowjson::Value;
12
+
13
+ namespace {
14
+
15
+ // Length of the ECMAScript whitespace/line terminator encoded at text[i], or
16
+ // 0 when there is none.
17
+ size_t WhitespaceAt(const std::string& text, size_t i) {
18
+ const size_t left = text.size() - i;
19
+ const auto byte = [&](size_t k) {
20
+ return static_cast<unsigned char>(text[i + k]);
21
+ };
22
+ const unsigned char b0 = byte(0);
23
+ if (b0 == ' ' || (b0 >= 0x09 && b0 <= 0x0d)) {
24
+ return 1;
25
+ }
26
+ if (b0 == 0xc2 && left >= 2 && byte(1) == 0xa0) {
27
+ return 2; // U+00A0
28
+ }
29
+ if (left < 3) {
30
+ return 0;
31
+ }
32
+ const unsigned char b1 = byte(1);
33
+ const unsigned char b2 = byte(2);
34
+ if (b0 == 0xe1 && b1 == 0x9a && b2 == 0x80) {
35
+ return 3; // U+1680
36
+ }
37
+ if (b0 == 0xe2 && b1 == 0x80 &&
38
+ (b2 <= 0x8a || b2 == 0xa8 || b2 == 0xa9 || b2 == 0xaf) && b2 >= 0x80) {
39
+ return 3; // U+2000-U+200A, U+2028, U+2029, U+202F
40
+ }
41
+ if (b0 == 0xe2 && b1 == 0x81 && b2 == 0x9f) {
42
+ return 3; // U+205F
43
+ }
44
+ if (b0 == 0xe3 && b1 == 0x80 && b2 == 0x80) {
45
+ return 3; // U+3000
46
+ }
47
+ if (b0 == 0xef && b1 == 0xbb && b2 == 0xbf) {
48
+ return 3; // U+FEFF
49
+ }
50
+ return 0;
51
+ }
52
+
53
+ bool ContainsJsWhitespace(const std::string& text) {
54
+ for (size_t i = 0; i < text.size(); i++) {
55
+ if (WhitespaceAt(text, i) != 0) {
56
+ return true;
57
+ }
58
+ }
59
+ return false;
60
+ }
61
+
62
+ std::string StripTrailingSlashes(std::string text) {
63
+ while (!text.empty() && text.back() == '/') {
64
+ text.pop_back();
65
+ }
66
+ return text;
67
+ }
68
+
69
+ // Strings lose their trailing '/' first; then every entry (string or not)
70
+ // already present by value is dropped.
71
+ Value CanonicalUrlList(const Value& list) {
72
+ if (!list.IsArray()) {
73
+ return list;
74
+ }
75
+ Value result = Value::Array();
76
+ std::unordered_set<std::string> strings;
77
+ std::vector<const Value*> others;
78
+ for (const Value& item : list.elements()) {
79
+ if (item.IsString()) {
80
+ std::string url = StripTrailingSlashes(item.AsString());
81
+ if (strings.insert(url).second) {
82
+ result.Push(Value::String(std::move(url)));
83
+ }
84
+ continue;
85
+ }
86
+ bool seen = false;
87
+ for (const Value* other : others) {
88
+ if (Value::DeepEquals(*other, item)) {
89
+ seen = true;
90
+ break;
91
+ }
92
+ }
93
+ if (!seen) {
94
+ others.push_back(&item);
95
+ result.Push(item);
96
+ }
97
+ }
98
+ return result;
99
+ }
100
+
101
+ Value ParseObject(const std::string& json) {
102
+ bool ok = false;
103
+ Value value = flowjson::Parse(json, &ok);
104
+ return ok && value.IsObject() ? value : Value::Undefined();
105
+ }
106
+
107
+ const std::string& PolicyNextLaunch() {
108
+ static const std::string value =
109
+ pushy::text::Reveal("2912e0ffab8e6c70323b1dedd3");
110
+ return value;
111
+ }
112
+
113
+ // Pushy's own service addresses, encoded by scripts/encode-native-text.ts.
114
+ std::vector<std::string> DefaultEndpoints() {
115
+ return {
116
+ pushy::text::Reveal(
117
+ "3203e0c1bdd1270a372f18f8c2b6de7f4f2607f5b3d5b9817b592947e5cdefbc8a7e"),
118
+ pushy::text::Reveal(
119
+ "3203e0c1bdd1270a372f18f8c2b6de7f4f2607f5f0daac9c644a620ae88ca1ad93"),
120
+ };
121
+ }
122
+
123
+ // One literal per address: src/__tests__/nativeTextEncoding.test.ts finds
124
+ // them by pattern.
125
+ std::vector<std::string> DefaultQueryUrls() {
126
+ return {
127
+ pushy::text::Reveal("3203e0c1bdd1270a253608fcd3fd9362476817f4f0d5a1996342631be3c2a3a9d779552507fdcde8928a6f512f5ce2ccbdc869404d2f1de79daa826d562c0913eec4fa9b7d4426"),
128
+ pushy::text::Reveal("3203e0c1bdd1270a213b12b7dca09468462e12f3b0d5bd813d482446f4c6a1be8e79552507fdcda68cd06e5c3710e480a4867048483e55e0c2ab8d7d43030d1ce9c3b183214e2601f2f0d5b782601e2719e8ca"),
129
+ };
130
+ }
131
+
132
+ Value StringArray(const std::vector<std::string>& values) {
133
+ Value array = Value::Array();
134
+ for (const std::string& value : values) {
135
+ array.Push(Value::String(value));
136
+ }
137
+ return array;
138
+ }
139
+
140
+ bool IsHexDigit(char c) {
141
+ return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
142
+ (c >= 'A' && c <= 'F');
143
+ }
144
+
145
+ bool IsDigit(char c) { return c >= '0' && c <= '9'; }
146
+
147
+ bool IsHostChar(char c) {
148
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || IsDigit(c) ||
149
+ c == '.' || c == '-';
150
+ }
151
+
152
+ // ^https?://(\[[0-9a-fA-F:]+\]|[a-zA-Z0-9.-]+)(:[0-9]+)?([/?#]|$) — the
153
+ // scheme and authority check; anything else in the authority (userinfo,
154
+ // spaces, a missing host) fails it. Every quantifier is greedy and its
155
+ // continuation cannot start with a character it consumes, so a single
156
+ // left-to-right scan matches exactly what the backtracking regex does.
157
+ bool HasHttpAuthority(const std::string& url) {
158
+ size_t i = 0;
159
+ const std::string http = "http";
160
+ if (url.compare(0, http.size(), http) != 0) {
161
+ return false;
162
+ }
163
+ i = http.size();
164
+ if (i < url.size() && url[i] == 's') {
165
+ i++;
166
+ }
167
+ if (url.compare(i, 3, "://") != 0) {
168
+ return false;
169
+ }
170
+ i += 3;
171
+ const size_t host = i;
172
+ if (i < url.size() && url[i] == '[') {
173
+ i++;
174
+ while (i < url.size() && (IsHexDigit(url[i]) || url[i] == ':')) {
175
+ i++;
176
+ }
177
+ if (i == host + 1 || i >= url.size() || url[i] != ']') {
178
+ return false;
179
+ }
180
+ i++;
181
+ } else {
182
+ while (i < url.size() && IsHostChar(url[i])) {
183
+ i++;
184
+ }
185
+ if (i == host) {
186
+ return false;
187
+ }
188
+ }
189
+ if (i < url.size() && url[i] == ':') {
190
+ const size_t port = ++i;
191
+ while (i < url.size() && IsDigit(url[i])) {
192
+ i++;
193
+ }
194
+ if (i == port) {
195
+ return false;
196
+ }
197
+ }
198
+ return i == url.size() || url[i] == '/' || url[i] == '?' || url[i] == '#';
199
+ }
200
+
201
+ // The normalizer's checks report the first problem in *error (without the
202
+ // prefix) and return false; no exceptions, the cores build without unwind
203
+ // tables on Android.
204
+ bool ConfigString(const Value& value, const std::string& name, bool allowEmpty,
205
+ std::string* out, std::string* error) {
206
+ if (!value.IsString() ||
207
+ (!allowEmpty && TrimJsWhitespace(value.AsString()).empty())) {
208
+ *error = name + " must be a string" +
209
+ (allowEmpty ? "" : " and must not be blank");
210
+ return false;
211
+ }
212
+ *out = value.AsString();
213
+ return true;
214
+ }
215
+
216
+ // `base` lists (endpoints) are request bases: non-empty, no query or
217
+ // fragment, trailing '/' dropped. Discovery lists keep their URLs whole.
218
+ bool ConfigUrls(const Value& values, const std::string& name, bool base,
219
+ Value* out, std::string* error) {
220
+ if (!values.IsArray() || (base && values.Size() == 0)) {
221
+ *error = name + " must be " + (base ? "a non-empty" : "an") + " array";
222
+ return false;
223
+ }
224
+ Value result = Value::Array();
225
+ std::unordered_set<std::string> seen;
226
+ for (const Value& item : values.elements()) {
227
+ std::string url;
228
+ if (!ConfigString(item, name, false, &url, error)) {
229
+ return false;
230
+ }
231
+ url = TrimJsWhitespace(url);
232
+ if (!HasHttpAuthority(url) || ContainsJsWhitespace(url) ||
233
+ url.find('\\') != std::string::npos ||
234
+ (base && url.find_first_of("?#") != std::string::npos)) {
235
+ *error = name + " requires absolute HTTP(S) URLs without credentials" +
236
+ (base ? ", queries or fragments" : "");
237
+ return false;
238
+ }
239
+ if (base) {
240
+ url = StripTrailingSlashes(std::move(url));
241
+ }
242
+ if (seen.insert(url).second) {
243
+ result.Push(Value::String(std::move(url)));
244
+ }
245
+ }
246
+ *out = std::move(result);
247
+ return true;
248
+ }
249
+
250
+ bool Normalize(const std::string& optionsJson, Value* out,
251
+ std::string* error) {
252
+ const Value options = ParseObject(optionsJson);
253
+ if (!options.IsObject()) {
254
+ *error = "expected an object";
255
+ return false;
256
+ }
257
+ static const char* const kKeys[] = {
258
+ "appKey", "endpoints", "queryUrls", "afterDownload",
259
+ "disabled", "packageVersion", "rnu", "rn"};
260
+ for (const auto& member : options.members()) {
261
+ bool known = false;
262
+ for (const char* key : kKeys) {
263
+ known = known || member.first == key;
264
+ }
265
+ if (!known) {
266
+ *error = "unknown option " + member.first;
267
+ return false;
268
+ }
269
+ }
270
+ const auto has = [&](const char* key) {
271
+ return !options.Get(key).IsUndefined();
272
+ };
273
+ // A string option: absent takes `fallback`, present must pass ConfigString.
274
+ const auto string = [&](const char* key, bool allowEmpty,
275
+ const std::string& fallback, Value* result) {
276
+ std::string value = fallback;
277
+ if (has(key) &&
278
+ !ConfigString(options.Get(key), key, allowEmpty, &value, error)) {
279
+ return false;
280
+ }
281
+ *result = Value::String(std::move(value));
282
+ return true;
283
+ };
284
+
285
+ Value appKey, endpoints, queryUrls, rnu, rn, packageVersion;
286
+ if (!has("appKey")) {
287
+ *error = "appKey must be a string and must not be blank";
288
+ return false;
289
+ }
290
+ if (!string("appKey", false, "", &appKey)) {
291
+ return false;
292
+ }
293
+ const bool customEndpoints = has("endpoints");
294
+ if (!ConfigUrls(customEndpoints ? options.Get("endpoints")
295
+ : StringArray(DefaultEndpoints()),
296
+ "endpoints", true, &endpoints, error) ||
297
+ !ConfigUrls(has("queryUrls") ? options.Get("queryUrls")
298
+ : customEndpoints ? Value::Array()
299
+ : StringArray(DefaultQueryUrls()),
300
+ "queryUrls", false, &queryUrls, error)) {
301
+ return false;
302
+ }
303
+ Value afterDownload = Value::String("none");
304
+ if (has("afterDownload")) {
305
+ afterDownload = options.Get("afterDownload");
306
+ if (!afterDownload.IsString() || (afterDownload.AsString() != "none" &&
307
+ afterDownload.AsString() !=
308
+ PolicyNextLaunch())) {
309
+ *error = "afterDownload must be none or " + PolicyNextLaunch();
310
+ return false;
311
+ }
312
+ }
313
+ const Value& disabled = options.Get("disabled");
314
+ if (!disabled.IsUndefined() && !disabled.IsBool()) {
315
+ *error = "disabled must be a boolean";
316
+ return false;
317
+ }
318
+ if (!string("rnu", true, "", &rnu) || !string("rn", true, "", &rn) ||
319
+ (has("packageVersion") &&
320
+ !string("packageVersion", false, "", &packageVersion))) {
321
+ return false;
322
+ }
323
+ Value result = Value::Object();
324
+ result.Set("appKey", std::move(appKey));
325
+ result.Set("endpoints", std::move(endpoints));
326
+ result.Set("queryUrls", std::move(queryUrls));
327
+ result.Set("afterDownload", std::move(afterDownload));
328
+ result.Set("disabled", Value::Bool(disabled.IsBool() && disabled.AsBool()));
329
+ result.Set("rnu", std::move(rnu));
330
+ result.Set("rn", std::move(rn));
331
+ if (!packageVersion.IsUndefined()) {
332
+ result.Set("packageVersion", std::move(packageVersion));
333
+ }
334
+ *out = std::move(result);
335
+ return true;
336
+ }
337
+
338
+ } // namespace
339
+
340
+ std::string TrimJsWhitespace(const std::string& text) {
341
+ size_t begin = 0;
342
+ while (begin < text.size()) {
343
+ const size_t length = WhitespaceAt(text, begin);
344
+ if (length == 0) {
345
+ break;
346
+ }
347
+ begin += length;
348
+ }
349
+ size_t end = text.size();
350
+ while (end > begin) {
351
+ // A whitespace sequence is 1-3 bytes; try each length ending at `end`.
352
+ size_t length = 0;
353
+ for (size_t candidate = 1; candidate <= 3 && candidate <= end - begin;
354
+ candidate++) {
355
+ if (WhitespaceAt(text, end - candidate) == candidate) {
356
+ length = candidate;
357
+ break;
358
+ }
359
+ }
360
+ if (length == 0) {
361
+ break;
362
+ }
363
+ end -= length;
364
+ }
365
+ return text.substr(begin, end - begin);
366
+ }
367
+
368
+ Value CanonicalNativeConfig(const std::string& json) {
369
+ Value config = ParseObject(json);
370
+ if (!config.IsObject()) {
371
+ return config;
372
+ }
373
+ const std::pair<const char*, Value> defaults[] = {
374
+ {"disabled", Value::Bool(false)}, {"queryUrls", Value::Array()},
375
+ {"rnu", Value::String("")}, {"rn", Value::String("")},
376
+ {"afterDownload", Value::String("none")},
377
+ };
378
+ for (const auto& entry : defaults) {
379
+ if (config.Get(entry.first).IsUndefined()) {
380
+ config.Set(entry.first, entry.second);
381
+ }
382
+ }
383
+ for (const char* key : {"endpoints", "queryUrls"}) {
384
+ const Value& list = config.Get(key);
385
+ if (list.IsArray()) {
386
+ config.Set(key, CanonicalUrlList(list));
387
+ }
388
+ }
389
+ return config;
390
+ }
391
+
392
+ bool ConfigsEquivalent(const std::string& a, const std::string& b) {
393
+ const Value left = CanonicalNativeConfig(a);
394
+ if (!left.IsObject()) {
395
+ return false;
396
+ }
397
+ const Value right = CanonicalNativeConfig(b);
398
+ return right.IsObject() && Value::DeepEquals(left, right);
399
+ }
400
+
401
+ RunnableConfig ParseRunnableConfig(const std::string& json) {
402
+ RunnableConfig result;
403
+ if (json.empty()) {
404
+ result.status = "skipped";
405
+ result.reason = "not_configured";
406
+ return result;
407
+ }
408
+ Value config = ParseObject(json);
409
+ if (!config.IsObject()) {
410
+ result.status = "failed";
411
+ result.reason = "invalid_config";
412
+ return result;
413
+ }
414
+ if (config.Get("disabled").Truthy()) {
415
+ result.status = "skipped";
416
+ result.reason = "disabled";
417
+ return result;
418
+ }
419
+ const Value& appKey = config.Get("appKey");
420
+ std::string trimmed =
421
+ appKey.IsString() ? TrimJsWhitespace(appKey.AsString()) : std::string();
422
+ if (trimmed.empty()) {
423
+ result.status = "failed";
424
+ result.reason = "invalid_config";
425
+ return result;
426
+ }
427
+ result.appKey = std::move(trimmed);
428
+ result.config = std::move(config);
429
+ return result;
430
+ }
431
+
432
+ std::string RunnableConfigJson(const std::string& json) {
433
+ const RunnableConfig parsed = ParseRunnableConfig(json);
434
+ Value result = Value::Object();
435
+ if (parsed.runnable()) {
436
+ result.Set("appKey", Value::String(parsed.appKey));
437
+ } else {
438
+ result.Set("status", Value::String(parsed.status));
439
+ result.Set("reason", Value::String(parsed.reason));
440
+ }
441
+ return flowjson::Stringify(result);
442
+ }
443
+
444
+ bool NormalizeConfiguration(const std::string& optionsJson, std::string* out,
445
+ std::string* error) {
446
+ Value result;
447
+ std::string why;
448
+ if (!Normalize(optionsJson, &result, &why)) {
449
+ *error = "Invalid native configuration: " + why;
450
+ return false;
451
+ }
452
+ *out = flowjson::Stringify(result);
453
+ return true;
454
+ }
455
+
456
+ } // namespace flowcore
@@ -0,0 +1,84 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+
5
+ #include "flow_json.h"
6
+
7
+ // The persisted native check config: its validation when a host configures
8
+ // it, the gate that decides whether it can run a round, and the equivalence
9
+ // that decides whether a new one is a change. One implementation for the
10
+ // Android, iOS and Harmony orchestrators (each used to carry its own copy);
11
+ // JSON strings in and out so the bindings stay thin.
12
+ //
13
+ // The JS client keeps its own TS copy of the equivalence
14
+ // (src/updateFlowCore.ts nativeConfigsEquivalent — it must also run on web and
15
+ // on natives that predate this file); both replay
16
+ // tests/config-equivalence-vectors.json. The gate and the normalizer have no
17
+ // JS counterpart; tests/native-config-vectors.json pins them.
18
+ // Keep the core internal to whichever binary links it (see podspec Core).
19
+ #pragma GCC visibility push(hidden)
20
+
21
+ namespace flowcore {
22
+
23
+ // ECMAScript String.prototype.trim: strips WhiteSpace and LineTerminator
24
+ // code points (TAB, LF, VT, FF, CR, SPACE, NBSP, U+1680, U+2000-U+200A,
25
+ // U+2028, U+2029, U+202F, U+205F, U+3000, U+FEFF) from both ends of a UTF-8
26
+ // string. Other control characters are kept.
27
+ std::string TrimJsWhitespace(const std::string& text);
28
+
29
+ // Contract C1. Canonical form of a persisted config: the parsed object with
30
+ // absent keys defaulted ({disabled:false, queryUrls:[], rnu:"", rn:"",
31
+ // afterDownload:"none"}) and each `endpoints`/`queryUrls` array's strings
32
+ // stripped of every trailing '/', then deduplicated keeping the first
33
+ // occurrence. Present values are never coerced (0 is not false, null is not
34
+ // a default). Undefined when the text is not a JSON object.
35
+ flowjson::Value CanonicalNativeConfig(const std::string& json);
36
+
37
+ // Two configs are equivalent iff both are JSON objects whose canonical forms
38
+ // are DeepEquals (key order ignored). Anything else is never equivalent, not
39
+ // even to itself — callers pass "" for an absent config.
40
+ bool ConfigsEquivalent(const std::string& a, const std::string& b);
41
+
42
+ // The gate every entry point (round start, crash-hold install, the host's
43
+ // prepareBundle, the round itself) applies to the persisted config, so they
44
+ // agree on what is runnable:
45
+ // "" -> skipped / not_configured
46
+ // not a JSON object -> failed / invalid_config
47
+ // `disabled` truthy (JS truthiness) -> skipped / disabled
48
+ // `appKey` not a string, or blank -> failed / invalid_config
49
+ // after TrimJsWhitespace
50
+ // otherwise runnable, with the trimmed appKey and the parsed config.
51
+ struct RunnableConfig {
52
+ std::string status; // empty when runnable, else "skipped" / "failed"
53
+ std::string reason;
54
+ std::string appKey;
55
+ flowjson::Value config;
56
+
57
+ bool runnable() const { return status.empty(); }
58
+ };
59
+ RunnableConfig ParseRunnableConfig(const std::string& json);
60
+
61
+ // Binding form of ParseRunnableConfig: {"appKey": "<trimmed>"} when runnable,
62
+ // otherwise {"status": "...", "reason": "..."}. The binding re-parses the
63
+ // config itself when it needs more fields.
64
+ std::string RunnableConfigJson(const std::string& json);
65
+
66
+ // Validates the options a host passes to configure() (a JSON object) and
67
+ // returns the config to persist:
68
+ // {appKey, endpoints, queryUrls, afterDownload, disabled, rnu, rn,
69
+ // packageVersion?}
70
+ // Only those keys are accepted. appKey/packageVersion are non-blank strings
71
+ // (kept as given), rnu/rn strings. endpoints default to Pushy's own and must
72
+ // be a non-empty array of absolute http(s) URLs without credentials, queries
73
+ // or fragments; each is trimmed, stripped of trailing '/' and deduplicated.
74
+ // queryUrls default to Pushy's discovery URLs, or to [] once endpoints are
75
+ // custom; each is trimmed and deduplicated (queries allowed). afterDownload
76
+ // is "none" (default) or the next-launch policy; disabled a boolean. A
77
+ // present null is invalid, never a default. On failure returns false and
78
+ // sets *error to "Invalid native configuration: <why>".
79
+ bool NormalizeConfiguration(const std::string& optionsJson, std::string* out,
80
+ std::string* error);
81
+
82
+ } // namespace flowcore
83
+
84
+ #pragma GCC visibility pop