react-native-update 10.37.20 → 10.38.0-beta.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 (131) hide show
  1. package/README-CN.md +4 -4
  2. package/README.md +2 -12
  3. package/android/bin/.settings/org.eclipse.buildship.core.prefs +13 -0
  4. package/android/build.gradle +4 -0
  5. package/android/jni/Android.mk +14 -1
  6. package/android/jni/Application.mk +5 -2
  7. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  8. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  9. package/android/lib/x86/librnupdate.so +0 -0
  10. package/android/lib/x86_64/librnupdate.so +0 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +6 -0
  12. package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +6 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +136 -136
  14. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateCore.java +34 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/StateCoreResult.java +16 -0
  16. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +131 -48
  17. package/cpp/patch_core/archive_patch_core.cpp +125 -0
  18. package/cpp/patch_core/archive_patch_core.h +59 -0
  19. package/cpp/patch_core/patch_core.cpp +533 -0
  20. package/cpp/patch_core/patch_core.h +68 -0
  21. package/cpp/patch_core/patch_core_android.cpp +112 -0
  22. package/cpp/patch_core/state_core.cpp +110 -0
  23. package/cpp/patch_core/state_core.h +58 -0
  24. package/cpp/patch_core/tests/patch_core_test.cpp +473 -0
  25. package/cpp/patch_core/update_core_android.cpp +469 -0
  26. package/harmony/pushy.har +0 -0
  27. package/ios/RCTPushy/RCTPushy.mm +233 -143
  28. package/package.json +17 -15
  29. package/react-native-update.podspec +3 -0
  30. package/scripts/build-harmony-har.js +12 -0
  31. package/scripts/prepublish.ts +49 -3
  32. package/scripts/prune-host-stl.sh +6 -0
  33. package/scripts/test-patch-core.sh +39 -0
  34. package/src/client.ts +129 -76
  35. package/src/core.ts +2 -1
  36. package/src/endpoint.ts +171 -0
  37. package/src/utils.ts +40 -27
  38. package/android/jni/lzma/DOC/7zC.txt +0 -187
  39. package/android/jni/lzma/DOC/7zFormat.txt +0 -469
  40. package/android/jni/lzma/DOC/Methods.txt +0 -173
  41. package/android/jni/lzma/DOC/installer.txt +0 -166
  42. package/android/jni/lzma/DOC/lzma-history.txt +0 -446
  43. package/android/jni/lzma/DOC/lzma-sdk.txt +0 -357
  44. package/android/jni/lzma/DOC/lzma-specification.txt +0 -1176
  45. package/android/jni/lzma/DOC/lzma.txt +0 -328
  46. package/android/jni/lzma/bin/7zS2.sfx +0 -0
  47. package/android/jni/lzma/bin/7zS2con.sfx +0 -0
  48. package/android/jni/lzma/bin/7zSD.sfx +0 -0
  49. package/android/jni/lzma/bin/7zdec.exe +0 -0
  50. package/android/jni/lzma/bin/7zr.exe +0 -0
  51. package/android/jni/lzma/bin/installer/config.txt +0 -5
  52. package/android/jni/lzma/bin/installer/cr.bat +0 -5
  53. package/android/jni/lzma/bin/lzma.exe +0 -0
  54. package/android/jni/lzma/bin/x64/7zr.exe +0 -0
  55. package/error.js +0 -1609
  56. package/harmony/har-wrapper/AppScope/app.json5 +0 -8
  57. package/harmony/har-wrapper/build-profile.json5 +0 -35
  58. package/harmony/har-wrapper/hvigor/hvigor-config.json5 +0 -5
  59. package/harmony/har-wrapper/hvigorfile.ts +0 -6
  60. package/harmony/har-wrapper/oh-package.json5 +0 -4
  61. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-77b153ce45aba0ed28ef.json +0 -1415
  62. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-b65a07793384e0ce3e08.json +0 -809
  63. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-ce0e89410afd8bf3a057.json +0 -60
  64. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json +0 -14
  65. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-03-18T12-50-36-0050.json +0 -89
  66. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-rnupdate-Release-267153624504c9c3ffdd.json +0 -222
  67. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.ninja_deps +0 -0
  68. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.ninja_log +0 -8
  69. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeCache.txt +0 -415
  70. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake +0 -74
  71. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake +0 -85
  72. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_C.bin +0 -0
  73. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
  74. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake +0 -15
  75. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c +0 -880
  76. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.o +0 -0
  77. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +0 -869
  78. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o +0 -0
  79. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeConfigureLog.yaml +0 -388
  80. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt +0 -3
  81. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache +0 -1
  82. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/HDiffPatch/file_for_patch.c.o +0 -0
  83. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/HDiffPatch/libHDiffPatch/HPatch/patch.c.o +0 -0
  84. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/hpatch.c.o +0 -0
  85. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/lzma/C/Lzma2Dec.c.o +0 -0
  86. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/lzma/C/LzmaDec.c.o +0 -0
  87. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/pushy.c.o +0 -0
  88. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rules.ninja +0 -64
  89. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/additional_project_files.txt +0 -0
  90. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/build.ninja +0 -206
  91. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/build_file_index.txt +0 -1
  92. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/cmake_install.cmake +0 -54
  93. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/compile_commands.json +0 -38
  94. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/configure_fingerprint.json +0 -1
  95. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/hvigor_native_config.json +0 -1
  96. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/metadata_generation_command.txt +0 -17
  97. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/native_work_dir.txt +0 -1
  98. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/output.log +0 -14
  99. package/harmony/pushy/.cxx/default/default/release/hvigor/arm64-v8a/summary.cmake +0 -0
  100. package/harmony/pushy/BuildProfile.ets +0 -17
  101. package/harmony/pushy/OAT.xml +0 -38
  102. package/harmony/pushy/README.md +0 -0
  103. package/harmony/pushy/build-profile.json5 +0 -15
  104. package/harmony/pushy/hvigor-plugin.ts +0 -34
  105. package/harmony/pushy/hvigorfile.ts +0 -1
  106. package/harmony/pushy/index.ets +0 -2
  107. package/harmony/pushy/oh-package-lock.json5 +0 -20
  108. package/harmony/pushy/oh-package.json5 +0 -13
  109. package/harmony/pushy/src/main/cpp/CMakeLists.txt +0 -51
  110. package/harmony/pushy/src/main/cpp/PushyPackage.h +0 -55
  111. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +0 -142
  112. package/harmony/pushy/src/main/cpp/PushyTurboModule.h +0 -38
  113. package/harmony/pushy/src/main/cpp/pushy.c +0 -117
  114. package/harmony/pushy/src/main/cpp/pushy.h +0 -8
  115. package/harmony/pushy/src/main/ets/DownloadTask.ts +0 -570
  116. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +0 -19
  117. package/harmony/pushy/src/main/ets/EventHub.ts +0 -39
  118. package/harmony/pushy/src/main/ets/Logger.ts +0 -52
  119. package/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +0 -50
  120. package/harmony/pushy/src/main/ets/PushyPackage.ts +0 -22
  121. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +0 -171
  122. package/harmony/pushy/src/main/ets/SaveFile.ts +0 -34
  123. package/harmony/pushy/src/main/ets/UpdateContext.ts +0 -262
  124. package/harmony/pushy/src/main/ets/UpdateModuleImpl.ts +0 -123
  125. package/harmony/pushy/src/main/module.json5 +0 -7
  126. package/harmony/pushy/src/main/resources/base/element/string.json +0 -8
  127. package/harmony/pushy/src/main/resources/en_US/element/string.json +0 -8
  128. package/harmony/pushy/src/main/resources/zh_CN/element/string.json +0 -8
  129. package/harmony/pushy/ts.ts +0 -3
  130. package/src/__tests__/core.test.ts +0 -103
  131. package/src/__tests__/utils.test.ts +0 -36
