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.
Files changed (125) hide show
  1. package/README-CN.md +7 -7
  2. package/README.md +7 -6
  3. package/android/build.gradle +25 -33
  4. package/android/jni/Android.mk +16 -24
  5. package/android/lib/arm64-v8a/librnpushy.so +0 -0
  6. package/android/lib/armeabi-v7a/librnpushy.so +0 -0
  7. package/android/lib/x86/librnpushy.so +0 -0
  8. package/android/lib/x86_64/librnpushy.so +0 -0
  9. package/android/proguard.pro +9 -7
  10. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +2 -7
  11. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
  12. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
  13. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
  14. package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
  17. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
  18. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
  21. package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
  22. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
  23. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
  24. package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
  25. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
  26. package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
  27. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
  28. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
  29. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
  30. package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
  31. package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
  32. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
  33. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
  34. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
  35. package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
  36. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
  37. package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
  38. package/cpp/android_jni/archive_limits_jni.cpp +83 -0
  39. package/cpp/android_jni/install_record_jni.cpp +89 -0
  40. package/cpp/android_jni/jni_log.h +31 -0
  41. package/cpp/android_jni/jni_natives.h +40 -0
  42. package/cpp/android_jni/jni_registration.cpp +133 -0
  43. package/cpp/android_jni/jni_util.h +160 -0
  44. package/cpp/android_jni/update_core_android.cpp +450 -0
  45. package/cpp/android_jni/update_flow_jni.cpp +133 -0
  46. package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
  47. package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
  48. package/cpp/patch_core/archive_limits.cpp +194 -0
  49. package/cpp/patch_core/archive_limits.h +99 -10
  50. package/cpp/patch_core/archive_patch_core.cpp +96 -12
  51. package/cpp/patch_core/archive_patch_core.h +49 -0
  52. package/cpp/patch_core/digest.h +5 -4
  53. package/cpp/patch_core/error_codes.h +5 -3
  54. package/cpp/patch_core/install_record.cpp +430 -0
  55. package/cpp/patch_core/install_record.h +163 -9
  56. package/cpp/patch_core/patch_core.cpp +62 -16
  57. package/cpp/patch_core/patch_core.h +27 -7
  58. package/cpp/patch_core/patch_manifest.cpp +201 -0
  59. package/cpp/patch_core/patch_manifest.h +74 -0
  60. package/cpp/patch_core/state_ops.h +3 -3
  61. package/cpp/update_flow_core/flow_json.cpp +82 -8
  62. package/cpp/update_flow_core/flow_json.h +21 -0
  63. package/cpp/update_flow_core/native_config.cpp +456 -0
  64. package/cpp/update_flow_core/native_config.h +84 -0
  65. package/cpp/update_flow_core/native_round.cpp +326 -0
  66. package/cpp/update_flow_core/native_round.h +90 -0
  67. package/cpp/update_flow_core/update_flow_core.h +2 -2
  68. package/harmony/hvigor-plugin.ts +11 -19
  69. package/harmony/pushy/src/main/cpp/CMakeLists.txt +17 -26
  70. package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
  71. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
  72. package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
  73. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
  74. package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
  75. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
  76. package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
  77. package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
  78. package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
  79. package/harmony/pushy/src/main/ets/Logger.ts +20 -34
  80. package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
  81. package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
  82. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
  83. package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
  84. package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
  85. package/harmony/pushy/src/main/ets/Texts.ts +0 -4
  86. package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
  87. package/harmony/pushy.har +0 -0
  88. package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
  89. package/ios/RCTPushy/RCTPushy.h +12 -0
  90. package/ios/RCTPushy/RCTPushy.mm +87 -2634
  91. package/ios/RCTPushy/RCTPushyCommon.h +34 -0
  92. package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
  93. package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
  94. package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
  95. package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
  96. package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
  97. package/ios/RCTPushy/RCTPushyInternal.h +144 -0
  98. package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
  99. package/ios/RCTPushy/RCTPushyState.mm +314 -0
  100. package/package.json +25 -23
  101. package/react-native-update.podspec +73 -82
  102. package/src/client.ts +361 -451
  103. package/src/context.ts +1 -1
  104. package/src/core.ts +35 -8
  105. package/src/endpoint.ts +0 -2
  106. package/src/error.ts +13 -32
  107. package/src/i18n.ts +18 -83
  108. package/src/locales/en.ts +0 -4
  109. package/src/locales/zh.ts +0 -4
  110. package/src/provider.tsx +60 -53
  111. package/src/telemetry.ts +0 -13
  112. package/src/type.ts +0 -15
  113. package/src/updateFlowCore.ts +116 -10
  114. package/src/utils.ts +49 -42
  115. package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
  116. package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
  117. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
  118. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
  119. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
  120. package/cpp/patch_core/jni_natives.h +0 -27
  121. package/cpp/patch_core/jni_registration.cpp +0 -103
  122. package/cpp/patch_core/jni_util.h +0 -60
  123. package/cpp/patch_core/patch_core_android.cpp +0 -97
  124. package/cpp/patch_core/update_core_android.cpp +0 -485
  125. 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. A corrupt or hostile archive must be able
6
- // to cost at most a bounded amount of disk, memory and time — never a full
7
- // disk or a zip bomb. The caps are generous for real packages (a large full
8
- // bundle with assets is tens of MB) and exist purely as damage bounds.
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
- // Mirrors that cannot include this header MUST stay in sync by hand:
11
- // - android/.../ArchiveLimits.java
12
- // - harmony/pushy/src/main/ets/ArchiveLimits.ts
13
- // iOS (RCTPushy.mm) includes this header directly.
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
- // Per-entry uncompressed/compressed ratio, checked only above
30
- // kRatioCheckMinBytes (tiny highly-compressible files are legitimate).
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
- out_groups->clear();
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
- bool appended = false;
106
- for (CopyGroup& group : *out_groups) {
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
- group.to_paths.push_back(copy.to);
117
- out_groups->push_back(group);
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,
@@ -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 intentionally does NOT use this implementation: librnpushy.so is a
19
- // prebuilt artifact, so the Java layer hashes with java.security.MessageDigest
20
- // instead. The NIST vectors in the tests anchor both implementations to the
21
- // same standard.
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
- // - src/error.ts (UpdateErrorCode union, JS layer)
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
- // iOS (RCTPushy.mm) includes this header directly.
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.