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.
- package/README-CN.md +7 -7
- package/README.md +7 -6
- package/android/build.gradle +25 -33
- package/android/jni/Android.mk +16 -24
- package/android/lib/arm64-v8a/librnpushy.so +0 -0
- package/android/lib/armeabi-v7a/librnpushy.so +0 -0
- package/android/lib/x86/librnpushy.so +0 -0
- package/android/lib/x86_64/librnpushy.so +0 -0
- package/android/proguard.pro +9 -7
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +11 -12
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
- package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
- package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
- package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
- package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
- package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
- package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
- package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
- package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
- package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
- package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
- package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
- package/cpp/android_jni/archive_limits_jni.cpp +83 -0
- package/cpp/android_jni/install_record_jni.cpp +89 -0
- package/cpp/android_jni/jni_log.h +31 -0
- package/cpp/android_jni/jni_natives.h +40 -0
- package/cpp/android_jni/jni_registration.cpp +133 -0
- package/cpp/android_jni/jni_util.h +160 -0
- package/cpp/android_jni/update_core_android.cpp +450 -0
- package/cpp/android_jni/update_flow_jni.cpp +133 -0
- package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
- package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
- package/cpp/patch_core/archive_limits.cpp +194 -0
- package/cpp/patch_core/archive_limits.h +99 -10
- package/cpp/patch_core/archive_patch_core.cpp +96 -12
- package/cpp/patch_core/archive_patch_core.h +49 -0
- package/cpp/patch_core/digest.h +5 -4
- package/cpp/patch_core/error_codes.h +5 -3
- package/cpp/patch_core/install_record.cpp +430 -0
- package/cpp/patch_core/install_record.h +163 -9
- package/cpp/patch_core/patch_core.cpp +62 -16
- package/cpp/patch_core/patch_core.h +27 -7
- package/cpp/patch_core/patch_manifest.cpp +201 -0
- package/cpp/patch_core/patch_manifest.h +74 -0
- package/cpp/patch_core/state_ops.h +3 -3
- package/cpp/update_flow_core/flow_json.cpp +82 -8
- package/cpp/update_flow_core/flow_json.h +21 -0
- package/cpp/update_flow_core/native_config.cpp +456 -0
- package/cpp/update_flow_core/native_config.h +84 -0
- package/cpp/update_flow_core/native_round.cpp +326 -0
- package/cpp/update_flow_core/native_round.h +90 -0
- package/cpp/update_flow_core/update_flow_core.h +2 -2
- package/harmony/hvigor-plugin.ts +11 -19
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +19 -26
- package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
- package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
- package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
- package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
- package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
- package/harmony/pushy/src/main/ets/Logger.ts +20 -34
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
- package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
- package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
- package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
- package/harmony/pushy/src/main/ets/Texts.ts +0 -4
- package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
- package/ios/RCTPushy/RCTPushy.h +12 -0
- package/ios/RCTPushy/RCTPushy.mm +87 -2628
- package/ios/RCTPushy/RCTPushyCommon.h +34 -0
- package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
- package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
- package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
- package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
- package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
- package/ios/RCTPushy/RCTPushyInternal.h +144 -0
- package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
- package/ios/RCTPushy/RCTPushyState.mm +314 -0
- package/package.json +25 -23
- package/react-native-update.podspec +74 -83
- package/src/client.ts +361 -451
- package/src/context.ts +1 -1
- package/src/core.ts +35 -8
- package/src/endpoint.ts +0 -2
- package/src/error.ts +13 -32
- package/src/i18n.ts +18 -83
- package/src/locales/en.ts +0 -4
- package/src/locales/zh.ts +0 -4
- package/src/provider.tsx +60 -53
- package/src/telemetry.ts +0 -13
- package/src/type.ts +0 -15
- package/src/updateFlowCore.ts +116 -10
- package/src/utils.ts +49 -42
- package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
- package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
- package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
- package/cpp/patch_core/jni_natives.h +0 -27
- package/cpp/patch_core/jni_registration.cpp +0 -103
- package/cpp/patch_core/jni_util.h +0 -60
- package/cpp/patch_core/patch_core_android.cpp +0 -97
- package/cpp/patch_core/update_core_android.cpp +0 -485
- 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
|