@@ -0,0 +1,110 @@
1
+ #include "state_core.h"
2
+
3
+ namespace pushy {
4
+ namespace state {
5
+
6
+ BinaryVersionSyncResult SyncBinaryVersion(
7
+ const State& state,
8
+ const std::string& package_version,
9
+ const std::string& build_time) {
10
+ BinaryVersionSyncResult result;
11
+ result.state = state;
12
+ result.changed =
13
+ state.package_version != package_version || state.build_time != build_time;
14
+ if (!result.changed) {
15
+ return result;
16
+ }
17
+
18
+ result.state.package_version = package_version;
19
+ result.state.build_time = build_time;
20
+ result.state.current_version.clear();
21
+ result.state.last_version.clear();
22
+ result.state.first_time = false;
23
+ result.state.first_time_ok = true;
24
+ result.state.rolled_back_version.clear();
25
+ return result;
26
+ }
27
+
28
+ State SwitchVersion(const State& state, const std::string& hash) {
29
+ State next = state;
30
+ if (!state.current_version.empty() && state.current_version != hash) {
31
+ next.last_version = state.current_version;
32
+ }
33
+ next.current_version = hash;
34
+ next.first_time = true;
35
+ next.first_time_ok = false;
36
+ next.rolled_back_version.clear();
37
+ return next;
38
+ }
39
+
40
+ MarkSuccessResult MarkSuccess(const State& state) {
41
+ MarkSuccessResult result;
42
+ result.state = state;
43
+ result.state.first_time = false;
44
+ result.state.first_time_ok = true;
45
+ if (!state.last_version.empty() && state.last_version != state.current_version) {
46
+ result.stale_version_to_delete = state.last_version;
47
+ result.state.last_version.clear();
48
+ }
49
+ return result;
50
+ }
51
+
52
+ State ClearFirstTime(const State& state) {
53
+ State next = state;
54
+ next.first_time = false;
55
+ return next;
56
+ }
57
+
58
+ State ClearRollbackMark(const State& state) {
59
+ State next = state;
60
+ next.rolled_back_version.clear();
61
+ return next;
62
+ }
63
+
64
+ State Rollback(const State& state) {
65
+ State next = state;
66
+ const std::string rolled_back_version = state.current_version;
67
+ if (state.last_version.empty()) {
68
+ next.current_version.clear();
69
+ } else {
70
+ next.current_version = state.last_version;
71
+ next.last_version.clear();
72
+ }
73
+ next.first_time = false;
74
+ next.first_time_ok = true;
75
+ next.rolled_back_version = rolled_back_version;
76
+ return next;
77
+ }
78
+
79
+ bool ShouldRollbackForBrokenFirstLoad(const State& state) {
80
+ return !state.first_time && !state.first_time_ok;
81
+ }
82
+
83
+ LaunchDecision ResolveLaunchState(
84
+ const State& state,
85
+ bool ignore_rollback,
86
+ bool consume_first_time_on_launch) {
87
+ LaunchDecision decision;
88
+ decision.state = state;
89
+ decision.load_version = state.current_version;
90
+ if (decision.load_version.empty()) {
91
+ return decision;
92
+ }
93
+
94
+ if (!ignore_rollback && ShouldRollbackForBrokenFirstLoad(decision.state)) {
95
+ decision.state = Rollback(decision.state);
96
+ decision.load_version = decision.state.current_version;
97
+ decision.did_rollback = true;
98
+ return decision;
99
+ }
100
+
101
+ if (!ignore_rollback && consume_first_time_on_launch && decision.state.first_time) {
102
+ decision.state.first_time = false;
103
+ decision.consumed_first_time = true;
104
+ }
105
+
106
+ return decision;
107
+ }
108
+
109
+ } // namespace state
110
+ } // namespace pushy
@@ -0,0 +1,58 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+
5
+ namespace pushy {
6
+ namespace state {
7
+
8
+ struct State {
9
+ std::string package_version;
10
+ std::string build_time;
11
+ std::string current_version;
12
+ std::string last_version;
13
+ bool first_time = false;
14
+ bool first_time_ok = true;
15
+ std::string rolled_back_version;
16
+ };
17
+
18
+ struct BinaryVersionSyncResult {
19
+ State state;
20
+ bool changed = false;
21
+ };
22
+
23
+ struct MarkSuccessResult {
24
+ State state;
25
+ std::string stale_version_to_delete;
26
+ };
27
+
28
+ struct LaunchDecision {
29
+ State state;
30
+ std::string load_version;
31
+ bool did_rollback = false;
32
+ bool consumed_first_time = false;
33
+ };
34
+
35
+ BinaryVersionSyncResult SyncBinaryVersion(
36
+ const State& state,
37
+ const std::string& package_version,
38
+ const std::string& build_time);
39
+
40
+ State SwitchVersion(const State& state, const std::string& hash);
41
+
42
+ MarkSuccessResult MarkSuccess(const State& state);
43
+
44
+ State ClearFirstTime(const State& state);
45
+
46
+ State ClearRollbackMark(const State& state);
47
+
48
+ State Rollback(const State& state);
49
+
50
+ bool ShouldRollbackForBrokenFirstLoad(const State& state);
51
+
52
+ LaunchDecision ResolveLaunchState(
53
+ const State& state,
54
+ bool ignore_rollback,
55
+ bool consume_first_time_on_launch);
56
+
57
+ } // namespace state
58
+ } // namespace pushy
@@ -0,0 +1,473 @@
1
+ #include "../archive_patch_core.h"
2
+ #include "../patch_core.h"
3
+ #include "../state_core.h"
4
+
5
+ #include <cstdio>
6
+ #include <cstdlib>
7
+ #include <cstring>
8
+ #include <fstream>
9
+ #include <sstream>
10
+ #include <stdexcept>
11
+ #include <string>
12
+ #include <sys/stat.h>
13
+ #include <sys/time.h>
14
+ #include <sys/types.h>
15
+ #include <unistd.h>
16
+ #include <vector>
17
+
18
+ namespace {
19
+
20
+ using pushy::patch::ApplyPatchFromFileSource;
21
+ using pushy::patch::BundlePatcher;
22
+ using pushy::patch::CleanupOldEntries;
23
+ using pushy::patch::CopyOperation;
24
+ using pushy::patch::FileSourcePatchOptions;
25
+ using pushy::patch::PatchManifest;
26
+ using pushy::patch::Status;
27
+ using pushy::state::BinaryVersionSyncResult;
28
+ using pushy::state::LaunchDecision;
29
+ using pushy::state::MarkSuccessResult;
30
+ using pushy::state::State;
31
+
32
+ void EnsureDirectory(const std::string& path);
33
+
34
+ class FakeBundlePatcher final : public BundlePatcher {
35
+ public:
36
+ mutable int calls = 0;
37
+ std::string output;
38
+
39
+ explicit FakeBundlePatcher(std::string output_value)
40
+ : output(std::move(output_value)) {}
41
+
42
+ Status Apply(
43
+ const std::string&,
44
+ const std::string&,
45
+ const std::string& destination_bundle_path) const override {
46
+ ++calls;
47
+ size_t slash = destination_bundle_path.find_last_of('/');
48
+ if (slash != std::string::npos) {
49
+ EnsureDirectory(destination_bundle_path.substr(0, slash));
50
+ }
51
+ std::ofstream out(destination_bundle_path, std::ios::binary);
52
+ out << output;
53
+ return out.good() ? Status::Ok() : Status::Error("Failed to write fake bundle");
54
+ }
55
+ };
56
+
57
+ struct TempDir {
58
+ std::string path;
59
+
60
+ TempDir() {
61
+ char templ[] = "/tmp/pushy-patch-core-XXXXXX";
62
+ char* created = mkdtemp(templ);
63
+ if (!created) {
64
+ throw std::runtime_error("Failed to create temp dir");
65
+ }
66
+ path = created;
67
+ }
68
+
69
+ ~TempDir() {
70
+ if (!path.empty()) {
71
+ std::string command = "rm -rf \"" + path + "\"";
72
+ std::system(command.c_str());
73
+ }
74
+ }
75
+ };
76
+
77
+ std::string JoinPath(const std::string& base, const std::string& relative) {
78
+ if (base.empty()) {
79
+ return relative;
80
+ }
81
+ if (relative.empty()) {
82
+ return base;
83
+ }
84
+ return base + "/" + relative;
85
+ }
86
+
87
+ void EnsureDirectory(const std::string& path) {
88
+ if (path.empty()) {
89
+ return;
90
+ }
91
+
92
+ size_t slash = path.find_last_of('/');
93
+ if (slash != std::string::npos) {
94
+ EnsureDirectory(path.substr(0, slash));
95
+ }
96
+ mkdir(path.c_str(), 0755);
97
+ }
98
+
99
+ void WriteFile(const std::string& path, const std::string& content) {
100
+ EnsureDirectory(path.substr(0, path.find_last_of('/')));
101
+ std::ofstream out(path, std::ios::binary);
102
+ out << content;
103
+ }
104
+
105
+ std::string ReadFile(const std::string& path) {
106
+ std::ifstream in(path, std::ios::binary);
107
+ std::ostringstream stream;
108
+ stream << in.rdbuf();
109
+ return stream.str();
110
+ }
111
+
112
+ bool Exists(const std::string& path) {
113
+ struct stat st;
114
+ return stat(path.c_str(), &st) == 0;
115
+ }
116
+
117
+ void SetMtime(const std::string& path, std::time_t value) {
118
+ struct timeval times[2];
119
+ times[0].tv_sec = value;
120
+ times[0].tv_usec = 0;
121
+ times[1].tv_sec = value;
122
+ times[1].tv_usec = 0;
123
+ if (utimes(path.c_str(), times) != 0) {
124
+ throw std::runtime_error("Failed to set mtime");
125
+ }
126
+ }
127
+
128
+ void Expect(bool condition, const std::string& message) {
129
+ if (!condition) {
130
+ throw std::runtime_error(message);
131
+ }
132
+ }
133
+
134
+ void ExpectEq(const std::string& left, const std::string& right, const std::string& message) {
135
+ if (left != right) {
136
+ throw std::runtime_error(message + ": expected [" + right + "] got [" + left + "]");
137
+ }
138
+ }
139
+
140
+ void TestApplyPatchFromFileSourceMergesAndCopies() {
141
+ TempDir temp;
142
+ const std::string source = JoinPath(temp.path, "origin");
143
+ const std::string target = JoinPath(temp.path, "target");
144
+ const std::string patch = JoinPath(temp.path, "bundle.patch");
145
+
146
+ WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
147
+ WriteFile(JoinPath(source, "assets/keep.txt"), "keep");
148
+ WriteFile(JoinPath(source, "assets/delete.txt"), "delete");
149
+ WriteFile(JoinPath(source, "assets/from.txt"), "rename");
150
+ WriteFile(JoinPath(source, "config.json"), "config");
151
+
152
+ WriteFile(JoinPath(target, "assets/new.txt"), "new");
153
+ WriteFile(patch, "unused patch");
154
+
155
+ FakeBundlePatcher patcher("patched bundle");
156
+ FileSourcePatchOptions options;
157
+ options.source_root = source;
158
+ options.target_root = target;
159
+ options.origin_bundle_path = JoinPath(source, "index.bundlejs");
160
+ options.bundle_patch_path = patch;
161
+ options.bundle_output_path = JoinPath(target, "index.bundlejs");
162
+ options.merge_source_subdir = "";
163
+ options.manifest.copies.push_back(CopyOperation{"assets/from.txt", "assets/renamed.txt"});
164
+ options.manifest.deletes.push_back("assets/delete.txt");
165
+
166
+ Status status = ApplyPatchFromFileSource(options, patcher);
167
+ Expect(status.ok, status.message);
168
+ Expect(patcher.calls == 1, "bundle patcher should run exactly once");
169
+
170
+ ExpectEq(ReadFile(JoinPath(target, "index.bundlejs")), "patched bundle", "bundle output mismatch");
171
+ ExpectEq(ReadFile(JoinPath(target, "assets/keep.txt")), "keep", "merged asset mismatch");
172
+ ExpectEq(ReadFile(JoinPath(target, "assets/renamed.txt")), "rename", "copied asset mismatch");
173
+ ExpectEq(ReadFile(JoinPath(target, "assets/new.txt")), "new", "existing unzip file should be preserved");
174
+ ExpectEq(ReadFile(JoinPath(target, "config.json")), "config", "root file should be merged");
175
+ Expect(!Exists(JoinPath(target, "assets/delete.txt")), "deleted asset should not be copied");
176
+ }
177
+
178
+ void TestApplyPatchFromFileSourceCanLimitMergeSubdir() {
179
+ TempDir temp;
180
+ const std::string source = JoinPath(temp.path, "origin");
181
+ const std::string target = JoinPath(temp.path, "target");
182
+ const std::string patch = JoinPath(temp.path, "bundle.patch");
183
+
184
+ WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
185
+ WriteFile(JoinPath(source, "assets/keep.txt"), "keep");
186
+ WriteFile(JoinPath(source, "config.json"), "config");
187
+ WriteFile(patch, "unused patch");
188
+
189
+ FakeBundlePatcher patcher("patched bundle");
190
+ FileSourcePatchOptions options;
191
+ options.source_root = source;
192
+ options.target_root = target;
193
+ options.origin_bundle_path = JoinPath(source, "index.bundlejs");
194
+ options.bundle_patch_path = patch;
195
+ options.bundle_output_path = JoinPath(target, "index.bundlejs");
196
+ options.merge_source_subdir = "assets";
197
+
198
+ Status status = ApplyPatchFromFileSource(options, patcher);
199
+ Expect(status.ok, status.message);
200
+
201
+ ExpectEq(ReadFile(JoinPath(target, "assets/keep.txt")), "keep", "assets merge mismatch");
202
+ Expect(!Exists(JoinPath(target, "config.json")), "non-assets root file should not be merged");
203
+ }
204
+
205
+ void TestApplyPatchFromFileSourceRejectsUnsafePaths() {
206
+ TempDir temp;
207
+ const std::string source = JoinPath(temp.path, "origin");
208
+ const std::string target = JoinPath(temp.path, "target");
209
+ const std::string patch = JoinPath(temp.path, "bundle.patch");
210
+
211
+ WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
212
+ WriteFile(JoinPath(source, "assets/file.txt"), "content");
213
+ WriteFile(patch, "unused patch");
214
+
215
+ FakeBundlePatcher patcher("patched bundle");
216
+ FileSourcePatchOptions options;
217
+ options.source_root = source;
218
+ options.target_root = target;
219
+ options.origin_bundle_path = JoinPath(source, "index.bundlejs");
220
+ options.bundle_patch_path = patch;
221
+ options.bundle_output_path = JoinPath(target, "index.bundlejs");
222
+ options.merge_source_subdir = "";
223
+ options.manifest.copies.push_back(CopyOperation{"assets/file.txt", "../escape.txt"});
224
+
225
+ Status status = ApplyPatchFromFileSource(options, patcher);
226
+ Expect(!status.ok, "unsafe path should fail");
227
+ Expect(patcher.calls == 0, "bundle patcher should not run when validation fails");
228
+ }
229
+
230
+ void TestCleanupOldEntriesRemovesOnlyExpiredPaths() {
231
+ TempDir temp;
232
+ const std::string root = JoinPath(temp.path, "cleanup");
233
+ EnsureDirectory(root);
234
+
235
+ WriteFile(JoinPath(root, "current/index.bundlejs"), "current");
236
+ WriteFile(JoinPath(root, "previous/index.bundlejs"), "previous");
237
+ WriteFile(JoinPath(root, "stale/index.bundlejs"), "stale");
238
+ WriteFile(JoinPath(root, "recent/index.bundlejs"), "recent");
239
+ WriteFile(JoinPath(root, "old.tmp"), "old");
240
+ WriteFile(JoinPath(root, ".hidden"), "hidden");
241
+
242
+ const std::time_t now = 1'700'000'000;
243
+ const std::time_t old_time = now - (9 * 24 * 60 * 60);
244
+ const std::time_t recent_time = now - (2 * 24 * 60 * 60);
245
+
246
+ SetMtime(JoinPath(root, "current"), old_time);
247
+ SetMtime(JoinPath(root, "previous"), old_time);
248
+ SetMtime(JoinPath(root, "stale"), old_time);
249
+ SetMtime(JoinPath(root, "recent"), recent_time);
250
+ SetMtime(JoinPath(root, "old.tmp"), old_time);
251
+ SetMtime(JoinPath(root, ".hidden"), old_time);
252
+
253
+ Status status = CleanupOldEntries(root, "current", "previous", 7, now);
254
+ Expect(status.ok, status.message);
255
+
256
+ Expect(Exists(JoinPath(root, "current")), "current entry should be kept");
257
+ Expect(Exists(JoinPath(root, "previous")), "previous entry should be kept");
258
+ Expect(!Exists(JoinPath(root, "stale")), "stale directory should be removed");
259
+ Expect(!Exists(JoinPath(root, "old.tmp")), "stale file should be removed");
260
+ Expect(Exists(JoinPath(root, "recent")), "recent entry should be kept");
261
+ Expect(Exists(JoinPath(root, ".hidden")), "hidden entry should be kept");
262
+ }
263
+
264
+ void TestStateCoreSyncBinaryVersionResetsUpdates() {
265
+ State state;
266
+ state.package_version = "1.0.0";
267
+ state.build_time = "100";
268
+ state.current_version = "current";
269
+ state.last_version = "last";
270
+ state.first_time = true;
271
+ state.first_time_ok = false;
272
+ state.rolled_back_version = "rolled";
273
+
274
+ BinaryVersionSyncResult result =
275
+ pushy::state::SyncBinaryVersion(state, "1.1.0", "200");
276
+ Expect(result.changed, "binary version sync should detect changes");
277
+ ExpectEq(result.state.package_version, "1.1.0", "package version mismatch");
278
+ ExpectEq(result.state.build_time, "200", "build time mismatch");
279
+ Expect(result.state.current_version.empty(), "current version should reset");
280
+ Expect(result.state.last_version.empty(), "last version should reset");
281
+ Expect(!result.state.first_time, "first_time should reset");
282
+ Expect(result.state.first_time_ok, "first_time_ok should reset");
283
+ Expect(result.state.rolled_back_version.empty(), "rolled_back_version should reset");
284
+ }
285
+
286
+ void TestStateCoreSwitchVersionAndMarkSuccess() {
287
+ State state;
288
+ state.package_version = "1.0.0";
289
+ state.build_time = "100";
290
+ state.current_version = "old";
291
+ state.last_version = "older";
292
+ state.first_time_ok = true;
293
+
294
+ State switched = pushy::state::SwitchVersion(state, "new");
295
+ ExpectEq(switched.current_version, "new", "current version mismatch");
296
+ ExpectEq(switched.last_version, "old", "last version mismatch");
297
+ Expect(switched.first_time, "first_time should be set");
298
+ Expect(!switched.first_time_ok, "first_time_ok should be false");
299
+
300
+ MarkSuccessResult success = pushy::state::MarkSuccess(switched);
301
+ ExpectEq(success.state.current_version, "new", "markSuccess current version mismatch");
302
+ Expect(success.state.last_version.empty(), "last version should be cleared");
303
+ ExpectEq(success.stale_version_to_delete, "old", "stale version mismatch");
304
+ Expect(!success.state.first_time, "first_time should clear after success");
305
+ Expect(success.state.first_time_ok, "first_time_ok should be true after success");
306
+ }
307
+
308
+ void TestStateCoreResolveLaunchStateAndRollback() {
309
+ State state;
310
+ state.current_version = "current";
311
+ state.last_version = "previous";
312
+ state.first_time = false;
313
+ state.first_time_ok = false;
314
+
315
+ LaunchDecision rollback =
316
+ pushy::state::ResolveLaunchState(state, false, true);
317
+ Expect(rollback.did_rollback, "launch decision should roll back");
318
+ ExpectEq(rollback.load_version, "previous", "rollback load version mismatch");
319
+ ExpectEq(rollback.state.current_version, "previous", "rollback current version mismatch");
320
+ ExpectEq(rollback.state.rolled_back_version, "current", "rolled back version mismatch");
321
+
322
+ State first_load;
323
+ first_load.current_version = "fresh";
324
+ first_load.first_time = true;
325
+ first_load.first_time_ok = false;
326
+ LaunchDecision consume =
327
+ pushy::state::ResolveLaunchState(first_load, false, true);
328
+ Expect(!consume.did_rollback, "first load should not roll back");
329
+ Expect(consume.consumed_first_time, "first load should be consumed");
330
+ ExpectEq(consume.load_version, "fresh", "first load version mismatch");
331
+ Expect(!consume.state.first_time, "first_time should clear when consumed");
332
+
333
+ LaunchDecision preserve =
334
+ pushy::state::ResolveLaunchState(first_load, false, false);
335
+ Expect(!preserve.consumed_first_time, "first load should not be consumed when disabled");
336
+ Expect(preserve.state.first_time, "first_time should be preserved when not consumed");
337
+ }
338
+
339
+ void TestStateCoreCanClearMarkers() {
340
+ State state;
341
+ state.current_version = "current";
342
+ state.first_time = true;
343
+ state.rolled_back_version = "rolled";
344
+
345
+ State clear_first_time = pushy::state::ClearFirstTime(state);
346
+ Expect(!clear_first_time.first_time, "clearFirstTime should clear first_time");
347
+ ExpectEq(
348
+ clear_first_time.rolled_back_version,
349
+ "rolled",
350
+ "clearFirstTime should preserve rollback marker");
351
+
352
+ State clear_rollback = pushy::state::ClearRollbackMark(state);
353
+ Expect(
354
+ clear_rollback.rolled_back_version.empty(),
355
+ "clearRollbackMark should clear rollback marker");
356
+ Expect(clear_rollback.first_time, "clearRollbackMark should preserve first_time");
357
+ }
358
+
359
+ void TestArchivePatchCoreBuildPlanAndCopyGroups() {
360
+ PatchManifest manifest;
361
+ manifest.copies.push_back(CopyOperation{"assets/a.png", "assets/x.png"});
362
+ manifest.copies.push_back(CopyOperation{"assets/a.png", "assets/y.png"});
363
+ manifest.deletes.push_back("assets/old.png");
364
+
365
+ pushy::archive_patch::ArchivePatchPlan plan;
366
+ Status status = pushy::archive_patch::BuildArchivePatchPlan(
367
+ pushy::archive_patch::ArchivePatchType::kPatchFromPpk,
368
+ manifest,
369
+ {"__diff.json", "index.bundlejs.patch", "assets/new.png"},
370
+ &plan);
371
+ Expect(status.ok, status.message);
372
+ Expect(plan.enable_merge, "ppk plan should enable merge");
373
+ ExpectEq(plan.merge_source_subdir, "", "ppk merge subdir mismatch");
374
+
375
+ std::vector<pushy::archive_patch::CopyGroup> groups;
376
+ status = pushy::archive_patch::BuildCopyGroups(manifest, &groups);
377
+ Expect(status.ok, status.message);
378
+ Expect(groups.size() == 1, "copy groups should merge identical sources");
379
+ ExpectEq(groups[0].from, "assets/a.png", "copy group source mismatch");
380
+ Expect(groups[0].to_paths.size() == 2, "copy group target count mismatch");
381
+
382
+ FileSourcePatchOptions options;
383
+ status = pushy::archive_patch::BuildFileSourcePatchOptions(
384
+ plan,
385
+ "/tmp/source",
386
+ "/tmp/target",
387
+ "/tmp/source/index.bundlejs",
388
+ "/tmp/target/index.bundlejs.patch",
389
+ "/tmp/target/index.bundlejs",
390
+ &options);
391
+ Expect(status.ok, status.message);
392
+ ExpectEq(options.source_root, "/tmp/source", "file source root mismatch");
393
+ ExpectEq(options.target_root, "/tmp/target", "file target root mismatch");
394
+ ExpectEq(options.merge_source_subdir, "", "file patch merge subdir mismatch");
395
+ }
396
+
397
+ void TestArchivePatchCoreRejectsMissingEntries() {
398
+ PatchManifest manifest;
399
+ Status status = pushy::archive_patch::BuildArchivePatchPlan(
400
+ pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
401
+ manifest,
402
+ {"index.bundlejs.patch"},
403
+ nullptr);
404
+ Expect(!status.ok, "null output plan should fail");
405
+
406
+ pushy::archive_patch::ArchivePatchPlan plan;
407
+ status = pushy::archive_patch::BuildArchivePatchPlan(
408
+ pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
409
+ manifest,
410
+ {"__diff.json"},
411
+ &plan);
412
+ Expect(!status.ok, "missing bundle patch should fail");
413
+
414
+ status = pushy::archive_patch::BuildArchivePatchPlan(
415
+ pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
416
+ manifest,
417
+ {"__diff.json", "index.bundlejs.patch"},
418
+ &plan);
419
+ Expect(status.ok, status.message);
420
+ ExpectEq(plan.merge_source_subdir, "assets", "package merge subdir mismatch");
421
+ Expect(plan.enable_merge, "package plan should enable merge");
422
+ Expect(
423
+ pushy::archive_patch::ClassifyEntry(
424
+ pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
425
+ "__diff.json") == pushy::archive_patch::EntryAction::kSkip,
426
+ "manifest entry should be skipped");
427
+ }
428
+
429
+ void TestArchivePatchCoreSupportsCustomBundlePatchEntry() {
430
+ PatchManifest manifest;
431
+ manifest.copies.push_back(CopyOperation{"assets/a.png", "assets/a.png"});
432
+
433
+ pushy::archive_patch::ArchivePatchPlan plan;
434
+ Status status = pushy::archive_patch::BuildArchivePatchPlan(
435
+ pushy::archive_patch::ArchivePatchType::kPatchFromPpk,
436
+ manifest,
437
+ {"__diff.json", "bundle.harmony.js.patch"},
438
+ &plan,
439
+ "bundle.harmony.js.patch");
440
+ Expect(status.ok, status.message);
441
+ Expect(plan.enable_merge, "custom bundle patch plan should enable merge");
442
+ ExpectEq(plan.merge_source_subdir, "", "custom bundle patch merge subdir mismatch");
443
+ }
444
+
445
+ } // namespace
446
+
447
+ int main() {
448
+ const std::vector<std::pair<std::string, void (*)()>> tests = {
449
+ {"ApplyPatchFromFileSourceMergesAndCopies", TestApplyPatchFromFileSourceMergesAndCopies},
450
+ {"ApplyPatchFromFileSourceCanLimitMergeSubdir", TestApplyPatchFromFileSourceCanLimitMergeSubdir},
451
+ {"ApplyPatchFromFileSourceRejectsUnsafePaths", TestApplyPatchFromFileSourceRejectsUnsafePaths},
452
+ {"CleanupOldEntriesRemovesOnlyExpiredPaths", TestCleanupOldEntriesRemovesOnlyExpiredPaths},
453
+ {"StateCoreSyncBinaryVersionResetsUpdates", TestStateCoreSyncBinaryVersionResetsUpdates},
454
+ {"StateCoreSwitchVersionAndMarkSuccess", TestStateCoreSwitchVersionAndMarkSuccess},
455
+ {"StateCoreResolveLaunchStateAndRollback", TestStateCoreResolveLaunchStateAndRollback},
456
+ {"StateCoreCanClearMarkers", TestStateCoreCanClearMarkers},
457
+ {"ArchivePatchCoreBuildPlanAndCopyGroups", TestArchivePatchCoreBuildPlanAndCopyGroups},
458
+ {"ArchivePatchCoreRejectsMissingEntries", TestArchivePatchCoreRejectsMissingEntries},
459
+ {"ArchivePatchCoreSupportsCustomBundlePatchEntry", TestArchivePatchCoreSupportsCustomBundlePatchEntry},
460
+ };
461
+
462
+ for (const auto& test : tests) {
463
+ try {
464
+ test.second();
465
+ std::fprintf(stdout, "[PASS] %s\n", test.first.c_str());
466
+ } catch (const std::exception& error) {
467
+ std::fprintf(stderr, "[FAIL] %s: %s\n", test.first.c_str(), error.what());
468
+ return 1;
469
+ }
470
+ }
471
+
472
+ return 0;
473
+ }