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,121 @@
1
+ // JNI surface of the update-flow decision layer for the Android orchestrator
2
+ // (NativeCheckOrchestrator.java). Pure string-in/string-out: every payload is
3
+ // JSON, matching the decision layer's own boundary. A null return means
4
+ // "input did not parse" and the caller skips the check round.
5
+ #include <jni.h>
6
+
7
+ #include <cstdint>
8
+ #include <limits>
9
+ #include <string>
10
+ #include <vector>
11
+
12
+ #include "../patch_core/jni_util.h"
13
+ #include "flow_json.h"
14
+ #include "update_flow_core.h"
15
+
16
+ namespace {
17
+
18
+ jstring ToJString(JNIEnv* env, const std::string& value) {
19
+ // flow_json emits standard UTF-8, while NewStringUTF expects JNI's modified
20
+ // UTF-8 and corrupts supplementary code points. Decode explicitly and pass
21
+ // UTF-16 code units to NewString. Accept encoded surrogate code points too:
22
+ // strings originating in Java may have entered through modified UTF-8.
23
+ std::vector<jchar> utf16;
24
+ utf16.reserve(value.size());
25
+ size_t i = 0;
26
+ while (i < value.size()) {
27
+ const unsigned char first = static_cast<unsigned char>(value[i]);
28
+ uint32_t codePoint = 0xfffd;
29
+ size_t width = 1;
30
+ if (first < 0x80) {
31
+ codePoint = first;
32
+ } else if (first >= 0xc2 && first <= 0xdf && i + 1 < value.size()) {
33
+ const unsigned char b1 = static_cast<unsigned char>(value[i + 1]);
34
+ if ((b1 & 0xc0) == 0x80) {
35
+ codePoint = ((first & 0x1f) << 6) | (b1 & 0x3f);
36
+ width = 2;
37
+ }
38
+ } else if (first == 0xc0 && i + 1 < value.size() &&
39
+ static_cast<unsigned char>(value[i + 1]) == 0x80) {
40
+ codePoint = 0; // modified UTF-8 encoding of U+0000
41
+ width = 2;
42
+ } else if (first >= 0xe0 && first <= 0xef && i + 2 < value.size()) {
43
+ const unsigned char b1 = static_cast<unsigned char>(value[i + 1]);
44
+ const unsigned char b2 = static_cast<unsigned char>(value[i + 2]);
45
+ if ((b1 & 0xc0) == 0x80 && (b2 & 0xc0) == 0x80 &&
46
+ !(first == 0xe0 && b1 < 0xa0)) {
47
+ codePoint = ((first & 0x0f) << 12) | ((b1 & 0x3f) << 6) |
48
+ (b2 & 0x3f);
49
+ width = 3;
50
+ }
51
+ } else if (first >= 0xf0 && first <= 0xf4 && i + 3 < value.size()) {
52
+ const unsigned char b1 = static_cast<unsigned char>(value[i + 1]);
53
+ const unsigned char b2 = static_cast<unsigned char>(value[i + 2]);
54
+ const unsigned char b3 = static_cast<unsigned char>(value[i + 3]);
55
+ if ((b1 & 0xc0) == 0x80 && (b2 & 0xc0) == 0x80 &&
56
+ (b3 & 0xc0) == 0x80 && !(first == 0xf0 && b1 < 0x90) &&
57
+ !(first == 0xf4 && b1 > 0x8f)) {
58
+ codePoint = ((first & 0x07) << 18) | ((b1 & 0x3f) << 12) |
59
+ ((b2 & 0x3f) << 6) | (b3 & 0x3f);
60
+ width = 4;
61
+ }
62
+ }
63
+ i += width;
64
+ if (codePoint <= 0xffff) {
65
+ utf16.push_back(static_cast<jchar>(codePoint));
66
+ } else {
67
+ codePoint -= 0x10000;
68
+ utf16.push_back(static_cast<jchar>(0xd800 + (codePoint >> 10)));
69
+ utf16.push_back(static_cast<jchar>(0xdc00 + (codePoint & 0x3ff)));
70
+ }
71
+ }
72
+ if (utf16.size() > static_cast<size_t>(std::numeric_limits<jsize>::max())) {
73
+ return nullptr;
74
+ }
75
+ return env->NewString(utf16.empty() ? nullptr : utf16.data(),
76
+ static_cast<jsize>(utf16.size()));
77
+ }
78
+
79
+ } // namespace
80
+
81
+ extern "C" JNIEXPORT jstring JNICALL
82
+ Java_cn_reactnative_modules_update_NativeUpdateFlow_buildCheckRequestBody(
83
+ JNIEnv* env, jclass, jstring inputJson) {
84
+ bool ok = false;
85
+ flowjson::Value input = flowjson::Parse(
86
+ pushy::jni_util::JStringToString(env, inputJson), &ok);
87
+ if (!ok || !input.IsObject()) {
88
+ return nullptr;
89
+ }
90
+ return ToJString(
91
+ env, flowjson::Stringify(updateflow::BuildCheckRequestBody(input)));
92
+ }
93
+
94
+ extern "C" JNIEXPORT jstring JNICALL
95
+ Java_cn_reactnative_modules_update_NativeUpdateFlow_orderEndpointCandidates(
96
+ JNIEnv* env, jclass, jstring endpointsJson, jdouble randomSample) {
97
+ bool ok = false;
98
+ flowjson::Value endpoints = flowjson::Parse(
99
+ pushy::jni_util::JStringToString(env, endpointsJson), &ok);
100
+ if (!ok || !endpoints.IsArray()) {
101
+ return nullptr;
102
+ }
103
+ return ToJString(env, flowjson::Stringify(updateflow::OrderEndpointCandidates(
104
+ endpoints, randomSample)));
105
+ }
106
+
107
+ extern "C" JNIEXPORT jstring JNICALL
108
+ Java_cn_reactnative_modules_update_NativeUpdateFlow_handleCheckResponse(
109
+ JNIEnv* env, jclass, jstring responseText, jstring identityJson,
110
+ jstring afterDownload) {
111
+ bool ok = false;
112
+ flowjson::Value identity = flowjson::Parse(
113
+ pushy::jni_util::JStringToString(env, identityJson), &ok);
114
+ if (!ok || !identity.IsObject()) {
115
+ return nullptr;
116
+ }
117
+ return ToJString(env, flowjson::Stringify(updateflow::HandleCheckResponse(
118
+ pushy::jni_util::JStringToString(env, responseText),
119
+ identity, false,
120
+ pushy::jni_util::JStringToString(env, afterDownload))));
121
+ }
@@ -17,6 +17,10 @@ set(PATCH_CORE_DIR ${REPO_ROOT}/cpp/patch_core)
17
17
  if(NOT EXISTS ${PATCH_CORE_DIR}/patch_core.cpp)
