react-native-update 10.55.1 → 10.56.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 (76) hide show
  1. package/README-CN.md +35 -0
  2. package/README.md +37 -0
  3. package/android/build.gradle +53 -27
  4. package/android/jni/Application.mk +6 -1
  5. package/android/jni/hpatch.c +59 -4
  6. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  7. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  8. package/android/lib/x86/librnupdate.so +0 -0
  9. package/android/lib/x86_64/librnupdate.so +0 -0
  10. package/android/proguard.pro +70 -14
  11. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
  12. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
  14. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
  18. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
  21. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
  22. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
  23. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
  24. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
  25. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
  26. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
  27. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
  28. package/cpp/patch_core/archive_limits.h +6 -0
  29. package/cpp/patch_core/jni_util.h +6 -2
  30. package/cpp/patch_core/patch_core.cpp +237 -84
  31. package/cpp/patch_core/patch_core.h +11 -0
  32. package/cpp/patch_core/patch_core_android.cpp +49 -33
  33. package/cpp/patch_core/update_core_android.cpp +134 -103
  34. package/cpp/update_flow_core/flow_json.cpp +150 -17
  35. package/cpp/update_flow_core/flow_json.h +13 -1
  36. package/cpp/update_flow_core/update_flow_core.cpp +78 -8
  37. package/cpp/update_flow_core/update_flow_core.h +9 -0
  38. package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
  39. package/harmony/hvigor-plugin.ts +168 -21
  40. package/harmony/pushy/src/README.md +74 -0
  41. package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
  42. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
  43. package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
  44. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
  45. package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
  46. package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
  47. package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
  48. package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
  49. package/harmony/pushy/src/main/ets/Logger.ts +42 -16
  50. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
  51. package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
  52. package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
  53. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
  54. package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
  55. package/harmony/pushy/src/main/module.json5 +5 -1
  56. package/harmony/pushy.har +0 -0
  57. package/ios/PrivacyInfo.xcprivacy +79 -0
  58. package/ios/RCTPushy/RCTPushy.mm +399 -170
  59. package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
  60. package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
  61. package/package.json +14 -24
  62. package/react-native-update.podspec +36 -3
  63. package/src/client.ts +236 -82
  64. package/src/context.ts +40 -27
  65. package/src/core.ts +41 -16
  66. package/src/endpoint.ts +2 -1
  67. package/src/error.ts +49 -11
  68. package/src/index.ts +14 -0
  69. package/src/locales/en.ts +1 -23
  70. package/src/locales/zh.ts +1 -22
  71. package/src/provider.tsx +159 -111
  72. package/src/type.ts +0 -2
  73. package/src/updateFlowCore.ts +10 -2
  74. package/src/utils.ts +61 -4
  75. package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
  76. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
@@ -3,13 +3,18 @@
3
3
  #include "digest.h"
4
4
 
5
5
  #include <cerrno>
6
+ #include <cstdint>
6
7
  #include <cstdio>
7
8
  #include <cstring>
8
9
  #include <dirent.h>
10
+ #include <fcntl.h>
11
+ #include <sys/mman.h>
9
12
  #include <sys/stat.h>
10
13
  #include <sys/types.h>
11
14
  #include <unistd.h>
12
15
 
16
+ #include <algorithm>
17
+ #include <limits>
13
18
  #include <vector>
14
19
 
15
20
  #include "hbc_transform.h"
