react-native-update 10.55.1 → 10.56.1

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 (76) hide show
  1. package/README-CN.md +35 -0
  2. package/README.md +37 -0
  3. package/android/build.gradle +53 -27
  4. package/android/jni/Application.mk +6 -1
  5. package/android/jni/hpatch.c +59 -4
  6. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  7. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  8. package/android/lib/x86/librnupdate.so +0 -0
  9. package/android/lib/x86_64/librnupdate.so +0 -0
  10. package/android/proguard.pro +70 -14
  11. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
  12. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
  14. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
  18. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
  21. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
  22. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
  23. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
  24. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
  25. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
  26. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
  27. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
  28. package/cpp/patch_core/archive_limits.h +6 -0
  29. package/cpp/patch_core/jni_util.h +6 -2
  30. package/cpp/patch_core/patch_core.cpp +237 -84
  31. package/cpp/patch_core/patch_core.h +11 -0
  32. package/cpp/patch_core/patch_core_android.cpp +49 -33
  33. package/cpp/patch_core/update_core_android.cpp +134 -103
  34. package/cpp/update_flow_core/flow_json.cpp +150 -17
  35. package/cpp/update_flow_core/flow_json.h +13 -1
  36. package/cpp/update_flow_core/update_flow_core.cpp +78 -8
  37. package/cpp/update_flow_core/update_flow_core.h +9 -0
  38. package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
  39. package/harmony/hvigor-plugin.ts +168 -21
  40. package/harmony/pushy/src/README.md +74 -0
  41. package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
  42. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
  43. package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
  44. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
  45. package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
  46. package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
  47. package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
  48. package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
  49. package/harmony/pushy/src/main/ets/Logger.ts +42 -16
  50. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
  51. package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
  52. package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
  53. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
  54. package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
  55. package/harmony/pushy/src/main/module.json5 +5 -1
  56. package/harmony/pushy.har +0 -0
  57. package/ios/PrivacyInfo.xcprivacy +79 -0
  58. package/ios/RCTPushy/RCTPushy.mm +399 -170
  59. package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
  60. package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
  61. package/package.json +14 -24
  62. package/react-native-update.podspec +43 -3
  63. package/src/client.ts +236 -82
  64. package/src/context.ts +40 -27
  65. package/src/core.ts +41 -16
  66. package/src/endpoint.ts +2 -1
  67. package/src/error.ts +49 -11
  68. package/src/index.ts +14 -0
  69. package/src/locales/en.ts +1 -23
  70. package/src/locales/zh.ts +1 -22
  71. package/src/provider.tsx +159 -111
  72. package/src/type.ts +0 -2
  73. package/src/updateFlowCore.ts +10 -2
  74. package/src/utils.ts +61 -4
  75. package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
  76. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
@@ -1,5 +1,6 @@
1
1
  #include <jni.h>
2
2
 
3
+ #include <exception>
3
4
  #include <string>
4
5
  #include <vector>
5
6
 
@@ -306,12 +307,22 @@ Java_cn_reactnative_modules_update_UpdateContext_syncStateWithBinaryVersion(
306
307
  jstring package_version,
307
308
  jstring build_time,
308
309
  jobject state_result) {
309
- pushy::state::State state = ReadStateFromResult(env, state_result);
310
- pushy::state::BinaryVersionSyncResult result = pushy::state::SyncBinaryVersion(
311
- state,
312
- JStringToString(env, package_version),
313
- JStringToString(env, build_time));
314
- return MakeStateResult(env, result.state, result.changed);
310
+ // C++ exceptions (bad_alloc/length_error from input-proportional STL
311
+ // allocations) must not unwind through the JNI boundary: convert them to a
312
+ // Java RuntimeException like every other failure of these entry points.
313
+ try {
314
+ pushy::state::State state = ReadStateFromResult(env, state_result);
315
+ pushy::state::BinaryVersionSyncResult result = pushy::state::SyncBinaryVersion(
316
+ state,
317
+ JStringToString(env, package_version),
318
+ JStringToString(env, build_time));
319
+ return MakeStateResult(env, result.state, result.changed);
320
+ } catch (const std::exception& error) {
321
+ ThrowRuntimeException(env, error.what());
322
+ } catch (...) {
323
+ ThrowRuntimeException(env, "Unexpected native exception in syncStateWithBinaryVersion");
324
+ }
325
+ return nullptr;
315
326
  }
