react-native-update 10.50.0 → 10.51.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/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
- package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
- package/README-CN.md +14 -1
- package/README.md +14 -1
- package/android/jni/Android.mk +5 -1
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +76 -21
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +160 -42
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +44 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/cpp/update_flow_core/flow_json.cpp +522 -0
- package/cpp/update_flow_core/flow_json.h +103 -0
- package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
- package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
- package/cpp/update_flow_core/update_flow_core.cpp +301 -0
- package/cpp/update_flow_core/update_flow_core.h +77 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -1
- package/harmony/pushy/src/main/ets/DownloadTask.ts +48 -9
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
- package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +14 -0
- package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +148 -69
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +779 -61
- package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
- package/package.json +3 -1
- package/react-native-update.podspec +2 -0
- package/src/NativePushy.ts +14 -0
- package/src/client.ts +341 -118
- package/src/endpoint.ts +7 -29
- package/src/provider.tsx +38 -2
- package/src/type.ts +26 -0
- package/src/updateFlowCore.ts +320 -0
- package/src/utils.ts +1 -12
- package/src/isInRollout.ts +0 -87
- package/src/resolveCheckResult.ts +0 -30
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Replays the golden vectors generated by scripts/generate-flow-vectors.ts
|
|
2
|
+
// against the C++ port and compares canonical serializations. A mismatch
|
|
3
|
+
// means the two implementations of the decision layer disagree — fix the C++
|
|
4
|
+
// side (the TS side is the reference) or regenerate stale vectors.
|
|
5
|
+
#include <cstdio>
|
|
6
|
+
#include <fstream>
|
|
7
|
+
#include <sstream>
|
|
8
|
+
#include <string>
|
|
9
|
+
|
|
10
|
+
#include "../flow_json.h"
|
|
11
|
+
#include "../update_flow_core.h"
|
|
12
|
+
|
|
13
|
+
using flowjson::Parse;
|
|
14
|
+
using flowjson::Stringify;
|
|
15
|
+
using flowjson::Value;
|
|
16
|
+
|
|
17
|
+
namespace {
|
|
18
|
+
|
|
19
|
+
Value Dispatch(const std::string& fn, const Value& args, bool* known) {
|
|
20
|
+
*known = true;
|
|
21
|
+
if (fn == "murmurhash3_32_gc") {
|
|
22
|
+
return Value::Number(updateflow::Murmur3_32(args.At(0).AsString()));
|
|
23
|
+
}
|
|
24
|
+
if (fn == "isInRollout") {
|
|
25
|
+
return Value::Bool(
|
|
26
|
+
updateflow::IsInRollout(args.At(0).AsNumber(), args.At(1).AsString()));
|
|
27
|
+
}
|
|
28
|
+
if (fn == "joinUrls") {
|
|
29
|
+
return updateflow::JoinUrls(args.At(0), args.At(1));
|
|
30
|
+
}
|
|
31
|
+
if (fn == "orderEndpointCandidates") {
|
|
32
|
+
return updateflow::OrderEndpointCandidates(args.At(0),
|
|
33
|
+
args.At(1).AsNumber());
|
|
34
|
+
}
|
|
35
|
+
if (fn == "buildCheckRequestBody") {
|
|
36
|
+
return updateflow::BuildCheckRequestBody(args.At(0));
|
|
37
|
+
}
|
|
38
|
+
if (fn == "resolveCheckResult") {
|
|
39
|
+
return updateflow::ResolveCheckResult(args.At(0), args.At(1));
|
|
40
|
+
}
|
|
41
|
+
if (fn == "decideDownload") {
|
|
42
|
+
return updateflow::DecideDownload(args.At(0), args.At(1),
|
|
43
|
+
args.At(2).Truthy());
|
|
44
|
+
}
|
|
45
|
+
if (fn == "shouldActivateAfterDownload") {
|
|
46
|
+
return Value::Bool(updateflow::ShouldActivateAfterDownload(
|
|
47
|
+
args.At(0), args.At(1).AsString()));
|
|
48
|
+
}
|
|
49
|
+
*known = false;
|
|
50
|
+
return Value::Undefined();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The parser consumes network data (checkUpdate responses); malformed and
|
|
54
|
+
// hostile inputs must set ok=false without crashing — this suite runs under
|
|
55
|
+
// ASan+UBSan in CI. Valid-but-tricky inputs pin the JS JSON.parse semantics
|
|
56
|
+
// the port relies on.
|
|
57
|
+
int RunParserRobustness() {
|
|
58
|
+
int failures = 0;
|
|
59
|
+
|
|
60
|
+
const char* malformed[] = {
|
|
61
|
+
"",
|
|
62
|
+
"{",
|
|
63
|
+
"[1,2",
|
|
64
|
+
"\"abc",
|
|
65
|
+
"{\"a\":}",
|
|
66
|
+
"{\"a\" 1}",
|
|
67
|
+
"{\"a\":1,}",
|
|
68
|
+
"[1,]",
|
|
69
|
+
"tru",
|
|
70
|
+
"nul",
|
|
71
|
+
"falsy",
|
|
72
|
+
"1x",
|
|
73
|
+
"1 2",
|
|
74
|
+
"- 1",
|
|
75
|
+
"01",
|
|
76
|
+
"-01",
|
|
77
|
+
"1.",
|
|
78
|
+
"1e",
|
|
79
|
+
"1e+",
|
|
80
|
+
"\"\\q\"",
|
|
81
|
+
"\"\\u12\"",
|
|
82
|
+
"\"\\ud800\\u0041\"", // high surrogate followed by a non-low escape
|
|
83
|
+
"{\"a\":1}garbage",
|
|
84
|
+
"[}",
|
|
85
|
+
"{]",
|
|
86
|
+
};
|
|
87
|
+
for (const char* input : malformed) {
|
|
88
|
+
bool ok = true;
|
|
89
|
+
Parse(input, &ok);
|
|
90
|
+
if (ok) {
|
|
91
|
+
std::fprintf(stderr, "robustness: accepted malformed input: %s\n", input);
|
|
92
|
+
failures++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Nesting: 64 levels parse, 65 are rejected (stack-depth cap).
|
|
97
|
+
for (int depth : {64, 65}) {
|
|
98
|
+
std::string nested;
|
|
99
|
+
for (int i = 0; i < depth; i++) {
|
|
100
|
+
nested.push_back('[');
|
|
101
|
+
}
|
|
102
|
+
for (int i = 0; i < depth; i++) {
|
|
103
|
+
nested.push_back(']');
|
|
104
|
+
}
|
|
105
|
+
bool ok = false;
|
|
106
|
+
Parse(nested, &ok);
|
|
107
|
+
bool expected = depth <= 64;
|
|
108
|
+
if (ok != expected) {
|
|
109
|
+
std::fprintf(stderr, "robustness: depth %d parsed=%d, expected %d\n",
|
|
110
|
+
depth, ok, expected);
|
|
111
|
+
failures++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// A deeply nested bomb must fail cleanly, not overflow the stack.
|
|
115
|
+
{
|
|
116
|
+
std::string bomb(100000, '[');
|
|
117
|
+
bool ok = true;
|
|
118
|
+
Parse(bomb, &ok);
|
|
119
|
+
if (ok) {
|
|
120
|
+
std::fprintf(stderr, "robustness: accepted nesting bomb\n");
|
|
121
|
+
failures++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Valid-but-tricky inputs: assert the parsed value via canonical stringify.
|
|
126
|
+
const struct {
|
|
127
|
+
const char* input;
|
|
128
|
+
const char* expected;
|
|
129
|
+
} tricky[] = {
|
|
130
|
+
// Duplicate keys: first position, last value (JSON.parse semantics).
|
|
131
|
+
{"{\"a\":1,\"b\":2,\"a\":3}", "{\"a\":3,\"b\":2}"},
|
|
132
|
+
{"\"\\u00e9\"", "\"\xc3\xa9\""}, // BMP escape -> UTF-8
|
|
133
|
+
{"\"\\ud83d\\ude00\"", "\"\xf0\x9f\x98\x80\""}, // surrogate pair
|
|
134
|
+
{" { \"a\" : [ 1 , true , null ] } ", "{\"a\":[1,true,null]}"},
|
|
135
|
+
{"-0.5", "-0.5"},
|
|
136
|
+
{"1e2", "100"},
|
|
137
|
+
};
|
|
138
|
+
for (const auto& t : tricky) {
|
|
139
|
+
bool ok = false;
|
|
140
|
+
Value v = Parse(t.input, &ok);
|
|
141
|
+
std::string actual = Stringify(v);
|
|
142
|
+
if (!ok || actual != t.expected) {
|
|
143
|
+
std::fprintf(stderr,
|
|
144
|
+
"robustness: %s -> ok=%d %s (expected %s)\n", t.input, ok,
|
|
145
|
+
actual.c_str(), t.expected);
|
|
146
|
+
failures++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return failures;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// HandleCheckResponse is pure composition (no decision logic of its own), so
|
|
154
|
+
// it is tested directly here instead of via TS-generated vectors: response
|
|
155
|
+
// text in, canonical decision out.
|
|
156
|
+
int RunHandleCheckResponse() {
|
|
157
|
+
int failures = 0;
|
|
158
|
+
Value identity = Value::Object();
|
|
159
|
+
identity.Set("packageVersion", Value::String("2.3.4"));
|
|
160
|
+
identity.Set("currentVersion", Value::String("cur"));
|
|
161
|
+
identity.Set("uuid", Value::String("test1")); // bucket 62
|
|
162
|
+
identity.Set("rolledBackVersion", Value::String("bad"));
|
|
163
|
+
|
|
164
|
+
const struct {
|
|
165
|
+
const char* name;
|
|
166
|
+
const char* response;
|
|
167
|
+
const char* afterDownload;
|
|
168
|
+
const char* expected;
|
|
169
|
+
} cases[] = {
|
|
170
|
+
{"malformed", "not json{", "none",
|
|
171
|
+
"{\"action\":\"none\",\"reason\":\"invalidResponse\"}"},
|
|
172
|
+
{"non-object", "[1,2]", "none",
|
|
173
|
+
"{\"action\":\"none\",\"reason\":\"invalidResponse\"}"},
|
|
174
|
+
{"upToDate", "{\"upToDate\":true}", "none",
|
|
175
|
+
"{\"action\":\"none\",\"reason\":\"noUpdate\",\"info\":{\"upToDate\":"
|
|
176
|
+
"true}}"},
|
|
177
|
+
{"expired",
|
|
178
|
+
"{\"expired\":true,\"downloadUrl\":\"https://x/app.apk\"}", "none",
|
|
179
|
+
"{\"action\":\"none\",\"reason\":\"noUpdate\",\"info\":{\"expired\":"
|
|
180
|
+
"true,\"downloadUrl\":\"https://x/app.apk\"}}"},
|
|
181
|
+
{"alreadyCurrent",
|
|
182
|
+
"{\"update\":true,\"hash\":\"cur\",\"full\":\"cur.ppk\","
|
|
183
|
+
"\"paths\":[\"cdn.x.com\"]}",
|
|
184
|
+
"none",
|
|
185
|
+
"{\"action\":\"none\",\"reason\":\"noUpdate\",\"info\":{\"upToDate\":"
|
|
186
|
+
"true}}"},
|
|
187
|
+
{"download",
|
|
188
|
+
"{\"update\":true,\"hash\":\"h2\",\"full\":\"h2.ppk\","
|
|
189
|
+
"\"paths\":[\"cdn.x.com\"],\"name\":\"v2\"}",
|
|
190
|
+
"none",
|
|
191
|
+
"{\"action\":\"download\",\"hash\":\"h2\",\"attempts\":[{\"type\":"
|
|
192
|
+
"\"full\",\"urls\":[\"https://cdn.x.com/h2.ppk\"]}],\"devNoop\":false,"
|
|
193
|
+
"\"activate\":false,"
|
|
194
|
+
"\"info\":{\"update\":true,\"hash\":\"h2\",\"full\":\"h2.ppk\","
|
|
195
|
+
"\"paths\":[\"cdn.x.com\"],\"name\":\"v2\"}}"},
|
|
196
|
+
{"download-silent-strategy",
|
|
197
|
+
"{\"update\":true,\"hash\":\"h2\",\"full\":\"h2.ppk\","
|
|
198
|
+
"\"paths\":[\"cdn.x.com\"]}",
|
|
199
|
+
"setNeedUpdate",
|
|
200
|
+
"{\"action\":\"download\",\"hash\":\"h2\",\"attempts\":[{\"type\":"
|
|
201
|
+
"\"full\",\"urls\":[\"https://cdn.x.com/h2.ppk\"]}],\"devNoop\":false,"
|
|
202
|
+
"\"activate\":true,"
|
|
203
|
+
"\"info\":{\"update\":true,\"hash\":\"h2\",\"full\":\"h2.ppk\","
|
|
204
|
+
"\"paths\":[\"cdn.x.com\"]}}"},
|
|
205
|
+
{"download-forceBoot",
|
|
206
|
+
"{\"update\":true,\"hash\":\"h2\",\"full\":\"h2.ppk\","
|
|
207
|
+
"\"paths\":[\"cdn.x.com\"],\"config\":{\"forceBoot\":true}}",
|
|
208
|
+
"none",
|
|
209
|
+
"{\"action\":\"download\",\"hash\":\"h2\",\"attempts\":[{\"type\":"
|
|
210
|
+
"\"full\",\"urls\":[\"https://cdn.x.com/h2.ppk\"]}],\"devNoop\":false,"
|
|
211
|
+
"\"activate\":true,"
|
|
212
|
+
"\"info\":{\"update\":true,\"hash\":\"h2\",\"full\":\"h2.ppk\","
|
|
213
|
+
"\"paths\":[\"cdn.x.com\"],\"config\":{\"forceBoot\":true}}}"},
|
|
214
|
+
{"rollout-in-forceBoot",
|
|
215
|
+
"{\"update\":true,\"hash\":\"root\",\"full\":\"root.ppk\","
|
|
216
|
+
"\"paths\":[\"cdn.x.com\"],\"expVersion\":{\"name\":\"g\",\"hash\":"
|
|
217
|
+
"\"gray\",\"full\":\"gray.ppk\",\"config\":{\"rollout\":{\"2.3.4\":"
|
|
218
|
+
"63},\"forceBoot\":true}}}",
|
|
219
|
+
"none",
|
|
220
|
+
"{\"action\":\"download\",\"hash\":\"gray\",\"attempts\":[{\"type\":"
|
|
221
|
+
"\"full\",\"urls\":[\"https://cdn.x.com/gray.ppk\"]}],\"devNoop\":"
|
|
222
|
+
"false,\"activate\":true,"
|
|
223
|
+
"\"info\":{\"update\":true,\"name\":\"g\",\"hash\":\"gray\","
|
|
224
|
+
"\"full\":\"gray.ppk\",\"config\":{\"rollout\":{\"2.3.4\":63},"
|
|
225
|
+
"\"forceBoot\":true},\"paths\":[\"cdn.x.com\"]}}"},
|
|
226
|
+
{"rollout-out",
|
|
227
|
+
"{\"update\":true,\"hash\":\"root\",\"full\":\"root.ppk\","
|
|
228
|
+
"\"paths\":[\"cdn.x.com\"],\"expVersion\":{\"name\":\"g\",\"hash\":"
|
|
229
|
+
"\"gray\",\"full\":\"gray.ppk\",\"config\":{\"rollout\":{\"2.3.4\":"
|
|
230
|
+
"62}}}}",
|
|
231
|
+
"none",
|
|
232
|
+
"{\"action\":\"download\",\"hash\":\"root\",\"attempts\":[{\"type\":"
|
|
233
|
+
"\"full\",\"urls\":[\"https://cdn.x.com/root.ppk\"]}],\"devNoop\":"
|
|
234
|
+
"false,\"activate\":false,\"info\":{\"update\":true,\"hash\":\"root\","
|
|
235
|
+
"\"full\":\"root.ppk\",\"paths\":[\"cdn.x.com\"]}}"},
|
|
236
|
+
{"rolledBack-wins-over-forceBoot",
|
|
237
|
+
"{\"update\":true,\"hash\":\"bad\",\"full\":\"bad.ppk\","
|
|
238
|
+
"\"paths\":[\"cdn.x.com\"],\"config\":{\"forceBoot\":true}}",
|
|
239
|
+
"none",
|
|
240
|
+
"{\"action\":\"none\",\"reason\":\"rolledBack\",\"info\":{\"update\":"
|
|
241
|
+
"true,\"hash\":\"bad\",\"full\":\"bad.ppk\",\"paths\":[\"cdn.x.com\"],"
|
|
242
|
+
"\"config\":{\"forceBoot\":true}}}"},
|
|
243
|
+
};
|
|
244
|
+
for (const auto& c : cases) {
|
|
245
|
+
Value decision = updateflow::HandleCheckResponse(c.response, identity,
|
|
246
|
+
false, c.afterDownload);
|
|
247
|
+
std::string actual = Stringify(decision);
|
|
248
|
+
if (actual != c.expected) {
|
|
249
|
+
std::fprintf(stderr,
|
|
250
|
+
"handleCheckResponse %s MISMATCH\n expected: %s\n"
|
|
251
|
+
" actual: %s\n",
|
|
252
|
+
c.name, c.expected, actual.c_str());
|
|
253
|
+
failures++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return failures;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
} // namespace
|
|
260
|
+
|
|
261
|
+
int main(int argc, char* argv[]) {
|
|
262
|
+
if (argc < 2) {
|
|
263
|
+
std::fprintf(stderr, "usage: %s <flow_vectors.json>\n", argv[0]);
|
|
264
|
+
return 2;
|
|
265
|
+
}
|
|
266
|
+
std::ifstream file(argv[1]);
|
|
267
|
+
if (!file) {
|
|
268
|
+
std::fprintf(stderr, "cannot open %s\n", argv[1]);
|
|
269
|
+
return 2;
|
|
270
|
+
}
|
|
271
|
+
std::stringstream buffer;
|
|
272
|
+
buffer << file.rdbuf();
|
|
273
|
+
|
|
274
|
+
bool ok = false;
|
|
275
|
+
Value doc = Parse(buffer.str(), &ok);
|
|
276
|
+
if (!ok) {
|
|
277
|
+
std::fprintf(stderr, "failed to parse %s\n", argv[1]);
|
|
278
|
+
return 2;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const Value& cases = doc.Get("cases");
|
|
282
|
+
if (!cases.IsArray() || cases.Size() == 0) {
|
|
283
|
+
std::fprintf(stderr, "%s must contain a non-empty cases array\n", argv[1]);
|
|
284
|
+
return 2;
|
|
285
|
+
}
|
|
286
|
+
int failures = RunParserRobustness() + RunHandleCheckResponse();
|
|
287
|
+
for (size_t i = 0; i < cases.Size(); i++) {
|
|
288
|
+
const Value& testCase = cases.At(i);
|
|
289
|
+
const std::string& fn = testCase.Get("fn").AsString();
|
|
290
|
+
bool known = false;
|
|
291
|
+
Value actual = Dispatch(fn, testCase.Get("args"), &known);
|
|
292
|
+
if (!known) {
|
|
293
|
+
std::fprintf(stderr, "case %zu: unknown fn %s\n", i, fn.c_str());
|
|
294
|
+
failures++;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
std::string actualStr = Stringify(actual);
|
|
298
|
+
std::string expectedStr = Stringify(testCase.Get("expected"));
|
|
299
|
+
if (actualStr != expectedStr) {
|
|
300
|
+
std::fprintf(stderr,
|
|
301
|
+
"case %zu (%s) MISMATCH\n args: %s\n expected: %s\n"
|
|
302
|
+
" actual: %s\n",
|
|
303
|
+
i, fn.c_str(), Stringify(testCase.Get("args")).c_str(),
|
|
304
|
+
expectedStr.c_str(), actualStr.c_str());
|
|
305
|
+
failures++;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (failures) {
|
|
310
|
+
std::fprintf(stderr, "%d failures (%zu vectors + parser robustness)\n",
|
|
311
|
+
failures, cases.Size());
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
std::printf("all %zu flow vectors + parser robustness passed\n",
|
|
315
|
+
cases.Size());
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#include "update_flow_core.h"
|
|
2
|
+
|
|
3
|
+
#include <cmath>
|
|
4
|
+
|
|
5
|
+
namespace updateflow {
|
|
6
|
+
|
|
7
|
+
using flowjson::Value;
|
|
8
|
+
|
|
9
|
+
uint32_t Murmur3_32(const std::string& key, uint32_t seed) {
|
|
10
|
+
// The TS reference emulates 32-bit multiplication with 16-bit halves;
|
|
11
|
+
// native uint32_t arithmetic is exactly (x * y) mod 2^32, so the halved
|
|
12
|
+
// dance collapses to plain multiplies. Bytes are read as charCodeAt & 0xff,
|
|
13
|
+
// which is identical for the ASCII inputs this layer handles (uuids, keys).
|
|
14
|
+
const uint32_t c1 = 0xcc9e2d51;
|
|
15
|
+
const uint32_t c2 = 0x1b873593;
|
|
16
|
+
const size_t len = key.size();
|
|
17
|
+
const size_t nblocks = len / 4;
|
|
18
|
+
uint32_t h1 = seed;
|
|
19
|
+
|
|
20
|
+
const unsigned char* data =
|
|
21
|
+
reinterpret_cast<const unsigned char*>(key.data());
|
|
22
|
+
for (size_t i = 0; i < nblocks; i++) {
|
|
23
|
+
uint32_t k1 = static_cast<uint32_t>(data[i * 4]) |
|
|
24
|
+
(static_cast<uint32_t>(data[i * 4 + 1]) << 8) |
|
|
25
|
+
(static_cast<uint32_t>(data[i * 4 + 2]) << 16) |
|
|
26
|
+
(static_cast<uint32_t>(data[i * 4 + 3]) << 24);
|
|
27
|
+
k1 *= c1;
|
|
28
|
+
k1 = (k1 << 15) | (k1 >> 17);
|
|
29
|
+
k1 *= c2;
|
|
30
|
+
h1 ^= k1;
|
|
31
|
+
h1 = (h1 << 13) | (h1 >> 19);
|
|
32
|
+
h1 = h1 * 5 + 0xe6546b64;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
uint32_t k1 = 0;
|
|
36
|
+
const unsigned char* tail = data + nblocks * 4;
|
|
37
|
+
switch (len & 3) {
|
|
38
|
+
case 3:
|
|
39
|
+
k1 ^= static_cast<uint32_t>(tail[2]) << 16;
|
|
40
|
+
[[fallthrough]];
|
|
41
|
+
case 2:
|
|
42
|
+
k1 ^= static_cast<uint32_t>(tail[1]) << 8;
|
|
43
|
+
[[fallthrough]];
|
|
44
|
+
case 1:
|
|
45
|
+
k1 ^= tail[0];
|
|
46
|
+
k1 *= c1;
|
|
47
|
+
k1 = (k1 << 15) | (k1 >> 17);
|
|
48
|
+
k1 *= c2;
|
|
49
|
+
h1 ^= k1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
h1 ^= static_cast<uint32_t>(len);
|
|
53
|
+
h1 ^= h1 >> 16;
|
|
54
|
+
h1 *= 0x85ebca6b;
|
|
55
|
+
h1 ^= h1 >> 13;
|
|
56
|
+
h1 *= 0xc2b2ae35;
|
|
57
|
+
h1 ^= h1 >> 16;
|
|
58
|
+
return h1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
bool IsInRollout(double rollout, const std::string& uuid) {
|
|
62
|
+
return static_cast<double>(Murmur3_32(uuid) % 100) < rollout;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
namespace {
|
|
66
|
+
|
|
67
|
+
// ^[a-z][a-z0-9+.-]*:\/\/ (case-insensitive)
|
|
68
|
+
bool HasExplicitScheme(const std::string& s) {
|
|
69
|
+
size_t i = 0;
|
|
70
|
+
auto isAlpha = [](char c) {
|
|
71
|
+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
|
72
|
+
};
|
|
73
|
+
if (i >= s.size() || !isAlpha(s[i])) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
i++;
|
|
77
|
+
while (i < s.size()) {
|
|
78
|
+
char c = s[i];
|
|
79
|
+
if (isAlpha(c) || (c >= '0' && c <= '9') || c == '+' || c == '.' ||
|
|
80
|
+
c == '-') {
|
|
81
|
+
i++;
|
|
82
|
+
} else {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return s.compare(i, 3, "://") == 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
} // namespace
|
|
90
|
+
|
|
91
|
+
Value JoinUrls(const Value& paths, const Value& fileName) {
|
|
92
|
+
if (!fileName.Truthy()) {
|
|
93
|
+
return Value::Undefined();
|
|
94
|
+
}
|
|
95
|
+
Value urls = Value::Array();
|
|
96
|
+
for (const auto& path : paths.elements()) {
|
|
97
|
+
std::string normalized = path.AsString();
|
|
98
|
+
while (!normalized.empty() && normalized.back() == '/') {
|
|
99
|
+
normalized.pop_back();
|
|
100
|
+
}
|
|
101
|
+
std::string base = HasExplicitScheme(normalized)
|
|
102
|
+
? normalized
|
|
103
|
+
: "https://" + normalized;
|
|
104
|
+
urls.Push(Value::String(base + "/" + fileName.AsString()));
|
|
105
|
+
}
|
|
106
|
+
return urls;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Value OrderEndpointCandidates(const Value& endpoints, double randomSample) {
|
|
110
|
+
Value deduped = Value::Array();
|
|
111
|
+
for (const auto& endpoint : endpoints.elements()) {
|
|
112
|
+
if (!endpoint.Truthy()) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
bool seen = false;
|
|
116
|
+
for (const auto& kept : deduped.elements()) {
|
|
117
|
+
if (kept.AsString() == endpoint.AsString()) {
|
|
118
|
+
seen = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!seen) {
|
|
123
|
+
deduped.Push(endpoint);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const size_t n = deduped.Size();
|
|
127
|
+
if (n < 2) {
|
|
128
|
+
return deduped;
|
|
129
|
+
}
|
|
130
|
+
const double idx = std::isfinite(randomSample)
|
|
131
|
+
? std::floor(randomSample * static_cast<double>(n))
|
|
132
|
+
: 0;
|
|
133
|
+
// Validate in floating-point space before converting to size_t: converting
|
|
134
|
+
// NaN, infinity, or an out-of-range value is undefined behavior in C++.
|
|
135
|
+
const size_t first = idx <= 0 ? 0
|
|
136
|
+
: (idx >= static_cast<double>(n - 1)
|
|
137
|
+
? n - 1
|
|
138
|
+
: static_cast<size_t>(idx));
|
|
139
|
+
Value ordered = Value::Array();
|
|
140
|
+
ordered.Push(deduped.At(first));
|
|
141
|
+
for (size_t i = 0; i < n; i++) {
|
|
142
|
+
if (i != first) {
|
|
143
|
+
ordered.Push(deduped.At(i));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return ordered;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
Value BuildCheckRequestBody(const Value& input) {
|
|
150
|
+
Value body = Value::Object();
|
|
151
|
+
body.Set("packageVersion", input.Get("packageVersion"));
|
|
152
|
+
body.Set("hash", input.Get("currentVersion"));
|
|
153
|
+
body.Set("buildTime", input.Get("buildTime"));
|
|
154
|
+
body.Set("cInfo", input.Get("cInfo"));
|
|
155
|
+
const Value& diffV = input.Get("supportedDiffVersion");
|
|
156
|
+
if (diffV.Truthy()) {
|
|
157
|
+
body.Set("diffV", diffV);
|
|
158
|
+
}
|
|
159
|
+
const Value& bundleHash = input.Get("bundleHash");
|
|
160
|
+
if (bundleHash.Truthy()) {
|
|
161
|
+
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
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (input.Get("isDev").Truthy()) {
|
|
170
|
+
body.Remove("buildTime");
|
|
171
|
+
}
|
|
172
|
+
return body;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Value ResolveCheckResult(const Value& rootInfo, const Value& identity) {
|
|
176
|
+
Value rootResult = Value::Object();
|
|
177
|
+
for (const auto& member : rootInfo.members()) {
|
|
178
|
+
if (member.first != "expVersion") {
|
|
179
|
+
rootResult.Set(member.first, member.second);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const Value& expVersion = rootInfo.Get("expVersion");
|
|
183
|
+
const Value& currentVersion = identity.Get("currentVersion");
|
|
184
|
+
// expVersion?.config?.rollout?.[identity.packageVersion] — Get on a
|
|
185
|
+
// non-object returns Undefined, mirroring optional chaining.
|
|
186
|
+
const Value& rollout = expVersion.Get("config").Get("rollout").Get(
|
|
187
|
+
identity.Get("packageVersion").AsString());
|
|
188
|
+
if (rootResult.Get("update").Truthy() && expVersion.Truthy() &&
|
|
189
|
+
rollout.IsNumber()) {
|
|
190
|
+
if (IsInRollout(rollout.AsNumber(), identity.Get("uuid").AsString())) {
|
|
191
|
+
const Value& expHash = expVersion.Get("hash");
|
|
192
|
+
if (expHash.IsString() && !expHash.AsString().empty() &&
|
|
193
|
+
Value::StrictEquals(expHash, currentVersion)) {
|
|
194
|
+
Value upToDate = Value::Object();
|
|
195
|
+
upToDate.Set("upToDate", Value::Bool(true));
|
|
196
|
+
return upToDate;
|
|
197
|
+
}
|
|
198
|
+
Value info = Value::Object();
|
|
199
|
+
info.Set("update", Value::Bool(true));
|
|
200
|
+
for (const auto& member : expVersion.members()) {
|
|
201
|
+
info.Set(member.first, member.second);
|
|
202
|
+
}
|
|
203
|
+
if (rootResult.Get("paths").Truthy()) {
|
|
204
|
+
info.Set("paths", rootResult.Get("paths"));
|
|
205
|
+
}
|
|
206
|
+
return info;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const Value& rootHash = rootResult.Get("hash");
|
|
210
|
+
if (rootResult.Get("update").Truthy() && rootHash.IsString() &&
|
|
211
|
+
!rootHash.AsString().empty() &&
|
|
212
|
+
Value::StrictEquals(rootHash, currentVersion)) {
|
|
213
|
+
Value upToDate = Value::Object();
|
|
214
|
+
upToDate.Set("upToDate", Value::Bool(true));
|
|
215
|
+
return upToDate;
|
|
216
|
+
}
|
|
217
|
+
return rootResult;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
namespace {
|
|
221
|
+
|
|
222
|
+
Value DeclineDownload(const char* reason) {
|
|
223
|
+
Value none = Value::Object();
|
|
224
|
+
none.Set("action", Value::String("none"));
|
|
225
|
+
none.Set("reason", Value::String(reason));
|
|
226
|
+
return none;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
} // namespace
|
|
230
|
+
|
|
231
|
+
Value DecideDownload(const Value& info, const Value& identity, bool isDev) {
|
|
232
|
+
const Value& hash = info.Get("hash");
|
|
233
|
+
Value paths = info.Get("paths");
|
|
234
|
+
if (paths.IsUndefined()) {
|
|
235
|
+
paths = Value::Array(); // const { paths = [] } — undefined only
|
|
236
|
+
}
|
|
237
|
+
if (!info.Get("update").Truthy() || !hash.Truthy()) {
|
|
238
|
+
return DeclineDownload("noUpdate");
|
|
239
|
+
}
|
|
240
|
+
if (Value::StrictEquals(hash, identity.Get("currentVersion"))) {
|
|
241
|
+
return DeclineDownload("alreadyCurrent");
|
|
242
|
+
}
|
|
243
|
+
const Value& rolledBack = identity.Get("rolledBackVersion");
|
|
244
|
+
if (rolledBack.Truthy() && Value::StrictEquals(hash, rolledBack)) {
|
|
245
|
+
return DeclineDownload("rolledBack");
|
|
246
|
+
}
|
|
247
|
+
Value attempts = Value::Array();
|
|
248
|
+
auto pushAttempt = [&attempts](const char* type, const Value& urls) {
|
|
249
|
+
if (urls.IsArray() && urls.Size() > 0) {
|
|
250
|
+
Value attempt = Value::Object();
|
|
251
|
+
attempt.Set("type", Value::String(type));
|
|
252
|
+
attempt.Set("urls", urls);
|
|
253
|
+
attempts.Push(std::move(attempt));
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
if (!isDev) {
|
|
257
|
+
pushAttempt("diff", JoinUrls(paths, info.Get("diff")));
|
|
258
|
+
pushAttempt("pdiff", JoinUrls(paths, info.Get("pdiff")));
|
|
259
|
+
}
|
|
260
|
+
Value fullUrls = JoinUrls(paths, info.Get("full"));
|
|
261
|
+
pushAttempt("full", fullUrls);
|
|
262
|
+
|
|
263
|
+
const bool devNoop =
|
|
264
|
+
isDev && !(fullUrls.IsArray() && fullUrls.Size() > 0);
|
|
265
|
+
if (attempts.Size() == 0 && !devNoop) {
|
|
266
|
+
return DeclineDownload("noArtifact");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
Value decision = Value::Object();
|
|
270
|
+
decision.Set("action", Value::String("download"));
|
|
271
|
+
decision.Set("hash", hash);
|
|
272
|
+
decision.Set("attempts", std::move(attempts));
|
|
273
|
+
decision.Set("devNoop", Value::Bool(devNoop));
|
|
274
|
+
return decision;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
bool ShouldActivateAfterDownload(const Value& info,
|
|
278
|
+
const std::string& afterDownload) {
|
|
279
|
+
return afterDownload == "setNeedUpdate" ||
|
|
280
|
+
info.Get("config").Get("forceBoot").Truthy();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
Value HandleCheckResponse(const std::string& responseText,
|
|
284
|
+
const Value& identity, bool isDev,
|
|
285
|
+
const std::string& afterDownload) {
|
|
286
|
+
bool ok = false;
|
|
287
|
+
Value root = flowjson::Parse(responseText, &ok);
|
|
288
|
+
if (!ok || !root.IsObject()) {
|
|
289
|
+
return DeclineDownload("invalidResponse");
|
|
290
|
+
}
|
|
291
|
+
Value resolved = ResolveCheckResult(root, identity);
|
|
292
|
+
Value decision = DecideDownload(resolved, identity, isDev);
|
|
293
|
+
if (decision.Get("action").AsString() == "download") {
|
|
294
|
+
decision.Set("activate", Value::Bool(ShouldActivateAfterDownload(
|
|
295
|
+
resolved, afterDownload)));
|
|
296
|
+
}
|
|
297
|
+
decision.Set("info", std::move(resolved));
|
|
298
|
+
return decision;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
} // namespace updateflow
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <string>
|
|
5
|
+
|
|
6
|
+
#include "flow_json.h"
|
|
7
|
+
|
|
8
|
+
// C++ port of the update-flow decision layer.
|
|
9
|
+
//
|
|
10
|
+
// src/updateFlowCore.ts is the REFERENCE implementation: every function here
|
|
11
|
+
// mirrors its TS counterpart 1:1, including object key order and JS
|
|
12
|
+
// truthiness/strict-equality semantics, because parity is enforced by golden
|
|
13
|
+
// vectors (tests/flow_vectors.json, generated from the TS side). Any semantic
|
|
14
|
+
// change lands in the TS file first, regenerates the vectors, then gets
|
|
15
|
+
// ported here — never the other direction.
|
|
16
|
+
//
|
|
17
|
+
// Like the TS side this layer is pure: no IO, no time, no randomness — the
|
|
18
|
+
// random sample, identity and parsed JSON all arrive as parameters. The
|
|
19
|
+
// orchestrators (per-platform HTTP/download/state glue) own all effects.
|
|
20
|
+
namespace updateflow {
|
|
21
|
+
|
|
22
|
+
uint32_t Murmur3_32(const std::string& key, uint32_t seed = 0);
|
|
23
|
+
|
|
24
|
+
// murmur(uuid) % 100 < rollout — the gray-release bucketing predicate.
|
|
25
|
+
bool IsInRollout(double rollout, const std::string& uuid);
|
|
26
|
+
|
|
27
|
+
// paths × fileName -> candidate URL array; Undefined when fileName is falsy
|
|
28
|
+
// (mirrors joinUrls returning undefined without a file name).
|
|
29
|
+
flowjson::Value JoinUrls(const flowjson::Value& paths,
|
|
30
|
+
const flowjson::Value& fileName);
|
|
31
|
+
|
|
32
|
+
// Dedupe + move the sampled pick to the front, rest in configured order.
|
|
33
|
+
// randomSample ∈ [0, 1) is injected by the caller.
|
|
34
|
+
flowjson::Value OrderEndpointCandidates(const flowjson::Value& endpoints,
|
|
35
|
+
double randomSample);
|
|
36
|
+
|
|
37
|
+
// input: { packageVersion, currentVersion, buildTime, cInfo,
|
|
38
|
+
// supportedDiffVersion?, bundleHash?, isDev?, extra? }
|
|
39
|
+
flowjson::Value BuildCheckRequestBody(const flowjson::Value& input);
|
|
40
|
+
|
|
41
|
+
// identity: { packageVersion, currentVersion?, uuid }
|
|
42
|
+
flowjson::Value ResolveCheckResult(const flowjson::Value& rootInfo,
|
|
43
|
+
const flowjson::Value& identity);
|
|
44
|
+
|
|
45
|
+
// identity: { currentVersion?, rolledBackVersion? }
|
|
46
|
+
// -> { action: 'none', reason } | { action: 'download', hash, attempts,
|
|
47
|
+
// devNoop }
|
|
48
|
+
flowjson::Value DecideDownload(const flowjson::Value& info,
|
|
49
|
+
const flowjson::Value& identity, bool isDev);
|
|
50
|
+
|
|
51
|
+
// Whether the orchestrator should activate a downloaded version for the
|
|
52
|
+
// next launch: the client's silent strategies opt in locally
|
|
53
|
+
// (afterDownload == "setNeedUpdate"), or the server marks the version
|
|
54
|
+
// config.forceBoot — the per-version remote override that closes the
|
|
55
|
+
// brick-rescue gap for alert-strategy apps (a bricked device never runs
|
|
56
|
+
// JS, so activation cannot wait for it). Native-only; the device-local
|
|
57
|
+
// rolledBackVersion guard in DecideDownload still wins, and the activated
|
|
58
|
+
// version keeps the first_time crash-protection rollback.
|
|
59
|
+
bool ShouldActivateAfterDownload(const flowjson::Value& info,
|
|
60
|
+
const std::string& afterDownload);
|
|
61
|
+
|
|
62
|
+
// Composes Parse → ResolveCheckResult → DecideDownload: one call from the
|
|
63
|
+
// raw checkUpdate response text to a download decision, so the platform
|
|
64
|
+
// orchestrators contain no decision logic at all. identity is the union of
|
|
65
|
+
// both composed functions' needs: { packageVersion, currentVersion?, uuid,
|
|
66
|
+
// rolledBackVersion? }. afterDownload is the client's persisted activation
|
|
67
|
+
// policy; a download decision carries `activate` (ShouldActivateAfterDownload
|
|
68
|
+
// folded over it and the version's forceBoot) plus `info` — the resolved
|
|
69
|
+
// check result — so orchestrators can persist name/description/metaInfo
|
|
70
|
+
// alongside a downloaded version (the JS side's setLocalHashInfo).
|
|
71
|
+
// Malformed JSON yields { action: 'none', reason: 'invalidResponse' }.
|
|
72
|
+
flowjson::Value HandleCheckResponse(const std::string& responseText,
|
|
73
|
+
const flowjson::Value& identity,
|
|
74
|
+
bool isDev,
|
|
75
|
+
const std::string& afterDownload);
|
|
76
|
+
|
|
77
|
+
} // namespace updateflow
|