@@ -30,6 +35,12 @@ namespace {
30
35
 
31
36
  constexpr size_t kCopyBufferSize = 16 * 1024;
32
37
 
38
+ // Directory walkers recurse once per nesting level. A hostile archive can nest
39
+ // thousands of directories while staying well inside the entry-count cap, so
40
+ // bound the depth explicitly instead of letting the stack overflow (mirrors
41
+ // flowjson::kMaxDepth). Real bundles nest a handful of levels.
42
+ constexpr int kMaxDirectoryDepth = 64;
43
+
33
44
  class HdiffBundlePatcher final : public BundlePatcher {
34
45
  public:
35
46
  Status Apply(
@@ -124,28 +135,31 @@ Status EnsureDirectory(const std::string& path) {
124
135
  if (path.empty()) {
125
136
  return Status::Ok();
126
137
  }
127
- if (PathExists(path)) {
128
- if (IsDirectory(path)) {
129
- return Status::Ok();
130
- }
131
- return Status::Error("Expected directory path: " + path);
132
- }
133
-
134
- const std::string parent = Dirname(path);
135
- if (!parent.empty()) {
136
- Status parent_status = EnsureDirectory(parent);
137
- if (!parent_status) {
138
- return parent_status;
138
+ // Walk the components left to right instead of recursing on the parent:
139
+ // manifest paths are attacker-controlled and may carry thousands of
140
+ // segments.
141
+ size_t pos = 0;
142
+ while (pos <= path.size()) {
143
+ size_t slash = path.find('/', pos);
144
+ if (slash == std::string::npos) {
145
+ slash = path.size();
146
+ }
147
+ const std::string prefix = path.substr(0, slash);
148
+ if (!prefix.empty()) {
149
+ if (PathExists(prefix)) {
150
+ if (!IsDirectory(prefix)) {
151
+ return Status::Error("Expected directory path: " + prefix);
152
+ }
153
+ } else if (mkdir(prefix.c_str(), 0755) != 0 && errno != EEXIST) {
154
+ return MakeErrnoStatus("Failed to create directory " + prefix);
155
+ }
139
156
  }
140
- }
141
-
142
- if (mkdir(path.c_str(), 0755) != 0 && errno != EEXIST) {
143
- return MakeErrnoStatus("Failed to create directory " + path);
157
+ pos = slash + 1;
144
158
  }
145
159
  return Status::Ok();
146
160
  }
147
161
 
148
- Status RemovePathRecursively(const std::string& path) {
162
+ Status RemovePathRecursively(const std::string& path, int depth = 0) {
149
163
  struct stat st;
150
164
  if (lstat(path.c_str(), &st) != 0) {
151
165
  if (errno == ENOENT) {
@@ -155,6 +169,9 @@ Status RemovePathRecursively(const std::string& path) {
155
169
  }
156
170
 
157
171
  if (S_ISDIR(st.st_mode)) {
172
+ if (depth >= kMaxDirectoryDepth) {
173
+ return Status::Error("Directory nesting too deep: " + path);
174
+ }
158
175
  DIR* dir = opendir(path.c_str());
159
176
  if (!dir) {
160
177
  return MakeErrnoStatus("Failed to open directory " + path);
@@ -166,7 +183,8 @@ Status RemovePathRecursively(const std::string& path) {
166
183
  if (name == "." || name == "..") {
167
184
  continue;
168
185
  }
169
- Status remove_status = RemovePathRecursively(JoinPath(path, name));
186
+ Status remove_status =
187
+ RemovePathRecursively(JoinPath(path, name), depth + 1);
170
188
  if (!remove_status) {
171
189
  closedir(dir);
172
190
  return remove_status;
@@ -191,20 +209,26 @@ Status ReadFileBytes(const std::string& path, std::vector<uint8_t>* out) {
191
209
  if (file == nullptr) {
192
210
  return MakeErrnoStatus("Failed to open file for reading " + path);
193
211
  }
194
- if (fseek(file, 0, SEEK_END) != 0) {
212
+ // fstat rather than fseek/ftell: ftell returns a 32-bit long on the 32-bit
213
+ // ABIs (armeabi-v7a, x86), and the regular-file check keeps directories and
214
+ // devices out.
215
+ struct stat st;
216
+ if (fstat(fileno(file), &st) != 0) {
195
217
  fclose(file);
196
- return MakeErrnoStatus("Failed to seek file " + path);
218
+ return MakeErrnoStatus("Failed to size file " + path);
197
219
  }
198
- const long size = ftell(file);
199
- if (size < 0) {
220
+ if (!S_ISREG(st.st_mode)) {
200
221
  fclose(file);
201
- return MakeErrnoStatus("Failed to size file " + path);
222
+ return Status::Error("Not a regular file: " + path);
202
223
  }
203
- if (fseek(file, 0, SEEK_SET) != 0) {
224
+ if (st.st_size < 0 ||
225
+ static_cast<uint64_t>(st.st_size) >
226
+ static_cast<uint64_t>(std::numeric_limits<size_t>::max() / 2)) {
204
227
  fclose(file);
205
- return MakeErrnoStatus("Failed to rewind file " + path);
228
+ return Status::Error("File too large to load: " + path);
206
229
  }
207
- out->resize(static_cast<size_t>(size));
230
+ const size_t size = static_cast<size_t>(st.st_size);
231
+ out->resize(size);
208
232
  if (size > 0 &&
209
233
  fread(out->data(), 1, out->size(), file) != out->size()) {
210
234
  fclose(file);
@@ -214,6 +238,20 @@ Status ReadFileBytes(const std::string& path, std::vector<uint8_t>* out) {
214
238
  return Status::Ok();
215
239
  }
216
240
 
241
+ // fflush + fsync so payload bytes are durable before the caller's two-phase
242
+ // rename: a power cut must not leave a complete-looking version directory
243
+ // with a truncated bundle. EINVAL means the descriptor cannot be synced (some
244
+ // pseudo filesystems) and is not an error.
245
+ Status SyncFile(FILE* file, const std::string& path) {
246
+ if (fflush(file) != 0) {
247
+ return MakeErrnoStatus("Failed to flush file " + path);
248
+ }
249
+ if (fsync(fileno(file)) != 0 && errno != EINVAL) {
250
+ return MakeErrnoStatus("Failed to sync file " + path);
251
+ }
252
+ return Status::Ok();
253
+ }
254
+
217
255
  Status WriteFileBytes(const std::string& path, const std::vector<uint8_t>& data) {
218
256
  FILE* file = fopen(path.c_str(), "wb");
219
257
  if (file == nullptr) {
@@ -224,6 +262,12 @@ Status WriteFileBytes(const std::string& path, const std::vector<uint8_t>& data)
224
262
  remove(path.c_str());
225
263
  return Status::Error("Failed to write file " + path);
226
264
  }
265
+ Status sync_status = SyncFile(file, path);
266
+ if (!sync_status) {
267
+ fclose(file);
268
+ remove(path.c_str());
269
+ return sync_status;
270
+ }
227
271
  if (fclose(file) != 0) {
228
272
  remove(path.c_str());
229
273
  return MakeErrnoStatus("Failed to flush file " + path);
@@ -231,42 +275,10 @@ Status WriteFileBytes(const std::string& path, const std::vector<uint8_t>& data)
231
275
  return Status::Ok();
232
276
  }
233
277
 
234
- Status CopyFile(const std::string& from, const std::string& to, bool overwrite) {
235
- struct stat st;
236
- if (stat(from.c_str(), &st) != 0) {
237
- return MakeErrnoStatus("Failed to stat source file " + from);
238
- }
239
- if (!S_ISREG(st.st_mode)) {
240
- return Status::Error("Source is not a regular file: " + from);
241
- }
242
-
243
- const std::string parent = Dirname(to);
244
- Status dir_status = EnsureDirectory(parent);
245
- if (!dir_status) {
246
- return dir_status;
247
- }
248
-
249
- if (PathExists(to)) {
250
- if (!overwrite) {
251
- return Status::Ok();
252
- }
253
- Status remove_status = RemovePathRecursively(to);
254
- if (!remove_status) {
255
- return remove_status;
256
- }
257
- }
258
-
259
- // Prefer a hard link over copying bytes: unchanged files between versions
260
- // are identical, so linking is O(1) per file, writes nothing to flash, and
261
- // shares disk blocks. Version directories are immutable once created (patch
262
- // outputs are always written as new files), so sharing the inode with the
263
- // source is safe. Fall back to a byte copy whenever linking is not possible
264
- // (cross-device source such as the installed app bundle, EPERM, EMLINK, or
265
- // filesystems without hard-link support).
266
- if (!internal::g_disable_hard_links && link(from.c_str(), to.c_str()) == 0) {
267
- return Status::Ok();
268
- }
269
-
278
+ // Streams `from` into a fresh `to` through a small buffer: never a hard link
279
+ // (callers that go on to mutate `to` in place rely on that) and never the
280
+ // whole file in memory.
281
+ Status CopyFileBytes(const std::string& from, const std::string& to) {
270
282
  FILE* source = std::fopen(from.c_str(), "rb");
271
283
  if (!source) {
272
284
  return MakeErrnoStatus("Failed to open source file " + from);
@@ -301,12 +313,56 @@ Status CopyFile(const std::string& from, const std::string& to, bool overwrite)
301
313
  }
302
314
 
303
315
  std::fclose(source);
316
+ Status sync_status = SyncFile(destination, to);
317
+ if (!sync_status) {
318
+ std::fclose(destination);
319
+ return sync_status;
320
+ }
304
321
  if (std::fclose(destination) != 0) {
305
322
  return MakeErrnoStatus("Failed to close destination file " + to);
306
323
  }
307
324
  return Status::Ok();
308
325
  }
309
326
 
327
+ Status CopyFile(const std::string& from, const std::string& to, bool overwrite) {
328
+ struct stat st;
329
+ if (stat(from.c_str(), &st) != 0) {
330
+ return MakeErrnoStatus("Failed to stat source file " + from);
331
+ }
332
+ if (!S_ISREG(st.st_mode)) {
333
+ return Status::Error("Source is not a regular file: " + from);
334
+ }
335
+
336
+ const std::string parent = Dirname(to);
337
+ Status dir_status = EnsureDirectory(parent);
338
+ if (!dir_status) {
339
+ return dir_status;
340
+ }
341
+
342
+ if (PathExists(to)) {
343
+ if (!overwrite) {
344
+ return Status::Ok();
345
+ }
346
+ Status remove_status = RemovePathRecursively(to);
347
+ if (!remove_status) {
348
+ return remove_status;
349
+ }
350
+ }
351
+
352
+ // Prefer a hard link over copying bytes: unchanged files between versions
353
+ // are identical, so linking is O(1) per file, writes nothing to flash, and
354
+ // shares disk blocks. Version directories are immutable once created (patch
355
+ // outputs are always written as new files), so sharing the inode with the
356
+ // source is safe. Fall back to a byte copy whenever linking is not possible
357
+ // (cross-device source such as the installed app bundle, EPERM, EMLINK, or
358
+ // filesystems without hard-link support).
359
+ if (!internal::g_disable_hard_links && link(from.c_str(), to.c_str()) == 0) {
360
+ return Status::Ok();
361
+ }
362
+
363
+ return CopyFileBytes(from, to);
364
+ }
365
+
310
366
  struct DeleteRule {
311
367
  std::string path;
312
368
  bool directory_hint;
@@ -367,7 +423,8 @@ Status MergeDirectoryRecursively(
367
423
  const std::string& source_root,
368
424
  const std::string& target_root,
369
425
  const std::string& relative_root,
370
- const DeleteMatcher& deletes) {
426
+ const DeleteMatcher& deletes,
427
+ int depth = 0) {
371
428
  DIR* dir = opendir(source_root.c_str());
372
429
  if (!dir) {
373
430
  if (errno == ENOENT) {
@@ -399,14 +456,18 @@ Status MergeDirectoryRecursively(
399
456
 
400
457
  const std::string target_path = JoinPath(target_root, name);
401
458
  if (S_ISDIR(st.st_mode)) {
459
+ if (depth >= kMaxDirectoryDepth) {
460
+ closedir(dir);
461
+ return Status::Error("Directory nesting too deep: " + source_path);
462
+ }
402
463
  Status dir_status = EnsureDirectory(target_path);
403
464
  if (!dir_status) {
404
465
  closedir(dir);
405
466
  return dir_status;
406
467
  }
407
468
 
408
- Status merge_status =
409
- MergeDirectoryRecursively(source_path, target_path, relative_path, deletes);
469
+ Status merge_status = MergeDirectoryRecursively(
470
+ source_path, target_path, relative_path, deletes, depth + 1);
410
471
  if (!merge_status) {
411
472
  closedir(dir);
412
473
  return merge_status;
@@ -445,10 +506,76 @@ const BundlePatcher& DefaultBundlePatcher() {
445
506
 
446
507
  namespace {
447
508
 
509
+ // Applies T (or T⁻¹) to a file in place through a shared mapping, so a
510
+ // 10–50 MB bundle never has to be read into an anonymous heap buffer: the
511
+ // pages are backed by the file itself and can be written back and evicted
512
+ // under memory pressure. Falls back to read → transform → write when the
513
+ // mapping is unavailable. On a rejected transform the bytes are left as they
514
+ // were (TransformHbcInPlace never partially mutates).
515
+ Status TransformFileInPlace(
516
+ const std::string& path,
517
+ const hbc::HbcLayoutDesc& layout,
518
+ bool inverse) {
519
+ const char* failure = inverse
520
+ ? "hbcTransform inverse failed on patched bundle"
521
+ : "hbcTransform failed on origin bundle";
522
+
523
+ const int fd = open(path.c_str(), O_RDWR);
524
+ if (fd < 0) {
525
+ return MakeErrnoStatus("Failed to open bundle for transform " + path);
526
+ }
527
+ struct stat st;
528
+ if (fstat(fd, &st) != 0) {
529
+ close(fd);
530
+ return MakeErrnoStatus("Failed to size bundle " + path);
531
+ }
532
+ void* mapped = MAP_FAILED;
533
+ size_t size = 0;
534
+ if (S_ISREG(st.st_mode) && st.st_size > 0 &&
535
+ static_cast<uint64_t>(st.st_size) <=
536
+ static_cast<uint64_t>(std::numeric_limits<size_t>::max() / 2)) {
537
+ size = static_cast<size_t>(st.st_size);
538
+ mapped = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
539
+ }
540
+ if (mapped == MAP_FAILED) {
541
+ close(fd);
542
+ std::vector<uint8_t> bytes;
543
+ Status status = ReadFileBytes(path, &bytes);
544
+ if (!status) {
545
+ return status;
546
+ }
547
+ if (!hbc::TransformHbcInPlace(bytes.data(), bytes.size(), layout, inverse)) {
548
+ return Status::Error(failure);
549
+ }
550
+ return WriteFileBytes(path, bytes);
551
+ }
552
+
553
+ const bool transformed = hbc::TransformHbcInPlace(
554
+ static_cast<uint8_t*>(mapped), size, layout, inverse);
555
+ const bool synced = msync(mapped, size, MS_SYNC) == 0;
556
+ const int sync_errno = errno;
557
+ munmap(mapped, size);
558
+ const bool fsynced = fsync(fd) == 0 || errno == EINVAL;
559
+ const int fsync_errno = errno;
560
+ close(fd);
561
+ if (!transformed) {
562
+ return Status::Error(failure);
563
+ }
564
+ if (!synced) {
565
+ return MakeErrnoStatus("Failed to sync transformed bundle " + path, sync_errno);
566
+ }
567
+ if (!fsynced) {
568
+ return MakeErrnoStatus("Failed to sync transformed bundle " + path, fsync_errno);
569
+ }
570
+ return Status::Ok();
571
+ }
572
+
448
573
  // 变换域 bundle patch:T(origin) → hpatch → T⁻¹。
449
574
  // 元数据/变换的任何失败都返回错误——调用方沿既有失败路径回退整包;
450
575
  // 绝不能忽略元数据直接 hpatch(会产出损坏 bundle,虽然最终 hash 校验
451
576
  // 也会拦住,但应在此处快速失败)。
577
+ // 两次变换都在临时文件上原地完成(见 TransformFileInPlace),整个流程
578
+ // 端到端不把 bundle 整体读进内存。
452
579
  Status ApplyBundlePatchWithHbcTransform(
453
580
  const FileSourcePatchOptions& options,
454
581
  const BundlePatcher& bundle_patcher) {
@@ -464,26 +591,27 @@ Status ApplyBundlePatchWithHbcTransform(
464
591
  std::vector<hbc::HbcSectionDesc> sections_scratch;
465
592
  const hbc::HbcLayoutDesc layout = hbc::BuildLayout(meta, &sections_scratch);
466
593
 
467
- std::vector<uint8_t> origin;
468
- Status status = ReadFileBytes(options.origin_bundle_path, &origin);
469
- if (!status) {
470
- return status;
471
- }
472
- if (!hbc::TransformHbcInPlace(origin.data(), origin.size(), layout, false)) {
473
- return Status::Error("hbcTransform failed on origin bundle");
474
- }
475
-
476
594
  Status dir_status = EnsureDirectory(Dirname(options.bundle_output_path));
477
595
  if (!dir_status) {
478
596
  return dir_status;
479
597
  }
480
598
  const std::string temp_origin = options.bundle_output_path + ".hbct-origin";
481
599
  const std::string temp_patched = options.bundle_output_path + ".hbct-patched";
482
- status = WriteFileBytes(temp_origin, origin);
600
+ remove(temp_origin.c_str());
601
+ remove(temp_patched.c_str());
602
+
603
+ // A private byte copy — never a hard link — because the transform mutates
604
+ // it in place and the origin bundle must stay untouched.
605
+ Status status = CopyFileBytes(options.origin_bundle_path, temp_origin);
606
+ if (!status) {
607
+ remove(temp_origin.c_str());
608
+ return status;
609
+ }
610
+ status = TransformFileInPlace(temp_origin, layout, false);
483
611
  if (!status) {
612
+ remove(temp_origin.c_str());
484
613
  return status;
485
614
  }
486
- origin = std::vector<uint8_t>(); // 及早释放,patch 期间只保留文件副本
487
615
 
488
616
  Status patch_status =
489
617
  bundle_patcher.Apply(temp_origin, options.bundle_patch_path, temp_patched);
@@ -493,23 +621,26 @@ Status ApplyBundlePatchWithHbcTransform(
493
621
  return patch_status;
494
622
  }
495
623
 
496
- std::vector<uint8_t> patched;
497
- status = ReadFileBytes(temp_patched, &patched);
498
- remove(temp_patched.c_str());
624
+ status = TransformFileInPlace(temp_patched, layout, true);
499
625
  if (!status) {
626
+ remove(temp_patched.c_str());
500
627
  return status;
501
628
  }
502
- if (!hbc::TransformHbcInPlace(patched.data(), patched.size(), layout, true)) {
503
- return Status::Error("hbcTransform inverse failed on patched bundle");
504
- }
505
629
 
506
630
  if (PathExists(options.bundle_output_path)) {
507
631
  Status remove_status = RemovePathRecursively(options.bundle_output_path);
508
632
  if (!remove_status) {
633
+ remove(temp_patched.c_str());
509
634
  return remove_status;
510
635
  }
511
636
  }
512
- return WriteFileBytes(options.bundle_output_path, patched);
637
+ if (rename(temp_patched.c_str(), options.bundle_output_path.c_str()) != 0) {
638
+ Status rename_status = MakeErrnoStatus(
639
+ "Failed to move patched bundle into place " + options.bundle_output_path);
640
+ remove(temp_patched.c_str());
641
+ return rename_status;
642
+ }
643
+ return Status::Ok();
513
644
  }
514
645
 
515
646
  } // namespace
@@ -591,6 +722,16 @@ Status CleanupOldEntries(
591
722
  const std::string& keep_previous,
592
723
  int max_age_days,
593
724
  std::time_t now) {
725
+ return CleanupOldEntries(
726
+ root_dir, std::vector<std::string>{keep_current, keep_previous},
727
+ max_age_days, now);
728
+ }
729
+
730
+ Status CleanupOldEntries(
731
+ const std::string& root_dir,
732
+ const std::vector<std::string>& keep_names,
733
+ int max_age_days,
734
+ std::time_t now) {
594
735
  DIR* dir = opendir(root_dir.c_str());
595
736
  if (!dir) {
596
737
  if (errno == ENOENT) {
@@ -609,7 +750,8 @@ Status CleanupOldEntries(
609
750
  if (name == "." || name == ".." || (!name.empty() && name[0] == '.')) {
610
751
  continue;
611
752
  }
612
- if (name == keep_current || name == keep_previous) {
753
+ if (std::find(keep_names.begin(), keep_names.end(), name) !=
754
+ keep_names.end()) {
613
755
  continue;
614
756
  }
615
757
 
@@ -648,6 +790,17 @@ bool IsSafeRelativePath(const std::string& path) {
648
790
  if (segment.empty() || segment == "." || segment == "..") {
649
791
  return false;
650
792
  }
793
+ for (const char ch : segment) {
794
+ // Control bytes (NUL above all) are never legitimate in a manifest
795
+ // path: everything downstream goes through c_str(), so an embedded NUL
796
+ // would silently truncate "..\0x" to ".." and escape the staging root
797
+ // (reachable through the HarmonyOS NAPI string bridge, which preserves
798
+ // NULs). Non-ASCII UTF-8 (>= 0x80) stays allowed.
799
+ const unsigned char byte = static_cast<unsigned char>(ch);
800
+ if (byte < 0x20 || byte == 0x7f) {
801
+ return false;
802
+ }
803
+ }
651
804
  }
652
805
  return true;
653
806
  }
@@ -68,6 +68,17 @@ Status ApplyPatchFromFileSource(
68
68
  const FileSourcePatchOptions& options,
69
69
  const BundlePatcher& bundle_patcher = DefaultBundlePatcher());
70
70
 
71
+ // Removes every non-dot entry under root_dir older than max_age_days whose
72
+ // name is not in keep_names. Callers must include every version that may
73
+ // still be in use: the persisted current/last versions AND the version this
74
+ // process booted from (two switches without a restart otherwise evict the
75
+ // running bundle while its on-demand assets are still being served).
76
+ Status CleanupOldEntries(
77
+ const std::string& root_dir,
78
+ const std::vector<std::string>& keep_names,
79
+ int max_age_days,
80
+ std::time_t now = 0);
81
+
71
82
  Status CleanupOldEntries(
72
83
  const std::string& root_dir,
73
84
  const std::string& keep_current,
@@ -1,5 +1,6 @@
1
1
  #include <jni.h>
2
2
 
3
+ #include <exception>
3
4
  #include <string>
4
5
  #include <vector>
5
6
 
@@ -29,36 +30,45 @@ Java_cn_reactnative_modules_update_DownloadTask_applyPatchFromFileSource(
29
30
  jobjectArray copy_tos,
30
31
  jobjectArray deletes,
31
32
  jstring hbc_transform_meta) {
32
- const std::vector<std::string> from_values = JArrayToVector(env, copy_froms);
33
- const std::vector<std::string> to_values = JArrayToVector(env, copy_tos);
33
+ // C++ exceptions (bad_alloc/length_error from input-proportional STL
34
+ // allocations) must not unwind through the JNI boundary: convert them to a
35
+ // Java RuntimeException like every other failure of this entry point.
36
+ try {
37
+ const std::vector<std::string> from_values = JArrayToVector(env, copy_froms);
38
+ const std::vector<std::string> to_values = JArrayToVector(env, copy_tos);
34
39
 
35
- if (from_values.size() != to_values.size()) {
36
- ThrowRuntimeException(env, "copy_froms and copy_tos length mismatch");
37
- return;
38
- }
40
+ if (from_values.size() != to_values.size()) {
41
+ ThrowRuntimeException(env, "copy_froms and copy_tos length mismatch");
42
+ return;
43
+ }
39
44
 
40
- pushy::patch::FileSourcePatchOptions options;
41
- options.source_root = JStringToString(env, source_root);
42
- options.target_root = JStringToString(env, target_root);
43
- options.origin_bundle_path = JStringToString(env, origin_bundle_path);
44
- options.bundle_patch_path = JStringToString(env, bundle_patch_path);
45
- options.bundle_output_path = JStringToString(env, bundle_output_path);
46
- options.merge_source_subdir = JStringToString(env, merge_source_subdir);
47
- options.enable_merge = enable_merge == JNI_TRUE;
48
- options.bundle_hbc_transform_meta = JStringToString(env, hbc_transform_meta);
45
+ pushy::patch::FileSourcePatchOptions options;
46
+ options.source_root = JStringToString(env, source_root);
47
+ options.target_root = JStringToString(env, target_root);
48
+ options.origin_bundle_path = JStringToString(env, origin_bundle_path);
49
+ options.bundle_patch_path = JStringToString(env, bundle_patch_path);
50
+ options.bundle_output_path = JStringToString(env, bundle_output_path);
51
+ options.merge_source_subdir = JStringToString(env, merge_source_subdir);
52
+ options.enable_merge = enable_merge == JNI_TRUE;
53
+ options.bundle_hbc_transform_meta = JStringToString(env, hbc_transform_meta);
49
54
 
50
- for (size_t index = 0; index < from_values.size(); ++index) {
51
- options.manifest.copies.push_back(pushy::patch::CopyOperation{
52
- from_values[index],
53
- to_values[index],
54
- });
55
- }
56
- options.manifest.deletes = JArrayToVector(env, deletes);
55
+ for (size_t index = 0; index < from_values.size(); ++index) {
56
+ options.manifest.copies.push_back(pushy::patch::CopyOperation{
57
+ from_values[index],
58
+ to_values[index],
59
+ });
60
+ }
61
+ options.manifest.deletes = JArrayToVector(env, deletes);
57
62
 
58
- const pushy::patch::Status status =
59
- pushy::patch::ApplyPatchFromFileSource(options);
60
- if (!status.ok) {
61
- ThrowRuntimeException(env, status.message);
63
+ const pushy::patch::Status status =
64
+ pushy::patch::ApplyPatchFromFileSource(options);
65
+ if (!status.ok) {
66
+ ThrowRuntimeException(env, status.message);
67
+ }
68
+ } catch (const std::exception& error) {
69
+ ThrowRuntimeException(env, error.what());
70
+ } catch (...) {
71
+ ThrowRuntimeException(env, "Unexpected native exception in applyPatchFromFileSource");
62
72
  }
63
73
  }
64
74
 
@@ -70,12 +80,18 @@ Java_cn_reactnative_modules_update_DownloadTask_cleanupOldEntries(
70
80
  jstring keep_current,
71
81
  jstring keep_previous,
72
82
  jint max_age_days) {
73
- const pushy::patch::Status status = pushy::patch::CleanupOldEntries(
74
- JStringToString(env, root_dir),
75
- JStringToString(env, keep_current),
76
- JStringToString(env, keep_previous),
77
- static_cast<int>(max_age_days));
78
- if (!status.ok) {
79
- ThrowRuntimeException(env, status.message);
83
+ try {
84
+ const pushy::patch::Status status = pushy::patch::CleanupOldEntries(
85
+ JStringToString(env, root_dir),
86
+ JStringToString(env, keep_current),
87
+ JStringToString(env, keep_previous),
88
+ static_cast<int>(max_age_days));
89
+ if (!status.ok) {
90
+ ThrowRuntimeException(env, status.message);
91
+ }
92
+ } catch (const std::exception& error) {
93
+ ThrowRuntimeException(env, error.what());
94
+ } catch (...) {
95
+ ThrowRuntimeException(env, "Unexpected native exception in cleanupOldEntries");
80
96
  }
81
97
  }