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.
- package/BUNDLEHASH_MIGRATION.md +22 -2
- 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/BundledResourceCopier.java +42 -9
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -22
- 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/PatchFailedException.java +16 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +196 -42
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +56 -3
- 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/patch_core/digest.cpp +53 -0
- package/cpp/patch_core/digest.h +18 -0
- package/cpp/patch_core/patch_core.cpp +17 -1
- package/cpp/patch_core/patch_core.h +8 -0
- package/cpp/patch_core/tests/patch_core_test.cpp +99 -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 +150 -1
- package/harmony/pushy/src/main/ets/DownloadTask.ts +133 -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 +16 -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 +152 -52
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +840 -68
- 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 +439 -122
- package/src/endpoint.ts +105 -80
- package/src/error.ts +4 -0
- package/src/locales/en.ts +4 -0
- package/src/locales/zh.ts +4 -0
- package/src/provider.tsx +74 -9
- package/src/telemetry.ts +8 -0
- package/src/type.ts +31 -0
- package/src/updateFlowCore.ts +320 -0
- package/src/utils.ts +14 -12
- package/src/isInRollout.ts +0 -86
- 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, "
|
|
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
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
+
}
|