react-native-update 10.59.1 → 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 +11 -12
  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 +19 -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 -2628
  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 +74 -83
  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,430 @@
1
+ #include "install_record.h"
2
+
3
+ #include <cerrno>
4
+ #include <cstring>
5
+ #include <fcntl.h>
6
+ #include <sys/stat.h>
7
+ #include <unistd.h>
8
+ #include <utility>
9
+
10
+ #include "digest.h"
11
+ // Relative like every other cross-core include (see patch_manifest.cpp):
12
+ // use_frameworks! flattens the pod's headers.
13
+ #include "../update_flow_core/flow_json.h"
14
+
15
+ namespace pushy {
16
+ namespace install_record {
17
+ namespace {
18
+
19
+ // Written next to the record, then renamed over it. Its `.pushy-` prefix is
20
+ // reserved like the record's own name, so no archive can ship one.
21
+ constexpr const char* kTempSuffix = ".tmp";
22
+ // A record has five members; the cap only bounds a hostile file's tree.
23
+ constexpr size_t kMaxRecordNodes = 1024;
24
+
25
+ std::string JoinPath(const std::string& dir, const std::string& name) {
26
+ return dir + "/" + name;
27
+ }
28
+
29
+ std::string RecordPath(const std::string& dir) {
30
+ return JoinPath(dir, kFileName);
31
+ }
32
+
33
+ std::string ErrnoText() {
34
+ return std::strerror(errno);
35
+ }
36
+
37
+ bool IsRegularFile(const std::string& path) {
38
+ struct stat info;
39
+ return stat(path.c_str(), &info) == 0 && S_ISREG(info.st_mode);
40
+ }
41
+
42
+ // The record file's text and what it is. kPresent only when it parses as a
43
+ // JSON object; *text is filled for kPresent alone.
44
+ Record::State ReadRecordText(const std::string& dir, std::string* text) {
45
+ const std::string path = RecordPath(dir);
46
+ struct stat info;
47
+ if (stat(path.c_str(), &info) != 0) {
48
+ return errno == ENOENT || errno == ENOTDIR ? Record::State::kAbsent
49
+ : Record::State::kInvalid;
50
+ }
51
+ if (!S_ISREG(info.st_mode)) {
52
+ return Record::State::kInvalid;
53
+ }
54
+ if (info.st_size == 0) {
55
+ return Record::State::kLegacy;
56
+ }
57
+ if (info.st_size > kMaxRecordBytes) {
58
+ return Record::State::kInvalid;
59
+ }
60
+ // O_NONBLOCK: a FIFO swapped in after the stat must not hang the caller.
61
+ const int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC | O_NONBLOCK);
62
+ if (fd < 0) {
63
+ return Record::State::kInvalid;
64
+ }
65
+ std::string content;
66
+ char buffer[4096];
67
+ bool failed = false;
68
+ for (;;) {
69
+ const ssize_t count = read(fd, buffer, sizeof(buffer));
70
+ if (count < 0) {
71
+ if (errno == EINTR) {
72
+ continue;
73
+ }
74
+ failed = true;
75
+ break;
76
+ }
77
+ if (count == 0) {
78
+ break;
79
+ }
80
+ content.append(buffer, static_cast<size_t>(count));
81
+ if (static_cast<long long>(content.size()) > kMaxRecordBytes) {
82
+ failed = true;
83
+ break;
84
+ }
85
+ }
86
+ close(fd);
87
+ if (failed) {
88
+ return Record::State::kInvalid;
89
+ }
90
+ // Truncated to zero between the stat and the read: still the legacy shape.
91
+ const Record record = Parse(content);
92
+ if (record.state == Record::State::kPresent) {
93
+ *text = std::move(content);
94
+ }
95
+ return record.state;
96
+ }
97
+
98
+ std::string StringMember(const flowjson::Value& object, const char* key) {
99
+ const flowjson::Value& value = object.Get(key);
100
+ return value.IsString() ? value.AsString() : std::string();
101
+ }
102
+
103
+ bool EqualsIgnoringAsciiCase(const std::string& a, const std::string& b) {
104
+ if (a.size() != b.size()) {
105
+ return false;
106
+ }
107
+ for (size_t i = 0; i < a.size(); i++) {
108
+ char x = a[i];
109
+ char y = b[i];
110
+ if (x >= 'A' && x <= 'Z') {
111
+ x = static_cast<char>(x - 'A' + 'a');
112
+ }
113
+ if (y >= 'A' && y <= 'Z') {
114
+ y = static_cast<char>(y - 'A' + 'a');
115
+ }
116
+ if (x != y) {
117
+ return false;
118
+ }
119
+ }
120
+ return true;
121
+ }
122
+
123
+ bool WriteAll(int fd, const std::string& data) {
124
+ size_t written = 0;
125
+ while (written < data.size()) {
126
+ const ssize_t count = write(fd, data.data() + written, data.size() - written);
127
+ if (count < 0) {
128
+ if (errno == EINTR) {
129
+ continue;
130
+ }
131
+ return false;
132
+ }
133
+ written += static_cast<size_t>(count);
134
+ }
135
+ return true;
136
+ }
137
+
138
+ // Makes a directory's entries durable (a rename or a new file only is once
139
+ // the directory itself is synced). Best-effort by design: some file systems
140
+ // refuse to open or fsync a directory, which must not fail an install whose
141
+ // files are already synced.
142
+ void SyncDirectory(const std::string& dir) {
143
+ const int fd = open(dir.c_str(), O_RDONLY | O_CLOEXEC | O_DIRECTORY);
144
+ if (fd < 0) {
145
+ return;
146
+ }
147
+ while (fsync(fd) != 0 && errno == EINTR) {
148
+ }
149
+ close(fd);
150
+ }
151
+
152
+ std::string ParentDirectory(const std::string& path) {
153
+ const size_t slash = path.find_last_of('/');
154
+ if (slash == std::string::npos) {
155
+ return ".";
156
+ }
157
+ return slash == 0 ? "/" : path.substr(0, slash);
158
+ }
159
+
160
+ Status Rejected(const std::string& version_hash, const char* reason) {
161
+ return Status::Error("Bundle version " + version_hash + " " + reason);
162
+ }
163
+
164
+ Status RecordMismatch(const std::string& version_hash) {
165
+ return Rejected(version_hash, "completion record mismatch.");
166
+ }
167
+
168
+ } // namespace
169
+
170
+ Record Parse(const std::string& text) {
171
+ Record record;
172
+ if (text.empty()) {
173
+ record.state = Record::State::kLegacy;
174
+ return record;
175
+ }
176
+ record.state = Record::State::kInvalid;
177
+ if (static_cast<long long>(text.size()) > kMaxRecordBytes) {
178
+ return record;
179
+ }
180
+ flowjson::ParseLimits limits;
181
+ limits.max_input_bytes = static_cast<size_t>(kMaxRecordBytes);
182
+ limits.max_nodes = kMaxRecordNodes;
183
+ bool ok = false;
184
+ const flowjson::Value object = flowjson::Parse(text, limits, &ok);
185
+ if (!ok || !object.IsObject()) {
186
+ return record;
187
+ }
188
+ record.state = Record::State::kPresent;
189
+ const flowjson::Value& schema = object.Get("schema");
190
+ record.current_schema =
191
+ schema.IsNumber() && schema.AsNumber() == static_cast<double>(kSchema);
192
+ record.version_hash = StringMember(object, "versionHash");
193
+ record.bundle_sha256 = StringMember(object, "bundleSha256");
194
+ record.artifact_sha256 = StringMember(object, "artifactSha256");
195
+ return record;
196
+ }
197
+
198
+ bool Names(const Record& record, const std::string& version_hash) {
199
+ return record.state == Record::State::kPresent && record.current_schema &&
200
+ !version_hash.empty() && record.version_hash == version_hash;
201
+ }
202
+
203
+ Record Read(const std::string& version_dir) {
204
+ std::string text;
205
+ const Record::State state = ReadRecordText(version_dir, &text);
206
+ if (state == Record::State::kPresent) {
207
+ return Parse(text);
208
+ }
209
+ Record record;
210
+ record.state = state;
211
+ return record;
212
+ }
213
+
214
+ bool HasRecordFile(const std::string& version_dir) {
215
+ struct stat info;
216
+ return stat(RecordPath(version_dir).c_str(), &info) == 0;
217
+ }
218
+
219
+ Status Build(const std::string& version_hash,
220
+ const std::string& bundle_sha256,
221
+ const std::string& artifact_sha256,
222
+ std::string* out) {
223
+ if (bundle_sha256.empty()) {
224
+ return Rejected(version_hash, "has no bundle digest.");
225
+ }
226
+ flowjson::Value record = flowjson::Value::Object();
227
+ record.Set("schema", flowjson::Value::Number(kSchema));
228
+ record.Set("versionHash", flowjson::Value::String(version_hash));
229
+ record.Set("bundleSha256", flowjson::Value::String(bundle_sha256));
230
+ if (!artifact_sha256.empty()) {
231
+ record.Set("artifactSha256", flowjson::Value::String(artifact_sha256));
232
+ }
233
+ *out = flowjson::Stringify(record);
234
+ return Status::Ok();
235
+ }
236
+
237
+ Status Write(const std::string& dir, const std::string& record_json) {
238
+ const std::string path = RecordPath(dir);
239
+ const std::string temp = path + kTempSuffix;
240
+ const int fd = open(temp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0666);
241
+ if (fd < 0) {
242
+ return Status::Error("cannot create completion record " + temp + ": " +
243
+ ErrnoText());
244
+ }
245
+ bool ok = WriteAll(fd, record_json);
246
+ while (ok && fsync(fd) != 0) {
247
+ if (errno != EINTR) {
248
+ ok = false;
249
+ }
250
+ }
251
+ std::string error = ok ? std::string() : ErrnoText();
252
+ if (close(fd) != 0 && ok) {
253
+ ok = false;
254
+ error = ErrnoText();
255
+ }
256
+ if (!ok) {
257
+ unlink(temp.c_str());
258
+ return Status::Error("cannot write completion record " + temp + ": " + error);
259
+ }
260
+ if (rename(temp.c_str(), path.c_str()) != 0) {
261
+ error = ErrnoText();
262
+ unlink(temp.c_str());
263
+ return Status::Error("cannot place completion record " + path + ": " + error);
264
+ }
265
+ SyncDirectory(dir);
266
+ return Status::Ok();
267
+ }
268
+
269
+ SealResult Seal(const std::string& staging_dir,
270
+ const std::string& version_hash,
271
+ const std::string& bundle_file_name,
272
+ const std::string& artifact_sha256,
273
+ const std::string& known_bundle_sha256) {
274
+ SealResult result;
275
+ const std::string bundle = JoinPath(staging_dir, bundle_file_name);
276
+ if (!IsRegularFile(bundle)) {
277
+ result.status = Status::Error("bundle missing after install: " + bundle);
278
+ return result;
279
+ }
280
+ const std::string bundle_sha256 = known_bundle_sha256.empty()
281
+ ? digest::Sha256File(bundle)
282
+ : known_bundle_sha256;
283
+ std::string record;
284
+ result.status = Build(version_hash, bundle_sha256, artifact_sha256, &record);
285
+ if (!result.status.ok) {
286
+ return result;
287
+ }
288
+ result.status = Write(staging_dir, record);
289
+ result.file_failure = !result.status.ok;
290
+ return result;
291
+ }
292
+
293
+ Status Promote(const std::string& staging_dir, const std::string& version_dir) {
294
+ if (rename(staging_dir.c_str(), version_dir.c_str()) != 0) {
295
+ return Status::Error("cannot move " + staging_dir + " to " + version_dir +
296
+ ": " + ErrnoText());
297
+ }
298
+ SyncDirectory(ParentDirectory(version_dir));
299
+ return Status::Ok();
300
+ }
301
+
302
+ bool IsComplete(const std::string& version_dir,
303
+ const std::string& version_hash,
304
+ const std::string& bundle_file_name) {
305
+ if (!IsRegularFile(JoinPath(version_dir, bundle_file_name))) {
306
+ return false;
307
+ }
308
+ const Record record = Read(version_dir);
309
+ return record.state == Record::State::kLegacy || Names(record, version_hash);
310
+ }
311
+
312
+ std::string RecordedBundleSha256(const std::string& version_dir) {
313
+ return Read(version_dir).bundle_sha256;
314
+ }
315
+
316
+ bool FileStamp::operator==(const FileStamp& other) const {
317
+ return regular == other.regular && device == other.device &&
318
+ inode == other.inode && size == other.size &&
319
+ modified_sec == other.modified_sec &&
320
+ modified_nsec == other.modified_nsec &&
321
+ changed_sec == other.changed_sec && changed_nsec == other.changed_nsec;
322
+ }
323
+
324
+ FileStamp StampFile(const std::string& path) {
325
+ FileStamp stamp;
326
+ struct stat info;
327
+ if (stat(path.c_str(), &info) != 0 || !S_ISREG(info.st_mode)) {
328
+ return stamp;
329
+ }
330
+ stamp.regular = true;
331
+ stamp.device = static_cast<uint64_t>(info.st_dev);
332
+ stamp.inode = static_cast<uint64_t>(info.st_ino);
333
+ stamp.size = static_cast<int64_t>(info.st_size);
334
+ #if defined(__APPLE__)
335
+ stamp.modified_sec = info.st_mtimespec.tv_sec;
336
+ stamp.modified_nsec = info.st_mtimespec.tv_nsec;
337
+ stamp.changed_sec = info.st_ctimespec.tv_sec;
338
+ stamp.changed_nsec = info.st_ctimespec.tv_nsec;
339
+ #else
340
+ stamp.modified_sec = info.st_mtim.tv_sec;
341
+ stamp.modified_nsec = info.st_mtim.tv_nsec;
342
+ stamp.changed_sec = info.st_ctim.tv_sec;
343
+ stamp.changed_nsec = info.st_ctim.tv_nsec;
344
+ #endif
345
+ return stamp;
346
+ }
347
+
348
+ Status CheckActivation(const std::string& version_dir,
349
+ const std::string& version_hash,
350
+ const std::string& bundle_file_name,
351
+ bool grandfathered,
352
+ Activation* out) {
353
+ const std::string bundle = JoinPath(version_dir, bundle_file_name);
354
+ const FileStamp before = StampFile(bundle);
355
+ if (!before.regular) {
356
+ return Rejected(version_hash, "not found.");
357
+ }
358
+ std::string text;
359
+ const Record::State state = ReadRecordText(version_dir, &text);
360
+ // Versions activated before records existed are grandfathered through
361
+ // current/last state, but only while their directory has NO record file:
362
+ // a present record is verified like any other.
363
+ if (grandfathered && state == Record::State::kAbsent) {
364
+ out->basis = Activation::Basis::kGrandfathered;
365
+ out->record_text.clear();
366
+ out->bundle = before;
367
+ return Status::Ok();
368
+ }
369
+ // Any other directory without a record may be a crash-left partial
370
+ // install or something else put there.
371
+ if (state == Record::State::kAbsent) {
372
+ return Rejected(version_hash, "is incomplete.");
373
+ }
374
+ if (state == Record::State::kInvalid) {
375
+ return Rejected(version_hash, "has no valid completion record.");
376
+ }
377
+ if (state == Record::State::kLegacy) {
378
+ out->basis = Activation::Basis::kLegacyMarker;
379
+ out->record_text.clear();
380
+ out->bundle = before;
381
+ return Status::Ok();
382
+ }
383
+ const Record record = Parse(text);
384
+ if (!Names(record, version_hash)) {
385
+ return RecordMismatch(version_hash);
386
+ }
387
+ // Mandatory for schema 1: a record without it is never trusted.
388
+ if (record.bundle_sha256.empty()) {
389
+ return RecordMismatch(version_hash);
390
+ }
391
+ const std::string actual = digest::Sha256File(bundle);
392
+ if (actual.empty()) {
393
+ return Rejected(version_hash, "bundle could not be read.");
394
+ }
395
+ if (StampFile(bundle) != before) {
396
+ return Rejected(version_hash, "changed while being verified.");
397
+ }
398
+ if (!EqualsIgnoringAsciiCase(record.bundle_sha256, actual)) {
399
+ return Rejected(version_hash, "bundle digest mismatch.");
400
+ }
401
+ out->basis = Activation::Basis::kDigest;
402
+ out->record_text = text;
403
+ out->bundle = before;
404
+ return Status::Ok();
405
+ }
406
+
407
+ bool Unchanged(const std::string& version_dir,
408
+ const std::string& bundle_file_name,
409
+ bool grandfathered,
410
+ const Activation& passed) {
411
+ const FileStamp bundle = StampFile(JoinPath(version_dir, bundle_file_name));
412
+ if (!bundle.regular) {
413
+ return false;
414
+ }
415
+ std::string text;
416
+ const Record::State state = ReadRecordText(version_dir, &text);
417
+ switch (passed.basis) {
418
+ case Activation::Basis::kGrandfathered:
419
+ return state == Record::State::kAbsent && grandfathered;
420
+ case Activation::Basis::kLegacyMarker:
421
+ return state == Record::State::kLegacy && bundle == passed.bundle;
422
+ case Activation::Basis::kDigest:
423
+ return state == Record::State::kPresent && text == passed.record_text &&
424
+ bundle == passed.bundle;
425
+ }
426
+ return false;
427
+ }
428
+
429
+ } // namespace install_record
430
+ } // namespace pushy
@@ -1,6 +1,11 @@
1
1
  #ifndef PUSHY_PATCH_CORE_INSTALL_RECORD_H_
