react-native-update 10.53.1 → 10.54.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 (105) hide show
  1. package/README-CN.md +31 -31
  2. package/README.md +31 -31
  3. package/android/jni/HDiffPatch/.travis.yml +33 -0
  4. package/android/jni/HDiffPatch/builds/xcode/bestParams.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  5. package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  6. package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  7. package/android/jni/HDiffPatch/builds/xcode/hpatchz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  8. package/android/jni/HDiffPatch/builds/xcode/lz4.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  9. package/android/jni/HDiffPatch/builds/xcode/lzma.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  10. package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  11. package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  12. package/android/jni/HDiffPatch/builds/xcode/unitTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  13. package/android/jni/HDiffPatch/builds/xcode/zstd.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  14. package/android/jni/lzma/DOC/7zC.txt +187 -0
  15. package/android/jni/lzma/DOC/7zFormat.txt +469 -0
  16. package/android/jni/lzma/DOC/Methods.txt +173 -0
  17. package/android/jni/lzma/DOC/installer.txt +166 -0
  18. package/android/jni/lzma/DOC/lzma-history.txt +446 -0
  19. package/android/jni/lzma/DOC/lzma-sdk.txt +357 -0
  20. package/android/jni/lzma/DOC/lzma-specification.txt +1176 -0
  21. package/android/jni/lzma/DOC/lzma.txt +328 -0
  22. package/android/jni/lzma/bin/7zS2.sfx +0 -0
  23. package/android/jni/lzma/bin/7zS2con.sfx +0 -0
  24. package/android/jni/lzma/bin/7zSD.sfx +0 -0
  25. package/android/jni/lzma/bin/7zdec.exe +0 -0
  26. package/android/jni/lzma/bin/7zr.exe +0 -0
  27. package/android/jni/lzma/bin/installer/config.txt +5 -0
  28. package/android/jni/lzma/bin/installer/cr.bat +5 -0
  29. package/android/jni/lzma/bin/lzma.exe +0 -0
  30. package/android/jni/lzma/bin/x64/7zr.exe +0 -0
  31. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  32. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  33. package/android/lib/x86/librnupdate.so +0 -0
  34. package/android/lib/x86_64/librnupdate.so +0 -0
  35. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +44 -0
  36. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +124 -40
  37. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +125 -0
  38. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +43 -11
  39. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +2 -0
  40. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +90 -0
  41. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +79 -19
  42. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +21 -0
  43. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +33 -2
  44. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +1 -0
  45. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  46. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  47. package/cpp/patch_core/archive_limits.h +38 -0
  48. package/cpp/patch_core/install_record.h +36 -0
  49. package/cpp/update_flow_core/flow_json.h +1 -0
  50. package/cpp/update_flow_core/update_flow_core.cpp +41 -7
  51. package/cpp/update_flow_core/update_flow_core.h +9 -0
  52. package/cpp/update_flow_core/update_flow_jni.cpp +12 -0
  53. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +2 -0
  54. package/harmony/pushy/src/main/cpp/pushy.cpp +37 -0
  55. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +83 -0
  56. package/harmony/pushy/src/main/ets/DownloadTask.ts +126 -30
  57. package/harmony/pushy/src/main/ets/InstallRecord.ts +129 -0
  58. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +24 -2
  59. package/harmony/pushy/src/main/ets/NativePatchCore.ts +5 -0
  60. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
  61. package/harmony/pushy/src/main/ets/UpdateContext.ts +78 -15
  62. package/harmony/pushy.har +0 -0
  63. package/ios/RCTPushy/RCTPushy.mm +384 -25
  64. package/ios/RCTPushy/RCTPushyDownloader.mm +32 -0
  65. package/package.json +25 -2
  66. package/src/NativePushy.ts +13 -0
  67. package/src/client.ts +160 -43
  68. package/src/core.ts +17 -3
  69. package/src/error.ts +3 -0
  70. package/src/index.ts +8 -0
  71. package/src/locales/en.ts +3 -0
  72. package/src/locales/zh.ts +2 -0
  73. package/src/metadata.ts +166 -0
  74. package/src/provider.tsx +52 -17
  75. package/src/type.ts +22 -0
  76. package/src/updateFlowCore.ts +95 -4
  77. package/src/utils.ts +31 -19
  78. package/.gitmodules +0 -6
  79. package/BUNDLEHASH_MIGRATION.md +0 -355
  80. package/NATIVE_CHECKUPDATE_DESIGN.md +0 -539
  81. package/NATIVE_CHECK_FOLLOWUPS.md +0 -514
  82. package/android/jni/HDiffPatch/test/_private_searchBestParams.cpp +0 -301
  83. package/android/jni/HDiffPatch/test/hpatchz_test.cpp +0 -292
  84. package/android/jni/HDiffPatch/test/testHashClash.cpp +0 -436
  85. package/android/jni/HDiffPatch/test/unit_test.cpp +0 -417
  86. package/biome.json +0 -59
  87. package/cpp/patch_core/tests/fixtures/rawstored.lzma2label.patch.bin +0 -0
  88. package/cpp/patch_core/tests/fixtures/rawstored.new.bin +0 -0
  89. package/cpp/patch_core/tests/fixtures/rawstored.old.bin +0 -0
  90. package/cpp/patch_core/tests/fixtures/v96.hbc +0 -0
  91. package/cpp/patch_core/tests/fixtures/v96.meta.json +0 -1
  92. package/cpp/patch_core/tests/fixtures/v96.streampatch.bin +0 -0
  93. package/cpp/patch_core/tests/fixtures/v96.t.hbc +0 -0
  94. package/cpp/patch_core/tests/fixtures/v96.tpatch.bin +0 -0
  95. package/cpp/patch_core/tests/fixtures/v96.tstreampatch.bin +0 -0
  96. package/cpp/patch_core/tests/fixtures/v96b.hbc +0 -0
  97. package/cpp/patch_core/tests/fixtures/v98.hbc +0 -0
  98. package/cpp/patch_core/tests/fixtures/v98.t.hbc +0 -0
  99. package/cpp/patch_core/tests/fixtures/v98b.hbc +0 -0
  100. package/cpp/patch_core/tests/fixtures/v98b.t.hbc +0 -0
  101. package/cpp/patch_core/tests/hbc_transform_test.cpp +0 -384
  102. package/cpp/patch_core/tests/patch_core_test.cpp +0 -970
  103. package/cpp/update_flow_core/tests/flow_vectors.json +0 -1401
  104. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +0 -317
  105. package/scripts/check-expo-version.js +0 -92
