activity_sdk_one_shot.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. #include "activity_sdk_one_shot.h"
  2. #include <Windows.h>
  3. #include <algorithm>
  4. #include <array>
  5. #include <cwchar>
  6. #include <limits>
  7. #include <string>
  8. #include <string_view>
  9. #include <utility>
  10. #include "../../../core/filesystem/path.h"
  11. #include "../../../state/content_manifest/content_manifest_state_runtime.h"
  12. #include "../items/packages/internal.h"
  13. #include "activity_sdk_estate_validation.h"
  14. #include "activity_sdk_tree_publication.h"
  15. namespace sunrise::client::content::activity::sdk_generation::one_shot {
  16. namespace {
  17. namespace generated = state::activity_sdk::generated_world;
  18. namespace publication = tree_publication;
  19. namespace estate = estate_validation;
  20. // A stage name carries three sequence words so two passes never collide.
  21. constexpr std::wstring_view kStageSuffix = L".stage.%08lX.%08lX.%08lX";
  22. volatile LONG g_stageSequence{};
  23. [[nodiscard]] bool cancelled(OfflineCancelProbe probe, void* context) noexcept {
  24. return probe != nullptr && probe(context);
  25. }
  26. void report(OfflineProgressSink sink,
  27. void* context,
  28. state::activity_sdk::generation::Status status,
  29. std::uint32_t current,
  30. std::uint32_t total,
  31. std::string_view detail) noexcept {
  32. if (sink != nullptr) {
  33. sink(context, {status, current, total, 0, detail});
  34. }
  35. }
  36. [[nodiscard]] bool equal_path(std::wstring_view left, std::wstring_view right) noexcept {
  37. return left.size() == right.size()
  38. && CompareStringOrdinal(left.data(),
  39. static_cast<int>(left.size()),
  40. right.data(),
  41. static_cast<int>(right.size()),
  42. TRUE)
  43. == CSTR_EQUAL;
  44. }
  45. [[nodiscard]] bool path_prefix(std::wstring_view parent, std::wstring_view child) noexcept {
  46. return child.size() > parent.size() && child[parent.size()] == L'\\'
  47. && CompareStringOrdinal(parent.data(),
  48. static_cast<int>(parent.size()),
  49. child.data(),
  50. static_cast<int>(parent.size()),
  51. TRUE)
  52. == CSTR_EQUAL;
  53. }
  54. [[nodiscard]] bool overlaps(std::wstring_view left, std::wstring_view right) noexcept {
  55. return equal_path(left, right) || path_prefix(left, right) || path_prefix(right, left);
  56. }
  57. /** Resolves one bounded view through an owned null-terminated Win32 input. */
  58. [[nodiscard]] bool full_path(std::wstring_view input, std::wstring& output) noexcept {
  59. output.clear();
  60. if (input.empty() || input.size() > (std::numeric_limits<DWORD>::max)() - 1U) {
  61. return false;
  62. }
  63. try {
  64. const std::wstring terminated(input);
  65. const DWORD needed = GetFullPathNameW(terminated.c_str(), 0, nullptr, nullptr);
  66. if (needed == 0) {
  67. return false;
  68. }
  69. std::wstring buffer(static_cast<std::size_t>(needed), L'\0');
  70. const DWORD written = GetFullPathNameW(terminated.c_str(), needed, buffer.data(), nullptr);
  71. if (written == 0 || written >= needed) {
  72. return false;
  73. }
  74. buffer.resize(written);
  75. while (buffer.size() > 3U && (buffer.back() == L'\\' || buffer.back() == L'/')) {
  76. buffer.pop_back();
  77. }
  78. output = std::move(buffer);
  79. return true;
  80. } catch (...) {
  81. output.clear();
  82. return false;
  83. }
  84. }
  85. /** Requires every existing drive-path directory component to be ordinary. */
  86. [[nodiscard]] bool ordinary_ancestry(const std::wstring& directory) noexcept {
  87. if (directory.size() < 3U || directory[1] != L':' || directory[2] != L'\\') {
  88. return false;
  89. }
  90. try {
  91. std::size_t cursor = 3U;
  92. while (true) {
  93. const std::size_t separator = directory.find(L'\\', cursor);
  94. const std::size_t end = separator == std::wstring::npos ? directory.size() : separator;
  95. const std::wstring prefix = directory.substr(0, end);
  96. const DWORD attributes = GetFileAttributesW(prefix.c_str());
  97. if (attributes == INVALID_FILE_ATTRIBUTES
  98. || (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
  99. return false;
  100. }
  101. if (separator == std::wstring::npos) {
  102. return true;
  103. }
  104. cursor = separator + 1U;
  105. }
  106. } catch (...) {
  107. return false;
  108. }
  109. }
  110. /** Canonicalizes one existing directory. */
  111. [[nodiscard]] bool final_directory_path(std::wstring_view input, std::wstring& output) noexcept {
  112. std::wstring lexical;
  113. if (!full_path(input, lexical) || !ordinary_ancestry(lexical)) {
  114. return false;
  115. }
  116. const DWORD attributes = GetFileAttributesW(lexical.c_str());
  117. if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
  118. return false;
  119. }
  120. const HANDLE directory = CreateFileW(lexical.c_str(),
  121. FILE_READ_ATTRIBUTES,
  122. FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
  123. nullptr,
  124. OPEN_EXISTING,
  125. FILE_FLAG_BACKUP_SEMANTICS,
  126. nullptr);
  127. if (directory == INVALID_HANDLE_VALUE) {
  128. return false;
  129. }
  130. const DWORD needed =
  131. GetFinalPathNameByHandleW(directory, nullptr, 0, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
  132. bool complete = false;
  133. if (needed != 0) {
  134. try {
  135. std::wstring buffer(static_cast<std::size_t>(needed), L'\0');
  136. const DWORD written = GetFinalPathNameByHandleW(
  137. directory, buffer.data(), needed, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
  138. if (written != 0 && written < needed) {
  139. buffer.resize(written);
  140. // Windows returns the extended prefix; callers store the plain path.
  141. if (buffer.starts_with(core::path::kExtendedPrefix)) {
  142. buffer.erase(0, core::path::kExtendedPrefix.size());
  143. }
  144. while (buffer.size() > 3U && (buffer.back() == L'\\' || buffer.back() == L'/')) {
  145. buffer.pop_back();
  146. }
  147. output = std::move(buffer);
  148. complete = true;
  149. }
  150. } catch (...) {
  151. complete = false;
  152. }
  153. }
  154. (void)CloseHandle(directory);
  155. return complete;
  156. }
  157. /** Canonicalizes an existing output or a missing direct child of an ordinary parent. */
  158. [[nodiscard]] bool canonical_output(std::wstring_view input, std::wstring& output) noexcept {
  159. std::wstring lexical;
  160. if (!full_path(input, lexical) || lexical.size() <= 3U) {
  161. return false;
  162. }
  163. const DWORD attributes = GetFileAttributesW(lexical.c_str());
  164. if (attributes != INVALID_FILE_ATTRIBUTES) {
  165. return (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0
  166. && final_directory_path(lexical, output);
  167. }
  168. const std::size_t separator = lexical.find_last_of(L"\\/");
  169. if (separator == std::wstring::npos || separator + 1 >= lexical.size()) {
  170. return false;
  171. }
  172. std::wstring parent;
  173. if (!final_directory_path(std::wstring_view(lexical).substr(0, separator), parent)) {
  174. return false;
  175. }
  176. try {
  177. output = parent;
  178. output.push_back(L'\\');
  179. output.append(lexical.substr(separator + 1));
  180. return true;
  181. } catch (...) {
  182. output.clear();
  183. return false;
  184. }
  185. }
  186. /** Allocates one process-unique sibling used exclusively by the current generation. */
  187. [[nodiscard]] bool allocate_stage(const std::wstring& output, std::wstring& stage) noexcept {
  188. std::array<wchar_t, 80> suffix{};
  189. const DWORD sequence = static_cast<DWORD>(InterlockedIncrement(&g_stageSequence));
  190. const int length = std::swprintf(suffix.data(),
  191. suffix.size(),
  192. kStageSuffix.data(),
  193. GetCurrentProcessId(),
  194. GetCurrentThreadId(),
  195. sequence);
  196. if (length <= 0 || static_cast<std::size_t>(length) >= suffix.size()) {
  197. return false;
  198. }
  199. try {
  200. stage = output;
  201. stage.append(suffix.data(), static_cast<std::size_t>(length));
  202. } catch (...) {
  203. stage.clear();
  204. return false;
  205. }
  206. return CreateDirectoryW(stage.c_str(), nullptr) != FALSE;
  207. }
  208. /** Maps the retained-result layer without exposing its internal status partitions. */
  209. [[nodiscard]] Status map_estate_status(estate::Status value) noexcept {
  210. switch (value) {
  211. case estate::Status::ready:
  212. return Status::ready;
  213. case estate::Status::cancelled:
  214. return Status::cancelled;
  215. case estate::Status::invalidInput:
  216. return Status::invalidInput;
  217. case estate::Status::notGenerated:
  218. return Status::notGenerated;
  219. case estate::Status::publication:
  220. return Status::publication;
  221. }
  222. return Status::notGenerated;
  223. }
  224. /** Copies one retained generation result into the process-local result contract. */
  225. void copy_estate_result(const estate::Result& source, Result& output) noexcept {
  226. output = {};
  227. output.build.scenarioCount = source.scenarioCount;
  228. output.build.activityRootCount = source.activityRootCount;
  229. output.build.activityCount = source.activityCount;
  230. output.build.builtScenarioCount = source.builtScenarioCount;
  231. output.build.reusedScenarioCount = source.reusedScenarioCount;
  232. output.payloadSha256 = source.payloadSha256;
  233. output.packBytes = source.packBytes;
  234. output.luaFiles = source.luaFileCount;
  235. }
  236. } // namespace
  237. /** Returns one bounded diagnostic name without including any source or reader material. */
  238. const char* status_name(Status value) noexcept {
  239. switch (value) {
  240. case Status::ready:
  241. return "ready";
  242. case Status::cancelled:
  243. return "cancelled";
  244. case Status::busy:
  245. return "busy";
  246. case Status::invalidInput:
  247. return "invalid_input";
  248. case Status::stageAllocation:
  249. return "stage_allocation";
  250. case Status::generation:
  251. return "generation";
  252. case Status::notGenerated:
  253. return "not_generated";
  254. case Status::publication:
  255. return "publication";
  256. }
  257. return "invalid_input";
  258. }
  259. /** Runs the internal borrowed-source generator and publishes its isolated tree once. */
  260. Status run(const Request& request,
  261. OfflineCancelProbe cancel,
  262. void* cancelContext,
  263. OfflineProgressSink progress,
  264. void* progressContext,
  265. Result& output) noexcept {
  266. output = {};
  267. std::wstring packages;
  268. std::wstring target;
  269. if (!final_directory_path(request.packageDirectory, packages)
  270. || !canonical_output(request.outputArtifactDirectory, target)
  271. || overlaps(packages, target)) {
  272. return Status::invalidInput;
  273. }
  274. estate::forget_canonical(target);
  275. std::wstring stage;
  276. if (!allocate_stage(target, stage)) {
  277. return Status::stageAllocation;
  278. }
  279. const OfflineBuildRequest buildRequest{
  280. request.sourceFingerprint, request.keys, packages, {}, stage};
  281. OfflineBuildResult build{};
  282. const OfflineBuildStatus built =
  283. build_offline(buildRequest, cancel, cancelContext, progress, progressContext, build);
  284. if (built != OfflineBuildStatus::ready) {
  285. (void)publication::discard(stage.c_str());
  286. switch (built) {
  287. case OfflineBuildStatus::cancelled:
  288. return Status::cancelled;
  289. case OfflineBuildStatus::busy:
  290. return Status::busy;
  291. case OfflineBuildStatus::invalidInput:
  292. return Status::invalidInput;
  293. case OfflineBuildStatus::ready:
  294. break;
  295. case OfflineBuildStatus::failed:
  296. return Status::generation;
  297. }
  298. return Status::generation;
  299. }
  300. if (cancelled(cancel, cancelContext)) {
  301. (void)publication::discard(stage.c_str());
  302. return Status::cancelled;
  303. }
  304. report(progress,
  305. progressContext,
  306. state::activity_sdk::generation::Status::publishing,
  307. build.scenarioCount,
  308. build.scenarioCount,
  309. "replacing complete generated SDK tree");
  310. const publication::Status publicationStatus =
  311. publication::publish(stage.c_str(), target.c_str(), nullptr, nullptr);
  312. if (publicationStatus != publication::Status::ready) {
  313. (void)publication::discard(stage.c_str());
  314. return Status::publication;
  315. }
  316. output.build = build;
  317. output.payloadSha256 = build.payloadSha256;
  318. output.packBytes = build.packBytes;
  319. output.luaFiles = build.luaFiles;
  320. const estate::Result retained{build.scenarioCount,
  321. build.activityRootCount,
  322. build.activityCount,
  323. build.builtScenarioCount,
  324. build.reusedScenarioCount,
  325. build.luaFiles,
  326. build.payloadSha256,
  327. build.packBytes};
  328. estate::remember_canonical(std::move(target), request.sourceFingerprint, retained);
  329. report(progress,
  330. progressContext,
  331. state::activity_sdk::generation::Status::ready,
  332. build.scenarioCount,
  333. build.scenarioCount,
  334. "generated SDK tree is ready");
  335. return Status::ready;
  336. }
  337. /** Acquires the adopted process-local source, runs once, and securely clears its sole key copy. */
  338. Status run_process_local(std::wstring_view packageDirectory,
  339. std::wstring_view cacheArtifactDirectory,
  340. std::wstring_view outputArtifactDirectory,
  341. OfflineCancelProbe cancel,
  342. void* cancelContext,
  343. OfflineProgressSink progress,
  344. void* progressContext,
  345. Result& output) noexcept {
  346. output = {};
  347. static_cast<void>(cacheArtifactDirectory);
  348. generated::Digest fingerprint{};
  349. middleware::content::packages::reader::BlockKeys keys{};
  350. // The SDK keys on the installed identity; mods never change it.
  351. if (!state::content_manifest::installed_fingerprint(fingerprint)
  352. || !items::packages::collect_keys(keys)) {
  353. SecureZeroMemory(&keys, sizeof keys);
  354. return Status::invalidInput;
  355. }
  356. const Request request{fingerprint, &keys, packageDirectory, {}, outputArtifactDirectory};
  357. const Status status = run(request, cancel, cancelContext, progress, progressContext, output);
  358. SecureZeroMemory(&keys, sizeof keys);
  359. return status;
  360. }
  361. /** Returns one process-local result without reopening generated files. */
  362. Status accept_generated(std::wstring_view artifactDirectory,
  363. const generated::Digest& sourceFingerprint,
  364. OfflineCancelProbe cancel,
  365. void* cancelContext,
  366. Result& output) noexcept {
  367. output = {};
  368. estate::Result retained{};
  369. const estate::Status status = estate::accept_generated(
  370. artifactDirectory, sourceFingerprint, nullptr, cancel, cancelContext, retained);
  371. if (status == estate::Status::ready) {
  372. copy_estate_result(retained, output);
  373. }
  374. return map_estate_status(status);
  375. }
  376. /** Publishes one retained sibling without reopening generated files. */
  377. Status publish_generated(std::wstring_view stageArtifactDirectory,
  378. std::wstring_view outputArtifactDirectory,
  379. const generated::Digest& sourceFingerprint,
  380. OfflineCancelProbe cancel,
  381. void* cancelContext,
  382. Result& output) noexcept {
  383. output = {};
  384. estate::Result retained{};
  385. const estate::Status status = estate::publish_generated(stageArtifactDirectory,
  386. outputArtifactDirectory,
  387. sourceFingerprint,
  388. cancel,
  389. cancelContext,
  390. retained);
  391. if (status == estate::Status::ready) {
  392. copy_estate_result(retained, output);
  393. }
  394. return map_estate_status(status);
  395. }
  396. } // namespace sunrise::client::content::activity::sdk_generation::one_shot