2
2
  #define PUSHY_PATCH_CORE_INSTALL_RECORD_H_
3
3
 
4
+ #include <cstdint>
5
+ #include <string>
6
+
7
+ #include "patch_core.h"
8
+
4
9
  // The completion record every platform writes into a version directory once
5
10
  // an install fully succeeded, and reads back before trusting that directory.
6
11
  //
@@ -9,19 +14,26 @@
9
14
  //
10
15
  // - Written by the SDK only (archives may not ship a `.pushy-*` entry), as
11
16
  // the LAST step of a two-phase install: unpack/patch into
12
- // <root>/<hash>.staging, write the record there, then atomically rename
17
+ // <root>/<hash>.staging, Seal() the record there, then Promote() renames
13
18
  // the staging directory to <root>/<hash>.
14
19
  // - versionHash must equal the directory name; bundleSha256 is the digest of
15
- // the final index.bundlejs (bundle.harmony.js on Harmony) and is
20
+ // the final bundle (index.bundlejs; bundle.harmony.js on Harmony) and is
16
21
  // re-verified at switchVersion time; artifactSha256 is the digest of the
17
- // downloaded archive (diagnostics / CDN corruption attribution).
18
- // - Legacy: an EMPTY file is a completed install written by SDK < 10.53
19
- // (no digests); it stays trusted for presence, nothing to verify.
22
+ // downloaded archive (diagnostics / CDN corruption attribution, optional).
23
+ // - bundleSha256 is MANDATORY for schema 1. A writer whose bundle digest is
24
+ // empty or fails must fail the install (staging dropped, no record); a
25
+ // verifier must reject a non-empty record without one (record mismatch),
26
+ // never activate the version unverified.
27
+ // - Legacy: an EMPTY (zero-byte) file is a completed install written by
28
+ // SDK < 10.53 (no digests); it stays trusted for presence, nothing to
29
+ // verify. Any other content must be a schema-1 record.
20
30
  //
