react-native-update 10.37.19 → 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 (132) 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/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +88 -40
  18. package/cpp/patch_core/archive_patch_core.cpp +125 -0
  19. package/cpp/patch_core/archive_patch_core.h +59 -0
  20. package/cpp/patch_core/patch_core.cpp +533 -0
  21. package/cpp/patch_core/patch_core.h +68 -0
  22. package/cpp/patch_core/patch_core_android.cpp +112 -0
  23. package/cpp/patch_core/state_core.cpp +110 -0
  24. package/cpp/patch_core/state_core.h +58 -0
  25. package/cpp/patch_core/tests/patch_core_test.cpp +473 -0
  26. package/cpp/patch_core/update_core_android.cpp +469 -0
  27. package/harmony/pushy.har +0 -0
  28. package/ios/RCTPushy/RCTPushy.mm +233 -143
  29. package/package.json +17 -15
  30. package/react-native-update.podspec +3 -0
  31. package/scripts/build-harmony-har.js +12 -0
  32. package/scripts/prepublish.ts +49 -3
  33. package/scripts/prune-host-stl.sh +6 -0
  34. package/scripts/test-patch-core.sh +39 -0
  35. package/src/client.ts +129 -76
  36. package/src/core.ts +2 -1
  37. package/src/endpoint.ts +171 -0
  38. package/src/utils.ts +40 -27
  39. package/android/jni/lzma/DOC/7zC.txt +0 -187
  40. package/android/jni/lzma/DOC/7zFormat.txt +0 -469
  41. package/android/jni/lzma/DOC/Methods.txt +0 -173
  42. package/android/jni/lzma/DOC/installer.txt +0 -166
  43. package/android/jni/lzma/DOC/lzma-history.txt +0 -446
  44. package/android/jni/lzma/DOC/lzma-sdk.txt +0 -357
  45. package/android/jni/lzma/DOC/lzma-specification.txt +0 -1176
  46. package/android/jni/lzma/DOC/lzma.txt +0 -328
  47. package/android/jni/lzma/bin/7zS2.sfx +0 -0
  48. package/android/jni/lzma/bin/7zS2con.sfx +0 -0
  49. package/android/jni/lzma/bin/7zSD.sfx +0 -0
  50. package/android/jni/lzma/bin/7zdec.exe +0 -0
  51. package/android/jni/lzma/bin/7zr.exe +0 -0
  52. package/android/jni/lzma/bin/installer/config.txt +0 -5
  53. package/android/jni/lzma/bin/installer/cr.bat +0 -5
  54. package/android/jni/lzma/bin/lzma.exe +0 -0
  55. package/android/jni/lzma/bin/x64/7zr.exe +0 -0
  56. package/error.js +0 -1609
  57. package/harmony/har-wrapper/AppScope/app.json5 +0 -8
  58. package/harmony/har-wrapper/build-profile.json5 +0 -35
  59. package/harmony/har-wrapper/hvigor/hvigor-config.json5 +0 -5
  60. package/harmony/har-wrapper/hvigorfile.ts +0 -6
  61. package/harmony/har-wrapper/oh-package.json5 +0 -4
  62. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-77b153ce45aba0ed28ef.json +0 -1415
  63. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-b65a07793384e0ce3e08.json +0 -809
  64. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-ce0e89410afd8bf3a057.json +0 -60
  65. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json +0 -14
  66. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-03-18T12-02-38-0668.json +0 -89
  67. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-rnupdate-Release-267153624504c9c3ffdd.json +0 -222
  68. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.ninja_deps +0 -0
  69. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.ninja_log +0 -8
  70. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeCache.txt +0 -415
  71. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake +0 -74
  72. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake +0 -85
  73. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_C.bin +0 -0
  74. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
  75. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake +0 -15
  76. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c +0 -880
  77. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.o +0 -0
  78. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +0 -869
  79. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o +0 -0
  80. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeConfigureLog.yaml +0 -388
  81. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt +0 -3
  82. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache +0 -1
  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/pushy.c.o +0 -0
  89. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rules.ninja +0 -64
  90. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/additional_project_files.txt +0 -0
  91. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/build.ninja +0 -206
  92. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/build_file_index.txt +0 -1
  93. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/cmake_install.cmake +0 -54
  94. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/compile_commands.json +0 -38
  95. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/configure_fingerprint.json +0 -1
  96. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/hvigor_native_config.json +0 -1
  97. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/metadata_generation_command.txt +0 -17
  98. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/native_work_dir.txt +0 -1
  99. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/output.log +0 -14
  100. package/harmony/pushy/.cxx/default/default/release/hvigor/arm64-v8a/summary.cmake +0 -0
  101. package/harmony/pushy/BuildProfile.ets +0 -17
  102. package/harmony/pushy/OAT.xml +0 -38
  103. package/harmony/pushy/README.md +0 -0
  104. package/harmony/pushy/build-profile.json5 +0 -15
  105. package/harmony/pushy/hvigor-plugin.ts +0 -34
  106. package/harmony/pushy/hvigorfile.ts +0 -1
  107. package/harmony/pushy/index.ets +0 -2
  108. package/harmony/pushy/oh-package-lock.json5 +0 -20
  109. package/harmony/pushy/oh-package.json5 +0 -13
  110. package/harmony/pushy/src/main/cpp/CMakeLists.txt +0 -51
  111. package/harmony/pushy/src/main/cpp/PushyPackage.h +0 -55
  112. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +0 -142
  113. package/harmony/pushy/src/main/cpp/PushyTurboModule.h +0 -38
  114. package/harmony/pushy/src/main/cpp/pushy.c +0 -117
  115. package/harmony/pushy/src/main/cpp/pushy.h +0 -8
  116. package/harmony/pushy/src/main/ets/DownloadTask.ts +0 -570
  117. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +0 -19
  118. package/harmony/pushy/src/main/ets/EventHub.ts +0 -39
  119. package/harmony/pushy/src/main/ets/Logger.ts +0 -52
  120. package/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +0 -50
  121. package/harmony/pushy/src/main/ets/PushyPackage.ts +0 -22
  122. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +0 -171
  123. package/harmony/pushy/src/main/ets/SaveFile.ts +0 -34
  124. package/harmony/pushy/src/main/ets/UpdateContext.ts +0 -262
  125. package/harmony/pushy/src/main/ets/UpdateModuleImpl.ts +0 -123
  126. package/harmony/pushy/src/main/module.json5 +0 -7
  127. package/harmony/pushy/src/main/resources/base/element/string.json +0 -8
  128. package/harmony/pushy/src/main/resources/en_US/element/string.json +0 -8
  129. package/harmony/pushy/src/main/resources/zh_CN/element/string.json +0 -8
  130. package/harmony/pushy/ts.ts +0 -3
  131. package/src/__tests__/core.test.ts +0 -103
  132. 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
+ }