18
18
  set(PATCH_CORE_DIR ${STAGED_NATIVE_DIR}/patch_core)
19
19
  endif()
20
+ set(UPDATE_FLOW_CORE_DIR ${REPO_ROOT}/cpp/update_flow_core)
21
+ if(NOT EXISTS ${UPDATE_FLOW_CORE_DIR}/update_flow_core.cpp)
22
+ set(UPDATE_FLOW_CORE_DIR ${STAGED_NATIVE_DIR}/update_flow_core)
23
+ endif()
20
24
 
21
25
  # Always compile from source. A prebuilt-import branch used to live here, but
22
26
  # it had no symbol verification: a stale libs/<abi>/librnupdate.so missing a
@@ -32,6 +36,8 @@ set(HDP_SOURCES
32
36
  ${PATCH_CORE_DIR}/hbc_transform_wire.cpp
33
37
  ${PATCH_CORE_DIR}/patch_core.cpp
34
38
  ${PATCH_CORE_DIR}/state_core.cpp
39
+ ${UPDATE_FLOW_CORE_DIR}/flow_json.cpp
40
+ ${UPDATE_FLOW_CORE_DIR}/update_flow_core.cpp
35
41
  ${ANDROID_JNI_DIR}/hpatch.c
36
42
  ${HDIFFPATCH_DIR}/libHDiffPatch/HPatch/patch.c
37
43
  ${HDIFFPATCH_DIR}/file_for_patch.c
@@ -46,6 +52,7 @@ add_library(rnupdate SHARED
46
52
  target_include_directories(rnupdate PRIVATE
47
53
  ${CMAKE_CURRENT_SOURCE_DIR}
48
54
  ${PATCH_CORE_DIR}
55
+ ${UPDATE_FLOW_CORE_DIR}
49
56
  ${ANDROID_JNI_DIR}
50
57
  ${HDIFFPATCH_DIR}
51
58
  ${HDIFFPATCH_DIR}/libHDiffPatch/HPatch
@@ -1,4 +1,7 @@
1
1
  #include <napi/native_api.h>
2
+
3
+ #include "flow_json.h"
4
+ #include "update_flow_core.h"
2
5
  #include <js_native_api.h>
3
6
  #include <js_native_api_types.h>
4
7
 
@@ -936,6 +939,52 @@ static napi_value Sha256Hex(napi_env env, napi_callback_info info) {
936
939
  return result;
937
940
  }
938
941
 
942
+ // CRC32(zip/zlib 多项式)。同步:输入是已在内存的 rawfile 资源字节,用于
943
+ // pdiff 拷贝前与 __diff.json copiesCrc 比对(不符则整次 patch 失败落 full)。
944
+ static napi_value Crc32(napi_env env, napi_callback_info info) {
945
+ size_t argc = 1;
946
+ napi_value args[1] = {nullptr};
947
+ if (!GetArgCount(env, info, &argc, args) || argc < 1) {
948
+ ThrowError(env, "crc32: missing data argument");
949
+ return nullptr;
950
+ }
951
+
952
+ void* data = nullptr;
953
+ size_t length = 0;
954
+ bool is_typedarray = false;
955
+ napi_is_typedarray(env, args[0], &is_typedarray);
956
+ if (is_typedarray) {
957
+ napi_typedarray_type type;
958
+ napi_value arraybuffer = nullptr;
959
+ size_t offset = 0;
960
+ if (napi_get_typedarray_info(
961
+ env, args[0], &type, &length, &data, &arraybuffer, &offset) !=
962
+ napi_ok) {
963
+ ThrowError(env, "crc32: cannot read typed array");
964
+ return nullptr;
965
+ }
966
+ } else {
967
+ bool is_arraybuffer = false;
968
+ napi_is_arraybuffer(env, args[0], &is_arraybuffer);
969
+ if (!is_arraybuffer ||
970
+ napi_get_arraybuffer_info(env, args[0], &data, &length) != napi_ok) {
971
+ ThrowError(env, "crc32: expected Uint8Array or ArrayBuffer");
972
+ return nullptr;
973
+ }
974
+ }
975
+
976
+ pushy::digest::Crc32 crc;
977
+ if (length > 0) {
978
+ crc.Update(static_cast<const uint8_t*>(data), length);
979
+ }
980
+
981
+ napi_value result = nullptr;
982
+ // CRC32 fits a double exactly (32-bit unsigned), matching the JSON number
983
+ // representation in the manifest.
984
+ napi_create_double(env, static_cast<double>(crc.Value()), &result);
985
+ return result;
986
+ }
987
+
939
988
  // 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道)
940
989
  static napi_value GetSupportedDiffVersion(napi_env env, napi_callback_info) {
941
990
  napi_value result = nullptr;
@@ -946,6 +995,102 @@ static napi_value GetSupportedDiffVersion(napi_env env, napi_callback_info) {
946
995
  return result;
947
996
  }
948
997
 
998
+ // ---- update-flow decision layer (NATIVE_CHECKUPDATE_DESIGN §10) ----
999
+ // String-in/string-out JSON, matching the decision layer's own boundary.
1000
+ // Returning undefined (nullptr without a pending exception) means "input did
1001
+ // not parse"; the ArkTS orchestrator skips the check round.
1002
+
1003
+ static napi_value MakeUtf8String(napi_env env, const std::string& value) {
1004
+ napi_value result = nullptr;
1005
+ if (napi_create_string_utf8(env, value.c_str(), value.size(), &result) !=
1006
+ napi_ok) {
1007
+ ThrowError(env, "Failed to create string");
1008
+ return nullptr;
1009
+ }
1010
+ return result;
1011
+ }
1012
+
1013
+ static napi_value FlowBuildCheckRequestBody(napi_env env,
1014
+ napi_callback_info info) {
1015
+ size_t argc = 1;
1016
+ napi_value args[1] = {nullptr};
1017
+ if (!GetArgCount(env, info, &argc, args) || argc < 1) {
1018
+ ThrowError(env, "buildCheckRequestBody: missing input argument");
1019
+ return nullptr;
1020
+ }
1021
+ bool ok = false;
1022
+ std::string input_json = GetString(env, args[0], &ok);
1023
+ if (!ok) {
1024
+ return nullptr;
1025
+ }
1026
+ bool parsed = false;
1027
+ flowjson::Value input = flowjson::Parse(input_json, &parsed);
1028
+ if (!parsed || !input.IsObject()) {
1029
+ return nullptr;
1030
+ }
1031
+ return MakeUtf8String(
1032
+ env, flowjson::Stringify(updateflow::BuildCheckRequestBody(input)));
1033
+ }
1034
+
1035
+ static napi_value FlowOrderEndpointCandidates(napi_env env,
1036
+ napi_callback_info info) {
1037
+ size_t argc = 2;
1038
+ napi_value args[2] = {nullptr, nullptr};
1039
+ if (!GetArgCount(env, info, &argc, args) || argc < 2) {
1040
+ ThrowError(env, "orderEndpointCandidates: missing arguments");
1041
+ return nullptr;
1042
+ }
1043
+ bool ok = false;
1044
+ std::string endpoints_json = GetString(env, args[0], &ok);
1045
+ if (!ok) {
1046
+ return nullptr;
1047
+ }
1048
+ double sample = 0;
1049
+ if (napi_get_value_double(env, args[1], &sample) != napi_ok) {
1050
+ ThrowError(env, "orderEndpointCandidates: expected number sample");
1051
+ return nullptr;
1052
+ }
1053
+ bool parsed = false;
1054
+ flowjson::Value endpoints = flowjson::Parse(endpoints_json, &parsed);
1055
+ if (!parsed || !endpoints.IsArray()) {
1056
+ return nullptr;
1057
+ }
1058
+ return MakeUtf8String(env,
1059
+ flowjson::Stringify(updateflow::OrderEndpointCandidates(
1060
+ endpoints, sample)));
1061
+ }
1062
+
1063
+ static napi_value FlowHandleCheckResponse(napi_env env,
1064
+ napi_callback_info info) {
1065
+ size_t argc = 3;
1066
+ napi_value args[3] = {nullptr, nullptr, nullptr};
1067
+ if (!GetArgCount(env, info, &argc, args) || argc < 3) {
1068
+ ThrowError(env, "handleCheckResponse: missing arguments");
1069
+ return nullptr;
1070
+ }
1071
+ bool ok = false;
1072
+ std::string response_text = GetString(env, args[0], &ok);
1073
+ if (!ok) {
1074
+ return nullptr;
1075
+ }
1076
+ std::string identity_json = GetString(env, args[1], &ok);
1077
+ if (!ok) {
1078
+ return nullptr;
1079
+ }
1080
+ std::string after_download = GetString(env, args[2], &ok);
1081
+ if (!ok) {
1082
+ return nullptr;
1083
+ }
1084
+ bool parsed = false;
1085
+ flowjson::Value identity = flowjson::Parse(identity_json, &parsed);
1086
+ if (!parsed || !identity.IsObject()) {
1087
+ return nullptr;
1088
+ }
1089
+ return MakeUtf8String(env,
1090
+ flowjson::Stringify(updateflow::HandleCheckResponse(
1091
+ response_text, identity, false, after_download)));
1092
+ }
1093
+
949
1094
  napi_value Init(napi_env env, napi_value exports) {
950
1095
  if (!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
951
1096
  !ExportFunction(env, exports, "runStateCore", RunStateCore) ||
@@ -954,7 +1099,11 @@ napi_value Init(napi_env env, napi_value exports) {
954
1099
  !ExportFunction(env, exports, "applyPatchFromFileSource", ApplyPatchFromFileSource) ||
955
1100
  !ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) ||
956
1101
  !ExportFunction(env, exports, "sha256Hex", Sha256Hex) ||
957
- !ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion)) {
1102
+ !ExportFunction(env, exports, "crc32", Crc32) ||
1103
+ !ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion) ||
1104
+ !ExportFunction(env, exports, "buildCheckRequestBody", FlowBuildCheckRequestBody) ||
1105
+ !ExportFunction(env, exports, "orderEndpointCandidates", FlowOrderEndpointCandidates) ||
1106
+ !ExportFunction(env, exports, "handleCheckResponse", FlowHandleCheckResponse)) {
958
1107
  return nullptr;
959
1108
  }
960
1109
  return exports;
@@ -11,10 +11,17 @@ import NativePatchCore, {
11
11
  ARCHIVE_PATCH_TYPE_FROM_PPK,
12
12
  CopyGroupResult,
13
13
  } from './NativePatchCore';
14
+ import { monotonicNowMs } from './MonotonicClock';
15
+
16
+ export const VERSION_COMPLETE_FILE_NAME = '.pushy-complete';
14
17
 
15
18
  export interface PatchManifestArrays {
16
19
  copyFroms: string[];
17
20
  copyTos: string[];
21
+ // 与 copyFroms/copyTos 逐位对齐的 copiesCrc(CLI >= 2.21.2 的 pdiff
22
+ // manifest 携带,键是 to);无声明时为 null。拷贝前用它校验包内资源内容,
23
+ // 不符则整次 patch 失败落 full——重打包二进制不能静默拷出漂移的资源。
24
+ copyCrcs: (number | null)[];
18
25
  deletes: string[];
19
26
  // __diff.json 中对应 bundle patch 条目的 hbcTransform 元数据(原始 JSON
20
27
  // 字符串);为空时 native 走现状路径
@@ -27,6 +34,7 @@ export function parseManifestToArrays(
27
34
  ): PatchManifestArrays {
28
35
  const copyFroms: string[] = [];
29
36
  const copyTos: string[] = [];
37
+ const copyCrcs: (number | null)[] = [];
30
38
  const deletesValue = manifest.deletes;
31
39
  const deletes = Array.isArray(deletesValue)
32
40
  ? deletesValue.map(item => String(item))
@@ -35,6 +43,11 @@ export function parseManifestToArrays(
35
43
  : [];
36
44
 
37
45
  const copies = (manifest.copies || {}) as Record<string, string>;
46
+ const copiesCrcValue = manifest.copiesCrc;
47
+ const copiesCrc =
48
+ copiesCrcValue && typeof copiesCrcValue === 'object'
49
+ ? (copiesCrcValue as Record<string, number>)
50
+ : ({} as Record<string, number>);
38
51
  for (const [to, rawFrom] of Object.entries(copies)) {
39
52
  let from = String(rawFrom || '');
40
53
  if (normalizeResourceCopies) {
@@ -45,6 +58,8 @@ export function parseManifestToArrays(
45
58
  }
46
59
  copyFroms.push(from);
47
60
  copyTos.push(to);
61
+ const crc = copiesCrc[to];
62
+ copyCrcs.push(typeof crc === 'number' && Number.isFinite(crc) ? crc : null);
48
63
  }
49
64
 
50
65
  const hbcTransform = manifest.hbcTransform as
@@ -62,6 +77,7 @@ export function parseManifestToArrays(
62
77
  return {
63
78
  copyFroms,
64
79
  copyTos,
80
+ copyCrcs,
65
81
  deletes,
66
82
  hbcTransformMeta,
67
83
  };
@@ -82,6 +98,7 @@ const DIFF_MANIFEST_ENTRY = '__diff.json';
82
98
  const HARMONY_BUNDLE_PATCH_ENTRY = 'bundle.harmony.js.patch';
83
99
  const TEMP_ORIGIN_BUNDLE_ENTRY = '.origin.bundle.harmony.js';
84
100
  const FILE_COPY_BUFFER_SIZE = 64 * 1024;
101
+ const DOWNLOAD_CALL_TIMEOUT_MS = 10 * 60 * 1000;
85
102
 
86
103
  export class DownloadTask {
87
104
  private context: common.Context;
@@ -229,6 +246,7 @@ export class DownloadTask {
229
246
  return {
230
247
  copyFroms: [],
231
248
  copyTos: [],
249
+ copyCrcs: [],
232
250
  deletes: [],
233
251
  hbcTransformMeta: '',
234
252
  };
@@ -329,6 +347,12 @@ export class DownloadTask {
329
347
  let writeQueue = Promise.resolve();
330
348
  let lastReportedPercentage = -1;
331
349
  let lastReportedBytes = 0;
350
+ const deadlineUptimeMs = params.deadlineUptimeMs > 0
351
+ ? params.deadlineUptimeMs
352
+ : monotonicNowMs() + DOWNLOAD_CALL_TIMEOUT_MS;
353
+ if (deadlineUptimeMs <= monotonicNowMs()) {
354
+ throw Error('Download deadline expired before start');
355
+ }
332
356
 
333
357
  // Emit at most one progress event per whole-percent change (or per 256KB
334
358
  // when the total is unknown), and only from the dataReceive handler, so the
@@ -360,6 +384,7 @@ export class DownloadTask {
360
384
  // Promise (and the JS caller) forever.
361
385
  const INACTIVITY_TIMEOUT_MS = 60000;
362
386
  let watchdogTimer: number | null = null;
387
+ let deadlineTimer: number | null = null;
363
388
  let inactivityReject: ((error: Error) => void) | null = null;
364
389
  const clearWatchdog = () => {
365
390
  if (watchdogTimer !== null) {
@@ -468,14 +493,22 @@ export class DownloadTask {
468
493
  },
469
494
  );
470
495
 
471
- const responseCode = await httpRequest.requestInStream(params.url, {
472
- method: http.RequestMethod.GET,
473
- readTimeout: 60000,
474
- connectTimeout: 60000,
475
- header: {
476
- 'Content-Type': 'application/octet-stream',
477
- },
496
+ const deadlinePromise = new Promise<never>((_, reject) => {
497
+ deadlineTimer = setTimeout(() => {
498
+ reject(Error('Download exceeded its whole-call deadline'));
499
+ }, Math.max(1, deadlineUptimeMs - monotonicNowMs()));
478
500
  });
501
+ const responseCode = await Promise.race([
502
+ httpRequest.requestInStream(params.url, {
503
+ method: http.RequestMethod.GET,
504
+ readTimeout: 60000,
505
+ connectTimeout: 60000,
506
+ header: {
507
+ 'Content-Type': 'application/octet-stream',
508
+ },
509
+ }),
510
+ deadlinePromise,
511
+ ]);
479
512
  if (responseCode > 299) {
480
513
  throw Error(`Server error: ${responseCode}`);
481
514
  }
@@ -485,7 +518,7 @@ export class DownloadTask {
485
518
  // 时 reject(unhandled rejection),且 promise 一经 reject 无法复活——
486
519
  // 即使随后数据正常流入,race 也必然以 "Download stalled" 失败。
487
520
  refreshWatchdog();
488
- await Promise.race([dataEndPromise, inactivityPromise]);
521
+ await Promise.race([dataEndPromise, inactivityPromise, deadlinePromise]);
489
522
  const stats = await fileIo.stat(params.targetFile);
490
523
  const fileSize = stats.size;
491
524
  if (contentLength > 0 && fileSize !== contentLength) {
@@ -498,6 +531,9 @@ export class DownloadTask {
498
531
  throw error;
499
532
  } finally {
500
533
  clearWatchdog();
534
+ if (deadlineTimer !== null) {
535
+ clearTimeout(deadlineTimer);
536
+ }
501
537
  try {
502
538
  await closeWriter();
503
539
  } catch (closeError) {
@@ -534,6 +570,20 @@ export class DownloadTask {
534
570
 
535
571
  private async doPatchFromApp(params: DownloadTaskParams): Promise<void> {
536
572
  await this.downloadFile(params);
573
+ try {
574
+ await this.doPatchFromAppAfterDownload(params);
575
+ } catch (error: any) {
576
+ // 下载已完成,之后的失败都是 patch 应用失败(解压/hpatch/资源拷贝,
577
+ // 含 copiesCrc 校验)。打上稳定码,JS 层遥测按 patch_fail 归类,
578
+ // 而不是混进网络失败。
579
+ error.code = 'PATCH_FAILED';
580
+ throw error;
581
+ }
582
+ }
583
+
584
+ private async doPatchFromAppAfterDownload(
585
+ params: DownloadTaskParams,
586
+ ): Promise<void> {
537
587
  await this.recreateDirectory(params.unzipDirectory);
538
588
 
539
589
  await zlib.decompressFile(params.targetFile, params.unzipDirectory);
@@ -566,12 +616,22 @@ export class DownloadTask {
566
616
  `${params.unzipDirectory}/bundle.harmony.js`,
567
617
  manifestArrays.hbcTransformMeta,
568
618
  );
619
+ // 组按 from 聚合;同一 from 的内容必然一致,所以每组一个 CRC 即可
620
+ // (与 manifest 逐位对齐的 copyCrcs 里取第一个非空值)。
621
+ const crcByFrom = new Map<string, number>();
622
+ manifestArrays.copyFroms.forEach((from, index) => {
623
+ const crc = manifestArrays.copyCrcs[index];
624
+ if (crc !== null && !crcByFrom.has(from)) {
625
+ crcByFrom.set(from, crc);
626
+ }
627
+ });
569
628
  await this.copyFromResource(
570
629
  NativePatchCore.buildCopyGroups(
571
630
  manifestArrays.copyFroms,
572
631
  manifestArrays.copyTos,
573
632
  ),
574
633
  params.unzipDirectory,
634
+ crcByFrom,
575
635
  );
576
636
  try {
577
637
  if (fileIo.accessSync(params.targetFile)) {
@@ -584,6 +644,18 @@ export class DownloadTask {
584
644
 
585
645
  private async doPatchFromPpk(params: DownloadTaskParams): Promise<void> {
586
646
  await this.downloadFile(params);
647
+ try {
648
+ await this.doPatchFromPpkAfterDownload(params);
649
+ } catch (error: any) {
650
+ // 同 doPatchFromApp:下载完成后的失败按 patch 失败归类。
651
+ error.code = 'PATCH_FAILED';
652
+ throw error;
653
+ }
654
+ }
655
+
656
+ private async doPatchFromPpkAfterDownload(
657
+ params: DownloadTaskParams,
658
+ ): Promise<void> {
587
659
  await this.recreateDirectory(params.unzipDirectory);
588
660
 
589
661
  await zlib.decompressFile(params.targetFile, params.unzipDirectory);
@@ -626,6 +698,7 @@ export class DownloadTask {
626
698
  private async copyFromResource(
627
699
  copyGroups: CopyGroupResult[],
628
700
  targetRoot: string,
701
+ crcByFrom?: Map<string, number>,
629
702
  ): Promise<void> {
630
703
  let currentFrom = '';
631
704
  try {
@@ -637,6 +710,7 @@ export class DownloadTask {
637
710
  if (targets.length === 0) {
638
711
  continue;
639
712
  }
713
+ const expectedCrc = crcByFrom?.get(currentFrom);
640
714
 
641
715
  if (currentFrom.startsWith('resources/base/media/')) {
642
716
  // Strip only the final extension: 'icon.round.png' -> 'icon.round',
@@ -645,6 +719,7 @@ export class DownloadTask {
645
719
  .replace('resources/base/media/', '')
646
720
  .replace(/\.[^.]+$/, '');
647
721
  const mediaBuffer = await resourceManager.getMediaByName(mediaName);
722
+ this.verifyCopySourceCrc(mediaBuffer, expectedCrc, currentFrom);
648
723
  const parentDirs = [
649
724
  ...new Set(
650
725
  targets.map(t => t.substring(0, t.lastIndexOf('/'))).filter(Boolean),
@@ -658,6 +733,22 @@ export class DownloadTask {
658
733
  );
659
734
  continue;
660
735
  }
736
+
737
+ if (expectedCrc !== undefined) {
738
+ // 声明了 CRC 的条目改走字节路径:读出内容先校验再落盘。
739
+ // 重打包的二进制里同名 rawfile 内容可能已漂移,fd 直拷会把
740
+ // 错误字节静默装进版本目录。
741
+ const rawContent =
742
+ await resourceManager.getRawFileContent(currentFrom);
743
+ this.verifyCopySourceCrc(rawContent, expectedCrc, currentFrom);
744
+ const [firstTarget, ...restTargets] = targets;
745
+ await this.writeFileContent(firstTarget, rawContent);
746
+ await Promise.all(
747
+ restTargets.map(target => this.copySandboxFile(firstTarget, target)),
748
+ );
749
+ continue;
750
+ }
751
+
661
752
  const fromContent = await resourceManager.getRawFd(currentFrom);
662
753
  try {
663
754
  const [firstTarget, ...restTargets] = targets;
@@ -697,6 +788,22 @@ export class DownloadTask {
697
788
  }
698
789
  }
699
790
 
791
+ // pdiff 拷贝源内容校验(__diff.json copiesCrc):不符即抛错,整次 patch
792
+ // 失败后 JS 策略链自动回退 full,绝不把漂移的资源装进版本目录。
793
+ private verifyCopySourceCrc(
794
+ content: Uint8Array,
795
+ expectedCrc: number | undefined,
796
+ from: string,
797
+ ): void {
798
+ if (expectedCrc === undefined) {
799
+ return;
800
+ }
801
+ const actualCrc = NativePatchCore.crc32(content);
802
+ if (actualCrc !== expectedCrc) {
803
+ throw new Error(`resource content mismatch (crc32): ${from}`);
804
+ }
805
+ }
806
+
700
807
  private async doCleanUp(params: DownloadTaskParams): Promise<void> {
701
808
  try {
702
809
  await NativePatchCore.cleanupOldEntries(
@@ -713,6 +820,10 @@ export class DownloadTask {
713
820
  }
714
821
 
715
822
  public async execute(params: DownloadTaskParams): Promise<void> {
823
+ const isPatchTask =
824
+ params.type === DownloadTaskParams.TASK_TYPE_PATCH_FULL ||
825
+ params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP ||
826
+ params.type === DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
716
827
  try {
717
828
  switch (params.type) {
718
829
  case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
@@ -733,13 +844,26 @@ export class DownloadTask {
733
844
  default:
734
845
  throw Error(`Unknown task type: ${params.type}`);
735
846
  }
847
+ if (isPatchTask) {
848
+ await this.writeFileContent(
849
+ `${params.unzipDirectory}/${VERSION_COMPLETE_FILE_NAME}`,
850
+ new Uint8Array(0),
851
+ );
852
+ }
736
853
  } catch (error: any) {
737
854
  console.error('Task execution failed:', error.message);
738
855
  if (params.type !== DownloadTaskParams.TASK_TYPE_CLEANUP) {
739
856
  try {
740
857
  if (params.type === DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD) {
741
858
  await fileIo.unlink(params.targetFile);
742
- } else {
859
+ } else if (
860
+ !fileIo.accessSync(
861
+ `${params.unzipDirectory}/${VERSION_COMPLETE_FILE_NAME}`,
862
+ ) ||
863
+ !fileIo.accessSync(`${params.unzipDirectory}/bundle.harmony.js`)
864
+ ) {
865
+ // Never let a failed duplicate task remove an install already
866
+ // handed off by an earlier task via marker + bundle.
743
867
  await this.removeDirectory(params.unzipDirectory);
744
868
  }
745
869
  } catch (cleanupError: any) {
@@ -16,4 +16,7 @@ export class DownloadTaskParams {
16
16
  targetFile: string = ''; // 目标文件路径
17
17
  unzipDirectory: string = ''; // 解压目录路径
18
18
  originDirectory: string = ''; // 原始文件目录路径
19
+ // Native cold-start orchestrator's absolute monotonic-uptime deadline. Zero
20
+ // uses the normal public download API's 10-minute whole-call cap.
21
+ deadlineUptimeMs: number = 0;
19
22
  }
@@ -0,0 +1,13 @@
1
+ import { systemDateTime } from '@kit.BasicServicesKit';
2
+
3
+ // Absolute deadlines must not use Date.now(): automatic time synchronization
4
+ // can move the wall clock while the cold-start rescue round is running.
5
+ //
6
+ // TimeType.ACTIVE (not STARTUP) is the semantic match for the other two
7
+ // platforms: iOS NSProcessInfo.systemUptime and Android System.nanoTime both
8
+ // stop during deep sleep, so a device that sleeps mid-download resumes with
9
+ // its budget intact. STARTUP keeps counting through sleep and would abort a
10
+ // rescue that the same network completes on iOS/Android.
11
+ export function monotonicNowMs(): number {
12
+ return systemDateTime.getUptime(systemDateTime.TimeType.ACTIVE);
13
+ }