react-native-update 10.49.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.
Files changed (61) hide show
  1. package/BUNDLEHASH_MIGRATION.md +22 -2
  2. package/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
  3. package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
  4. package/README-CN.md +14 -1
  5. package/README.md +14 -1
  6. package/android/jni/Android.mk +5 -1
  7. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  8. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  9. package/android/lib/x86/librnupdate.so +0 -0
  10. package/android/lib/x86_64/librnupdate.so +0 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +42 -9
  12. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -22
  13. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
  14. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
  16. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +16 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +196 -42
  18. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +56 -3
  19. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  20. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  21. package/cpp/patch_core/digest.cpp +53 -0
  22. package/cpp/patch_core/digest.h +18 -0
  23. package/cpp/patch_core/patch_core.cpp +17 -1
  24. package/cpp/patch_core/patch_core.h +8 -0
  25. package/cpp/patch_core/tests/patch_core_test.cpp +99 -0
  26. package/cpp/update_flow_core/flow_json.cpp +522 -0
  27. package/cpp/update_flow_core/flow_json.h +103 -0
  28. package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
  29. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
  30. package/cpp/update_flow_core/update_flow_core.cpp +301 -0
  31. package/cpp/update_flow_core/update_flow_core.h +77 -0
  32. package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
  33. package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
  34. package/harmony/pushy/src/main/cpp/pushy.cpp +150 -1
  35. package/harmony/pushy/src/main/ets/DownloadTask.ts +133 -9
  36. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
  37. package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
  38. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
  39. package/harmony/pushy/src/main/ets/NativePatchCore.ts +16 -0
  40. package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
  41. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
  42. package/harmony/pushy/src/main/ets/UpdateContext.ts +152 -52
  43. package/harmony/pushy.har +0 -0
  44. package/ios/RCTPushy/RCTPushy.mm +840 -68
  45. package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
  46. package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
  47. package/package.json +3 -1
  48. package/react-native-update.podspec +2 -0
  49. package/src/NativePushy.ts +14 -0
  50. package/src/client.ts +439 -122
  51. package/src/endpoint.ts +105 -80
  52. package/src/error.ts +4 -0
  53. package/src/locales/en.ts +4 -0
  54. package/src/locales/zh.ts +4 -0
  55. package/src/provider.tsx +74 -9
  56. package/src/telemetry.ts +8 -0
  57. package/src/type.ts +31 -0
  58. package/src/updateFlowCore.ts +320 -0
  59. package/src/utils.ts +14 -12
  60. package/src/isInRollout.ts +0 -86
  61. 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