yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
source_artifact_publisher.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <atomic>
6#include <cctype>
7#include <cerrno>
8#include <chrono>
9#include <cstdint>
10#include <cstring>
11#include <exception>
12#include <filesystem>
13#include <fstream>
14#include <iterator>
15#include <limits>
16#include <memory>
17#include <optional>
18#include <semaphore>
19#include <string>
20#include <string_view>
21#include <system_error>
22#include <utility>
23#include <vector>
24
25#if defined(__APPLE__) && !defined(YAZE_FORCE_PORTABLE_SOURCE_SHA256)
26#include <CommonCrypto/CommonDigest.h>
27#endif
28#if defined(__linux__) && defined(YAZE_SOURCE_SHA256_OPENSSL) && \
29 !defined(YAZE_FORCE_PORTABLE_SOURCE_SHA256)
30#include <openssl/evp.h>
31#endif
32
33#include "absl/status/status.h"
34#include "absl/strings/ascii.h"
35#include "absl/strings/str_cat.h"
36#include "absl/strings/str_format.h"
37#include "util/macro.h"
38
39#if defined(_WIN32)
40#ifndef NOMINMAX
41#define NOMINMAX
42#endif
43#include <windows.h>
44#if !defined(YAZE_FORCE_PORTABLE_SOURCE_SHA256)
45#include <bcrypt.h>
46#endif
47#elif !defined(__EMSCRIPTEN__)
48#include <fcntl.h>
49#include <sys/file.h>
50#include <sys/stat.h>
51#include <unistd.h>
52#endif
53
54namespace yaze::core {
55namespace {
56
57namespace fs = std::filesystem;
58
59std::string Capitalized(std::string value) {
60 if (!value.empty()) {
61 value.front() = static_cast<char>(
62 std::toupper(static_cast<unsigned char>(value.front())));
63 }
64 return value;
65}
66
68 fs::path target;
69 std::string content;
70 std::optional<std::string> original_content;
71 fs::path temp;
72 std::optional<fs::path> backup;
73 bool published = false;
74
75 bool IsReadOnlyGuard() const {
76 return original_content.has_value() && *original_content == content;
77 }
78};
79
80std::binary_semaphore& SourceArtifactWriteSemaphore() {
81 static std::binary_semaphore semaphore{1};
82 return semaphore;
83}
84
86 public:
87 static absl::StatusOr<std::unique_ptr<SourceArtifactWriteLocks>> Acquire(
88 const std::vector<fs::path>& lock_paths,
89 const SourceArtifactPublisherLabels& labels) {
90 if (lock_paths.empty()) {
91 return absl::InvalidArgumentError(
92 absl::StrFormat("%s publication requires at least one lock",
93 Capitalized(labels.subject)));
94 }
95 auto lock = std::unique_ptr<SourceArtifactWriteLocks>(
98 lock->process_lock_acquired_ = true;
99
100#if defined(_WIN32)
101 lock->handles_.reserve(lock_paths.size());
102 struct WindowsFileIdentity {
103 DWORD volume_serial;
104 DWORD file_index_high;
105 DWORD file_index_low;
106 };
107 std::vector<WindowsFileIdentity> lock_identities;
108 lock_identities.reserve(lock_paths.size());
109 for (const fs::path& path : lock_paths) {
110 HANDLE handle = CreateFileW(
111 path.wstring().c_str(), GENERIC_READ | GENERIC_WRITE,
112 FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS,
113 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, nullptr);
114 if (handle == INVALID_HANDLE_VALUE) {
115 const DWORD error = GetLastError();
116 return absl::PermissionDeniedError(absl::StrFormat(
117 "Cannot open %s lock %s: %s", labels.subject, path.string(),
118 std::error_code(static_cast<int>(error), std::system_category())
119 .message()));
120 }
121 BY_HANDLE_FILE_INFORMATION file_info = {};
122 if (!GetFileInformationByHandle(handle, &file_info)) {
123 const DWORD error = GetLastError();
124 CloseHandle(handle);
125 return absl::FailedPreconditionError(absl::StrFormat(
126 "Cannot inspect %s lock %s: %s", labels.subject, path.string(),
127 std::error_code(static_cast<int>(error), std::system_category())
128 .message()));
129 }
130 if ((file_info.dwFileAttributes &
131 (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)) != 0) {
132 CloseHandle(handle);
133 return absl::FailedPreconditionError(
134 absl::StrFormat("%s lock must be a regular file: %s",
135 Capitalized(labels.subject), path.string()));
136 }
137 if (file_info.nNumberOfLinks != 1) {
138 CloseHandle(handle);
139 return absl::FailedPreconditionError(
140 absl::StrFormat("%s lock must have exactly one hard link: %s",
141 Capitalized(labels.subject), path.string()));
142 }
143 const WindowsFileIdentity identity = {file_info.dwVolumeSerialNumber,
144 file_info.nFileIndexHigh,
145 file_info.nFileIndexLow};
146 const bool duplicate_identity = std::any_of(
147 lock_identities.begin(), lock_identities.end(),
148 [&](const WindowsFileIdentity& existing) {
149 return existing.volume_serial == identity.volume_serial &&
150 existing.file_index_high == identity.file_index_high &&
151 existing.file_index_low == identity.file_index_low;
152 });
153 if (duplicate_identity) {
154 CloseHandle(handle);
155 return absl::InvalidArgumentError(
156 absl::StrFormat("%s lock aliases another lock path: %s",
157 Capitalized(labels.subject), path.string()));
158 }
159 OVERLAPPED overlapped = {};
160 if (!LockFileEx(handle, LOCKFILE_EXCLUSIVE_LOCK, 0, MAXDWORD, MAXDWORD,
161 &overlapped)) {
162 const DWORD error = GetLastError();
163 CloseHandle(handle);
164 return absl::UnavailableError(absl::StrFormat(
165 "Cannot acquire %s lock %s: %s", labels.subject, path.string(),
166 std::error_code(static_cast<int>(error), std::system_category())
167 .message()));
168 }
169 lock_identities.push_back(identity);
170 lock->handles_.push_back(handle);
171 }
172#elif defined(__EMSCRIPTEN__)
173 return absl::FailedPreconditionError(absl::StrFormat(
174 "Durable %s locking is unavailable in browser builds", labels.subject));
175#else
176 lock->fds_.reserve(lock_paths.size());
177 struct PosixFileIdentity {
178 dev_t device;
179 ino_t inode;
180 };
181 std::vector<PosixFileIdentity> lock_identities;
182 lock_identities.reserve(lock_paths.size());
183 for (const fs::path& path : lock_paths) {
184 int open_flags = O_RDWR | O_CREAT;
185#ifdef O_CLOEXEC
186 open_flags |= O_CLOEXEC;
187#endif
188#ifdef O_NOFOLLOW
189 open_flags |= O_NOFOLLOW;
190#endif
191 const int fd = open(path.c_str(), open_flags, S_IRUSR | S_IWUSR);
192 if (fd < 0) {
193 return absl::PermissionDeniedError(absl::StrFormat(
194 "Cannot open %s lock %s: %s", labels.subject, path.string(),
195 std::error_code(errno, std::generic_category()).message()));
196 }
197 struct stat lock_stat{};
198 if (fstat(fd, &lock_stat) != 0) {
199 const int error = errno;
200 close(fd);
201 return absl::FailedPreconditionError(absl::StrFormat(
202 "Cannot inspect %s lock %s: %s", labels.subject, path.string(),
203 std::error_code(error, std::generic_category()).message()));
204 }
205 if (!S_ISREG(lock_stat.st_mode)) {
206 close(fd);
207 return absl::FailedPreconditionError(
208 absl::StrFormat("%s lock must be a regular file: %s",
209 Capitalized(labels.subject), path.string()));
210 }
211 if (lock_stat.st_nlink != 1) {
212 close(fd);
213 return absl::FailedPreconditionError(
214 absl::StrFormat("%s lock must have exactly one hard link: %s",
215 Capitalized(labels.subject), path.string()));
216 }
217 const PosixFileIdentity identity = {lock_stat.st_dev, lock_stat.st_ino};
218 const bool duplicate_identity =
219 std::any_of(lock_identities.begin(), lock_identities.end(),
220 [&](const PosixFileIdentity& existing) {
221 return existing.device == identity.device &&
222 existing.inode == identity.inode;
223 });
224 if (duplicate_identity) {
225 close(fd);
226 return absl::InvalidArgumentError(
227 absl::StrFormat("%s lock aliases another lock path: %s",
228 Capitalized(labels.subject), path.string()));
229 }
230 if (fchmod(fd, S_IRUSR | S_IWUSR) != 0) {
231 const int error = errno;
232 close(fd);
233 return absl::PermissionDeniedError(absl::StrFormat(
234 "Cannot secure %s lock %s: %s", labels.subject, path.string(),
235 std::error_code(error, std::generic_category()).message()));
236 }
237 while (flock(fd, LOCK_EX) != 0) {
238 if (errno == EINTR) {
239 continue;
240 }
241 const int error = errno;
242 close(fd);
243 return absl::UnavailableError(absl::StrFormat(
244 "Cannot acquire %s lock %s: %s", labels.subject, path.string(),
245 std::error_code(error, std::generic_category()).message()));
246 }
247 lock_identities.push_back(identity);
248 lock->fds_.push_back(fd);
249 }
250#endif
251 return lock;
252 }
253
255#if defined(_WIN32)
256 for (auto it = handles_.rbegin(); it != handles_.rend(); ++it) {
257 OVERLAPPED overlapped = {};
258 UnlockFileEx(*it, 0, MAXDWORD, MAXDWORD, &overlapped);
259 CloseHandle(*it);
260 }
261#elif !defined(__EMSCRIPTEN__)
262 for (auto it = fds_.rbegin(); it != fds_.rend(); ++it) {
263 while (flock(*it, LOCK_UN) != 0 && errno == EINTR) {}
264 close(*it);
265 }
266#endif
267 if (process_lock_acquired_) {
269 }
270 }
271
274
275 private:
277
278 bool process_lock_acquired_ = false;
279#if defined(_WIN32)
280 std::vector<HANDLE> handles_;
281#elif !defined(__EMSCRIPTEN__)
282 std::vector<int> fds_;
283#endif
284};
285
286constexpr uint32_t kSha256Constants[64] = {
287 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
288 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
289 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
290 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
291 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
292 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
293 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
294 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
295 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
296 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
297 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
298
299constexpr uint32_t RotateRight(uint32_t value, uint32_t count) {
300 return (value >> count) | (value << (32 - count));
301}
302
303void TransformSha256(uint32_t state[8], const uint8_t block[64]) {
304 uint32_t schedule[64];
305 for (int index = 0; index < 16; ++index) {
306 schedule[index] = (static_cast<uint32_t>(block[index * 4]) << 24) |
307 (static_cast<uint32_t>(block[index * 4 + 1]) << 16) |
308 (static_cast<uint32_t>(block[index * 4 + 2]) << 8) |
309 static_cast<uint32_t>(block[index * 4 + 3]);
310 }
311 for (int index = 16; index < 64; ++index) {
312 const uint32_t low = RotateRight(schedule[index - 15], 7) ^
313 RotateRight(schedule[index - 15], 18) ^
314 (schedule[index - 15] >> 3);
315 const uint32_t high = RotateRight(schedule[index - 2], 17) ^
316 RotateRight(schedule[index - 2], 19) ^
317 (schedule[index - 2] >> 10);
318 schedule[index] = schedule[index - 16] + low + schedule[index - 7] + high;
319 }
320
321 uint32_t a = state[0];
322 uint32_t b = state[1];
323 uint32_t c = state[2];
324 uint32_t d = state[3];
325 uint32_t e = state[4];
326 uint32_t f = state[5];
327 uint32_t g = state[6];
328 uint32_t h = state[7];
329 for (int index = 0; index < 64; ++index) {
330 const uint32_t sigma1 =
331 RotateRight(e, 6) ^ RotateRight(e, 11) ^ RotateRight(e, 25);
332 const uint32_t choose = (e & f) ^ ((~e) & g);
333 const uint32_t temp1 =
334 h + sigma1 + choose + kSha256Constants[index] + schedule[index];
335 const uint32_t sigma0 =
336 RotateRight(a, 2) ^ RotateRight(a, 13) ^ RotateRight(a, 22);
337 const uint32_t majority = (a & b) ^ (a & c) ^ (b & c);
338 const uint32_t temp2 = sigma0 + majority;
339 h = g;
340 g = f;
341 f = e;
342 e = d + temp1;
343 d = c;
344 c = b;
345 b = a;
346 a = temp1 + temp2;
347 }
348
349 state[0] += a;
350 state[1] += b;
351 state[2] += c;
352 state[3] += d;
353 state[4] += e;
354 state[5] += f;
355 state[6] += g;
356 state[7] += h;
357}
358
360 public:
361 void Update(std::string_view content) {
362 bytes_ += content.size();
363 while (!content.empty()) {
364 const size_t count = std::min(content.size(), sizeof(block_) - used_);
365 std::memcpy(block_ + used_, content.data(), count);
366 content.remove_prefix(count);
367 used_ += count;
368 if (used_ == sizeof(block_)) {
369 TransformSha256(state_, block_);
370 used_ = 0;
371 }
372 }
373 }
374
375 std::string Finish() {
376 block_[used_++] = 0x80;
377 std::memset(block_ + used_, 0, sizeof(block_) - used_);
378 if (used_ > 56) {
379 TransformSha256(state_, block_);
380 std::memset(block_, 0, sizeof(block_));
381 }
382 const uint64_t bit_length = bytes_ * 8;
383 for (int index = 0; index < 8; ++index) {
384 block_[63 - index] = static_cast<uint8_t>(bit_length >> (index * 8));
385 }
386 TransformSha256(state_, block_);
387
388 std::string digest;
389 digest.reserve(64);
390 for (uint32_t value : state_) {
391 absl::StrAppend(&digest, absl::StrFormat("%08x", value));
392 }
393 return digest;
394 }
395
396 private:
397 uint32_t state_[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
398 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
399 uint8_t block_[64] = {};
400 size_t used_ = 0;
401 uint64_t bytes_ = 0;
402};
403
404#if !defined(YAZE_FORCE_PORTABLE_SOURCE_SHA256) && \
405 (defined(__APPLE__) || defined(_WIN32) || \
406 (defined(__linux__) && defined(YAZE_SOURCE_SHA256_OPENSSL)))
407class NativeSha256Digest {
408 public:
409 NativeSha256Digest() {
410#if defined(__APPLE__)
411 initialized_ = CC_SHA256_Init(&context_) == 1;
412#elif defined(_WIN32)
413 if (BCryptOpenAlgorithmProvider(&provider_, BCRYPT_SHA256_ALGORITHM,
414 nullptr, 0) < 0) {
415 provider_ = nullptr;
416 return;
417 }
418 // CNG owns the hash-object buffer when no caller buffer is supplied.
419 if (BCryptCreateHash(provider_, &hash_, nullptr, 0, nullptr, 0, 0) < 0) {
420 hash_ = nullptr;
421 return;
422 }
423 initialized_ = true;
424#else
425 context_ = EVP_MD_CTX_new();
426 initialized_ = context_ != nullptr &&
427 EVP_DigestInit_ex(context_, EVP_sha256(), nullptr) == 1;
428#endif
429 }
430
431 ~NativeSha256Digest() {
432#if defined(_WIN32)
433 if (hash_ != nullptr) {
434 BCryptDestroyHash(hash_);
435 }
436 if (provider_ != nullptr) {
437 BCryptCloseAlgorithmProvider(provider_, 0);
438 }
439#elif !defined(__APPLE__)
440 EVP_MD_CTX_free(context_);
441#endif
442 }
443
444 NativeSha256Digest(const NativeSha256Digest&) = delete;
445 NativeSha256Digest& operator=(const NativeSha256Digest&) = delete;
446
447 // Initialization selects the backend once. A later failure must not switch
448 // to a fresh portable state containing only the remaining input chunks.
449 bool available() const { return initialized_; }
450
451 void Update(std::string_view content) {
452 if (!initialized_ || failed_) {
453 return;
454 }
455#if defined(__APPLE__) || defined(_WIN32)
456 while (!content.empty()) {
457#if defined(__APPLE__)
458 const size_t count =
459 std::min(content.size(),
460 static_cast<size_t>(std::numeric_limits<CC_LONG>::max()));
461 const bool ok = CC_SHA256_Update(&context_, content.data(),
462 static_cast<CC_LONG>(count)) == 1;
463#else
464 const size_t count =
465 std::min(content.size(),
466 static_cast<size_t>(std::numeric_limits<ULONG>::max()));
467 const bool ok =
468 BCryptHashData(hash_,
469 const_cast<PUCHAR>(
470 reinterpret_cast<const UCHAR*>(content.data())),
471 static_cast<ULONG>(count), 0) >= 0;
472#endif
473 if (!ok) {
474 failed_ = true;
475 return;
476 }
477 content.remove_prefix(count);
478 }
479#else
480 if (!content.empty() &&
481 EVP_DigestUpdate(context_, content.data(), content.size()) != 1) {
482 failed_ = true;
483 }
484#endif
485 }
486
487 // An empty result is internal failure, never a valid SHA256 identity. The
488 // caller can then replay the complete input with the portable implementation.
489 std::string Finish() {
490 if (!initialized_ || failed_) {
491 return {};
492 }
493 std::array<unsigned char, 32> bytes{};
494#if defined(__APPLE__)
495 if (CC_SHA256_Final(bytes.data(), &context_) != 1) {
496 return {};
497 }
498#elif defined(_WIN32)
499 if (BCryptFinishHash(hash_, bytes.data(), static_cast<ULONG>(bytes.size()),
500 0) < 0) {
501 return {};
502 }
503#else
504 unsigned int size = 0;
505 if (EVP_DigestFinal_ex(context_, bytes.data(), &size) != 1 ||
506 size != bytes.size()) {
507 return {};
508 }
509#endif
510 constexpr char kHex[] = "0123456789abcdef";
511 std::string digest(bytes.size() * 2, '\0');
512 for (size_t index = 0; index < bytes.size(); ++index) {
513 digest[index * 2] = kHex[bytes[index] >> 4];
514 digest[index * 2 + 1] = kHex[bytes[index] & 0x0f];
515 }
516 return digest;
517 }
518
519 private:
520 bool initialized_ = false;
521 bool failed_ = false;
522#if defined(__APPLE__)
523 CC_SHA256_CTX context_;
524#elif defined(_WIN32)
525 BCRYPT_ALG_HANDLE provider_ = nullptr;
526 BCRYPT_HASH_HANDLE hash_ = nullptr;
527#else
528 EVP_MD_CTX* context_ = nullptr;
529#endif
530};
531#endif
532
534 public:
535 void Update(std::string_view content) {
536#if !defined(YAZE_FORCE_PORTABLE_SOURCE_SHA256) && \
537 (defined(__APPLE__) || defined(_WIN32) || \
538 (defined(__linux__) && defined(YAZE_SOURCE_SHA256_OPENSSL)))
539 if (native_.available()) {
540 native_.Update(content);
541 return;
542 }
543#endif
544 portable_.Update(content);
545 }
546
547 std::string Finish() {
548#if !defined(YAZE_FORCE_PORTABLE_SOURCE_SHA256) && \
549 (defined(__APPLE__) || defined(_WIN32) || \
550 (defined(__linux__) && defined(YAZE_SOURCE_SHA256_OPENSSL)))
551 if (native_.available()) {
552 return native_.Finish();
553 }
554#endif
555 return portable_.Finish();
556 }
557
558 private:
559#if !defined(YAZE_FORCE_PORTABLE_SOURCE_SHA256) && \
560 (defined(__APPLE__) || defined(_WIN32) || \
561 (defined(__linux__) && defined(YAZE_SOURCE_SHA256_OPENSSL)))
562 NativeSha256Digest native_;
563#endif
565};
566
567std::string Sha256Hex(std::string_view content) {
568 Sha256Digest digest;
569 digest.Update(content);
570 auto result = digest.Finish();
571 if (!result.empty()) {
572 return result;
573 }
574 PortableSha256Digest fallback;
575 fallback.Update(content);
576 return fallback.Finish();
577}
578
579absl::StatusOr<std::string> ReadTextFile(const fs::path& path,
580 absl::string_view label) {
581 std::ifstream input(path, std::ios::binary);
582 if (!input.is_open()) {
583 return absl::NotFoundError(
584 absl::StrFormat("Cannot open %s: %s", label, path.string()));
585 }
586 std::string content{std::istreambuf_iterator<char>(input),
587 std::istreambuf_iterator<char>()};
588 if (!input.good() && !input.eof()) {
589 return absl::DataLossError(
590 absl::StrFormat("Failed while reading %s: %s", label, path.string()));
591 }
592 return content;
593}
594
595std::string LowercasePath(const fs::path& path) {
596 return absl::AsciiStrToLower(path.generic_string());
597}
598
599absl::StatusOr<fs::path> ResolvePublicationTarget(
600 const fs::path& target, const SourceArtifactPublisherLabels& labels) {
601 if (target.empty() || !target.is_absolute()) {
602 return absl::InvalidArgumentError(
603 absl::StrFormat("%s target must be an absolute path: %s",
604 Capitalized(labels.subject), target.string()));
605 }
606
607 std::error_code status_ec;
608 const fs::file_status link_status = fs::symlink_status(target, status_ec);
609 if (status_ec != std::errc::no_such_file_or_directory && status_ec) {
610 return absl::FailedPreconditionError(
611 absl::StrFormat("Cannot inspect %s target %s: %s", labels.subject,
612 target.string(), status_ec.message()));
613 }
614 if (!status_ec && fs::is_symlink(link_status)) {
615 return absl::FailedPreconditionError(
616 absl::StrFormat("%s target may not be a symbolic link: %s",
617 Capitalized(labels.subject), target.string()));
618 }
619
620 std::error_code canonical_ec;
621 fs::path resolved = fs::weakly_canonical(target, canonical_ec);
622 if (canonical_ec) {
623 return absl::FailedPreconditionError(
624 absl::StrFormat("Cannot resolve %s target %s: %s", labels.subject,
625 target.string(), canonical_ec.message()));
626 }
627 return resolved.lexically_normal();
628}
629
630absl::StatusOr<std::vector<fs::path>> ResolvePublicationTargets(
631 const std::vector<fs::path>& targets,
632 const SourceArtifactPublisherLabels& labels) {
633 if (targets.empty()) {
634 return absl::InvalidArgumentError(
635 absl::StrFormat("%s publication requires at least one target",
636 Capitalized(labels.subject)));
637 }
638 std::vector<fs::path> resolved_targets;
639 resolved_targets.reserve(targets.size());
640 for (const fs::path& target : targets) {
641 fs::path resolved;
642 ASSIGN_OR_RETURN(resolved, ResolvePublicationTarget(target, labels));
643 resolved_targets.push_back(std::move(resolved));
644 }
645 return resolved_targets;
646}
647
649 const std::vector<fs::path>& targets,
650 const SourceArtifactPublisherLabels& labels) {
651 for (size_t left_index = 0; left_index < targets.size(); ++left_index) {
652 for (size_t right_index = left_index + 1; right_index < targets.size();
653 ++right_index) {
654 const fs::path& left = targets[left_index];
655 const fs::path& right = targets[right_index];
656 if (left == right || LowercasePath(left) == LowercasePath(right)) {
657 return absl::InvalidArgumentError(absl::StrFormat(
658 "%s publication targets must be distinct: %s and %s",
659 Capitalized(labels.subject), left.string(), right.string()));
660 }
661
662 std::error_code left_exists_ec;
663 const bool left_exists = fs::exists(left, left_exists_ec);
664 std::error_code right_exists_ec;
665 const bool right_exists = fs::exists(right, right_exists_ec);
666 if (left_exists_ec || right_exists_ec) {
667 return absl::FailedPreconditionError(absl::StrFormat(
668 "Cannot inspect %s publication targets %s and %s: %s",
669 labels.subject, left.string(), right.string(),
670 left_exists_ec ? left_exists_ec.message()
671 : right_exists_ec.message()));
672 }
673 if (!left_exists || !right_exists) {
674 continue;
675 }
676
677 std::error_code equivalent_ec;
678 const bool equivalent = fs::equivalent(left, right, equivalent_ec);
679 if (equivalent_ec) {
680 return absl::FailedPreconditionError(absl::StrFormat(
681 "Cannot compare %s publication targets %s and %s: %s",
682 labels.subject, left.string(), right.string(),
683 equivalent_ec.message()));
684 }
685 if (equivalent) {
686 return absl::InvalidArgumentError(absl::StrFormat(
687 "%s publication targets alias each other: %s and %s",
688 Capitalized(labels.subject), left.string(), right.string()));
689 }
690 }
691 }
692 return absl::OkStatus();
693}
694
695absl::StatusOr<std::vector<fs::path>> PublicationLockPaths(
696 const std::vector<fs::path>& targets,
697 const SourceArtifactPublisherLabels& labels) {
698 if (targets.empty()) {
699 return absl::InvalidArgumentError(
700 absl::StrFormat("%s publication requires at least one target",
701 Capitalized(labels.subject)));
702 }
703 std::vector<fs::path> lock_paths;
704 for (const fs::path& target : targets) {
705 std::error_code canonical_ec;
706 const fs::path parent = fs::canonical(target.parent_path(), canonical_ec);
707 if (canonical_ec) {
708 return absl::FailedPreconditionError(absl::StrFormat(
709 "Cannot resolve %s target directory %s: %s", labels.subject,
710 target.parent_path().string(), canonical_ec.message()));
711 }
712
713 bool duplicate = false;
714 for (const fs::path& existing_lock : lock_paths) {
715 std::error_code equivalent_ec;
716 const bool equivalent =
717 fs::equivalent(parent, existing_lock.parent_path(), equivalent_ec);
718 if (equivalent_ec) {
719 return absl::FailedPreconditionError(absl::StrFormat(
720 "Cannot compare %s target directories %s and %s: %s",
721 labels.subject, parent.string(),
722 existing_lock.parent_path().string(), equivalent_ec.message()));
723 }
724 if (equivalent) {
725 duplicate = true;
726 break;
727 }
728 }
729 if (!duplicate) {
730 lock_paths.push_back(parent / kSourceArtifactPublicationLockBasename);
731 }
732 }
733
734 std::sort(lock_paths.begin(), lock_paths.end(),
735 [](const fs::path& left, const fs::path& right) {
736 const std::string left_lower = LowercasePath(left);
737 const std::string right_lower = LowercasePath(right);
738 return left_lower == right_lower
739 ? left.generic_string() < right.generic_string()
740 : left_lower < right_lower;
741 });
742 return lock_paths;
743}
744
746 const std::vector<fs::path>& lock_paths,
747 const std::vector<fs::path>& targets,
748 const SourceArtifactPublisherLabels& labels) {
749 for (const fs::path& lock_path : lock_paths) {
750 for (const fs::path& target : targets) {
751 if (target == lock_path ||
752 LowercasePath(target) == LowercasePath(lock_path)) {
753 return absl::InvalidArgumentError(
754 absl::StrFormat("%s targets may not use a persistent lock path: %s",
755 Capitalized(labels.subject), lock_path.string()));
756 }
757 }
758
759 std::error_code lock_exists_ec;
760 const bool lock_exists = fs::exists(lock_path, lock_exists_ec);
761 if (lock_exists_ec) {
762 return absl::FailedPreconditionError(absl::StrFormat(
763 "Cannot inspect persistent %s lock %s: %s", labels.subject,
764 lock_path.string(), lock_exists_ec.message()));
765 }
766 if (!lock_exists) {
767 continue;
768 }
769
770 for (const fs::path& target : targets) {
771 std::error_code target_exists_ec;
772 const bool target_exists = fs::exists(target, target_exists_ec);
773 if (target_exists_ec) {
774 return absl::FailedPreconditionError(
775 absl::StrFormat("Cannot inspect %s target %s: %s", labels.subject,
776 target.string(), target_exists_ec.message()));
777 }
778 if (!target_exists) {
779 continue;
780 }
781 std::error_code equivalent_ec;
782 const bool equivalent = fs::equivalent(lock_path, target, equivalent_ec);
783 if (equivalent_ec) {
784 return absl::FailedPreconditionError(absl::StrFormat(
785 "Cannot compare %s target %s with persistent lock %s: %s",
786 labels.subject, target.string(), lock_path.string(),
787 equivalent_ec.message()));
788 }
789 if (equivalent) {
790 return absl::InvalidArgumentError(
791 absl::StrFormat("%s target aliases a persistent lock path: %s",
792 Capitalized(labels.subject), target.string()));
793 }
794 }
795 }
796 return absl::OkStatus();
797}
798
799absl::StatusOr<std::vector<fs::path>> PreparePublicationTargets(
800 const std::vector<fs::path>& targets,
801 const SourceArtifactPublisherLabels& labels) {
802 std::vector<fs::path> resolved_targets;
803 ASSIGN_OR_RETURN(resolved_targets,
804 ResolvePublicationTargets(targets, labels));
805 RETURN_IF_ERROR(ValidateDistinctPublicationTargets(resolved_targets, labels));
806
807 std::vector<fs::path> lock_paths;
808 ASSIGN_OR_RETURN(lock_paths, PublicationLockPaths(resolved_targets, labels));
810 ValidateTargetsDoNotAliasLocks(lock_paths, resolved_targets, labels));
811 return resolved_targets;
812}
813
814fs::path NextSiblingPath(const fs::path& target, absl::string_view purpose) {
815 static std::atomic<uint64_t> sequence{0};
816 const uint64_t tick = static_cast<uint64_t>(
817 std::chrono::steady_clock::now().time_since_epoch().count());
818 const uint64_t id = sequence.fetch_add(1, std::memory_order_relaxed);
819 fs::path name = target.filename();
820 name += absl::StrFormat(".yaze-%s-%016x-%016x", purpose, tick, id);
821 return target.parent_path() / name;
822}
823
824absl::StatusOr<fs::path> WriteExclusiveTemp(const fs::path& target,
825 std::string_view content) {
826 constexpr int kMaxAttempts = 100;
827#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
828 mode_t create_mode =
829 S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
830 bool preserve_mode = false;
831 struct stat target_stat{};
832 if (stat(target.c_str(), &target_stat) == 0) {
833 if (!S_ISREG(target_stat.st_mode)) {
834 return absl::FailedPreconditionError(absl::StrFormat(
835 "Publication target must be a regular file: %s", target.string()));
836 }
837 create_mode = target_stat.st_mode & 07777;
838 preserve_mode = true;
839 } else if (errno != ENOENT) {
840 return absl::FailedPreconditionError(absl::StrFormat(
841 "Cannot inspect publication target mode %s: %s", target.string(),
842 std::error_code(errno, std::generic_category()).message()));
843 }
844#endif
845 for (int attempt = 0; attempt < kMaxAttempts; ++attempt) {
846 const fs::path temp = NextSiblingPath(target, "tmp");
847#if defined(_WIN32)
848 HANDLE file = CreateFileW(
849 temp.wstring().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW,
850 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, nullptr);
851 if (file == INVALID_HANDLE_VALUE) {
852 const DWORD error = GetLastError();
853 if (error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS) {
854 continue;
855 }
856 return absl::PermissionDeniedError(absl::StrFormat(
857 "Cannot create temporary file for %s: %s", target.string(),
858 std::error_code(static_cast<int>(error), std::system_category())
859 .message()));
860 }
861 size_t written_total = 0;
862 while (written_total < content.size()) {
863 const DWORD chunk = static_cast<DWORD>(std::min<size_t>(
864 content.size() - written_total, std::numeric_limits<DWORD>::max()));
865 DWORD written = 0;
866 if (!WriteFile(file, content.data() + written_total, chunk, &written,
867 nullptr) ||
868 written == 0) {
869 const DWORD error = GetLastError();
870 CloseHandle(file);
871 std::error_code cleanup_ec;
872 fs::remove(temp, cleanup_ec);
873 return absl::InternalError(absl::StrFormat(
874 "Failed to write temporary file for %s: %s", target.string(),
875 std::error_code(static_cast<int>(error), std::system_category())
876 .message()));
877 }
878 written_total += written;
879 }
880 if (!FlushFileBuffers(file)) {
881 const DWORD error = GetLastError();
882 CloseHandle(file);
883 std::error_code cleanup_ec;
884 fs::remove(temp, cleanup_ec);
885 return absl::InternalError(absl::StrFormat(
886 "Failed to flush temporary file for %s: %s", target.string(),
887 std::error_code(static_cast<int>(error), std::system_category())
888 .message()));
889 }
890 if (!CloseHandle(file)) {
891 const DWORD error = GetLastError();
892 std::error_code cleanup_ec;
893 fs::remove(temp, cleanup_ec);
894 return absl::InternalError(absl::StrFormat(
895 "Failed to flush temporary file for %s: %s", target.string(),
896 std::error_code(static_cast<int>(error), std::system_category())
897 .message()));
898 }
899#elif defined(__EMSCRIPTEN__)
900 std::ofstream output(temp, std::ios::binary | std::ios::trunc);
901 if (!output.is_open()) {
902 return absl::PermissionDeniedError(absl::StrFormat(
903 "Cannot create temporary file for %s", target.string()));
904 }
905 output.write(content.data(), static_cast<std::streamsize>(content.size()));
906 if (!output.good()) {
907 output.close();
908 std::error_code cleanup_ec;
909 fs::remove(temp, cleanup_ec);
910 return absl::InternalError(absl::StrFormat(
911 "Failed to write temporary file for %s", target.string()));
912 }
913 output.close();
914#else
915 const int fd = open(temp.c_str(), O_WRONLY | O_CREAT | O_EXCL, create_mode);
916 if (fd < 0) {
917 if (errno == EEXIST) {
918 continue;
919 }
920 return absl::PermissionDeniedError(absl::StrFormat(
921 "Cannot create temporary file for %s: %s", target.string(),
922 std::error_code(errno, std::generic_category()).message()));
923 }
924 if (preserve_mode && fchmod(fd, create_mode) != 0) {
925 const int error = errno;
926 close(fd);
927 std::error_code cleanup_ec;
928 fs::remove(temp, cleanup_ec);
929 return absl::InternalError(absl::StrFormat(
930 "Cannot preserve publication target mode for %s: %s", target.string(),
931 std::error_code(error, std::generic_category()).message()));
932 }
933 size_t written_total = 0;
934 while (written_total < content.size()) {
935 const ssize_t written =
936 write(fd, content.data() + written_total,
937 std::min<size_t>(content.size() - written_total, 1024 * 1024));
938 if (written < 0 && errno == EINTR) {
939 continue;
940 }
941 if (written <= 0) {
942 const int error = written < 0 ? errno : EIO;
943 close(fd);
944 std::error_code cleanup_ec;
945 fs::remove(temp, cleanup_ec);
946 return absl::InternalError(absl::StrFormat(
947 "Failed to write temporary file for %s: %s", target.string(),
948 std::error_code(error, std::generic_category()).message()));
949 }
950 written_total += static_cast<size_t>(written);
951 }
952 if (fsync(fd) != 0) {
953 const int error = errno;
954 close(fd);
955 std::error_code cleanup_ec;
956 fs::remove(temp, cleanup_ec);
957 return absl::InternalError(absl::StrFormat(
958 "Failed to flush temporary file for %s: %s", target.string(),
959 std::error_code(error, std::generic_category()).message()));
960 }
961 if (close(fd) != 0) {
962 const int error = errno;
963 std::error_code cleanup_ec;
964 fs::remove(temp, cleanup_ec);
965 return absl::InternalError(absl::StrFormat(
966 "Failed to flush temporary file for %s: %s", target.string(),
967 std::error_code(error, std::generic_category()).message()));
968 }
969#endif
970 return temp;
971 }
972 return absl::ResourceExhaustedError(absl::StrFormat(
973 "Could not allocate a unique temporary file for %s", target.string()));
974}
975
976absl::Status SyncParentDirectory(const fs::path& target) {
977#if defined(_WIN32) || defined(__EMSCRIPTEN__)
978 return absl::OkStatus();
979#else
980 int open_flags = O_RDONLY;
981#ifdef O_CLOEXEC
982 open_flags |= O_CLOEXEC;
983#endif
984#ifdef O_DIRECTORY
985 open_flags |= O_DIRECTORY;
986#endif
987 const fs::path parent = target.parent_path();
988 const int fd = open(parent.c_str(), open_flags);
989 if (fd < 0) {
990 return absl::InternalError(absl::StrFormat(
991 "Cannot open parent directory for durable publication %s: %s",
992 parent.string(),
993 std::error_code(errno, std::generic_category()).message()));
994 }
995 int sync_result = 0;
996 do {
997 sync_result = fsync(fd);
998 } while (sync_result != 0 && errno == EINTR);
999 const int sync_error = sync_result == 0 ? 0 : errno;
1000 const int close_result = close(fd);
1001 const int close_error = close_result == 0 ? 0 : errno;
1002 if (sync_error != 0) {
1003 return absl::InternalError(absl::StrFormat(
1004 "Cannot sync parent directory after publishing %s: %s", target.string(),
1005 std::error_code(sync_error, std::generic_category()).message()));
1006 }
1007 if (close_result != 0) {
1008 return absl::InternalError(absl::StrFormat(
1009 "Cannot close parent directory after publishing %s: %s",
1010 target.string(),
1011 std::error_code(close_error, std::generic_category()).message()));
1012 }
1013 return absl::OkStatus();
1014#endif
1015}
1016
1017absl::Status ReplaceFromTemp(const fs::path& temp, const fs::path& target,
1018 bool* replaced = nullptr,
1019 bool replace_existing = true) {
1020 if (replaced != nullptr) {
1021 *replaced = false;
1022 }
1023#if defined(_WIN32)
1024 const DWORD flags = MOVEFILE_WRITE_THROUGH |
1025 (replace_existing ? MOVEFILE_REPLACE_EXISTING : 0);
1026 if (!MoveFileExW(temp.wstring().c_str(), target.wstring().c_str(), flags)) {
1027 const DWORD error = GetLastError();
1028 return absl::InternalError(absl::StrFormat(
1029 "Failed to publish %s: %s", target.string(),
1030 std::error_code(static_cast<int>(error), std::system_category())
1031 .message()));
1032 }
1033 if (replaced != nullptr) {
1034 *replaced = true;
1035 }
1036#else
1037 std::error_code rename_ec;
1038 if (replace_existing) {
1039 fs::rename(temp, target, rename_ec);
1040 } else {
1041 fs::create_hard_link(temp, target, rename_ec);
1042 }
1043 if (rename_ec) {
1044 return absl::InternalError(absl::StrFormat(
1045 "Failed to publish %s: %s", target.string(), rename_ec.message()));
1046 }
1047 if (replaced != nullptr) {
1048 *replaced = true;
1049 }
1050 if (!replace_existing) {
1051 fs::remove(temp, rename_ec);
1052 if (rename_ec) {
1053 return absl::InternalError(
1054 absl::StrFormat("Failed to remove published temporary file %s: %s",
1055 temp.string(), rename_ec.message()));
1056 }
1057 }
1058#endif
1059 return SyncParentDirectory(target);
1060}
1061
1062absl::StatusOr<fs::path> CreateBackup(const fs::path& target) {
1063 constexpr int kMaxAttempts = 100;
1064 for (int attempt = 0; attempt < kMaxAttempts; ++attempt) {
1065 const fs::path backup = NextSiblingPath(target, "backup");
1066 std::error_code copy_ec;
1067 fs::copy_file(target, backup, fs::copy_options::none, copy_ec);
1068 if (!copy_ec) {
1069 return backup;
1070 }
1071 if (copy_ec != std::errc::file_exists) {
1072 std::error_code cleanup_ec;
1073 fs::remove(backup, cleanup_ec);
1074 if (cleanup_ec) {
1075 return absl::DataLossError(absl::StrFormat(
1076 "Cannot back up %s (%s) or remove incomplete backup %s (%s)",
1077 target.string(), copy_ec.message(), backup.string(),
1078 cleanup_ec.message()));
1079 }
1080 return absl::FailedPreconditionError(absl::StrFormat(
1081 "Cannot back up %s: %s", target.string(), copy_ec.message()));
1082 }
1083 }
1084 return absl::ResourceExhaustedError(absl::StrFormat(
1085 "Could not allocate a backup path for %s", target.string()));
1086}
1087
1089 const std::vector<fs::path>& paths, absl::string_view artifact_kind,
1090 const SourceArtifactPublisherLabels& labels) {
1091 std::string failures;
1092 for (const fs::path& path : paths) {
1093 if (path.empty()) {
1094 continue;
1095 }
1096 std::error_code ec;
1097 fs::remove(path, ec);
1098 if (!ec) {
1099 continue;
1100 }
1101 if (!failures.empty()) {
1102 absl::StrAppend(&failures, "; ");
1103 }
1104 absl::StrAppend(&failures, path.string(), " (", ec.message(), ")");
1105 }
1106 if (!failures.empty()) {
1107 return absl::InternalError(absl::StrFormat(
1108 "Could not remove %s %s: %s", labels.subject, artifact_kind, failures));
1109 }
1110 return absl::OkStatus();
1111}
1112
1113absl::Status CleanupTemps(const std::vector<PublicationArtifact>& artifacts,
1114 const SourceArtifactPublisherLabels& labels) {
1115 std::vector<fs::path> paths;
1116 paths.reserve(artifacts.size());
1117 for (const auto& artifact : artifacts) {
1118 if (!artifact.temp.empty()) {
1119 paths.push_back(artifact.temp);
1120 }
1121 }
1122 return CleanupPublicationPaths(paths, "temporary files", labels);
1123}
1124
1125absl::Status CleanupBackups(std::vector<PublicationArtifact>* artifacts,
1126 const SourceArtifactPublisherLabels& labels) {
1127 std::string failures;
1128 for (auto& artifact : *artifacts) {
1129 if (!artifact.backup.has_value()) {
1130 continue;
1131 }
1132 std::error_code ec;
1133 fs::remove(*artifact.backup, ec);
1134 if (!ec) {
1135 artifact.backup.reset();
1136 continue;
1137 }
1138 if (!failures.empty()) {
1139 absl::StrAppend(&failures, "; ");
1140 }
1141 absl::StrAppend(&failures, artifact.backup->string(), " (", ec.message(),
1142 ")");
1143 }
1144 if (!failures.empty()) {
1145 return absl::InternalError(absl::StrFormat(
1146 "Could not remove %s backup files: %s", labels.subject, failures));
1147 }
1148 return absl::OkStatus();
1149}
1150
1152 const std::vector<PublicationArtifact>& artifacts) {
1153 std::string paths;
1154 for (const auto& artifact : artifacts) {
1155 if (!artifact.backup.has_value()) {
1156 continue;
1157 }
1158 if (!paths.empty()) {
1159 absl::StrAppend(&paths, ", ");
1160 }
1161 absl::StrAppend(&paths, artifact.backup->string());
1162 }
1163 return paths.empty() ? "<none>" : paths;
1164}
1165
1166std::string CleanupStatusSummary(const absl::Status& status) {
1167 return status.ok() ? "ok" : std::string(status.message());
1168}
1169
1171 std::vector<PublicationArtifact>* artifacts,
1172 const absl::Status& publication_failure,
1173 const SourceArtifactPublisherLabels& labels) {
1174 const absl::Status temp_cleanup = CleanupTemps(*artifacts, labels);
1175 const absl::Status backup_cleanup = CleanupBackups(artifacts, labels);
1176 if (temp_cleanup.ok() && backup_cleanup.ok()) {
1177 return publication_failure;
1178 }
1179 return absl::DataLossError(absl::StrFormat(
1180 "%s publication failed (%s), then artifact cleanup failed "
1181 "(temporary files: %s; backup files: %s)",
1182 Capitalized(labels.subject), publication_failure.message(),
1183 CleanupStatusSummary(temp_cleanup),
1184 CleanupStatusSummary(backup_cleanup)));
1185}
1186
1187absl::Status RestoreArtifact(const PublicationArtifact& artifact) {
1188 std::error_code status_ec;
1189 const auto status = fs::symlink_status(artifact.target, status_ec);
1190 if (status_ec || !fs::is_regular_file(status)) {
1191 return absl::AbortedError(
1192 absl::StrFormat("Published file changed before rollback; preserving %s",
1193 artifact.target.string()));
1194 }
1195 ASSIGN_OR_RETURN(auto current,
1196 ReadTextFile(artifact.target, "published file"));
1197 if (current != artifact.content) {
1198 return absl::AbortedError(
1199 absl::StrFormat("Published file changed before rollback; preserving %s",
1200 artifact.target.string()));
1201 }
1202 if (artifact.original_content.has_value()) {
1203 fs::path restore_temp;
1205 restore_temp,
1206 WriteExclusiveTemp(artifact.target, *artifact.original_content));
1207 const absl::Status status = ReplaceFromTemp(restore_temp, artifact.target);
1208 if (!status.ok()) {
1209 std::error_code cleanup_ec;
1210 fs::remove(restore_temp, cleanup_ec);
1211 if (cleanup_ec) {
1212 return absl::DataLossError(absl::StrFormat(
1213 "Could not restore %s (%s) or remove restore file %s (%s)",
1214 artifact.target.string(), status.message(), restore_temp.string(),
1215 cleanup_ec.message()));
1216 }
1217 }
1218 return status;
1219 }
1220 std::error_code remove_ec;
1221 const bool removed = fs::remove(artifact.target, remove_ec);
1222 if (remove_ec || !removed) {
1223 return absl::InternalError(absl::StrFormat(
1224 "Could not remove newly published file %s during rollback: %s",
1225 artifact.target.string(),
1226 remove_ec ? remove_ec.message() : "file was missing"));
1227 }
1228 return SyncParentDirectory(artifact.target);
1229}
1230
1231absl::Status RollBackPublication(std::vector<PublicationArtifact>* artifacts,
1232 const absl::Status& publication_failure,
1233 const SourceArtifactPublisherLabels& labels) {
1234 absl::Status rollback_failure = absl::OkStatus();
1235 for (auto it = artifacts->rbegin(); it != artifacts->rend(); ++it) {
1236 if (!it->published) {
1237 continue;
1238 }
1239 const absl::Status restore_status = RestoreArtifact(*it);
1240 if (!restore_status.ok() && rollback_failure.ok()) {
1241 rollback_failure = restore_status;
1242 }
1243 it->published = false;
1244 }
1245 const absl::Status temp_cleanup = CleanupTemps(*artifacts, labels);
1246 if (!rollback_failure.ok()) {
1247 return absl::DataLossError(absl::StrFormat(
1248 "%s publication failed (%s) and rollback failed (%s). "
1249 "Temporary-file cleanup: %s. Preserved recovery backups: %s",
1250 Capitalized(labels.subject), publication_failure.message(),
1251 rollback_failure.message(), CleanupStatusSummary(temp_cleanup),
1252 BackupRecoveryPaths(*artifacts)));
1253 }
1254 const absl::Status backup_cleanup = CleanupBackups(artifacts, labels);
1255 if (!temp_cleanup.ok() || !backup_cleanup.ok()) {
1256 return absl::DataLossError(absl::StrFormat(
1257 "%s publication failed (%s); rollback completed, but "
1258 "artifact cleanup failed (temporary files: %s; backup files: %s)",
1259 Capitalized(labels.subject), publication_failure.message(),
1260 CleanupStatusSummary(temp_cleanup),
1261 CleanupStatusSummary(backup_cleanup)));
1262 }
1263 return publication_failure;
1264}
1265
1267 const std::vector<PublicationArtifact>& artifacts,
1268 std::string_view expected_source_sha256,
1269 const SourceArtifactPublisherLabels& labels) {
1270 for (const auto& artifact : artifacts) {
1271 std::error_code exists_ec;
1272 const bool exists = fs::exists(artifact.target, exists_ec);
1273 if (exists_ec) {
1274 return absl::FailedPreconditionError(
1275 absl::StrFormat("Cannot recheck publication target %s: %s",
1276 artifact.target.string(), exists_ec.message()));
1277 }
1278 if (artifact.original_content.has_value()) {
1279 if (!exists) {
1280 return absl::AbortedError(absl::StrFormat(
1281 "Publication target disappeared after preflight: %s",
1282 artifact.target.string()));
1283 }
1284 std::string current;
1285 ASSIGN_OR_RETURN(current,
1286 ReadTextFile(artifact.target, "publication target"));
1287 if (current != *artifact.original_content) {
1288 return absl::AbortedError(
1289 absl::StrFormat("Publication target changed after preflight: %s",
1290 artifact.target.string()));
1291 }
1292 } else if (exists) {
1293 return absl::AbortedError(
1294 absl::StrFormat("Publication target appeared after preflight: %s",
1295 artifact.target.string()));
1296 }
1297 }
1298 if (Sha256Hex(*artifacts.front().original_content) !=
1299 expected_source_sha256) {
1300 return absl::AbortedError(absl::StrFormat(
1301 "Canonical %s changed after SHA-256 preflight", labels.subject));
1302 }
1303 return absl::OkStatus();
1304}
1305
1306absl::Status PublishArtifactSet(std::vector<PublicationArtifact>* artifacts,
1307 std::string_view expected_source_sha256,
1308 const SourceArtifactPublisherLabels& labels) {
1309 for (auto& artifact : *artifacts) {
1310 if (artifact.IsReadOnlyGuard()) {
1311 continue;
1312 }
1313 auto temp_or = WriteExclusiveTemp(artifact.target, artifact.content);
1314 if (!temp_or.ok()) {
1315 return ReturnFailureAfterCleanup(artifacts, temp_or.status(), labels);
1316 }
1317 artifact.temp = std::move(*temp_or);
1318 if (artifact.original_content.has_value()) {
1319 auto backup_or = CreateBackup(artifact.target);
1320 if (!backup_or.ok()) {
1321 return ReturnFailureAfterCleanup(artifacts, backup_or.status(), labels);
1322 }
1323 artifact.backup = std::move(*backup_or);
1324 }
1325 }
1326
1327 const absl::Status unchanged_status = VerifyUnchangedBeforePublication(
1328 *artifacts, expected_source_sha256, labels);
1329 if (!unchanged_status.ok()) {
1330 return ReturnFailureAfterCleanup(artifacts, unchanged_status, labels);
1331 }
1332
1333 for (auto& artifact : *artifacts) {
1334 if (artifact.IsReadOnlyGuard()) {
1335 continue;
1336 }
1337 bool replaced = false;
1338 const absl::Status replace_status =
1339 ReplaceFromTemp(artifact.temp, artifact.target, &replaced,
1340 artifact.original_content.has_value());
1341 artifact.published = replaced;
1342 if (!replace_status.ok()) {
1343 return RollBackPublication(artifacts, replace_status, labels);
1344 }
1345 artifact.temp.clear();
1346 }
1347
1348 for (const auto& artifact : *artifacts) {
1349 std::string reopened;
1350 auto reopened_or = ReadTextFile(artifact.target, labels.published_file);
1351 if (!reopened_or.ok()) {
1352 return RollBackPublication(artifacts, reopened_or.status(), labels);
1353 }
1354 reopened = std::move(*reopened_or);
1355 if (reopened != artifact.content) {
1356 return RollBackPublication(
1357 artifacts,
1358 absl::DataLossError(absl::StrFormat(
1359 "%s failed exact readback: %s",
1360 Capitalized(labels.published_file), artifact.target.string())),
1361 labels);
1362 }
1363 }
1364 const absl::Status temp_cleanup = CleanupTemps(*artifacts, labels);
1365 if (!temp_cleanup.ok()) {
1366 return RollBackPublication(artifacts, temp_cleanup, labels);
1367 }
1368 return absl::OkStatus();
1369}
1370
1371} // namespace
1372
1375 std::vector<fs::path> targets;
1376 std::vector<fs::path> read_only_targets;
1377 std::unique_ptr<SourceArtifactWriteLocks> write_locks;
1378 std::atomic_flag publication_in_progress = ATOMIC_FLAG_INIT;
1379};
1380
1381namespace {
1382
1384 public:
1386 std::atomic_flag* publication_in_progress) noexcept
1387 : publication_in_progress_(publication_in_progress) {}
1388
1390 publication_in_progress_->clear(std::memory_order_release);
1391 }
1392
1395
1396 private:
1397 std::atomic_flag* publication_in_progress_;
1398};
1399
1400} // namespace
1401
1403 std::unique_ptr<Impl> impl)
1404 : impl_(std::move(impl)) {}
1405
1407
1409 const std::vector<fs::path>& targets,
1410 const SourceArtifactPublisherLabels& labels) {
1411 if (labels.subject.empty() || labels.published_file.empty()) {
1412 return absl::InvalidArgumentError(
1413 "Source artifact diagnostic labels must not be empty");
1414 }
1415 return PreparePublicationTargets(targets, labels).status();
1416}
1417
1418absl::StatusOr<std::unique_ptr<SourceArtifactPublicationLock>>
1420 const std::vector<fs::path>& targets,
1421 const SourceArtifactPublisherLabels& labels,
1422 const std::vector<fs::path>& read_only_targets) {
1423 if (labels.subject.empty() || labels.published_file.empty()) {
1424 return absl::InvalidArgumentError(
1425 "Source artifact diagnostic labels must not be empty");
1426 }
1427 if (targets.empty()) {
1428 return absl::InvalidArgumentError(
1429 "Source artifact publication requires a writable target");
1430 }
1431
1432 std::vector<fs::path> all_targets = targets;
1433 all_targets.insert(all_targets.end(), read_only_targets.begin(),
1434 read_only_targets.end());
1435
1436 std::vector<fs::path> resolved_targets;
1437 ASSIGN_OR_RETURN(resolved_targets,
1438 ResolvePublicationTargets(all_targets, labels));
1439 RETURN_IF_ERROR(ValidateDistinctPublicationTargets(resolved_targets, labels));
1440 const std::vector<fs::path> resolved_writable(
1441 resolved_targets.begin(), resolved_targets.begin() + targets.size());
1442 const std::vector<fs::path> resolved_read_only(
1443 resolved_targets.begin() + targets.size(), resolved_targets.end());
1444
1445 std::vector<fs::path> lock_paths;
1446 ASSIGN_OR_RETURN(lock_paths, PublicationLockPaths(resolved_writable, labels));
1447 // Read-only references may name future control files whose directories do
1448 // not exist yet. Only writable directories need persistent lock paths.
1450 ValidateTargetsDoNotAliasLocks(lock_paths, resolved_targets, labels));
1451 std::unique_ptr<SourceArtifactWriteLocks> write_locks;
1452 ASSIGN_OR_RETURN(write_locks,
1453 SourceArtifactWriteLocks::Acquire(lock_paths, labels));
1454
1455 std::vector<fs::path> resolved_after_lock;
1456 ASSIGN_OR_RETURN(resolved_after_lock,
1457 ResolvePublicationTargets(all_targets, labels));
1458 if (resolved_after_lock != resolved_targets) {
1459 return absl::AbortedError(absl::StrFormat(
1460 "%s publication target changed while acquiring its lock",
1461 Capitalized(labels.subject)));
1462 }
1463
1464 auto impl = std::make_unique<SourceArtifactPublicationLock::Impl>();
1465 impl->labels = labels;
1466 impl->targets = std::move(resolved_targets);
1467 impl->read_only_targets = resolved_read_only;
1468 impl->write_locks = std::move(write_locks);
1469 return std::unique_ptr<SourceArtifactPublicationLock>(
1470 new SourceArtifactPublicationLock(std::move(impl)));
1471}
1472
1473std::string ComputeSourceArtifactSha256(std::string_view content) {
1474 return Sha256Hex(content);
1475}
1476
1477absl::StatusOr<std::string> ComputeSourceArtifactFileSha256(
1478 const fs::path& path) {
1479 std::error_code error;
1480 if (!fs::is_regular_file(path, error) || error) {
1481 return absl::FailedPreconditionError(
1482 "SHA256 input is not a readable regular file: " + path.string());
1483 }
1484 std::ifstream input(path, std::ios::binary);
1485 if (!input) {
1486 return absl::PermissionDeniedError("Cannot open SHA256 input: " +
1487 path.string());
1488 }
1489 Sha256Digest digest;
1490 std::array<char, 64 * 1024> buffer;
1491 while (input.read(buffer.data(), buffer.size()) || input.gcount() > 0) {
1492 digest.Update(std::string_view(buffer.data(), input.gcount()));
1493 }
1494 if (!input.eof() || input.bad()) {
1495 return absl::DataLossError("Cannot read SHA256 input: " + path.string());
1496 }
1497 auto result = digest.Finish();
1498 if (!result.empty()) {
1499 return result;
1500 }
1501 // Reuse the same open file, but restart the hash from its first byte after
1502 // native failure. Replaying just the last chunk would produce a false hash.
1503 input.clear();
1504 input.seekg(0, std::ios::beg);
1505 if (!input) {
1506 return absl::DataLossError("Cannot rewind SHA256 input: " + path.string());
1507 }
1508 PortableSha256Digest fallback;
1509 while (input.read(buffer.data(), buffer.size()) || input.gcount() > 0) {
1510 fallback.Update(std::string_view(buffer.data(), input.gcount()));
1511 }
1512 if (!input.eof() || input.bad()) {
1513 return absl::DataLossError("Cannot reread SHA256 input: " + path.string());
1514 }
1515 return fallback.Finish();
1516}
1517
1520 std::vector<SourceArtifactUpdate> updates,
1521 std::string_view expected_primary_sha256,
1522 SourceArtifactReadbackValidator readback_validator) {
1523 if (lock.impl_ == nullptr) {
1524 return absl::FailedPreconditionError(
1525 "Source artifact publication lock is not initialized");
1526 }
1527 if (lock.impl_->publication_in_progress.test_and_set(
1528 std::memory_order_acquire)) {
1529 return absl::FailedPreconditionError(
1530 "Source artifact publication lock is already in use");
1531 }
1532 const ScopedPublicationUse publication_use(
1533 &lock.impl_->publication_in_progress);
1534 if (updates.empty()) {
1535 return absl::InvalidArgumentError(
1536 "Source artifact publication requires at least one update");
1537 }
1538 if (!updates.front().before.has_value()) {
1539 return absl::InvalidArgumentError(
1540 "Primary source artifact must contain its preflight bytes");
1541 }
1542
1543 std::vector<std::string> seen_targets;
1544 seen_targets.reserve(updates.size());
1545 for (SourceArtifactUpdate& update : updates) {
1546 fs::path resolved_target;
1547 ASSIGN_OR_RETURN(resolved_target, ResolvePublicationTarget(
1548 update.target, lock.impl_->labels));
1549 const auto covered = std::find(lock.impl_->targets.begin(),
1550 lock.impl_->targets.end(), resolved_target);
1551 if (covered == lock.impl_->targets.end()) {
1552 return absl::FailedPreconditionError(absl::StrFormat(
1553 "Publication target is not covered by the acquired lock: %s",
1554 update.target.string()));
1555 }
1556 update.target = *covered;
1557 if (std::find(lock.impl_->read_only_targets.begin(),
1558 lock.impl_->read_only_targets.end(),
1559 update.target) != lock.impl_->read_only_targets.end() &&
1560 (!update.before.has_value() || *update.before != update.after)) {
1561 return absl::FailedPreconditionError(
1562 absl::StrFormat("Read-only publication target cannot change: %s",
1563 update.target.string()));
1564 }
1565 const std::string normalized_target = LowercasePath(resolved_target);
1566 if (std::find(seen_targets.begin(), seen_targets.end(),
1567 normalized_target) != seen_targets.end()) {
1568 return absl::InvalidArgumentError(absl::StrFormat(
1569 "Source artifact publication target is duplicated: %s",
1570 update.target.string()));
1571 }
1572 seen_targets.push_back(normalized_target);
1573 }
1574
1575 std::vector<PublicationArtifact> artifacts;
1576 artifacts.reserve(updates.size());
1577 for (SourceArtifactUpdate& update : updates) {
1578 artifacts.push_back(PublicationArtifact{
1579 .target = std::move(update.target),
1580 .content = std::move(update.after),
1581 .original_content = std::move(update.before),
1582 });
1583 }
1584
1585 RETURN_IF_ERROR(PublishArtifactSet(&artifacts, expected_primary_sha256,
1586 lock.impl_->labels));
1587 if (readback_validator) {
1588 absl::Status validation_status;
1589 try {
1590 validation_status = readback_validator();
1591 } catch (const std::exception& error) {
1592 validation_status = absl::InternalError(absl::StrFormat(
1593 "%s readback validator threw an exception: %s",
1594 Capitalized(lock.impl_->labels.subject), error.what()));
1595 } catch (...) {
1596 validation_status = absl::InternalError(
1597 absl::StrFormat("%s readback validator threw an unknown exception",
1598 Capitalized(lock.impl_->labels.subject)));
1599 }
1600 if (!validation_status.ok()) {
1601 return RollBackPublication(&artifacts, validation_status,
1602 lock.impl_->labels);
1603 }
1604 }
1605
1606 // The domain validator can take time (or observe a concurrent source edit).
1607 // Recheck inputs after it completes; rollback only our output replacements.
1608 for (const auto& artifact : artifacts) {
1609 if (!artifact.IsReadOnlyGuard()) {
1610 continue;
1611 }
1612 auto current = ReadTextFile(artifact.target, "read-only publication input");
1613 if (!current.ok() || *current != artifact.content) {
1614 const auto status = current.ok()
1615 ? absl::AbortedError(absl::StrFormat(
1616 "Read-only publication input changed: %s",
1617 artifact.target.string()))
1618 : current.status();
1619 return RollBackPublication(&artifacts, status, lock.impl_->labels);
1620 }
1621 }
1622
1623 const absl::Status backup_cleanup =
1624 CleanupBackups(&artifacts, lock.impl_->labels);
1625 if (!backup_cleanup.ok()) {
1626 return RollBackPublication(&artifacts, backup_cleanup, lock.impl_->labels);
1627 }
1628 return absl::OkStatus();
1629}
1630
1631} // namespace yaze::core
SourceArtifactPublicationLock(const SourceArtifactPublicationLock &)=delete
ScopedPublicationUse & operator=(const ScopedPublicationUse &)=delete
static absl::StatusOr< std::unique_ptr< SourceArtifactWriteLocks > > Acquire(const std::vector< fs::path > &lock_paths, const SourceArtifactPublisherLabels &labels)
SourceArtifactWriteLocks & operator=(const SourceArtifactWriteLocks &)=delete
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::string BackupRecoveryPaths(const std::vector< PublicationArtifact > &artifacts)
absl::StatusOr< fs::path > CreateBackup(const fs::path &target)
absl::StatusOr< std::vector< fs::path > > ResolvePublicationTargets(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
absl::StatusOr< std::vector< fs::path > > PreparePublicationTargets(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
absl::StatusOr< fs::path > WriteExclusiveTemp(const fs::path &target, std::string_view content)
absl::Status ValidateDistinctPublicationTargets(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
absl::StatusOr< std::vector< fs::path > > PublicationLockPaths(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
absl::Status PublishArtifactSet(std::vector< PublicationArtifact > *artifacts, std::string_view expected_source_sha256, const SourceArtifactPublisherLabels &labels)
constexpr uint32_t RotateRight(uint32_t value, uint32_t count)
fs::path NextSiblingPath(const fs::path &target, absl::string_view purpose)
absl::Status CleanupPublicationPaths(const std::vector< fs::path > &paths, absl::string_view artifact_kind, const SourceArtifactPublisherLabels &labels)
absl::Status RestoreArtifact(const PublicationArtifact &artifact)
void TransformSha256(uint32_t state[8], const uint8_t block[64])
absl::Status CleanupTemps(const std::vector< PublicationArtifact > &artifacts, const SourceArtifactPublisherLabels &labels)
absl::Status CleanupBackups(std::vector< PublicationArtifact > *artifacts, const SourceArtifactPublisherLabels &labels)
absl::Status RollBackPublication(std::vector< PublicationArtifact > *artifacts, const absl::Status &publication_failure, const SourceArtifactPublisherLabels &labels)
absl::Status ReturnFailureAfterCleanup(std::vector< PublicationArtifact > *artifacts, const absl::Status &publication_failure, const SourceArtifactPublisherLabels &labels)
absl::StatusOr< fs::path > ResolvePublicationTarget(const fs::path &target, const SourceArtifactPublisherLabels &labels)
absl::Status ValidateTargetsDoNotAliasLocks(const std::vector< fs::path > &lock_paths, const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
absl::Status ReplaceFromTemp(const fs::path &temp, const fs::path &target, bool *replaced=nullptr, bool replace_existing=true)
absl::Status VerifyUnchangedBeforePublication(const std::vector< PublicationArtifact > &artifacts, std::string_view expected_source_sha256, const SourceArtifactPublisherLabels &labels)
absl::Status PublishSourceArtifacts(const SourceArtifactPublicationLock &lock, std::vector< SourceArtifactUpdate > updates, std::string_view expected_primary_sha256, SourceArtifactReadbackValidator readback_validator)
constexpr char kSourceArtifactPublicationLockBasename[]
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::Status ValidateSourceArtifactPublicationTargets(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
absl::StatusOr< std::string > ComputeSourceArtifactFileSha256(const fs::path &path)
std::function< absl::Status()> SourceArtifactReadbackValidator
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels, const std::vector< fs::path > &read_only_targets)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::unique_ptr< SourceArtifactWriteLocks > write_locks