316
327
 
317
328
  extern "C" JNIEXPORT jobject JNICALL
@@ -323,46 +334,52 @@ Java_cn_reactnative_modules_update_UpdateContext_runStateCore(
323
334
  jstring string_arg,
324
335
  jboolean flag_a,
325
336
  jboolean flag_b) {
326
- const pushy::state::State state = ReadStateFromResult(env, state_result);
327
- switch (static_cast<StateOperation>(operation)) {
328
- case StateOperation::kSwitchVersion:
329
- return MakeStateResult(
330
- env,
331
- pushy::state::SwitchVersion(state, JStringToString(env, string_arg)));
332
- case StateOperation::kMarkSuccess: {
333
- const pushy::state::MarkSuccessResult result =
334
- pushy::state::MarkSuccess(state);
335
- return MakeStateResult(
336
- env,
337
- result.state,
338
- false,
339
- result.stale_version_to_delete);
340
- }
341
- case StateOperation::kRollback: {
342
- const pushy::state::State next = pushy::state::Rollback(state);
343
- return MakeStateResult(
344
- env, next, false, std::string(), next.current_version, true);
345
- }
346
- case StateOperation::kClearFirstTime:
347
- return MakeStateResult(env, pushy::state::ClearFirstTime(state));
348
- case StateOperation::kClearRollbackMark:
349
- return MakeStateResult(env, pushy::state::ClearRollbackMark(state));
350
- case StateOperation::kResolveLaunch: {
351
- const pushy::state::LaunchDecision decision =
352
- pushy::state::ResolveLaunchState(
353
- state, flag_a == JNI_TRUE, flag_b == JNI_TRUE);
354
- return MakeStateResult(
355
- env,
356
- decision.state,
357
- false,
358
- std::string(),
359
- decision.load_version,
360
- decision.did_rollback,
361
- decision.consumed_first_time);
337
+ try {
338
+ const pushy::state::State state = ReadStateFromResult(env, state_result);
339
+ switch (static_cast<StateOperation>(operation)) {
340
+ case StateOperation::kSwitchVersion:
341
+ return MakeStateResult(
342
+ env,
343
+ pushy::state::SwitchVersion(state, JStringToString(env, string_arg)));
344
+ case StateOperation::kMarkSuccess: {
345
+ const pushy::state::MarkSuccessResult result =
346
+ pushy::state::MarkSuccess(state);
347
+ return MakeStateResult(
348
+ env,
349
+ result.state,
350
+ false,
351
+ result.stale_version_to_delete);
352
+ }
353
+ case StateOperation::kRollback: {
354
+ const pushy::state::State next = pushy::state::Rollback(state);
355
+ return MakeStateResult(
356
+ env, next, false, std::string(), next.current_version, true);
357
+ }
358
+ case StateOperation::kClearFirstTime:
359
+ return MakeStateResult(env, pushy::state::ClearFirstTime(state));
360
+ case StateOperation::kClearRollbackMark:
361
+ return MakeStateResult(env, pushy::state::ClearRollbackMark(state));
362
+ case StateOperation::kResolveLaunch: {
363
+ const pushy::state::LaunchDecision decision =
364
+ pushy::state::ResolveLaunchState(
365
+ state, flag_a == JNI_TRUE, flag_b == JNI_TRUE);
366
+ return MakeStateResult(
367
+ env,
368
+ decision.state,
369
+ false,
370
+ std::string(),
371
+ decision.load_version,
372
+ decision.did_rollback,
373
+ decision.consumed_first_time);
374
+ }
362
375
  }
363
- }
364
376
 
365
- ThrowRuntimeException(env, "Unknown state operation");
377
+ ThrowRuntimeException(env, "Unknown state operation");
378
+ } catch (const std::exception& error) {
379
+ ThrowRuntimeException(env, error.what());
380
+ } catch (...) {
381
+ ThrowRuntimeException(env, "Unexpected native exception in runStateCore");
382
+ }
366
383
  return nullptr;
367
384
  }
368
385
 
@@ -375,32 +392,39 @@ Java_cn_reactnative_modules_update_DownloadTask_buildArchivePatchPlan(
375
392
  jobjectArray copy_froms,
376
393
  jobjectArray copy_tos,
377
394
  jobjectArray deletes) {
378
- const std::vector<std::string> from_values = JArrayToVector(env, copy_froms);
379
- const std::vector<std::string> to_values = JArrayToVector(env, copy_tos);
380
- if (from_values.size() != to_values.size()) {
381
- ThrowRuntimeException(env, "copy_froms and copy_tos length mismatch");
382
- return nullptr;
383
- }
395
+ try {
396
+ const std::vector<std::string> from_values = JArrayToVector(env, copy_froms);
397
+ const std::vector<std::string> to_values = JArrayToVector(env, copy_tos);
398
+ if (from_values.size() != to_values.size()) {
399
+ ThrowRuntimeException(env, "copy_froms and copy_tos length mismatch");
400
+ return nullptr;
401
+ }
384
402
 
385
- pushy::patch::PatchManifest manifest =
386
- BuildManifest(from_values, to_values, JArrayToVector(env, deletes));
387
- pushy::archive_patch::ArchivePatchType archive_type;
388
- if (!ToArchivePatchType(patch_type, &archive_type)) {
389
- ThrowRuntimeException(env, "Unknown archive patch type");
390
- return nullptr;
391
- }
392
- pushy::archive_patch::ArchivePatchPlan plan;
393
- pushy::patch::Status status = pushy::archive_patch::BuildArchivePatchPlan(
394
- archive_type,
395
- manifest,
396
- JArrayToVector(env, entry_names),
397
- &plan);
398
- if (!status.ok) {
399
- ThrowRuntimeException(env, status.message);
400
- return nullptr;
401
- }
403
+ pushy::patch::PatchManifest manifest =
404
+ BuildManifest(from_values, to_values, JArrayToVector(env, deletes));
405
+ pushy::archive_patch::ArchivePatchType archive_type;
406
+ if (!ToArchivePatchType(patch_type, &archive_type)) {
407
+ ThrowRuntimeException(env, "Unknown archive patch type");
408
+ return nullptr;
409
+ }
410
+ pushy::archive_patch::ArchivePatchPlan plan;
411
+ pushy::patch::Status status = pushy::archive_patch::BuildArchivePatchPlan(
412
+ archive_type,
413
+ manifest,
414
+ JArrayToVector(env, entry_names),
415
+ &plan);
416
+ if (!status.ok) {
417
+ ThrowRuntimeException(env, status.message);
418
+ return nullptr;
419
+ }
402
420
 
403
- return NewArchivePatchPlanResult(env, plan);
421
+ return NewArchivePatchPlanResult(env, plan);
422
+ } catch (const std::exception& error) {
423
+ ThrowRuntimeException(env, error.what());
424
+ } catch (...) {
425
+ ThrowRuntimeException(env, "Unexpected native exception in buildArchivePatchPlan");
426
+ }
427
+ return nullptr;
404
428
  }
405
429
 
406
430
  extern "C" JNIEXPORT jobjectArray JNICALL
@@ -409,45 +433,52 @@ Java_cn_reactnative_modules_update_DownloadTask_buildCopyGroups(
409
433
  jclass,
410
434
  jobjectArray copy_froms,
411
435
  jobjectArray copy_tos) {
412
- const std::vector<std::string> from_values = JArrayToVector(env, copy_froms);
413
- const std::vector<std::string> to_values = JArrayToVector(env, copy_tos);
414
- if (from_values.size() != to_values.size()) {
415
- ThrowRuntimeException(env, "copy_froms and copy_tos length mismatch");
416
- return nullptr;
417
- }
436
+ try {
437
+ const std::vector<std::string> from_values = JArrayToVector(env, copy_froms);
438
+ const std::vector<std::string> to_values = JArrayToVector(env, copy_tos);
439
+ if (from_values.size() != to_values.size()) {
440
+ ThrowRuntimeException(env, "copy_froms and copy_tos length mismatch");
441
+ return nullptr;
442
+ }
418
443
 
419
- pushy::patch::PatchManifest manifest = BuildManifest(
420
- from_values, to_values, std::vector<std::string>());
421
- std::vector<pushy::archive_patch::CopyGroup> groups;
422
- pushy::patch::Status status =
423
- pushy::archive_patch::BuildCopyGroups(manifest, &groups);
424
- if (!status.ok) {
425
- ThrowRuntimeException(env, status.message);
426
- return nullptr;
427
- }
444
+ pushy::patch::PatchManifest manifest = BuildManifest(
445
+ from_values, to_values, std::vector<std::string>());
446
+ std::vector<pushy::archive_patch::CopyGroup> groups;
447
+ pushy::patch::Status status =
448
+ pushy::archive_patch::BuildCopyGroups(manifest, &groups);
449
+ if (!status.ok) {
450
+ ThrowRuntimeException(env, status.message);
451
+ return nullptr;
452
+ }
428
453
 
429
- jclass result_class =
430
- env->FindClass("cn/reactnative/modules/update/CopyGroupResult");
431
- if (result_class == nullptr) {
432
- return nullptr;
433
- }
454
+ jclass result_class =
455
+ env->FindClass("cn/reactnative/modules/update/CopyGroupResult");
456
+ if (result_class == nullptr) {
457
+ return nullptr;
458
+ }
434
459
 
435
- jclass string_class = env->FindClass("java/lang/String");
436
- if (string_class == nullptr) {
437
- return nullptr;
438
- }
460
+ jclass string_class = env->FindClass("java/lang/String");
461
+ if (string_class == nullptr) {
462
+ return nullptr;
463
+ }
439
464
 
440
- jobjectArray result = env->NewObjectArray(
441
- static_cast<jsize>(groups.size()), result_class, nullptr);
442
- if (result == nullptr) {
443
- return nullptr;
444
- }
465
+ jobjectArray result = env->NewObjectArray(
466
+ static_cast<jsize>(groups.size()), result_class, nullptr);
467
+ if (result == nullptr) {
468
+ return nullptr;
469
+ }
445
470
 
446
- for (jsize index = 0; index < static_cast<jsize>(groups.size()); ++index) {
447
- jobject group = NewCopyGroupResult(env, result_class, string_class, groups[index]);
448
- env->SetObjectArrayElement(result, index, group);
449
- env->DeleteLocalRef(group);
471
+ for (jsize index = 0; index < static_cast<jsize>(groups.size()); ++index) {
472
+ jobject group = NewCopyGroupResult(env, result_class, string_class, groups[index]);
473
+ env->SetObjectArrayElement(result, index, group);
474
+ env->DeleteLocalRef(group);
475
+ }
476
+ env->DeleteLocalRef(string_class);
477
+ return result;
478
+ } catch (const std::exception& error) {
479
+ ThrowRuntimeException(env, error.what());
480
+ } catch (...) {
481
+ ThrowRuntimeException(env, "Unexpected native exception in buildCopyGroups");
450
482
  }
451
- env->DeleteLocalRef(string_class);
452
- return result;
483
+ return nullptr;
453
484
  }
@@ -1,8 +1,8 @@
1
1
  #include "flow_json.h"
2
2
 
3
3
  #include <cmath>
4
+ #include <cstdint>
4
5
  #include <cstdio>
5
- #include <cstdlib>
6
6
 
7
7
  namespace flowjson {
8
8
 
@@ -127,18 +127,41 @@ void AppendEscaped(const std::string& s, std::string* out) {
127
127
  }
128
128
 
129
129
  void AppendNumber(double n, std::string* out) {
130
+ // JSON.stringify prints non-finite numbers as null.
131
+ if (!std::isfinite(n)) {
132
+ out->append("null");
133
+ return;
134
+ }
130
135
  // The decision layer only ever emits integral numbers (hashes, counters,
131
136
  // rollout percentages); print them the way JSON.stringify does. The %.17g
132
137
  // fallback exists so an unexpected fractional value surfaces as a vector
133
138
  // mismatch instead of silent truncation.
134
- if (std::isfinite(n) && n == std::floor(n) && std::fabs(n) <= 9007199254740992.0) {
139
+ if (n == std::floor(n) && std::fabs(n) <= 9007199254740992.0) {
135
140
  char buf[32];
136
141
  std::snprintf(buf, sizeof(buf), "%lld", static_cast<long long>(n));
137
142
  out->append(buf);
138
- } else {
139
- char buf[40];
140
- std::snprintf(buf, sizeof(buf), "%.17g", n);
141
- out->append(buf);
143
+ return;
144
+ }
145
+ char buf[40];
146
+ const int len = std::snprintf(buf, sizeof(buf), "%.17g", n);
147
+ if (len <= 0) {
148
+ out->append("null");
149
+ return;
150
+ }
151
+ // %g writes the LC_NUMERIC decimal point ("," — or a multibyte sequence in
152
+ // some locales). Everything else it can produce for a finite value is
153
+ // [0-9eE+-], so normalize whatever else appears back to '.' instead of
154
+ // depending on the process locale.
155
+ bool point_written = false;
156
+ for (int i = 0; i < len && i < static_cast<int>(sizeof(buf)); i++) {
157
+ const char c = buf[i];
158
+ if ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == 'e' ||
159
+ c == 'E') {
160
+ out->push_back(c);
161
+ } else if (!point_written) {
162
+ out->push_back('.');
163
+ point_written = true;
164
+ }
142
165
  }
143
166
  }
144
167
 
@@ -206,9 +229,102 @@ namespace {
206
229
 
207
230
  // The parser also consumes server checkUpdate responses, so hostile input
208
231
  // must fail cleanly: nesting is capped to keep recursive descent off the
209
- // stack limit. Real responses nest ~4 levels.
232
+ // stack limit (size and node count are capped in flow_json.h). Real
233
+ // responses nest ~4 levels.
210
234
  constexpr int kMaxDepth = 64;
211
235
 
236
+ // Locale-independent decimal -> double. strtod() honours LC_NUMERIC, so a
237
+ // host process that called setlocale() into a comma-decimal locale would stop
238
+ // at the '.' of every fractional literal and reject the whole response. The
239
+ // token has already been validated against the JSON number grammar.
240
+ // Correctly rounded whenever the significand fits in 53 bits and the decimal
241
+ // exponent is within +-22 (exact double powers of ten) or the value is an
242
+ // integer below 2^53 x 10^15 — which covers everything the decision layer
243
+ // handles (hashes, counters, rollout percentages, timestamps). Anything more
244
+ // exotic is computed as significand x 10^exponent in double arithmetic and
245
+ // may differ from JSON.parse by an ulp; extreme exponents saturate to
246
+ // infinity / zero like JS.
247
+ double DecimalToDouble(const std::string& token) {
248
+ size_t i = 0;
249
+ bool negative = false;
250
+ if (i < token.size() && token[i] == '-') {
251
+ negative = true;
252
+ i++;
253
+ }
254
+ uint64_t significand = 0;
255
+ int digits = 0; // significant digits folded into `significand`
256
+ int exponent = 0; // decimal exponent applied to `significand`
257
+ bool fraction = false;
258
+ for (; i < token.size(); i++) {
259
+ const char c = token[i];
260
+ if (c == '.') {
261
+ fraction = true;
262
+ continue;
263
+ }
264
+ if (c == 'e' || c == 'E') {
265
+ break;
266
+ }
267
+ const int digit = c - '0';
268
+ if (significand == 0 && digit == 0) {
269
+ if (fraction) {
270
+ exponent--; // leading zeros after the point only shift the scale
271
+ }
272
+ continue;
273
+ }
274
+ if (digits < 19) {
275
+ significand = significand * 10 + static_cast<uint64_t>(digit);
276
+ digits++;
277
+ if (fraction) {
278
+ exponent--;
279
+ }
280
+ } else if (!fraction) {
281
+ exponent++; // beyond 19 digits: keep the magnitude, drop precision
282
+ }
283
+ }
284
+ if (i < token.size()) { // exponent part
285
+ i++;
286
+ bool exp_negative = false;
287
+ if (i < token.size() && (token[i] == '+' || token[i] == '-')) {
288
+ exp_negative = token[i] == '-';
289
+ i++;
290
+ }
291
+ int exp_value = 0;
292
+ for (; i < token.size(); i++) {
293
+ if (exp_value < 100000) { // saturate: the result is inf/0 anyway
294
+ exp_value = exp_value * 10 + (token[i] - '0');
295
+ }
296
+ }
297
+ exponent += exp_negative ? -exp_value : exp_value;
298
+ }
299
+
300
+ double result = 0.0;
301
+ if (significand != 0) {
302
+ static const double kPow10[] = {
303
+ 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
304
+ 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
305
+ static const uint64_t kPow10Int[] = {
306
+ 1ull, 10ull, 100ull, 1000ull,
307
+ 10000ull, 100000ull, 1000000ull, 10000000ull,
308
+ 100000000ull, 1000000000ull, 10000000000ull, 100000000000ull,
309
+ 1000000000000ull, 10000000000000ull, 100000000000000ull,
310
+ 1000000000000000ull};
311
+ const uint64_t kExact = 1ull << 53;
312
+ if (significand <= kExact && exponent >= -22 && exponent <= 22) {
313
+ result = exponent >= 0
314
+ ? static_cast<double>(significand) * kPow10[exponent]
315
+ : static_cast<double>(significand) / kPow10[-exponent];
316
+ } else if (significand <= kExact && exponent > 22 && exponent <= 22 + 15 &&
317
+ significand <= kExact / kPow10Int[exponent - 22]) {
318
+ result = static_cast<double>(significand * kPow10Int[exponent - 22]) *
319
+ kPow10[22];
320
+ } else {
321
+ result = static_cast<double>(significand) *
322
+ std::pow(10.0, static_cast<double>(exponent));
323
+ }
324
+ }
325
+ return negative ? -result : result;
326
+ }
327
+
212
328
  class Parser {
213
329
  public:
214
330
  Parser(const std::string& text) : text_(text) {}
@@ -260,6 +376,10 @@ class Parser {
260
376
  ok_ = false;
261
377
  return Value::Undefined();
262
378
  }
379
+ if (++nodes_ > kMaxNodes) {
380
+ ok_ = false;
381
+ return Value::Undefined();
382
+ }
263
383
  char c = text_[pos_];
264
384
  switch (c) {
265
385
  case '{':
@@ -376,17 +496,32 @@ class Parser {
376
496
  break;
377
497
  case 'u': {
378
498
  unsigned code = ParseHex4();
379
- // BMP code point to UTF-8 (surrogate pairs are combined).
499
+ if (!ok_) {
500
+ return out;
501
+ }
502
+ // A high surrogate combines with an immediately following low
503
+ // surrogate escape into one supplementary code point.
380
504
  if (code >= 0xd800 && code <= 0xdbff &&
381
505
  text_.compare(pos_, 2, "\\u") == 0) {
506
+ const size_t escape_start = pos_;
382
507
  pos_ += 2;
383
508
  unsigned low = ParseHex4();
509
+ if (!ok_) {
510
+ return out;
511
+ }
384
512
  if (low >= 0xdc00 && low <= 0xdfff) {
385
513
  code = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00);
386
514
  } else {
387
- ok_ = false;
515
+ pos_ = escape_start; // the loop re-reads that escape on its own
388
516
  }
389
517
  }
518
+ // JSON.parse accepts a lone surrogate, but it has no UTF-8 form:
519
+ // emitting one byte-encoded (ED A0 80..) would push invalid UTF-8
520
+ // into napi_create_string_utf8 / NewString. Decode it to U+FFFD,
521
+ // the way TextEncoder serializes such a JS string.
522
+ if (code >= 0xd800 && code <= 0xdfff) {
523
+ code = 0xfffd;
524
+ }
390
525
  AppendUtf8(code, &out);
391
526
  break;
392
527
  }
@@ -496,25 +631,23 @@ class Parser {
496
631
  }
497
632
  }
498
633
 
499
- char* end = nullptr;
500
- std::string token = text_.substr(start, pos_ - start);
501
- double n = std::strtod(token.c_str(), &end);
502
- if (end == nullptr || *end != '\0') {
503
- ok_ = false;
504
- return Value::Undefined();
505
- }
506
- return Value::Number(n);
634
+ return Value::Number(DecimalToDouble(text_.substr(start, pos_ - start)));
507
635
  }
508
636
 
509
637
  const std::string& text_;
510
638
  size_t pos_ = 0;
511
639
  int depth_ = 0;
640
+ size_t nodes_ = 0;
512
641
  bool ok_ = true;
513
642
  };
514
643
 
515
644
  } // namespace
516
645
 
517
646
  Value Parse(const std::string& text, bool* ok) {
647
+ if (text.size() > kMaxInputBytes) {
648
+ *ok = false;
649
+ return Value::Undefined();
650
+ }
518
651
  Parser parser(text);
519
652
  return parser.Run(ok);
520
653
  }
@@ -96,9 +96,21 @@ class Value {
96
96
  // equal.
97
97
  std::string Stringify(const Value& v);
98
98
 
99
+ // Hard limits for Parse. The TS reference has none (JSON.parse is unbounded)
100
+ // and the divergence is deliberate: this parser runs inside the native
101
+ // orchestrators on the raw checkUpdate body, and with sizeof(Value) ~ 96 a
102
+ // 1 MB "[0,0,...]" balloons to ~100 MB of heap. A response over either cap is
103
+ // treated as malformed (HandleCheckResponse -> invalidResponse). Real
104
+ // responses are a few KB and a few hundred nodes.
105
+ constexpr size_t kMaxInputBytes = 1024 * 1024;
106
+ constexpr size_t kMaxNodes = 65536;
107
+
99
108
  // Strict JSON parser (the vectors file and checkUpdate responses). Returns
100
109
  // Undefined and sets *ok to false on malformed input; nesting beyond 64
101
- // levels is rejected so hostile server data cannot exhaust the stack.
110
+ // levels, more than kMaxInputBytes of text or more than kMaxNodes values are
111
+ // rejected so hostile server data cannot exhaust the stack or the heap.
112
+ // Number parsing/printing is locale-independent (never strtod/LC_NUMERIC).
113
+ // Lone surrogate escapes decode to U+FFFD so the output is always valid UTF-8.
102
114
  Value Parse(const std::string& text, bool* ok);
103
115
 
104
116
  } // namespace flowjson
@@ -6,19 +6,83 @@ namespace updateflow {
6
6
 
7
7
  using flowjson::Value;
8
8
 
9
+ namespace {
10
+
11
+ // The low byte of every UTF-16 code unit of `utf8`, which is exactly what the
12
+ // TS reference hashes (`key.charCodeAt(i) & 0xff` over `key.length` units).
13
+ // Identity for ASCII; an astral code point contributes its two surrogate
14
+ // units. Accepts the JNI modified-UTF-8 forms too (C0 80 for U+0000 and
15
+ // byte-encoded surrogates count as one unit each, as charCodeAt would see
16
+ // them); malformed bytes decode to U+FFFD.
17
+ std::vector<unsigned char> Utf16LowBytes(const std::string& utf8) {
18
+ std::vector<unsigned char> units;
19
+ units.reserve(utf8.size());
20
+ size_t i = 0;
21
+ while (i < utf8.size()) {
22
+ const unsigned char first = static_cast<unsigned char>(utf8[i]);
23
+ uint32_t codePoint = 0xfffd;
24
+ size_t width = 1;
25
+ if (first < 0x80) {
26
+ codePoint = first;
27
+ } else if (first >= 0xc2 && first <= 0xdf && i + 1 < utf8.size()) {
28
+ const unsigned char b1 = static_cast<unsigned char>(utf8[i + 1]);
29
+ if ((b1 & 0xc0) == 0x80) {
30
+ codePoint = ((first & 0x1f) << 6) | (b1 & 0x3f);
31
+ width = 2;
32
+ }
33
+ } else if (first == 0xc0 && i + 1 < utf8.size() &&
34
+ static_cast<unsigned char>(utf8[i + 1]) == 0x80) {
35
+ codePoint = 0;
36
+ width = 2;
37
+ } else if (first >= 0xe0 && first <= 0xef && i + 2 < utf8.size()) {
38
+ const unsigned char b1 = static_cast<unsigned char>(utf8[i + 1]);
39
+ const unsigned char b2 = static_cast<unsigned char>(utf8[i + 2]);
40
+ if ((b1 & 0xc0) == 0x80 && (b2 & 0xc0) == 0x80 &&
41
+ !(first == 0xe0 && b1 < 0xa0)) {
42
+ codePoint = ((first & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f);
43
+ width = 3;
44
+ }
45
+ } else if (first >= 0xf0 && first <= 0xf4 && i + 3 < utf8.size()) {
46
+ const unsigned char b1 = static_cast<unsigned char>(utf8[i + 1]);
47
+ const unsigned char b2 = static_cast<unsigned char>(utf8[i + 2]);
48
+ const unsigned char b3 = static_cast<unsigned char>(utf8[i + 3]);
49
+ if ((b1 & 0xc0) == 0x80 && (b2 & 0xc0) == 0x80 &&
50
+ (b3 & 0xc0) == 0x80 && !(first == 0xf0 && b1 < 0x90) &&
51
+ !(first == 0xf4 && b1 > 0x8f)) {
52
+ codePoint = ((first & 0x07) << 18) | ((b1 & 0x3f) << 12) |
53
+ ((b2 & 0x3f) << 6) | (b3 & 0x3f);
54
+ width = 4;
55
+ }
56
+ }
57
+ i += width;
58
+ if (codePoint <= 0xffff) {
59
+ units.push_back(static_cast<unsigned char>(codePoint & 0xff));
60
+ } else {
61
+ codePoint -= 0x10000;
62
+ units.push_back(static_cast<unsigned char>((0xd800 + (codePoint >> 10)) & 0xff));
63
+ units.push_back(static_cast<unsigned char>((0xdc00 + (codePoint & 0x3ff)) & 0xff));
64
+ }
65
+ }
66
+ return units;
67
+ }
68
+
69
+ } // namespace
70
+
9
71
  uint32_t Murmur3_32(const std::string& key, uint32_t seed) {
10
72
  // The TS reference emulates 32-bit multiplication with 16-bit halves;
11
73
  // 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).
74
+ // dance collapses to plain multiplies. Input bytes are the low byte of each
75
+ // UTF-16 code unit (charCodeAt & 0xff), not the UTF-8 bytes: hashing UTF-8
76
+ // would bucket non-ASCII keys differently from the TS reference.
14
77
  const uint32_t c1 = 0xcc9e2d51;
15
78
  const uint32_t c2 = 0x1b873593;
16
- const size_t len = key.size();
79
+ const std::vector<unsigned char> units = Utf16LowBytes(key);
80
+ static const unsigned char kEmpty = 0;
81
+ const unsigned char* data = units.empty() ? &kEmpty : units.data();
82
+ const size_t len = units.size();
17
83
  const size_t nblocks = len / 4;
18
84
  uint32_t h1 = seed;
19
85
 
20
- const unsigned char* data =
21
- reinterpret_cast<const unsigned char*>(key.data());
22
86
  for (size_t i = 0; i < nblocks; i++) {
23
87
  uint32_t k1 = static_cast<uint32_t>(data[i * 4]) |
24
88
  (static_cast<uint32_t>(data[i * 4 + 1]) << 8) |
@@ -62,6 +126,10 @@ bool IsInRollout(double rollout, const std::string& uuid) {
62
126
  return static_cast<double>(Murmur3_32(uuid) % 100) < rollout;
63
127
  }
64
128
 
129
+ bool IsMirrorRetryableCode(const std::string& code) {
130
+ return code != "PATCH_FAILED";
131
+ }
132
+
65
133
  namespace {
66
134
 
67
135
  // ^[a-z][a-z0-9+.-]*:\/\/ (case-insensitive)
@@ -114,7 +182,9 @@ Value OrderEndpointCandidates(const Value& endpoints, double randomSample) {
114
182
  }
115
183
  bool seen = false;
116
184
  for (const auto& kept : deduped.elements()) {
117
- if (kept.AsString() == endpoint.AsString()) {
185
+ // Set membership is ===: a stray non-string entry (a number, `true`)
186
+ // dedupes against equal primitives only, never against strings.
187
+ if (Value::StrictEquals(kept, endpoint)) {
118
188
  seen = true;
119
189
  break;
120
190
  }
@@ -247,8 +317,8 @@ Value DeclineDownload(const char* reason) {
247
317
  Value DecideDownload(const Value& info, const Value& identity, bool isDev) {
248
318
  const Value& hash = info.Get("hash");
249
319
  Value paths = info.Get("paths");
250
- if (paths.IsUndefined()) {
251
- paths = Value::Array(); // const { paths = [] } — undefined only
320
+ if (paths.IsUndefined() || paths.kind() == Value::Kind::Null) {
321
+ paths = Value::Array(); // info.paths ?? [] — a server `null` too
252
322
  }
253
323
  if (!info.Get("update").Truthy() || !hash.Truthy()) {
254
324
  return DeclineDownload("noUpdate");