@@ -1,970 +0,0 @@
1
- #include "../archive_patch_core.h"
2
- #include "../digest.h"
3
- #include "../patch_core.h"
4
- #include "../state_core.h"
5
-
6
- #include <cstdio>
7
- #include <cstdlib>
8
- #include <cstring>
9
- #include <fstream>
10
- #include <sstream>
11
- #include <stdexcept>
12
- #include <string>
13
- #include <sys/stat.h>
14
- #include <sys/time.h>
15
- #include <sys/types.h>
16
- #include <unistd.h>
17
- #include <vector>
18
-
19
- namespace {
20
-
21
- using pushy::patch::ApplyPatchFromFileSource;
22
- using pushy::patch::BundlePatcher;
23
- using pushy::patch::CleanupOldEntries;
24
- using pushy::patch::CopyOperation;
25
- using pushy::patch::FileSourcePatchOptions;
26
- using pushy::patch::PatchManifest;
27
- using pushy::patch::Status;
28
- using pushy::state::BinaryVersionSyncResult;
29
- using pushy::state::LaunchDecision;
30
- using pushy::state::MarkSuccessResult;
31
- using pushy::state::State;
32
-
33
- void EnsureDirectory(const std::string& path);
34
-
35
- class FakeBundlePatcher final : public BundlePatcher {
36
- public:
37
- mutable int calls = 0;
38
- std::string output;
39
-
40
- explicit FakeBundlePatcher(std::string output_value)
41
- : output(std::move(output_value)) {}
42
-
43
- Status Apply(
44
- const std::string&,
45
- const std::string&,
46
- const std::string& destination_bundle_path) const override {
47
- ++calls;
48
- size_t slash = destination_bundle_path.find_last_of('/');
49
- if (slash != std::string::npos) {
50
- EnsureDirectory(destination_bundle_path.substr(0, slash));
51
- }
52
- std::ofstream out(destination_bundle_path, std::ios::binary);
53
- out << output;
54
- return out.good() ? Status::Ok() : Status::Error("Failed to write fake bundle");
55
- }
56
- };
57
-
58
- struct TempDir {
59
- std::string path;
60
-
61
- TempDir() {
62
- char templ[] = "/tmp/pushy-patch-core-XXXXXX";
63
- char* created = mkdtemp(templ);
64
- if (!created) {
65
- throw std::runtime_error("Failed to create temp dir");
66
- }
67
- path = created;
68
- }
69
-
70
- ~TempDir() {
71
- if (!path.empty()) {
72
- std::string command = "rm -rf \"" + path + "\"";
73
- std::system(command.c_str());
74
- }
75
- }
76
- };
77
-
78
- std::string JoinPath(const std::string& base, const std::string& relative) {
79
- if (base.empty()) {
80
- return relative;
81
- }
82
- if (relative.empty()) {
83
- return base;
84
- }
85
- return base + "/" + relative;
86
- }
87
-
88
- void EnsureDirectory(const std::string& path) {
89
- if (path.empty()) {
90
- return;
91
- }
92
-
93
- size_t slash = path.find_last_of('/');
94
- if (slash != std::string::npos) {
95
- EnsureDirectory(path.substr(0, slash));
96
- }
97
- mkdir(path.c_str(), 0755);
98
- }
99
-
100
- void WriteFile(const std::string& path, const std::string& content) {
101
- EnsureDirectory(path.substr(0, path.find_last_of('/')));
102
- std::ofstream out(path, std::ios::binary);
103
- out << content;
104
- }
105
-
106
- std::string ReadFile(const std::string& path) {
107
- std::ifstream in(path, std::ios::binary);
108
- std::ostringstream stream;
109
- stream << in.rdbuf();
110
- return stream.str();
111
- }
112
-
113
- bool Exists(const std::string& path) {
114
- struct stat st;
115
- return stat(path.c_str(), &st) == 0;
116
- }
117
-
118
- void SetMtime(const std::string& path, std::time_t value) {
119
- struct timeval times[2];
120
- times[0].tv_sec = value;
121
- times[0].tv_usec = 0;
122
- times[1].tv_sec = value;
123
- times[1].tv_usec = 0;
124
- if (utimes(path.c_str(), times) != 0) {
125
- throw std::runtime_error("Failed to set mtime");
126
- }
127
- }
128
-
129
- void Expect(bool condition, const std::string& message) {
130
- if (!condition) {
131
- throw std::runtime_error(message);
132
- }
133
- }
134
-
135
- void ExpectEq(const std::string& left, const std::string& right, const std::string& message) {
136
- if (left != right) {
137
- throw std::runtime_error(message + ": expected [" + right + "] got [" + left + "]");
138
- }
139
- }
140
-
141
- std::string g_fixtures_dir = "cpp/patch_core/tests/fixtures";
142
-
143
- // 变换域 bundle patch 端到端:真实 hpatch + wire 元数据 → T(origin) →
144
- // hpatch → T⁻¹,结果必须与新 bundle 逐字节一致(fixtures 由 CLI 的 JS
145
- // 实现生成,同时充当跨实现 golden)。
146
- void TestApplyPatchWithHbcTransform() {
147
- TempDir temp;
148
- const std::string origin = JoinPath(g_fixtures_dir, "v96.hbc");
149
- const std::string patch = JoinPath(g_fixtures_dir, "v96.tpatch.bin");
150
- const std::string expected = ReadFile(JoinPath(g_fixtures_dir, "v96b.hbc"));
151
- const std::string meta = ReadFile(JoinPath(g_fixtures_dir, "v96.meta.json"));
152
- Expect(!expected.empty() && !meta.empty(), "hbc fixtures must exist");
153
-
154
- FileSourcePatchOptions options;
155
- options.source_root = JoinPath(temp.path, "src");
156
- options.target_root = JoinPath(temp.path, "dst");
157
- options.origin_bundle_path = origin;
158
- options.bundle_patch_path = patch;
159
- options.bundle_output_path = JoinPath(temp.path, "out/index.bundlejs");
160
- options.enable_merge = false;
161
- options.bundle_hbc_transform_meta = meta;
162
-
163
- Status status = ApplyPatchFromFileSource(options);
164
- Expect(status.ok, "hbc transform patch should succeed: " + status.message);
165
- ExpectEq(
166
- ReadFile(options.bundle_output_path).size() == expected.size()
167
- ? std::string("same-size")
168
- : std::string("size-mismatch"),
169
- "same-size",
170
- "restored bundle size");
171
- Expect(
172
- ReadFile(options.bundle_output_path) == expected,
173
- "restored bundle must equal new bundle byte-for-byte");
174
-
175
- // 临时文件必须被清理
176
- Expect(
177
- !Exists(options.bundle_output_path + ".hbct-origin") &&
178
- !Exists(options.bundle_output_path + ".hbct-patched"),
179
- "hbc transform temp files must be removed");
180
- }
181
-
182
- void TestApplyPatchWithHbcTransformRejectsBadMeta() {
183
- TempDir temp;
184
- FileSourcePatchOptions options;
185
- options.source_root = JoinPath(temp.path, "src");
186
- options.target_root = JoinPath(temp.path, "dst");
187
- options.origin_bundle_path = JoinPath(g_fixtures_dir, "v96.hbc");
188
- options.bundle_patch_path = JoinPath(g_fixtures_dir, "v96.tpatch.bin");
189
- options.bundle_output_path = JoinPath(temp.path, "out/index.bundlejs");
190
- options.enable_merge = false;
191
-
192
- // 不可解析的元数据 → 失败(绝不能忽略元数据直接 hpatch)
193
- options.bundle_hbc_transform_meta = "not json";
194
- Expect(
195
- !ApplyPatchFromFileSource(options).ok,
196
- "malformed hbcTransform metadata must fail");
197
- Expect(!Exists(options.bundle_output_path), "no output on failure");
198
-
199
- // 不支持的变换规范版本 → 失败(调用方回退整包)
200
- std::string meta = ReadFile(JoinPath(g_fixtures_dir, "v96.meta.json"));
201
- const std::string needle = "\"v\":1";
202
- const size_t pos = meta.find(needle);
203
- Expect(pos != std::string::npos, "meta fixture must contain v:1");
204
- meta.replace(pos, needle.size(), "\"v\":9");
205
- options.bundle_hbc_transform_meta = meta;
206
- Status status = ApplyPatchFromFileSource(options);
207
- Expect(!status.ok, "unsupported hbcTransform version must fail");
208
- Expect(
209
- status.message.find("Unsupported hbcTransform version") !=
210
- std::string::npos,
211
- "error should identify unsupported version: " + status.message);
212
- }
213
-
214
- // HDIFF13(diffStream)格式:hpatch_by_file 按 magic 自动分派,
215
- // v2 轨道的大 bundle patch 走此路径。fixtures 由 node-hdiffpatch 生成并
216
- // 已在 JS 侧验证过 round-trip。
217
- void TestApplyStreamFormatBundlePatch() {
218
- TempDir temp;
219
- FileSourcePatchOptions options;
220
- options.source_root = JoinPath(temp.path, "src");
221
- options.target_root = JoinPath(temp.path, "dst");
222
- options.origin_bundle_path = JoinPath(g_fixtures_dir, "v96.hbc");
223
- options.bundle_patch_path = JoinPath(g_fixtures_dir, "v96.streampatch.bin");
224
- options.bundle_output_path = JoinPath(temp.path, "out/index.bundlejs");
225
- options.enable_merge = false;
226
-
227
- Status status = ApplyPatchFromFileSource(options);
228
- Expect(status.ok, "stream-format patch should apply: " + status.message);
229
- Expect(
230
- ReadFile(options.bundle_output_path) ==
231
- ReadFile(JoinPath(g_fixtures_dir, "v96b.hbc")),
232
- "stream-format restored bundle must equal new bundle");
233
- }
234
-
235
- void TestApplyStreamFormatWithHbcTransform() {
236
- TempDir temp;
237
- FileSourcePatchOptions options;
238
- options.source_root = JoinPath(temp.path, "src");
239
- options.target_root = JoinPath(temp.path, "dst");
240
- options.origin_bundle_path = JoinPath(g_fixtures_dir, "v96.hbc");
241
- options.bundle_patch_path = JoinPath(g_fixtures_dir, "v96.tstreampatch.bin");
242
- options.bundle_output_path = JoinPath(temp.path, "out/index.bundlejs");
243
- options.enable_merge = false;
244
- options.bundle_hbc_transform_meta =
245
- ReadFile(JoinPath(g_fixtures_dir, "v96.meta.json"));
246
-
247
- Status status = ApplyPatchFromFileSource(options);
248
- Expect(
249
- status.ok,
250
- "transform + stream-format patch should apply: " + status.message);
251
- Expect(
252
- ReadFile(options.bundle_output_path) ==
253
- ReadFile(JoinPath(g_fixtures_dir, "v96b.hbc")),
254
- "transform+stream restored bundle must equal new bundle");
255
- }
256
-
257
- // HDIFFSF20 允许 compressedSize==0 表示 diff payload 为 RAW。node-hdiffpatch
258
- // 2.x 的 v5 writer 在这种小 diff 上仍会保留 lzma2 标签,客户端必须按
259
- // compressedSize 判定是否解压,不能只看 compressType。
260
- void TestApplySingleFormatRawStoredWithLzma2Label() {
261
- TempDir temp;
262
- FileSourcePatchOptions options;
263
- options.source_root = JoinPath(temp.path, "src");
264
- options.target_root = JoinPath(temp.path, "dst");
265
- options.origin_bundle_path = JoinPath(g_fixtures_dir, "rawstored.old.bin");
266
- options.bundle_patch_path =
267
- JoinPath(g_fixtures_dir, "rawstored.lzma2label.patch.bin");
268
- options.bundle_output_path = JoinPath(temp.path, "out/index.bundlejs");
269
- options.enable_merge = false;
270
-
271
- Status status = ApplyPatchFromFileSource(options);
272
- Expect(
273
- status.ok,
274
- "raw-stored single-format patch should apply: " + status.message);
275
- Expect(
276
- ReadFile(options.bundle_output_path) ==
277
- ReadFile(JoinPath(g_fixtures_dir, "rawstored.new.bin")),
278
- "raw-stored single-format restored bundle must equal new bundle");
279
- }
280
-
281
- void TestApplyPatchFromFileSourceMergesAndCopies() {
282
- TempDir temp;
283
- const std::string source = JoinPath(temp.path, "origin");
284
- const std::string target = JoinPath(temp.path, "target");
285
- const std::string patch = JoinPath(temp.path, "bundle.patch");
286
-
287
- WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
288
- WriteFile(JoinPath(source, "assets/keep.txt"), "keep");
289
- WriteFile(JoinPath(source, "assets/delete.txt"), "delete");
290
- WriteFile(JoinPath(source, "assets/from.txt"), "rename");
291
- WriteFile(JoinPath(source, "config.json"), "config");
292
-
293
- WriteFile(JoinPath(target, "assets/new.txt"), "new");
294
- WriteFile(patch, "unused patch");
295
-
296
- FakeBundlePatcher patcher("patched bundle");
297
- FileSourcePatchOptions options;
298
- options.source_root = source;
299
- options.target_root = target;
300
- options.origin_bundle_path = JoinPath(source, "index.bundlejs");
301
- options.bundle_patch_path = patch;
302
- options.bundle_output_path = JoinPath(target, "index.bundlejs");
303
- options.merge_source_subdir = "";
304
- options.manifest.copies.push_back(CopyOperation{"assets/from.txt", "assets/renamed.txt"});
305
- options.manifest.deletes.push_back("assets/delete.txt");
306
-
307
- Status status = ApplyPatchFromFileSource(options, patcher);
308
- Expect(status.ok, status.message);
309
- Expect(patcher.calls == 1, "bundle patcher should run exactly once");
310
-
311
- ExpectEq(ReadFile(JoinPath(target, "index.bundlejs")), "patched bundle", "bundle output mismatch");
312
- ExpectEq(ReadFile(JoinPath(target, "assets/keep.txt")), "keep", "merged asset mismatch");
313
- ExpectEq(ReadFile(JoinPath(target, "assets/renamed.txt")), "rename", "copied asset mismatch");
314
- ExpectEq(ReadFile(JoinPath(target, "assets/new.txt")), "new", "existing unzip file should be preserved");
315
- ExpectEq(ReadFile(JoinPath(target, "config.json")), "config", "root file should be merged");
316
- Expect(!Exists(JoinPath(target, "assets/delete.txt")), "deleted asset should not be copied");
317
- }
318
-
319
- void TestApplyPatchMergeHardLinksUnchangedFiles() {
320
- TempDir temp;
321
- const std::string source = JoinPath(temp.path, "origin");
322
- const std::string target = JoinPath(temp.path, "target");
323
- const std::string patch = JoinPath(temp.path, "bundle.patch");
324
-
325
- WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
326
- WriteFile(JoinPath(source, "assets/keep.txt"), "keep");
327
- WriteFile(patch, "unused patch");
328
-
329
- FakeBundlePatcher patcher("patched bundle");
330
- FileSourcePatchOptions options;
331
- options.source_root = source;
332
- options.target_root = target;
333
- options.origin_bundle_path = JoinPath(source, "index.bundlejs");
334
- options.bundle_patch_path = patch;
335
- options.bundle_output_path = JoinPath(target, "index.bundlejs");
336
- options.manifest.copies.push_back(CopyOperation{"assets/keep.txt", "assets/copied.txt"});
337
-
338
- Status status = ApplyPatchFromFileSource(options, patcher);
339
- Expect(status.ok, status.message);
340
-
341
- // Unchanged files must share the source inode (hard link) instead of being
342
- // byte-copied: same filesystem, so the fast path has to kick in.
343
- struct stat source_stat;
344
- struct stat merged_stat;
345
- struct stat copied_stat;
346
- Expect(stat(JoinPath(source, "assets/keep.txt").c_str(), &source_stat) == 0, "stat source");
347
- Expect(stat(JoinPath(target, "assets/keep.txt").c_str(), &merged_stat) == 0, "stat merged");
348
- Expect(stat(JoinPath(target, "assets/copied.txt").c_str(), &copied_stat) == 0, "stat copied");
349
- Expect(merged_stat.st_ino == source_stat.st_ino, "merged file should hard-link the source");
350
- Expect(copied_stat.st_ino == source_stat.st_ino, "manifest copy should hard-link the source");
351
- Expect(source_stat.st_nlink >= 3, "source should have three names");
352
- ExpectEq(ReadFile(JoinPath(target, "assets/keep.txt")), "keep", "merged content mismatch");
353
-
354
- // The patched bundle must be a fresh file, never a link into the source dir.
355
- struct stat bundle_stat;
356
- struct stat origin_stat;
357
- Expect(stat(JoinPath(target, "index.bundlejs").c_str(), &bundle_stat) == 0, "stat bundle");
358
- Expect(stat(JoinPath(source, "index.bundlejs").c_str(), &origin_stat) == 0, "stat origin bundle");
359
- Expect(bundle_stat.st_ino != origin_stat.st_ino, "patched bundle must not link the origin");
360
- }
361
-
362
- void TestApplyPatchMergeFallsBackToByteCopy() {
363
- TempDir temp;
364
- const std::string source = JoinPath(temp.path, "origin");
365
- const std::string target = JoinPath(temp.path, "target");
366
- const std::string patch = JoinPath(temp.path, "bundle.patch");
367
-
368
- WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
369
- WriteFile(JoinPath(source, "assets/keep.txt"), "keep");
370
- WriteFile(patch, "unused patch");
371
-
372
- FakeBundlePatcher patcher("patched bundle");
373
- FileSourcePatchOptions options;
374
- options.source_root = source;
375
- options.target_root = target;
376
- options.origin_bundle_path = JoinPath(source, "index.bundlejs");
377
- options.bundle_patch_path = patch;
378
- options.bundle_output_path = JoinPath(target, "index.bundlejs");
379
-
380
- pushy::patch::internal::g_disable_hard_links = true;
381
- Status status = ApplyPatchFromFileSource(options, patcher);
382
- pushy::patch::internal::g_disable_hard_links = false;
383
- Expect(status.ok, status.message);
384
-
385
- struct stat source_stat;
386
- struct stat merged_stat;
387
- Expect(stat(JoinPath(source, "assets/keep.txt").c_str(), &source_stat) == 0, "stat source");
388
- Expect(stat(JoinPath(target, "assets/keep.txt").c_str(), &merged_stat) == 0, "stat merged");
389
- Expect(merged_stat.st_ino != source_stat.st_ino, "fallback should produce an independent copy");
390
- Expect(merged_stat.st_nlink == 1, "fallback copy should have a single name");
391
- ExpectEq(ReadFile(JoinPath(target, "assets/keep.txt")), "keep", "fallback content mismatch");
392
- }
393
-
394
- void TestApplyPatchFromFileSourceCanLimitMergeSubdir() {
395
- TempDir temp;
396
- const std::string source = JoinPath(temp.path, "origin");
397
- const std::string target = JoinPath(temp.path, "target");
398
- const std::string patch = JoinPath(temp.path, "bundle.patch");
399
-
400
- WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
401
- WriteFile(JoinPath(source, "assets/keep.txt"), "keep");
402
- WriteFile(JoinPath(source, "config.json"), "config");
403
- WriteFile(patch, "unused patch");
404
-
405
- FakeBundlePatcher patcher("patched bundle");
406
- FileSourcePatchOptions options;
407
- options.source_root = source;
408
- options.target_root = target;
409
- options.origin_bundle_path = JoinPath(source, "index.bundlejs");
410
- options.bundle_patch_path = patch;
411
- options.bundle_output_path = JoinPath(target, "index.bundlejs");
412
- options.merge_source_subdir = "assets";
413
-
414
- Status status = ApplyPatchFromFileSource(options, patcher);
415
- Expect(status.ok, status.message);
416
-
417
- ExpectEq(ReadFile(JoinPath(target, "assets/keep.txt")), "keep", "assets merge mismatch");
418
- Expect(!Exists(JoinPath(target, "config.json")), "non-assets root file should not be merged");
419
- }
420
-
421
- void TestApplyPatchFromFileSourceRejectsUnsafePaths() {
422
- TempDir temp;
423
- const std::string source = JoinPath(temp.path, "origin");
424
- const std::string target = JoinPath(temp.path, "target");
425
- const std::string patch = JoinPath(temp.path, "bundle.patch");
426
-
427
- WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
428
- WriteFile(JoinPath(source, "assets/file.txt"), "content");
429
- WriteFile(patch, "unused patch");
430
-
431
- FakeBundlePatcher patcher("patched bundle");
432
- FileSourcePatchOptions options;
433
- options.source_root = source;
434
- options.target_root = target;
435
- options.origin_bundle_path = JoinPath(source, "index.bundlejs");
436
- options.bundle_patch_path = patch;
437
- options.bundle_output_path = JoinPath(target, "index.bundlejs");
438
- options.merge_source_subdir = "";
439
- options.manifest.copies.push_back(CopyOperation{"assets/file.txt", "../escape.txt"});
440
-
441
- Status status = ApplyPatchFromFileSource(options, patcher);
442
- Expect(!status.ok, "unsafe path should fail");
443
- Expect(patcher.calls == 0, "bundle patcher should not run when validation fails");
444
- }
445
-
446
- void TestCleanupOldEntriesRemovesOnlyExpiredPaths() {
447
- TempDir temp;
448
- const std::string root = JoinPath(temp.path, "cleanup");
449
- EnsureDirectory(root);
450
-
451
- WriteFile(JoinPath(root, "current/index.bundlejs"), "current");
452
- WriteFile(JoinPath(root, "previous/index.bundlejs"), "previous");
453
- WriteFile(JoinPath(root, "stale/index.bundlejs"), "stale");
454
- WriteFile(JoinPath(root, "recent/index.bundlejs"), "recent");
455
- WriteFile(JoinPath(root, "old.tmp"), "old");
456
- WriteFile(JoinPath(root, ".hidden"), "hidden");
457
-
458
- const std::time_t now = 1'700'000'000;
459
- const std::time_t old_time = now - (9 * 24 * 60 * 60);
460
- const std::time_t recent_time = now - (2 * 24 * 60 * 60);
461
-
462
- SetMtime(JoinPath(root, "current"), old_time);
463
- SetMtime(JoinPath(root, "previous"), old_time);
464
- SetMtime(JoinPath(root, "stale"), old_time);
465
- SetMtime(JoinPath(root, "recent"), recent_time);
466
- SetMtime(JoinPath(root, "old.tmp"), old_time);
467
- SetMtime(JoinPath(root, ".hidden"), old_time);
468
-
469
- Status status = CleanupOldEntries(root, "current", "previous", 7, now);
470
- Expect(status.ok, status.message);
471
-
472
- Expect(Exists(JoinPath(root, "current")), "current entry should be kept");
473
- Expect(Exists(JoinPath(root, "previous")), "previous entry should be kept");
474
- Expect(!Exists(JoinPath(root, "stale")), "stale directory should be removed");
475
- Expect(!Exists(JoinPath(root, "old.tmp")), "stale file should be removed");
476
- Expect(Exists(JoinPath(root, "recent")), "recent entry should be kept");
477
- Expect(Exists(JoinPath(root, ".hidden")), "hidden entry should be kept");
478
- }
479
-
480
- void TestStateCoreSyncBinaryVersionResetsUpdates() {
481
- State state;
482
- state.package_version = "1.0.0";
483
- state.build_time = "100";
484
- state.current_version = "current";
485
- state.last_version = "last";
486
- state.first_time = true;
487
- state.first_time_ok = false;
488
- state.rolled_back_version = "rolled";
489
-
490
- BinaryVersionSyncResult result =
491
- pushy::state::SyncBinaryVersion(state, "1.1.0", "200");
492
- Expect(result.changed, "binary version sync should detect changes");
493
- ExpectEq(result.state.package_version, "1.1.0", "package version mismatch");
494
- ExpectEq(result.state.build_time, "200", "build time mismatch");
495
- Expect(result.state.current_version.empty(), "current version should reset");
496
- Expect(result.state.last_version.empty(), "last version should reset");
497
- Expect(!result.state.first_time, "first_time should reset");
498
- Expect(result.state.first_time_ok, "first_time_ok should reset");
499
- Expect(result.state.rolled_back_version.empty(), "rolled_back_version should reset");
500
- }
501
-
502
- void TestStateCoreSwitchVersionAndMarkSuccess() {
503
- State state;
504
- state.package_version = "1.0.0";
505
- state.build_time = "100";
506
- state.current_version = "old";
507
- state.last_version = "older";
508
- state.first_time_ok = true;
509
-
510
- State switched = pushy::state::SwitchVersion(state, "new");
511
- ExpectEq(switched.current_version, "new", "current version mismatch");
512
- ExpectEq(switched.last_version, "old", "last version mismatch");
513
- Expect(switched.first_time, "first_time should be set");
514
- Expect(!switched.first_time_ok, "first_time_ok should be false");
515
-
516
- MarkSuccessResult success = pushy::state::MarkSuccess(switched);
517
- ExpectEq(success.state.current_version, "new", "markSuccess current version mismatch");
518
- Expect(success.state.last_version.empty(), "last version should be cleared");
519
- ExpectEq(success.stale_version_to_delete, "old", "stale version mismatch");
520
- Expect(!success.state.first_time, "first_time should clear after success");
521
- Expect(success.state.first_time_ok, "first_time_ok should be true after success");
522
- }
523
-
524
- void TestStateCoreResolveLaunchStateAndRollback() {
525
- State state;
526
- state.current_version = "current";
527
- state.last_version = "previous";
528
- state.first_time = false;
529
- state.first_time_ok = false;
530
-
531
- LaunchDecision rollback =
532
- pushy::state::ResolveLaunchState(state, false, true);
533
- Expect(rollback.did_rollback, "launch decision should roll back");
534
- ExpectEq(rollback.load_version, "previous", "rollback load version mismatch");
535
- ExpectEq(rollback.state.current_version, "previous", "rollback current version mismatch");
536
- ExpectEq(rollback.state.rolled_back_version, "current", "rolled back version mismatch");
537
-
538
- State first_load;
539
- first_load.current_version = "fresh";
540
- first_load.first_time = true;
541
- first_load.first_time_ok = false;
542
- LaunchDecision consume =
543
- pushy::state::ResolveLaunchState(first_load, false, true);
544
- Expect(!consume.did_rollback, "first load should not roll back");
545
- Expect(consume.consumed_first_time, "first load should be consumed");
546
- ExpectEq(consume.load_version, "fresh", "first load version mismatch");
547
- Expect(!consume.state.first_time, "first_time should clear when consumed");
548
-
549
- LaunchDecision preserve =
550
- pushy::state::ResolveLaunchState(first_load, false, false);
551
- Expect(!preserve.consumed_first_time, "first load should not be consumed when disabled");
552
- Expect(preserve.state.first_time, "first_time should be preserved when not consumed");
553
- }
554
-
555
- void TestStateCoreCanClearMarkers() {
556
- State state;
557
- state.current_version = "current";
558
- state.first_time = true;
559
- state.rolled_back_version = "rolled";
560
-
561
- State clear_first_time = pushy::state::ClearFirstTime(state);
562
- Expect(!clear_first_time.first_time, "clearFirstTime should clear first_time");
563
- ExpectEq(
564
- clear_first_time.rolled_back_version,
565
- "rolled",
566
- "clearFirstTime should preserve rollback marker");
567
-
568
- State clear_rollback = pushy::state::ClearRollbackMark(state);
569
- Expect(
570
- clear_rollback.rolled_back_version.empty(),
571
- "clearRollbackMark should clear rollback marker");
572
- Expect(clear_rollback.first_time, "clearRollbackMark should preserve first_time");
573
- }
574
-
575
- void TestArchivePatchCoreBuildPlanAndCopyGroups() {
576
- PatchManifest manifest;
577
- manifest.copies.push_back(CopyOperation{"assets/a.png", "assets/x.png"});
578
- manifest.copies.push_back(CopyOperation{"assets/a.png", "assets/y.png"});
579
- manifest.deletes.push_back("assets/old.png");
580
-
581
- pushy::archive_patch::ArchivePatchPlan plan;
582
- Status status = pushy::archive_patch::BuildArchivePatchPlan(
583
- pushy::archive_patch::ArchivePatchType::kPatchFromPpk,
584
- manifest,
585
- {"__diff.json", "index.bundlejs.patch", "assets/new.png"},
586
- &plan);
587
- Expect(status.ok, status.message);
588
- Expect(plan.enable_merge, "ppk plan should enable merge");
589
- ExpectEq(plan.merge_source_subdir, "", "ppk merge subdir mismatch");
590
-
591
- std::vector<pushy::archive_patch::CopyGroup> groups;
592
- status = pushy::archive_patch::BuildCopyGroups(manifest, &groups);
593
- Expect(status.ok, status.message);
594
- Expect(groups.size() == 1, "copy groups should merge identical sources");
595
- ExpectEq(groups[0].from, "assets/a.png", "copy group source mismatch");
596
- Expect(groups[0].to_paths.size() == 2, "copy group target count mismatch");
597
-
598
- FileSourcePatchOptions options;
599
- status = pushy::archive_patch::BuildFileSourcePatchOptions(
600
- plan,
601
- "/tmp/source",
602
- "/tmp/target",
603
- "/tmp/source/index.bundlejs",
604
- "/tmp/target/index.bundlejs.patch",
605
- "/tmp/target/index.bundlejs",
606
- &options);
607
- Expect(status.ok, status.message);
608
- ExpectEq(options.source_root, "/tmp/source", "file source root mismatch");
609
- ExpectEq(options.target_root, "/tmp/target", "file target root mismatch");
610
- ExpectEq(options.merge_source_subdir, "", "file patch merge subdir mismatch");
611
- }
612
-
613
- void TestArchivePatchCoreRejectsMissingEntries() {
614
- PatchManifest manifest;
615
- Status status = pushy::archive_patch::BuildArchivePatchPlan(
616
- pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
617
- manifest,
618
- {"index.bundlejs.patch"},
619
- nullptr);
620
- Expect(!status.ok, "null output plan should fail");
621
-
622
- pushy::archive_patch::ArchivePatchPlan plan;
623
- status = pushy::archive_patch::BuildArchivePatchPlan(
624
- pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
625
- manifest,
626
- {"__diff.json"},
627
- &plan);
628
- Expect(!status.ok, "missing bundle patch should fail");
629
-
630
- status = pushy::archive_patch::BuildArchivePatchPlan(
631
- pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
632
- manifest,
633
- {"__diff.json", "index.bundlejs.patch"},
634
- &plan);
635
- Expect(status.ok, status.message);
636
- ExpectEq(plan.merge_source_subdir, "assets", "package merge subdir mismatch");
637
- Expect(plan.enable_merge, "package plan should enable merge");
638
- Expect(
639
- pushy::archive_patch::ClassifyEntry(
640
- pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
641
- "__diff.json") == pushy::archive_patch::EntryAction::kSkip,
642
- "manifest entry should be skipped");
643
- }
644
-
645
- void TestTryParseArchivePatchType() {
646
- pushy::archive_patch::ArchivePatchType type;
647
- Expect(
648
- pushy::archive_patch::TryParseArchivePatchType(1, &type) &&
649
- type == pushy::archive_patch::ArchivePatchType::kFull,
650
- "1 should parse to kFull");
651
- Expect(
652
- pushy::archive_patch::TryParseArchivePatchType(2, &type) &&
653
- type == pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
654
- "2 should parse to kPatchFromPackage");
655
- Expect(
656
- pushy::archive_patch::TryParseArchivePatchType(3, &type) &&
657
- type == pushy::archive_patch::ArchivePatchType::kPatchFromPpk,
658
- "3 should parse to kPatchFromPpk");
659
- Expect(
660
- !pushy::archive_patch::TryParseArchivePatchType(0, &type),
661
- "0 should be rejected");
662
- Expect(
663
- !pushy::archive_patch::TryParseArchivePatchType(4, &type),
664
- "unknown type should be rejected, not silently coerced to kFull");
665
- }
666
-
667
- void TestArchivePatchCoreSupportsCustomBundlePatchEntry() {
668
- PatchManifest manifest;
669
- manifest.copies.push_back(CopyOperation{"assets/a.png", "assets/a.png"});
670
-
671
- pushy::archive_patch::ArchivePatchPlan plan;
672
- Status status = pushy::archive_patch::BuildArchivePatchPlan(
673
- pushy::archive_patch::ArchivePatchType::kPatchFromPpk,
674
- manifest,
675
- {"__diff.json", "bundle.harmony.js.patch"},
676
- &plan,
677
- "bundle.harmony.js.patch");
678
- Expect(status.ok, status.message);
679
- Expect(plan.enable_merge, "custom bundle patch plan should enable merge");
680
- ExpectEq(plan.merge_source_subdir, "", "custom bundle patch merge subdir mismatch");
681
- }
682
-
683
- void TestArchivePatchCoreHarmonyBundlePatchFromPackage() {
684
- PatchManifest manifest;
685
- manifest.copies.push_back(CopyOperation{"assets/a.png", "assets/b.png"});
686
-
687
- pushy::archive_patch::ArchivePatchPlan plan;
688
- Status status = pushy::archive_patch::BuildArchivePatchPlan(
689
- pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
690
- manifest,
691
- {"__diff.json", "bundle.harmony.js.patch", "assets/new.png"},
692
- &plan,
693
- "bundle.harmony.js.patch");
694
- Expect(status.ok, status.message);
695
- Expect(plan.enable_merge, "harmony package plan should enable merge");
696
- ExpectEq(plan.merge_source_subdir, "assets", "harmony package merge subdir should be assets");
697
-
698
- // ppk variant uses empty merge subdir
699
- pushy::archive_patch::ArchivePatchPlan ppk_plan;
700
- status = pushy::archive_patch::BuildArchivePatchPlan(
701
- pushy::archive_patch::ArchivePatchType::kPatchFromPpk,
702
- manifest,
703
- {"__diff.json", "bundle.harmony.js.patch", "assets/new.png"},
704
- &ppk_plan,
705
- "bundle.harmony.js.patch");
706
- Expect(status.ok, status.message);
707
- Expect(ppk_plan.enable_merge, "harmony ppk plan should enable merge");
708
- ExpectEq(ppk_plan.merge_source_subdir, "", "harmony ppk merge subdir should be empty");
709
- }
710
-
711
- void TestStateCoreRollbackToEmptyVersion() {
712
- State state;
713
- state.current_version = "current";
714
- state.last_version = "";
715
- state.first_time = false;
716
- state.first_time_ok = true;
717
-
718
- State rolled = pushy::state::Rollback(state);
719
- Expect(rolled.current_version.empty(), "rollback with empty last should clear current");
720
- Expect(rolled.last_version.empty(), "last_version should remain empty");
721
- ExpectEq(rolled.rolled_back_version, "current", "rolled_back_version should record original");
722
- Expect(!rolled.first_time, "first_time should be false after rollback");
723
- Expect(rolled.first_time_ok, "first_time_ok should be true after rollback");
724
- }
725
-
726
- void TestStateCoreResolveLaunchNoCurrentVersion() {
727
- State state;
728
- state.current_version = "";
729
- state.first_time = false;
730
- state.first_time_ok = true;
731
-
732
- LaunchDecision decision = pushy::state::ResolveLaunchState(state, false, true);
733
- Expect(decision.load_version.empty(), "empty current should yield empty load_version");
734
- Expect(!decision.did_rollback, "should not rollback when no current version");
735
- Expect(!decision.consumed_first_time, "should not consume first_time when no current version");
736
- }
737
-
738
- void TestStateCoreSwitchToSameVersion() {
739
- State state;
740
- state.package_version = "1.0.0";
741
- state.current_version = "same_hash";
742
- state.last_version = "old_hash";
743
-
744
- State switched = pushy::state::SwitchVersion(state, "same_hash");
745
- ExpectEq(switched.current_version, "same_hash", "current should remain same_hash");
746
- ExpectEq(switched.last_version, "old_hash", "last_version should not change when switching to same");
747
- Expect(switched.first_time, "first_time should be set even when switching to same");
748
- Expect(!switched.first_time_ok, "first_time_ok should be false");
749
- }
750
-
751
- // NIST FIPS 180-4 vectors. These anchor the C++ implementation (iOS/Harmony)
752
- // and, by transitivity, Android's java.security.MessageDigest and the CLI's
753
- // node:crypto to the same standard — the bundleHash produced on any of them
754
- // must be byte-identical for the same input.
755
- void TestSha256KnownVectors() {
756
- {
757
- pushy::digest::Sha256 hasher;
758
- ExpectEq(
759
- hasher.HexDigest(),
760
- "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
761
- "sha256 of empty input");
762
- }
763
- {
764
- pushy::digest::Sha256 hasher;
765
- const char* input = "abc";
766
- hasher.Update(reinterpret_cast<const uint8_t*>(input), 3);
767
- ExpectEq(
768
- hasher.HexDigest(),
769
- "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
770
- "sha256 of \"abc\"");
771
- }
772
- {
773
- pushy::digest::Sha256 hasher;
774
- const std::string input =
775
- "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
776
- hasher.Update(
777
- reinterpret_cast<const uint8_t*>(input.data()), input.size());
778
- ExpectEq(
779
- hasher.HexDigest(),
780
- "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1",
781
- "sha256 of NIST two-block message");
782
- }
783
- }
784
-
785
- void TestSha256StreamingMatchesOneShot() {
786
- // One million 'a' (NIST long vector), fed in deliberately awkward chunk
787
- // sizes to cross every buffer boundary case in Update.
788
- const std::string chunk(4096 + 37, 'a');
789
- size_t remaining = 1000000;
790
- pushy::digest::Sha256 hasher;
791
- while (remaining > 0) {
792
- const size_t take = remaining < chunk.size() ? remaining : chunk.size();
793
- hasher.Update(reinterpret_cast<const uint8_t*>(chunk.data()), take);
794
- remaining -= take;
795
- }
796
- ExpectEq(
797
- hasher.HexDigest(),
798
- "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0",
799
- "sha256 of one million 'a' fed in odd-sized chunks");
800
- }
801
-
802
- void TestSha256File() {
803
- TempDir temp;
804
- const std::string dir = temp.path;
805
- const std::string path = dir + "/input.bin";
806
- WriteFile(path, "abc");
807
- ExpectEq(
808
- pushy::digest::Sha256File(path),
809
- "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
810
- "Sha256File over a small file");
811
- ExpectEq(
812
- pushy::digest::Sha256File(dir + "/missing.bin"),
813
- "",
814
- "Sha256File over a missing file returns empty");
815
- }
816
-
817
- void TestCrc32KnownVectors() {
818
- {
819
- pushy::digest::Crc32 crc;
820
- Expect(crc.Value() == 0x00000000u, "crc32 of empty input");
821
- }
822
- {
823
- // The classic check value: crc32("123456789") == 0xCBF43926.
824
- pushy::digest::Crc32 crc;
825
- const char* input = "123456789";
826
- crc.Update(reinterpret_cast<const uint8_t*>(input), 9);
827
- Expect(crc.Value() == 0xCBF43926u, "crc32 check value of \"123456789\"");
828
- }
829
- {
830
- // Streaming in awkward chunks must match the one-shot value.
831
- const std::string input(100000, 'a');
832
- pushy::digest::Crc32 one_shot;
833
- one_shot.Update(
834
- reinterpret_cast<const uint8_t*>(input.data()), input.size());
835
- pushy::digest::Crc32 streamed;
836
- size_t offset = 0;
837
- size_t chunk = 1;
838
- while (offset < input.size()) {
839
- const size_t take =
840
- offset + chunk > input.size() ? input.size() - offset : chunk;
841
- streamed.Update(
842
- reinterpret_cast<const uint8_t*>(input.data()) + offset, take);
843
- offset += take;
844
- chunk = chunk * 2 + 1;
845
- }
846
- Expect(
847
- streamed.Value() == one_shot.Value(),
848
- "crc32 streaming matches one-shot");
849
- }
850
- }
851
-
852
- void TestCrc32File() {
853
- TempDir temp;
854
- const std::string dir = temp.path;
855
- const std::string path = dir + "/input.bin";
856
- WriteFile(path, "123456789");
857
- uint32_t value = 0;
858
- Expect(pushy::digest::Crc32File(path, &value), "Crc32File should read");
859
- Expect(value == 0xCBF43926u, "Crc32File check value");
860
- Expect(
861
- !pushy::digest::Crc32File(dir + "/missing.bin", &value),
862
- "Crc32File over a missing file returns false");
863
- }
864
-
865
- void TestApplyPatchCopiesVerifyExpectedCrc() {
866
- TempDir temp;
867
- const std::string source = JoinPath(temp.path, "origin");
868
- const std::string target = JoinPath(temp.path, "target");
869
- const std::string patch = JoinPath(temp.path, "bundle.patch");
870
-
871
- WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
872
- WriteFile(JoinPath(source, "assets/keep.txt"), "123456789");
873
- WriteFile(patch, "unused patch");
874
-
875
- FakeBundlePatcher patcher("patched bundle");
876
- FileSourcePatchOptions options;
877
- options.source_root = source;
878
- options.target_root = target;
879
- options.origin_bundle_path = JoinPath(source, "index.bundlejs");
880
- options.bundle_patch_path = patch;
881
- options.bundle_output_path = JoinPath(target, "index.bundlejs");
882
- options.enable_merge = false;
883
- CopyOperation copy;
884
- copy.from = "assets/keep.txt";
885
- copy.to = "assets/keep.txt";
886
- copy.has_expected_crc = true;
887
- copy.expected_crc = 0xCBF43926u; // crc32("123456789")
888
- options.manifest.copies.push_back(copy);
889
-
890
- Status status = ApplyPatchFromFileSource(options, patcher);
891
- Expect(status.ok, status.message);
892
- ExpectEq(
893
- ReadFile(JoinPath(target, "assets/keep.txt")),
894
- "123456789",
895
- "verified copy content");
896
-
897
- // Same manifest applied on a "rebuilt" source whose file drifted: the copy
898
- // must fail the whole patch (caller falls back to full), not copy silently.
899
- WriteFile(JoinPath(source, "assets/keep.txt"), "drifted-bytes");
900
- const std::string target2 = JoinPath(temp.path, "target2");
901
- options.target_root = target2;
902
- options.bundle_output_path = JoinPath(target2, "index.bundlejs");
903
- Status mismatch = ApplyPatchFromFileSource(options, patcher);
904
- Expect(!mismatch.ok, "drifted copy source must fail the patch");
905
- Expect(
906
- mismatch.message.find("crc32") != std::string::npos,
907
- "mismatch error should mention crc32");
908
- Expect(
909
- !Exists(JoinPath(target2, "assets/keep.txt")),
910
- "drifted content must not be copied");
911
- }
912
-
913
- } // namespace
914
-
915
- int main(int argc, char** argv) {
916
- if (argc > 1) {
917
- g_fixtures_dir = argv[1];
918
- }
919
- const std::vector<std::pair<std::string, void (*)()>> tests = {
920
- {"ApplyStreamFormatBundlePatch", TestApplyStreamFormatBundlePatch},
921
- {"ApplyStreamFormatWithHbcTransform", TestApplyStreamFormatWithHbcTransform},
922
- {"ApplySingleFormatRawStoredWithLzma2Label",
923
- TestApplySingleFormatRawStoredWithLzma2Label},
924
- {"ApplyPatchWithHbcTransform", TestApplyPatchWithHbcTransform},
925
- {"ApplyPatchWithHbcTransformRejectsBadMeta", TestApplyPatchWithHbcTransformRejectsBadMeta},
926
- {"ApplyPatchFromFileSourceMergesAndCopies", TestApplyPatchFromFileSourceMergesAndCopies},
927
- {"ApplyPatchMergeHardLinksUnchangedFiles", TestApplyPatchMergeHardLinksUnchangedFiles},
928
- {"ApplyPatchMergeFallsBackToByteCopy", TestApplyPatchMergeFallsBackToByteCopy},
929
- {"ApplyPatchFromFileSourceCanLimitMergeSubdir", TestApplyPatchFromFileSourceCanLimitMergeSubdir},
930
- {"ApplyPatchFromFileSourceRejectsUnsafePaths", TestApplyPatchFromFileSourceRejectsUnsafePaths},
931
- {"CleanupOldEntriesRemovesOnlyExpiredPaths", TestCleanupOldEntriesRemovesOnlyExpiredPaths},
932
- {"StateCoreSyncBinaryVersionResetsUpdates", TestStateCoreSyncBinaryVersionResetsUpdates},
933
- {"StateCoreSwitchVersionAndMarkSuccess", TestStateCoreSwitchVersionAndMarkSuccess},
934
- {"StateCoreResolveLaunchStateAndRollback", TestStateCoreResolveLaunchStateAndRollback},
935
- {"StateCoreCanClearMarkers", TestStateCoreCanClearMarkers},
936
- {"ArchivePatchCoreBuildPlanAndCopyGroups", TestArchivePatchCoreBuildPlanAndCopyGroups},
937
- {"ArchivePatchCoreRejectsMissingEntries", TestArchivePatchCoreRejectsMissingEntries},
938
- {"TryParseArchivePatchType", TestTryParseArchivePatchType},
939
- {"ArchivePatchCoreSupportsCustomBundlePatchEntry", TestArchivePatchCoreSupportsCustomBundlePatchEntry},
940
- {"ArchivePatchCoreHarmonyBundlePatchFromPackage", TestArchivePatchCoreHarmonyBundlePatchFromPackage},
941
- {"StateCoreRollbackToEmptyVersion", TestStateCoreRollbackToEmptyVersion},
942
- {"StateCoreResolveLaunchNoCurrentVersion", TestStateCoreResolveLaunchNoCurrentVersion},
943
- {"StateCoreSwitchToSameVersion", TestStateCoreSwitchToSameVersion},
944
- {"Sha256KnownVectors", TestSha256KnownVectors},
945
- {"Sha256StreamingMatchesOneShot", TestSha256StreamingMatchesOneShot},
946
- {"Sha256File", TestSha256File},
947
- {"Crc32KnownVectors", TestCrc32KnownVectors},
948
- {"Crc32File", TestCrc32File},
949
- {"ApplyPatchCopiesVerifyExpectedCrc", TestApplyPatchCopiesVerifyExpectedCrc},
950
- };
951
-
952
- int failures = 0;
953
- for (const auto& test : tests) {
954
- try {
955
- test.second();
956
- std::fprintf(stdout, "[PASS] %s\n", test.first.c_str());
957
- } catch (const std::exception& error) {
958
- std::fprintf(stderr, "[FAIL] %s: %s\n", test.first.c_str(), error.what());
959
- ++failures;
960
- }
961
- }
962
-
963
- std::fprintf(
964
- stdout,
965
- "\n%zu tests, %d passed, %d failed\n",
966
- tests.size(),
967
- static_cast<int>(tests.size()) - failures,
968
- failures);
969
- return failures == 0 ? 0 : 1;
970
- }