activity_sdk_tree_publication.cpp 15 KB

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