react-native-update 10.59.2 → 10.60.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/README-CN.md +7 -7
- package/README.md +7 -6
- package/android/build.gradle +25 -33
- package/android/jni/Android.mk +16 -24
- package/android/lib/arm64-v8a/librnpushy.so +0 -0
- package/android/lib/armeabi-v7a/librnpushy.so +0 -0
- package/android/lib/x86/librnpushy.so +0 -0
- package/android/lib/x86_64/librnpushy.so +0 -0
- package/android/proguard.pro +9 -7
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +2 -7
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
- package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
- package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
- package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
- package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
- package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
- package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
- package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
- package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
- package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
- package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
- package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
- package/cpp/android_jni/archive_limits_jni.cpp +83 -0
- package/cpp/android_jni/install_record_jni.cpp +89 -0
- package/cpp/android_jni/jni_log.h +31 -0
- package/cpp/android_jni/jni_natives.h +40 -0
- package/cpp/android_jni/jni_registration.cpp +133 -0
- package/cpp/android_jni/jni_util.h +160 -0
- package/cpp/android_jni/update_core_android.cpp +450 -0
- package/cpp/android_jni/update_flow_jni.cpp +133 -0
- package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
- package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
- package/cpp/patch_core/archive_limits.cpp +194 -0
- package/cpp/patch_core/archive_limits.h +99 -10
- package/cpp/patch_core/archive_patch_core.cpp +96 -12
- package/cpp/patch_core/archive_patch_core.h +49 -0
- package/cpp/patch_core/digest.h +5 -4
- package/cpp/patch_core/error_codes.h +5 -3
- package/cpp/patch_core/install_record.cpp +430 -0
- package/cpp/patch_core/install_record.h +163 -9
- package/cpp/patch_core/patch_core.cpp +62 -16
- package/cpp/patch_core/patch_core.h +27 -7
- package/cpp/patch_core/patch_manifest.cpp +201 -0
- package/cpp/patch_core/patch_manifest.h +74 -0
- package/cpp/patch_core/state_ops.h +3 -3
- package/cpp/update_flow_core/flow_json.cpp +82 -8
- package/cpp/update_flow_core/flow_json.h +21 -0
- package/cpp/update_flow_core/native_config.cpp +456 -0
- package/cpp/update_flow_core/native_config.h +84 -0
- package/cpp/update_flow_core/native_round.cpp +326 -0
- package/cpp/update_flow_core/native_round.h +90 -0
- package/cpp/update_flow_core/update_flow_core.h +2 -2
- package/harmony/hvigor-plugin.ts +11 -19
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +17 -26
- package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
- package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
- package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
- package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
- package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
- package/harmony/pushy/src/main/ets/Logger.ts +20 -34
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
- package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
- package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
- package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
- package/harmony/pushy/src/main/ets/Texts.ts +0 -4
- package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
- package/ios/RCTPushy/RCTPushy.h +12 -0
- package/ios/RCTPushy/RCTPushy.mm +87 -2634
- package/ios/RCTPushy/RCTPushyCommon.h +34 -0
- package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
- package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
- package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
- package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
- package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
- package/ios/RCTPushy/RCTPushyInternal.h +144 -0
- package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
- package/ios/RCTPushy/RCTPushyState.mm +314 -0
- package/package.json +25 -23
- package/react-native-update.podspec +73 -82
- package/src/client.ts +361 -451
- package/src/context.ts +1 -1
- package/src/core.ts +35 -8
- package/src/endpoint.ts +0 -2
- package/src/error.ts +13 -32
- package/src/i18n.ts +18 -83
- package/src/locales/en.ts +0 -4
- package/src/locales/zh.ts +0 -4
- package/src/provider.tsx +60 -53
- package/src/telemetry.ts +0 -13
- package/src/type.ts +0 -15
- package/src/updateFlowCore.ts +116 -10
- package/src/utils.ts +49 -42
- package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
- package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
- package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
- package/cpp/patch_core/jni_natives.h +0 -27
- package/cpp/patch_core/jni_registration.cpp +0 -103
- package/cpp/patch_core/jni_util.h +0 -60
- package/cpp/patch_core/patch_core_android.cpp +0 -97
- package/cpp/patch_core/update_core_android.cpp +0 -485
- package/cpp/update_flow_core/update_flow_jni.cpp +0 -150
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#include "archive_limits.h"
|
|
2
|
+
|
|
3
|
+
#include <dirent.h>
|
|
4
|
+
#include <sys/stat.h>
|
|
5
|
+
|
|
6
|
+
#include <limits>
|
|
7
|
+
#include <string>
|
|
8
|
+
#include <utility>
|
|
9
|
+
#include <vector>
|
|
10
|
+
|
|
11
|
+
#include "install_record.h"
|
|
12
|
+
|
|
13
|
+
namespace pushy {
|
|
14
|
+
namespace archive_limits {
|
|
15
|
+
namespace {
|
|
16
|
+
|
|
17
|
+
// expanded > compressed * kMaxCompressionRatio, past kRatioCheckMinBytes.
|
|
18
|
+
// A non-positive compressed size bounds nothing. Exact (no integer division
|
|
19
|
+
// rounding the ratio down) and overflow-free.
|
|
20
|
+
bool ExceedsRatio(long long expanded, long long compressed) {
|
|
21
|
+
if (expanded <= kRatioCheckMinBytes) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (compressed <= 0) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (compressed > std::numeric_limits<long long>::max() / kMaxCompressionRatio) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return expanded > compressed * kMaxCompressionRatio;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The entry for messages: its name, or its position when the host has none
|
|
34
|
+
// yet.
|
|
35
|
+
std::string Label(const std::string& name, long long index) {
|
|
36
|
+
return name.empty() ? "#" + std::to_string(index) : name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
} // namespace
|
|
40
|
+
|
|
41
|
+
bool IsReservedEntryName(const std::string& name) {
|
|
42
|
+
const std::string prefix = install_record::kReservedPrefix;
|
|
43
|
+
size_t start = 0;
|
|
44
|
+
while (start <= name.size()) {
|
|
45
|
+
size_t end = name.find_first_of("/\\", start);
|
|
46
|
+
if (end == std::string::npos) {
|
|
47
|
+
end = name.size();
|
|
48
|
+
}
|
|
49
|
+
if (name.compare(start, prefix.size(), prefix) == 0 &&
|
|
50
|
+
end - start >= prefix.size()) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
start = end + 1;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Status CheckEntryCount(long long entries) {
|
|
59
|
+
if (entries > kMaxEntries) {
|
|
60
|
+
return Status::Error("archive has too many entries (> " +
|
|
61
|
+
std::to_string(kMaxEntries) + ")");
|
|
62
|
+
}
|
|
63
|
+
return Status::Ok();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Status CheckUncompressedSize(long long archive_bytes, long long uncompressed_bytes) {
|
|
67
|
+
if (uncompressed_bytes > kMaxTotalUncompressedBytes) {
|
|
68
|
+
return Status::Error("archive expands beyond " +
|
|
69
|
+
std::to_string(kMaxTotalUncompressedBytes) + " bytes (" +
|
|
70
|
+
std::to_string(uncompressed_bytes) + ")");
|
|
71
|
+
}
|
|
72
|
+
if (ExceedsRatio(uncompressed_bytes, archive_bytes)) {
|
|
73
|
+
return Status::Error("archive compression ratio exceeds " +
|
|
74
|
+
std::to_string(kMaxCompressionRatio) + ":1 (" +
|
|
75
|
+
std::to_string(uncompressed_bytes) + "/" +
|
|
76
|
+
std::to_string(archive_bytes) + ")");
|
|
77
|
+
}
|
|
78
|
+
return Status::Ok();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
EntryAccumulator::EntryAccumulator(long long archive_bytes)
|
|
82
|
+
: archive_bytes_(archive_bytes) {}
|
|
83
|
+
|
|
84
|
+
Status EntryAccumulator::Accept(long long uncompressed_size,
|
|
85
|
+
long long compressed_size,
|
|
86
|
+
bool is_symlink,
|
|
87
|
+
const std::string& name) {
|
|
88
|
+
Status status = Add(uncompressed_size, is_symlink, name);
|
|
89
|
+
if (status.ok && ExceedsRatio(uncompressed_size, compressed_size)) {
|
|
90
|
+
return Status::Error("archive entry compression ratio too high: " +
|
|
91
|
+
Label(name, entries_ - 1));
|
|
92
|
+
}
|
|
93
|
+
return status;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
Status EntryAccumulator::AcceptExtracted(long long size,
|
|
97
|
+
bool is_symlink,
|
|
98
|
+
const std::string& name) {
|
|
99
|
+
return Add(size, is_symlink, name);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Status EntryAccumulator::Add(long long size,
|
|
103
|
+
bool is_symlink,
|
|
104
|
+
const std::string& name) {
|
|
105
|
+
const long long index = entries_++;
|
|
106
|
+
Status status = CheckEntryCount(entries_);
|
|
107
|
+
if (!status.ok) {
|
|
108
|
+
return status;
|
|
109
|
+
}
|
|
110
|
+
if (IsReservedEntryName(name)) {
|
|
111
|
+
return Status::Error("archive contains reserved entry " + name);
|
|
112
|
+
}
|
|
113
|
+
if (is_symlink) {
|
|
114
|
+
return Status::Error("archive entry is a symlink: " + Label(name, index));
|
|
115
|
+
}
|
|
116
|
+
if (size < 0) {
|
|
117
|
+
return Status::Error("archive entry size unknown: " + Label(name, index));
|
|
118
|
+
}
|
|
119
|
+
if (size > kMaxEntryBytes) {
|
|
120
|
+
return Status::Error("archive entry too large: " + Label(name, index) + " (" +
|
|
121
|
+
std::to_string(size) + " bytes)");
|
|
122
|
+
}
|
|
123
|
+
total_uncompressed_ += size;
|
|
124
|
+
return CheckUncompressedSize(archive_bytes_, total_uncompressed_);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
Status CheckExtractedDirectory(const std::string& dir, long long archive_bytes) {
|
|
128
|
+
EntryAccumulator accumulator(archive_bytes);
|
|
129
|
+
// Relative paths of the directories still to list; "" is `dir` itself.
|
|
130
|
+
std::vector<std::string> pending(1);
|
|
131
|
+
while (!pending.empty()) {
|
|
132
|
+
const std::string relative = std::move(pending.back());
|
|
133
|
+
pending.pop_back();
|
|
134
|
+
const std::string path = relative.empty() ? dir : dir + "/" + relative;
|
|
135
|
+
DIR* handle = opendir(path.c_str());
|
|
136
|
+
if (handle == nullptr) {
|
|
137
|
+
return Status::Error("cannot list extracted directory " + path);
|
|
138
|
+
}
|
|
139
|
+
Status status = Status::Ok();
|
|
140
|
+
while (status.ok) {
|
|
141
|
+
const dirent* item = readdir(handle);
|
|
142
|
+
if (item == nullptr) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
const std::string name = item->d_name;
|
|
146
|
+
if (name == "." || name == "..") {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const std::string child = relative.empty() ? name : relative + "/" + name;
|
|
150
|
+
struct stat st;
|
|
151
|
+
if (lstat((dir + "/" + child).c_str(), &st) != 0) {
|
|
152
|
+
status = Status::Error("cannot stat extracted entry " + child);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
// Directories are walked but not counted: the zip may omit directory
|
|
156
|
+
// entries, and iOS/Android count only what the archive declares.
|
|
157
|
+
if (S_ISDIR(st.st_mode)) {
|
|
158
|
+
if (IsReservedEntryName(child)) {
|
|
159
|
+
status = Status::Error("archive contains reserved entry " + child);
|
|
160
|
+
} else {
|
|
161
|
+
pending.push_back(child);
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
status = accumulator.AcceptExtracted(static_cast<long long>(st.st_size),
|
|
166
|
+
S_ISLNK(st.st_mode), child);
|
|
167
|
+
}
|
|
168
|
+
closedir(handle);
|
|
169
|
+
if (!status.ok) {
|
|
170
|
+
return status;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return Status::Ok();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
FreeSpaceProbe PlanUnknownLengthFreeSpaceProbe(long long known_total,
|
|
177
|
+
long long written_before,
|
|
178
|
+
long long chunk_bytes,
|
|
179
|
+
long long reserved_until) {
|
|
180
|
+
FreeSpaceProbe plan;
|
|
181
|
+
plan.reserve = chunk_bytes > kUnknownLengthFreeSpaceProbeBytes
|
|
182
|
+
? chunk_bytes
|
|
183
|
+
: kUnknownLengthFreeSpaceProbeBytes;
|
|
184
|
+
plan.reserved_until = reserved_until;
|
|
185
|
+
if (known_total > 0 || written_before + chunk_bytes <= reserved_until) {
|
|
186
|
+
return plan;
|
|
187
|
+
}
|
|
188
|
+
plan.probe = true;
|
|
189
|
+
plan.reserved_until = written_before + plan.reserve;
|
|
190
|
+
return plan;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
} // namespace archive_limits
|
|
194
|
+
} // namespace pushy
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
#ifndef PUSHY_PATCH_CORE_ARCHIVE_LIMITS_H_
|
|
2
2
|
#define PUSHY_PATCH_CORE_ARCHIVE_LIMITS_H_
|
|
3
3
|
|
|
4
|
+
#include <string>
|
|
5
|
+
|
|
6
|
+
#include "patch_core.h"
|
|
7
|
+
|
|
4
8
|
// Single source of truth for the resource caps applied to an update package
|
|
5
|
-
// before and while it is unpacked
|
|
6
|
-
//
|
|
7
|
-
// disk or a zip bomb. The caps are
|
|
8
|
-
// bundle with assets is tens of MB)
|
|
9
|
+
// before and while it is unpacked, and for the accounting that enforces them.
|
|
10
|
+
// A corrupt or hostile archive must be able to cost at most a bounded amount
|
|
11
|
+
// of disk, memory and time — never a full disk or a zip bomb. The caps are
|
|
12
|
+
// generous for real packages (a large full bundle with assets is tens of MB)
|
|
13
|
+
// and exist purely as damage bounds.
|
|
9
14
|
//
|
|
10
|
-
//
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
13
|
-
//
|
|
15
|
+
// Every platform runs the rules below (archive_limits.cpp):
|
|
16
|
+
// - Android: SafeZipFile.inspect hands the central directory to
|
|
17
|
+
// CheckArchiveEntries, and DownloadTask plans its free-space probes, over
|
|
18
|
+
// JNI (cpp/android_jni/archive_limits_jni.cpp);
|
|
19
|
+
// - iOS: RCTPushyInstaller.mm feeds an EntryAccumulator from the unzip
|
|
20
|
+
// delegate, RCTPushyDownloader.mm plans its probes;
|
|
21
|
+
// - Harmony: zlib only reports the whole archive's expansion, so
|
|
22
|
+
// DownloadTask.ts checks it with CheckUncompressedSize before unpacking
|
|
23
|
+
// and CheckExtractedDirectory after, over NAPI (pushy.cpp).
|
|
24
|
+
// The constants a host still compares against itself (download caps, the
|
|
25
|
+
// manifest cap, the free-space margin, the streamed-bytes backstop) are
|
|
26
|
+
// mirrored in android/.../ArchiveLimits.java and
|
|
27
|
+
// harmony/pushy/src/main/ets/ArchiveLimits.ts;
|
|
28
|
+
// scripts/check-constant-parity.js (part of `bun lint`) asserts they match.
|
|
14
29
|
|
|
15
30
|
// Keep the core internal to whichever binary links it (see podspec Core).
|
|
16
31
|
#pragma GCC visibility push(hidden)
|
|
@@ -18,6 +33,8 @@
|
|
|
18
33
|
namespace pushy {
|
|
19
34
|
namespace archive_limits {
|
|
20
35
|
|
|
36
|
+
using delta::Status;
|
|
37
|
+
|
|
21
38
|
// Downloaded archive (Content-Length up front, streamed bytes as backstop).
|
|
22
39
|
constexpr long long kMaxArchiveBytes = 512LL * 1024 * 1024;
|
|
23
40
|
// Sum of every entry's uncompressed size.
|
|
@@ -26,8 +43,9 @@ constexpr long long kMaxTotalUncompressedBytes = 2048LL * 1024 * 1024;
|
|
|
26
43
|
constexpr long long kMaxEntryBytes = 512LL * 1024 * 1024;
|
|
27
44
|
// Number of entries in one archive.
|
|
28
45
|
constexpr long long kMaxEntries = 20000;
|
|
29
|
-
//
|
|
30
|
-
// kRatioCheckMinBytes (tiny highly
|
|
46
|
+
// Uncompressed/compressed ratio, of each entry and of the archive as a whole,
|
|
47
|
+
// checked only above kRatioCheckMinBytes of expansion (tiny highly
|
|
48
|
+
// compressible files are legitimate).
|
|
31
49
|
constexpr long long kMaxCompressionRatio = 100;
|
|
32
50
|
constexpr long long kRatioCheckMinBytes = 1LL * 1024 * 1024;
|
|
33
51
|
// __diff.json is parsed fully in memory.
|
|
@@ -41,6 +59,77 @@ constexpr long long kFreeDiskMarginBytes = 64LL * 1024 * 1024;
|
|
|
41
59
|
// reserves its own size), so no write can ever eat into the margin.
|
|
42
60
|
constexpr long long kUnknownLengthFreeSpaceProbeBytes = 8LL * 1024 * 1024;
|
|
43
61
|
|
|
62
|
+
// Whether any component of an archive path ('/' or '\\' separated) starts
|
|
63
|
+
// with install_record::kReservedPrefix. Those names belong to the SDK (the
|
|
64
|
+
// completion record): a package unpacking its own record next to a bundle
|
|
65
|
+
// would be trusted as a finished install even if the steps after it fail.
|
|
66
|
+
bool IsReservedEntryName(const std::string& name);
|
|
67
|
+
|
|
68
|
+
// The entry count of a whole archive (iOS knows it before the first entry).
|
|
69
|
+
Status CheckEntryCount(long long entries);
|
|
70
|
+
|
|
71
|
+
// The whole archive: its total expansion and its overall ratio. An archive of
|
|
72
|
+
// unknown (non-positive) size has no ratio to trust and fails it once the
|
|
73
|
+
// expansion is past kRatioCheckMinBytes.
|
|
74
|
+
Status CheckUncompressedSize(long long archive_bytes, long long uncompressed_bytes);
|
|
75
|
+
|
|
76
|
+
// Per-entry accounting while an archive is walked, before (Android, iOS) or
|
|
77
|
+
// right after (Harmony) its bytes land. Each Accept checks, in order: the
|
|
78
|
+
// entry count, a reserved name, a symlink, the entry's own size (negative =
|
|
79
|
+
// unknown is refused), the running total and whole-archive ratio
|
|
80
|
+
// (CheckUncompressedSize), and the entry's ratio. The first failure is the
|
|
81
|
+
// verdict; the caller stops there.
|
|
82
|
+
class EntryAccumulator {
|
|
83
|
+
public:
|
|
84
|
+
// archive_bytes: the archive file's size, for the whole-archive ratio.
|
|
85
|
+
explicit EntryAccumulator(long long archive_bytes);
|
|
86
|
+
|
|
87
|
+
// An entry as the central directory declares it. `name` is the path inside
|
|
88
|
+
// the archive; a host that only learns it once the entry is on disk (iOS)
|
|
89
|
+
// passes "" and checks the name then with IsReservedEntryName. A
|
|
90
|
+
// non-positive compressed size with an expansion past kRatioCheckMinBytes
|
|
91
|
+
// is an unbounded ratio.
|
|
92
|
+
Status Accept(long long uncompressed_size,
|
|
93
|
+
long long compressed_size,
|
|
94
|
+
bool is_symlink,
|
|
95
|
+
const std::string& name);
|
|
96
|
+
|
|
97
|
+
// An entry already on disk whose compressed size the host never saw
|
|
98
|
+
// (Harmony): everything but the entry's own ratio; the whole archive's
|
|
99
|
+
// ratio still applies.
|
|
100
|
+
Status AcceptExtracted(long long size, bool is_symlink, const std::string& name);
|
|
101
|
+
|
|
102
|
+
long long entries() const { return entries_; }
|
|
103
|
+
long long total_uncompressed() const { return total_uncompressed_; }
|
|
104
|
+
|
|
105
|
+
private:
|
|
106
|
+
Status Add(long long size, bool is_symlink, const std::string& name);
|
|
107
|
+
|
|
108
|
+
long long archive_bytes_;
|
|
109
|
+
long long entries_ = 0;
|
|
110
|
+
long long total_uncompressed_ = 0;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Walks a freshly unpacked directory (iteratively, symlinks not followed)
|
|
114
|
+
// through an EntryAccumulator: every file and directory below `dir` is one
|
|
115
|
+
// entry named by its path relative to `dir`, a directory weighing 0 bytes.
|
|
116
|
+
Status CheckExtractedDirectory(const std::string& dir, long long archive_bytes);
|
|
117
|
+
|
|
118
|
+
// Before each chunk of a download is written. `known_total` > 0 means the
|
|
119
|
+
// response announced its length (checked once when it arrived: no probe);
|
|
120
|
+
// written_before counts this response's body bytes before the chunk.
|
|
121
|
+
struct FreeSpaceProbe {
|
|
122
|
+
// Probe the disk for `reserve` bytes (plus kFreeDiskMarginBytes) first.
|
|
123
|
+
bool probe = false;
|
|
124
|
+
long long reserve = 0;
|
|
125
|
+
// The reservation after this chunk, relative to the start of the body.
|
|
126
|
+
long long reserved_until = 0;
|
|
127
|
+
};
|
|
128
|
+
FreeSpaceProbe PlanUnknownLengthFreeSpaceProbe(long long known_total,
|
|
129
|
+
long long written_before,
|
|
130
|
+
long long chunk_bytes,
|
|
131
|
+
long long reserved_until);
|
|
132
|
+
|
|
44
133
|
} // namespace archive_limits
|
|
45
134
|
} // namespace pushy
|
|
46
135
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#include "archive_patch_core.h"
|
|
2
2
|
|
|
3
|
+
#include <unordered_map>
|
|
4
|
+
#include <utility>
|
|
5
|
+
|
|
3
6
|
#include "obscured_text.h"
|
|
4
7
|
|
|
5
8
|
namespace pushy {
|
|
@@ -100,24 +103,105 @@ delta::Status BuildCopyGroups(
|
|
|
100
103
|
return manifest_status;
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
// Groups in order of first appearance; the index keeps this linear in the
|
|
107
|
+
// number of copies (a package diff copies thousands of resources).
|
|
108
|
+
std::vector<CopyGroup> groups;
|
|
109
|
+
std::unordered_map<std::string, size_t> group_by_source;
|
|
104
110
|
for (const delta::CopyOperation& copy : manifest.copies) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (group.from == copy.from) {
|
|
108
|
-
group.to_paths.push_back(copy.to);
|
|
109
|
-
appended = true;
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (!appended) {
|
|
111
|
+
const auto inserted = group_by_source.emplace(copy.from, groups.size());
|
|
112
|
+
if (inserted.second) {
|
|
114
113
|
CopyGroup group;
|
|
115
114
|
group.from = copy.from;
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
groups.push_back(std::move(group));
|
|
116
|
+
}
|
|
117
|
+
CopyGroup& group = groups[inserted.first->second];
|
|
118
|
+
group.to_paths.push_back(copy.to);
|
|
119
|
+
if (copy.has_expected_crc) {
|
|
120
|
+
group.has_expected_crc = true;
|
|
121
|
+
group.expected_crc = copy.expected_crc;
|
|
118
122
|
}
|
|
119
123
|
}
|
|
124
|
+
*out_groups = std::move(groups);
|
|
125
|
+
|
|
126
|
+
return delta::Status::Ok();
|
|
127
|
+
}
|
|
120
128
|
|
|
129
|
+
delta::Status PlanArchivePatch(
|
|
130
|
+
const ArchivePatchInput& input,
|
|
131
|
+
ArchivePatchPlan* out_plan) {
|
|
132
|
+
if (out_plan == nullptr) {
|
|
133
|
+
return delta::Status::Error("Archive plan output is required");
|
|
134
|
+
}
|
|
135
|
+
if (input.type == ArchivePatchType::kFull) {
|
|
136
|
+
return BuildArchivePatchPlan(
|
|
137
|
+
input.type, delta::PatchManifest(), input.entry_names, out_plan,
|
|
138
|
+
input.bundle_patch_entry_name);
|
|
139
|
+
}
|
|
140
|
+
// Missing entries first: an archive without __diff.json has no text to
|
|
141
|
+
// parse, and the entry error is the one worth reporting.
|
|
142
|
+
if (!HasEntry(input.entry_names, kManifestEntryName)) {
|
|
143
|
+
return delta::Status::Error("diff.json not found");
|
|
144
|
+
}
|
|
145
|
+
if (!HasEntry(input.entry_names, input.bundle_patch_entry_name)) {
|
|
146
|
+
return delta::Status::Error("bundle delta not found");
|
|
147
|
+
}
|
|
148
|
+
delta::PatchManifestOptions options;
|
|
149
|
+
options.bundle_patch_entry_name = input.bundle_patch_entry_name;
|
|
150
|
+
options.copy_source_prefix = input.copy_source_prefix;
|
|
151
|
+
delta::PatchManifest manifest;
|
|
152
|
+
delta::Status status =
|
|
153
|
+
delta::ParsePatchManifest(input.manifest_json, options, &manifest);
|
|
154
|
+
if (!status.ok) {
|
|
155
|
+
return status;
|
|
156
|
+
}
|
|
157
|
+
return BuildArchivePatchPlan(
|
|
158
|
+
input.type, manifest, input.entry_names, out_plan,
|
|
159
|
+
input.bundle_patch_entry_name);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
delta::Status ApplyArchivePatch(
|
|
163
|
+
const ArchivePatchRequest& request,
|
|
164
|
+
std::vector<CopyGroup>* out_host_copies,
|
|
165
|
+
const delta::BundleRebuilder& bundle_patcher) {
|
|
166
|
+
if (request.archive.type == ArchivePatchType::kFull) {
|
|
167
|
+
return delta::Status::Error("A full package has no delta to apply");
|
|
168
|
+
}
|
|
169
|
+
if (request.host_copies_resources && out_host_copies == nullptr) {
|
|
170
|
+
return delta::Status::Error("Host copy groups output is required");
|
|
171
|
+
}
|
|
172
|
+
if (out_host_copies != nullptr) {
|
|
173
|
+
out_host_copies->clear();
|
|
174
|
+
}
|
|
175
|
+
ArchivePatchPlan plan;
|
|
176
|
+
delta::Status status = PlanArchivePatch(request.archive, &plan);
|
|
177
|
+
if (!status.ok) {
|
|
178
|
+
return status;
|
|
179
|
+
}
|
|
180
|
+
delta::FileSourcePatchOptions options;
|
|
181
|
+
status = BuildFileSourcePatchOptions(
|
|
182
|
+
plan, request.source_root, request.target_root,
|
|
183
|
+
request.origin_bundle_path, request.bundle_patch_path,
|
|
184
|
+
request.bundle_output_path, &options);
|
|
185
|
+
if (!status.ok) {
|
|
186
|
+
return status;
|
|
187
|
+
}
|
|
188
|
+
std::vector<CopyGroup> host_copies;
|
|
189
|
+
if (request.host_copies_resources) {
|
|
190
|
+
status = BuildCopyGroups(plan.manifest, &host_copies);
|
|
191
|
+
if (!status.ok) {
|
|
192
|
+
return status;
|
|
193
|
+
}
|
|
194
|
+
options.manifest.copies.clear();
|
|
195
|
+
options.manifest.deletes.clear();
|
|
196
|
+
options.enable_merge = false;
|
|
197
|
+
}
|
|
198
|
+
status = delta::ApplyPatchFromFileSource(options, bundle_patcher);
|
|
199
|
+
if (!status.ok) {
|
|
200
|
+
return status;
|
|
201
|
+
}
|
|
202
|
+
if (out_host_copies != nullptr) {
|
|
203
|
+
*out_host_copies = std::move(host_copies);
|
|
204
|
+
}
|
|
121
205
|
return delta::Status::Ok();
|
|
122
206
|
}
|
|
123
207
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
+
#include <cstdint>
|
|
3
4
|
#include <string>
|
|
4
5
|
#include <vector>
|
|
5
6
|
|
|
6
7
|
#include "patch_core.h"
|
|
8
|
+
#include "patch_manifest.h"
|
|
7
9
|
|
|
8
10
|
// Keep the core internal to whichever binary links it (see podspec Core).
|
|
9
11
|
#pragma GCC visibility push(hidden)
|
|
@@ -22,9 +24,13 @@ enum class EntryAction {
|
|
|
22
24
|
kExtract = 1,
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
// Every copy of one source. The manifest parser guarantees that copies of
|
|
28
|
+
// one source never declare different CRCs, so a group has at most one.
|
|
25
29
|
struct CopyGroup {
|
|
26
30
|
std::string from;
|
|
27
31
|
std::vector<std::string> to_paths;
|
|
32
|
+
bool has_expected_crc = false;
|
|
33
|
+
uint32_t expected_crc = 0;
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
struct ArchivePatchPlan {
|
|
@@ -58,6 +64,49 @@ delta::Status BuildCopyGroups(
|
|
|
58
64
|
const delta::PatchManifest& manifest,
|
|
59
65
|
std::vector<CopyGroup>* out_groups);
|
|
60
66
|
|
|
67
|
+
// What a staged patch archive declared: its entry names and the text of its
|
|
68
|
+
// __diff.json (empty when the archive has none).
|
|
69
|
+
struct ArchivePatchInput {
|
|
70
|
+
ArchivePatchType type = ArchivePatchType::kFull;
|
|
71
|
+
std::vector<std::string> entry_names;
|
|
72
|
+
std::string manifest_json;
|
|
73
|
+
std::string bundle_patch_entry_name = DefaultBundleDeltaEntryName();
|
|
74
|
+
// See PatchManifestOptions::copy_source_prefix (HarmonyOS rawfiles).
|
|
75
|
+
std::string copy_source_prefix;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// The patch types need the manifest and the bundle patch among the entries;
|
|
79
|
+
// the manifest is then parsed (ParsePatchManifest) and the merge planned.
|
|
80
|
+
// A full package plans nothing and ignores manifest_json.
|
|
81
|
+
delta::Status PlanArchivePatch(
|
|
82
|
+
const ArchivePatchInput& input,
|
|
83
|
+
ArchivePatchPlan* out_plan);
|
|
84
|
+
|
|
85
|
+
struct ArchivePatchRequest {
|
|
86
|
+
ArchivePatchInput archive;
|
|
87
|
+
std::string source_root;
|
|
88
|
+
std::string target_root;
|
|
89
|
+
std::string origin_bundle_path;
|
|
90
|
+
std::string bundle_patch_path;
|
|
91
|
+
std::string bundle_output_path;
|
|
92
|
+
// The package's resources are not files under source_root (Android APK
|
|
93
|
+
// entries, HarmonyOS rawfiles): only the bundle is rebuilt here, there is
|
|
94
|
+
// nothing to merge, and the host performs the manifest's copies itself
|
|
95
|
+
// from the groups handed back.
|
|
96
|
+
bool host_copies_resources = false;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// PlanArchivePatch + ApplyPatchFromFileSource: the whole native half of a
|
|
100
|
+
// patch install, driven by the manifest text so the platforms never parse
|
|
101
|
+
// __diff.json themselves. With host_copies_resources, *out_host_copies (which
|
|
102
|
+
// must then be non-null) receives the grouped copies the host still owes;
|
|
103
|
+
// otherwise it is left empty. Full packages are rejected.
|
|
104
|
+
delta::Status ApplyArchivePatch(
|
|
105
|
+
const ArchivePatchRequest& request,
|
|
106
|
+
std::vector<CopyGroup>* out_host_copies,
|
|
107
|
+
const delta::BundleRebuilder& bundle_patcher =
|
|
108
|
+
delta::DefaultBundleRebuilder());
|
|
109
|
+
|
|
61
110
|
delta::Status BuildFileSourcePatchOptions(
|
|
62
111
|
const ArchivePatchPlan& plan,
|
|
63
112
|
const std::string& source_root,
|
package/cpp/patch_core/digest.h
CHANGED
|
@@ -15,10 +15,11 @@ namespace digest {
|
|
|
15
15
|
// (IO-6). Streaming because Android/Harmony read the bundle from the package
|
|
16
16
|
// (AssetManager / rawfile) rather than from a plain file.
|
|
17
17
|
//
|
|
18
|
-
// Android
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// Android hashes in Java with java.security.MessageDigest instead (backed by
|
|
19
|
+
// the platform's native crypto provider): the bytes come from AssetManager
|
|
20
|
+
// streams and download sinks on the Java side, so a JNI round trip per chunk
|
|
21
|
+
// would buy nothing. The NIST vectors in the tests anchor both
|
|
22
|
+
// implementations to the same standard.
|
|
22
23
|
class Sha256 {
|
|
23
24
|
public:
|
|
24
25
|
Sha256();
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
// so the JS layer (src/error.ts UpdateErrorCode) and user loggers can
|
|
7
7
|
// aggregate errors across platforms and locales.
|
|
8
8
|
//
|
|
9
|
-
// Mirrors that cannot include this header MUST stay in sync by hand
|
|
10
|
-
//
|
|
9
|
+
// Mirrors that cannot include this header MUST stay in sync by hand
|
|
10
|
+
// (scripts/check-constant-parity.js, part of `bun lint`, asserts they do):
|
|
11
|
+
// - src/error.ts (UpdateErrorCode, JS layer)
|
|
11
12
|
// - android/.../ErrorCodes.java (Java constants)
|
|
12
|
-
//
|
|
13
|
+
// - harmony/pushy/src/main/ets/ErrorCodes.ts
|
|
14
|
+
// iOS (ios/RCTPushy/*.mm) includes this header directly.
|
|
13
15
|
//
|
|
14
16
|
// Human-readable messages are NOT part of this contract: they may differ per
|
|
15
17
|
// platform and locale. Only the codes are stable.
|