21
- // Mirrors that cannot include this header MUST stay in sync by hand:
22
- // - android/.../InstallRecord.java
23
- // - harmony/pushy/src/main/ets/InstallRecord.ts
24
- // iOS (RCTPushy.mm) includes this header directly.
31
+ // This is the only implementation: Android (InstallRecord.java over JNI,
32
+ // cpp/android_jni/install_record_jni.cpp), iOS (RCTPushyInstallRecord.mm)
33
+ // and Harmony (InstallRecord.ts over NAPI, pushy.cpp) call it. The bundle
34
+ // file name is the caller's, since it differs per platform. Constants a
35
+ // host still mirrors are listed in scripts/check-constant-parity.js
36
+ // (part of `bun lint`), which asserts they match.
25
37
 
26
38
  // Keep the core internal to whichever binary links it (see podspec Core).
27
39
  #pragma GCC visibility push(hidden)
@@ -29,9 +41,151 @@
29
41
  namespace pushy {
30
42
  namespace install_record {
31
43
 
44
+ using delta::Status;
45
+
32
46
  constexpr int kSchema = 1;
33
47
  constexpr const char* kFileName = ".pushy-complete";
48
+ // Every name starting with this prefix, at any depth of a version directory,
49
+ // belongs to the SDK (the record, its temp file): no archive may ship one
50
+ // (archive_limits::IsReservedEntryName).
51
+ constexpr const char* kReservedPrefix = ".pushy-";
34
52
  constexpr const char* kStagingSuffix = ".staging";
53
+ // A record is a few hundred bytes; anything larger is not one.
54
+ constexpr long long kMaxRecordBytes = 64 * 1024;
55
+
56
+ // A record file's contents.
57
+ struct Record {
58
+ enum class State {
59
+ kAbsent, // no such entry
60
+ kInvalid, // not a readable regular file of at most kMaxRecordBytes, or
61
+ // not a JSON object
62
+ kLegacy, // the zero-byte marker of SDK < 10.53
63
+ kPresent, // a JSON object
64
+ };
65
+ State state = State::kAbsent;
66
+ // kPresent only. A member that is missing or of another JSON type reads
67
+ // as false / empty: records are written by this code alone, so nothing is
68
+ // coerced.
69
+ bool current_schema = false; // "schema" is the number kSchema
70
+ std::string version_hash;
71
+ std::string bundle_sha256;
72
+ std::string artifact_sha256;
73
+ };
74
+
75
+ // The text of a record file: "" is kLegacy, a JSON object kPresent,
76
+ // anything else (or more than kMaxRecordBytes) kInvalid.
77
+ Record Parse(const std::string& text);
78
+
79
+ // Whether a kPresent record is a schema-1 record naming this version.
80
+ bool Names(const Record& record, const std::string& version_hash);
81
+
82
+ // <version_dir>/.pushy-complete, parsed.
83
+ Record Read(const std::string& version_dir);
84
+
85
+ // Whether <version_dir>/.pushy-complete exists at all (of any type). Only a
86
+ // directory WITHOUT one may be grandfathered through current/last state.
87
+ bool HasRecordFile(const std::string& version_dir);
88
+
89
+ // The schema-1 record text. Fails when bundle_sha256 is empty: a version
90
+ // whose bundle could not be hashed must never be recorded as installed.
91
+ // artifact_sha256 is optional and omitted when empty.
92
+ Status Build(const std::string& version_hash,
93
+ const std::string& bundle_sha256,
94
+ const std::string& artifact_sha256,
95
+ std::string* out);
96
+
97
+ // Writes record_json as <dir>/.pushy-complete atomically and durably: a
98
+ // temporary sibling is written and fsynced, renamed over the record, and the
99
+ // directory is fsynced (best-effort: a file system that refuses a directory
100
+ // fsync does not fail the install).
101
+ Status Write(const std::string& dir, const std::string& record_json);
102
+
103
+ struct SealResult {
104
+ Status status = Status::Ok();
105
+ // The record could not be written (a file-system failure), as opposed to
106
+ // an install that produced no hashable bundle.
107
+ bool file_failure = false;
108
+ };
109
+
110
+ // The second phase's first half: hashes <staging_dir>/<bundle_file_name>
111
+ // (unless the caller already knows its digest, e.g. from extracting it),
112
+ // builds the record and writes it into the staging directory.
113
+ SealResult Seal(const std::string& staging_dir,
114
+ const std::string& version_hash,
115
+ const std::string& bundle_file_name,
116
+ const std::string& artifact_sha256,
117
+ const std::string& known_bundle_sha256);
118
+
119
+ // The second phase's second half: renames the sealed staging directory to
120
+ // version_dir (which the caller has removed if it existed) and fsyncs the
121
+ // parent directory (best-effort, as in Write).
122
+ Status Promote(const std::string& staging_dir, const std::string& version_dir);
123
+
124
+ // Launch / dedup presence check, no digest work: the bundle is a regular
125
+ // file and the record is the legacy marker or names this version (a
126
+ // digest-less schema-1 record is still "installed"; only activation refuses
127
+ // it).
128
+ bool IsComplete(const std::string& version_dir,
129
+ const std::string& version_hash,
130
+ const std::string& bundle_file_name);
131
+
132
+ // bundleSha256 of the version's record (crash attribution); "" when the
133
+ // record is absent, legacy, unreadable or has none.
134
+ std::string RecordedBundleSha256(const std::string& version_dir);
135
+
136
+ // Identity of a file's bytes on disk without reading them: a rewrite or an
137
+ // atomic replacement (new inode) changes it.
138
+ struct FileStamp {
139
+ bool regular = false;
140
+ uint64_t device = 0;
141
+ uint64_t inode = 0;
142
+ int64_t size = 0;
143
+ int64_t modified_sec = 0;
144
+ int64_t modified_nsec = 0;
145
+ int64_t changed_sec = 0;
146
+ int64_t changed_nsec = 0;
147
+
148
+ bool operator==(const FileStamp& other) const;
149
+ bool operator!=(const FileStamp& other) const { return !(*this == other); }
150
+ };
151
+
152
+ FileStamp StampFile(const std::string& path);
153
+
154
+ // What CheckActivation let through, for Unchanged.
155
+ struct Activation {
156
+ enum class Basis {
157
+ kGrandfathered, // no record file; the version is the current or last one
158
+ kLegacyMarker, // the zero-byte marker: nothing to verify
159
+ kDigest, // the record's bundleSha256 matched the bundle's bytes
160
+ };
161
+ Basis basis = Basis::kDigest;
162
+ std::string record_text; // the verified record's exact text
163
+ FileStamp bundle; // the bundle as it was hashed
164
+ };
165
+
166
+ // The switchVersion gate (slow: re-hashes the bundle; never call it on a UI
167
+ // thread or under a lock the UI thread takes). Passes when the bundle is a
168
+ // regular file and either
169
+ // - the directory has no record file and `grandfathered` (the version is
170
+ // the persisted current or last one: activated before records existed),
171
+ // - the record is the legacy marker, or
172
+ // - the record is a schema-1 record naming this version whose bundleSha256
173
+ // matches the bundle, which did not change while it was hashed.
174
+ // Otherwise fails with the reason ("Bundle version <hash> ..."), every
175
+ // platform's SWITCH_VERSION_FAILED message. *out is set only on success.
176
+ Status CheckActivation(const std::string& version_dir,
177
+ const std::string& version_hash,
178
+ const std::string& bundle_file_name,
179
+ bool grandfathered,
180
+ Activation* out);
181
+
182
+ // Cheap re-check (no hashing; e.g. under the state lock right before the
183
+ // switch is committed) that the directory still passes on the same basis:
184
+ // same grandfathering, same record text, same bundle file.
185
+ bool Unchanged(const std::string& version_dir,
186
+ const std::string& bundle_file_name,
187
+ bool grandfathered,
188
+ const Activation& passed);
35
189
 
36
190
  } // namespace install_record
37
191
  } // namespace pushy