activity_sdk_tree_publication.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. #include "activity_sdk_tree_publication.h"
  2. #include <Windows.h>
  3. #include <array>
  4. #include <limits>
  5. #include <string>
  6. #include <string_view>
  7. namespace sunrise::client::content::activity::sdk_generation::tree_publication {
  8. namespace {
  9. // Both separators end a path component.
  10. constexpr std::wstring_view kSeparators = L"\\/";
  11. // Suffixes of the sibling names one publication owns.
  12. constexpr std::wstring_view kBackupSuffix = L".activity-sdk-backup";
  13. constexpr std::wstring_view kPendingSuffix = L".activity-sdk-publication.pending";
  14. constexpr std::wstring_view kCommittedSuffix = L".activity-sdk-publication.committed";
  15. // Marker magic; the bytes spell AST1.
  16. constexpr std::uint32_t kMarkerMagic = 0x31545341U;
  17. struct Marker final {
  18. std::uint32_t magic{kMarkerMagic};
  19. std::uint8_t hadOutput{};
  20. std::array<std::uint8_t, 3> reserved{};
  21. };
  22. /** Resolves one null-terminated lexical path into normalized absolute storage. */
  23. [[nodiscard]] bool full_path(const wchar_t* input, std::wstring& output) noexcept {
  24. output.clear();
  25. if (input == nullptr || input[0] == L'\0') {
  26. return false;
  27. }
  28. const DWORD needed = GetFullPathNameW(input, 0, nullptr, nullptr);
  29. if (needed == 0) {
  30. return false;
  31. }
  32. try {
  33. std::wstring pending(static_cast<std::size_t>(needed), L'\0');
  34. const DWORD written = GetFullPathNameW(input, needed, pending.data(), nullptr);
  35. if (written == 0 || written >= needed) {
  36. return false;
  37. }
  38. pending.resize(written);
  39. while (pending.size() > 3U && (pending.back() == L'\\' || pending.back() == L'/')) {
  40. pending.pop_back();
  41. }
  42. output = std::move(pending);
  43. return true;
  44. } catch (...) {
  45. output.clear();
  46. return false;
  47. }
  48. }
  49. /** Requires one existing ordinary directory. */
  50. [[nodiscard]] bool is_directory(const wchar_t* path) noexcept {
  51. const DWORD attributes = GetFileAttributesW(path);
  52. return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
  53. }
  54. /** Requires one existing ordinary file. */
  55. [[nodiscard]] bool is_file(const wchar_t* path) noexcept {
  56. const DWORD attributes = GetFileAttributesW(path);
  57. return attributes != INVALID_FILE_ATTRIBUTES
  58. && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
  59. }
  60. /** Requires every existing drive-path directory component to be ordinary. */
  61. [[nodiscard]] bool ordinary_ancestry(const std::wstring& directory) noexcept {
  62. if (directory.size() < 3U || directory[1] != L':' || directory[2] != L'\\'
  63. || !is_directory(directory.substr(0, 3U).c_str())) {
  64. return false;
  65. }
  66. std::size_t cursor = 3U;
  67. while (cursor < directory.size()) {
  68. const std::size_t separator = directory.find(L'\\', cursor);
  69. const std::size_t end = separator == std::wstring::npos ? directory.size() : separator;
  70. if (!is_directory(directory.substr(0, end).c_str())) {
  71. return false;
  72. }
  73. if (separator == std::wstring::npos) {
  74. break;
  75. }
  76. cursor = separator + 1U;
  77. }
  78. return true;
  79. }
  80. [[nodiscard]] bool same_text(std::wstring_view left, std::wstring_view right) noexcept {
  81. return left.size() == right.size()
  82. && CompareStringOrdinal(left.data(),
  83. static_cast<int>(left.size()),
  84. right.data(),
  85. static_cast<int>(right.size()),
  86. TRUE)
  87. == CSTR_EQUAL;
  88. }
  89. /** Splits one path at its last separator. @return False when there is no interior separator. */
  90. [[nodiscard]] bool
  91. split(std::wstring_view path, std::wstring_view& parent, std::wstring_view& leaf) noexcept {
  92. const std::size_t separator = path.find_last_of(kSeparators);
  93. if (path.empty() || separator == std::wstring_view::npos || separator == 0
  94. || separator + 1 >= path.size()) {
  95. return false;
  96. }
  97. parent = path.substr(0, separator);
  98. leaf = path.substr(separator + 1);
  99. return true;
  100. }
  101. [[nodiscard]] bool default_move(void*, const wchar_t* source, const wchar_t* target) noexcept {
  102. return MoveFileExW(source, target, MOVEFILE_WRITE_THROUGH) != FALSE;
  103. }
  104. /** Deletes one directory tree. Refuses to follow a reparse point. @return True when gone. */
  105. [[nodiscard]] bool remove_tree(const std::wstring& path) noexcept {
  106. const DWORD attributes = GetFileAttributesW(path.c_str());
  107. if (attributes == INVALID_FILE_ATTRIBUTES) {
  108. const DWORD error = GetLastError();
  109. return error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND;
  110. }
  111. if ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
  112. return (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0
  113. ? RemoveDirectoryW(path.c_str()) != FALSE
  114. : DeleteFileW(path.c_str()) != FALSE;
  115. }
  116. if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
  117. return DeleteFileW(path.c_str()) != FALSE;
  118. }
  119. WIN32_FIND_DATAW entry{};
  120. const std::wstring search = path + L"\\*";
  121. const HANDLE handle = FindFirstFileW(search.c_str(), &entry);
  122. if (handle == INVALID_HANDLE_VALUE) {
  123. return GetLastError() == ERROR_FILE_NOT_FOUND && RemoveDirectoryW(path.c_str()) != FALSE;
  124. }
  125. bool complete = true;
  126. do {
  127. const std::wstring_view name(entry.cFileName);
  128. if (name == L"." || name == L"..") {
  129. continue;
  130. }
  131. const std::wstring child = path + L"\\" + std::wstring(name);
  132. if ((entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0
  133. && (entry.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) {
  134. complete = remove_tree(child) && complete;
  135. } else if ((entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
  136. complete = RemoveDirectoryW(child.c_str()) != FALSE && complete;
  137. } else {
  138. complete = DeleteFileW(child.c_str()) != FALSE && complete;
  139. }
  140. } while (FindNextFileW(handle, &entry) != FALSE);
  141. complete = FindClose(handle) != FALSE && complete;
  142. return RemoveDirectoryW(path.c_str()) != FALSE && complete;
  143. }
  144. /** Appends one fixed transaction suffix without exposing a partially written result. */
  145. [[nodiscard]] bool
  146. sibling_path(std::wstring_view output, std::wstring_view suffix, std::wstring& sibling) noexcept {
  147. try {
  148. sibling.assign(output);
  149. sibling.append(suffix);
  150. return true;
  151. } catch (...) {
  152. sibling.clear();
  153. return false;
  154. }
  155. }
  156. /** Writes and flushes one bounded transaction marker before moving either tree. */
  157. [[nodiscard]] bool write_marker(const std::wstring& path, bool hadOutput) noexcept {
  158. const HANDLE file = CreateFileW(
  159. path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_HIDDEN, nullptr);
  160. if (file == INVALID_HANDLE_VALUE) {
  161. return false;
  162. }
  163. const Marker marker{kMarkerMagic, static_cast<std::uint8_t>(hadOutput ? 1U : 0U), {}};
  164. DWORD written = 0;
  165. const bool complete = WriteFile(file, &marker, sizeof marker, &written, nullptr) != FALSE
  166. && written == sizeof marker && FlushFileBuffers(file) != FALSE;
  167. const bool closed = CloseHandle(file) != FALSE;
  168. if (!complete || !closed) {
  169. (void)DeleteFileW(path.c_str());
  170. return false;
  171. }
  172. return true;
  173. }
  174. /** Reads one exact marker without accepting trailing bytes or noncanonical fields. */
  175. [[nodiscard]] bool read_marker(const std::wstring& path, Marker& output) noexcept {
  176. output = {};
  177. if (!is_file(path.c_str())) {
  178. return false;
  179. }
  180. const HANDLE file = CreateFileW(path.c_str(),
  181. GENERIC_READ,
  182. FILE_SHARE_READ | FILE_SHARE_DELETE,
  183. nullptr,
  184. OPEN_EXISTING,
  185. FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,
  186. nullptr);
  187. if (file == INVALID_HANDLE_VALUE) {
  188. return false;
  189. }
  190. DWORD read = 0;
  191. std::byte trailing{};
  192. DWORD trailingRead = 0;
  193. const bool complete =
  194. ReadFile(file, &output, sizeof output, &read, nullptr) != FALSE && read == sizeof output
  195. && ReadFile(file, &trailing, 1, &trailingRead, nullptr) != FALSE && trailingRead == 0;
  196. const bool closed = CloseHandle(file) != FALSE;
  197. return complete && closed && output.magic == kMarkerMagic && output.hadOutput <= 1U
  198. && output.reserved == std::array<std::uint8_t, 3>{};
  199. }
  200. /** Accepts one absent tree or requires an ordinary directory leaf. */
  201. [[nodiscard]] bool tree_state(const std::wstring& path, bool& exists) noexcept {
  202. exists = false;
  203. const DWORD attributes = GetFileAttributesW(path.c_str());
  204. if (attributes == INVALID_FILE_ATTRIBUTES) {
  205. const DWORD error = GetLastError();
  206. return error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND;
  207. }
  208. exists = true;
  209. return (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
  210. }
  211. /** Restores or finalizes one interrupted transaction before any new publication begins. */
  212. [[nodiscard]] bool recover(const std::wstring& output,
  213. const std::wstring& backup,
  214. const std::wstring& pending,
  215. const std::wstring& committed) noexcept {
  216. const DWORD pendingAttributes = GetFileAttributesW(pending.c_str());
  217. const DWORD committedAttributes = GetFileAttributesW(committed.c_str());
  218. if (pendingAttributes != INVALID_FILE_ATTRIBUTES
  219. && committedAttributes != INVALID_FILE_ATTRIBUTES) {
  220. return false;
  221. }
  222. bool outputExists = false;
  223. bool backupExists = false;
  224. if (!tree_state(output, outputExists) || !tree_state(backup, backupExists)) {
  225. return false;
  226. }
  227. if (committedAttributes != INVALID_FILE_ATTRIBUTES) {
  228. Marker marker{};
  229. if (!read_marker(committed, marker) || !outputExists) {
  230. return false;
  231. }
  232. if (backupExists && !remove_tree(backup)) {
  233. return false;
  234. }
  235. return DeleteFileW(committed.c_str()) != FALSE;
  236. }
  237. if (pendingAttributes == INVALID_FILE_ATTRIBUTES) {
  238. return !backupExists;
  239. }
  240. Marker marker{};
  241. if (!read_marker(pending, marker)) {
  242. return false;
  243. }
  244. if (backupExists) {
  245. if ((outputExists && !remove_tree(output))
  246. || !default_move(nullptr, backup.c_str(), output.c_str())) {
  247. return false;
  248. }
  249. } else if (marker.hadOutput != 0 && !outputExists) {
  250. return false;
  251. }
  252. return DeleteFileW(pending.c_str()) != FALSE;
  253. }
  254. } // namespace
  255. /** @return The stable log name of one publication status. */
  256. const char* status_name(Status value) noexcept {
  257. switch (value) {
  258. case Status::ready:
  259. return "ready";
  260. case Status::invalidInput:
  261. return "invalid_input";
  262. case Status::backupCollision:
  263. return "backup_collision";
  264. case Status::backupFailure:
  265. return "backup_failure";
  266. case Status::commitFailure:
  267. return "commit_failure";
  268. case Status::rollbackFailure:
  269. return "rollback_failure";
  270. }
  271. return "invalid_input";
  272. }
  273. /** Moves the stage tree over the output tree, restoring the old tree if the commit fails. */
  274. Status publish(const wchar_t* stage,
  275. const wchar_t* output,
  276. MoveOperation move,
  277. void* moveContext) noexcept {
  278. if (stage == nullptr || stage[0] == L'\0' || output == nullptr || output[0] == L'\0') {
  279. return Status::invalidInput;
  280. }
  281. std::wstring canonicalStage;
  282. std::wstring canonicalOutput;
  283. if (!full_path(stage, canonicalStage) || !full_path(output, canonicalOutput)) {
  284. return Status::invalidInput;
  285. }
  286. const std::wstring_view stageView(canonicalStage);
  287. const std::wstring_view outputView(canonicalOutput);
  288. std::wstring_view stageParent;
  289. std::wstring_view stageLeaf;
  290. std::wstring_view outputParent;
  291. std::wstring_view outputLeaf;
  292. if (!split(stageView, stageParent, stageLeaf) || !split(outputView, outputParent, outputLeaf)
  293. || !same_text(stageParent, outputParent) || same_text(stageLeaf, outputLeaf)
  294. || stageLeaf == L"." || stageLeaf == L".." || outputLeaf == L"." || outputLeaf == L"..") {
  295. return Status::invalidInput;
  296. }
  297. std::wstring parent;
  298. try {
  299. parent.assign(stageParent);
  300. } catch (...) {
  301. return Status::invalidInput;
  302. }
  303. if (!ordinary_ancestry(parent) || !is_directory(canonicalStage.c_str())) {
  304. return Status::invalidInput;
  305. }
  306. std::wstring backup;
  307. std::wstring pending;
  308. std::wstring committed;
  309. if (!sibling_path(outputView, kBackupSuffix, backup)
  310. || !sibling_path(outputView, kPendingSuffix, pending)
  311. || !sibling_path(outputView, kCommittedSuffix, committed)
  312. || !recover(canonicalOutput, backup, pending, committed)) {
  313. return Status::invalidInput;
  314. }
  315. if (GetFileAttributesW(backup.c_str()) != INVALID_FILE_ATTRIBUTES) {
  316. return Status::backupCollision;
  317. }
  318. bool hadOutput = false;
  319. if (!tree_state(canonicalOutput, hadOutput) || !write_marker(pending, hadOutput)) {
  320. return Status::invalidInput;
  321. }
  322. const MoveOperation rename = move != nullptr ? move : &default_move;
  323. if (hadOutput && !rename(moveContext, canonicalOutput.c_str(), backup.c_str())) {
  324. (void)DeleteFileW(pending.c_str());
  325. return Status::backupFailure;
  326. }
  327. if (!rename(moveContext, canonicalStage.c_str(), canonicalOutput.c_str())) {
  328. if (hadOutput && !rename(moveContext, backup.c_str(), canonicalOutput.c_str())) {
  329. return Status::rollbackFailure;
  330. }
  331. (void)DeleteFileW(pending.c_str());
  332. return Status::commitFailure;
  333. }
  334. if (!rename(moveContext, pending.c_str(), committed.c_str())) {
  335. const bool removed = remove_tree(canonicalOutput);
  336. const bool restored =
  337. !hadOutput || rename(moveContext, backup.c_str(), canonicalOutput.c_str());
  338. if (removed && restored) {
  339. (void)DeleteFileW(pending.c_str());
  340. return Status::commitFailure;
  341. }
  342. return Status::rollbackFailure;
  343. }
  344. if ((!hadOutput || remove_tree(backup)) && DeleteFileW(committed.c_str()) != FALSE) {
  345. return Status::ready;
  346. }
  347. return Status::ready;
  348. }
  349. bool discard(const wchar_t* stage) noexcept {
  350. if (stage == nullptr || stage[0] == L'\0') {
  351. return false;
  352. }
  353. try {
  354. return remove_tree(std::wstring(stage));
  355. } catch (...) {
  356. return false;
  357. }
  358. }
  359. } // namespace sunrise::client::content::activity::sdk_generation::tree_publication