Cleanup
publish / publish (push) Successful in 16m12s

This commit is contained in:
khodowany
2026-10-08 02:13:03 -05:00
parent b721729e2a
commit 2f3870baff
11 changed files with 2022 additions and 1856 deletions
+275
View File
@@ -0,0 +1,275 @@
#include "sqlite_support.hpp"
#include <noto/errors.hpp>
#include <array>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
namespace noto::detail {
constexpr auto sqlite_sidecar_suffixes = std::array<std::string_view, 3>{"-wal", "-shm", "-journal"};
auto errno_status(std::string operation, std::int32_t code) -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::error_unavailable, std::move(operation) + ": " + std::error_code{
code,
std::generic_category(),
}
.message());
}
auto filesystem_status(std::string operation, std::error_code const &code) -> std::expected<void, modulus::error> {
return noto::detail::fail_void(noto::detail::error_unavailable, std::move(operation) + ": " + code.message());
}
auto close_fd(std::int32_t fd, std::string_view operation) -> std::expected<void, modulus::error> {
if (::close(fd) == 0) {
return {};
}
return errno_status(std::string{operation}, errno);
}
auto sync_file_path(std::string const &path, std::string_view operation) -> std::expected<void, modulus::error> {
auto fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC);
if (fd < 0) {
return errno_status(std::string{operation}, errno);
}
if (::fsync(fd) != 0) {
auto status = errno_status(std::string{operation}, errno);
static_cast<void>(::close(fd));
return status;
}
return close_fd(fd, operation);
}
auto sync_parent_directory(std::string const &path, std::string_view operation) -> std::expected<void, modulus::error> {
auto parent = std::filesystem::path{path}.parent_path();
if (parent.empty()) {
parent = ".";
}
auto directory = parent.string();
auto fd = ::open(directory.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0) {
return errno_status(std::string{operation}, errno);
}
if (::fsync(fd) != 0) {
auto status = errno_status(std::string{operation}, errno);
static_cast<void>(::close(fd));
return status;
}
return close_fd(fd, operation);
}
auto make_temporary_path(std::string const &target, std::string_view operation) -> std::expected<std::string, modulus::error> {
auto target_path = std::filesystem::path{target};
auto parent = target_path.parent_path();
if (parent.empty()) {
parent = ".";
}
auto filename = target_path.filename().string();
if (filename.empty()) {
filename = "noto";
}
auto pattern = (parent / (filename + ".noto-tmp-XXXXXX")).string();
auto buffer = std::vector<std::char_t>{
pattern.begin(),
pattern.end(),
};
buffer.push_back('\0');
auto fd = ::mkstemp(buffer.data());
if (fd < 0) {
return std::unexpected(errno_status(std::string{operation}, errno).error());
}
auto path = std::string{buffer.data()};
auto closed = close_fd(fd, operation);
if (!closed) {
static_cast<void>(::unlink(path.c_str()));
return std::unexpected(closed.error());
}
return path;
}
auto remove_file_noexcept(std::string const &path) noexcept -> void {
if (!path.empty()) {
static_cast<void>(::unlink(path.c_str()));
}
}
auto remove_file_if_exists(std::string const &path, std::string_view operation) -> std::expected<void, modulus::error> {
if (::unlink(path.c_str()) == 0 || errno == ENOENT) {
return {};
}
return errno_status(std::string{operation}, errno);
}
auto remove_database_sidecars(std::string const &path, std::string_view operation) -> std::expected<void, modulus::error> {
for (auto suffix : sqlite_sidecar_suffixes) {
auto status = remove_file_if_exists(path + std::string{suffix}, operation);
if (!status) {
return status;
}
}
return {};
}
auto remove_database_files_noexcept(std::string const &path) noexcept -> void {
remove_file_noexcept(path);
for (auto suffix : sqlite_sidecar_suffixes) {
remove_file_noexcept(path + std::string{suffix});
}
}
auto replace_file_atomically(std::string const &temporary, std::string const &destination, std::string_view operation) -> std::expected<void, modulus::error> {
auto status = sync_file_path(temporary, operation);
if (!status) {
return status;
}
if (::rename(temporary.c_str(), destination.c_str()) != 0) {
return errno_status(std::string{operation}, errno);
}
return sync_parent_directory(destination, operation);
}
auto prepare_statement(sqlite3 *database, std::string const &sql, sqlite3_stmt **statement, std::string_view operation) -> std::expected<void, modulus::error> {
auto const *tail = static_cast<std::char_t const *>(nullptr);
auto code = sqlite3_prepare_v2(database, sql.c_str(), -1, statement, &tail);
auto status = noto::result_from(code, database, operation);
if (!status) {
return status;
}
if (*statement == nullptr) {
return {};
}
if (!trailing_sql_is_empty(tail)) {
sqlite3_finalize(*statement);
*statement = nullptr;
return invalid("only one SQL statement may be executed at a time");
}
return {};
}
auto same_identity(file_identity const &left, file_identity const &right) -> std::bool_t {
return left.device == right.device && left.inode == right.inode;
}
auto normalized_path(std::string const &path, std::string_view operation) -> std::expected<std::filesystem::path, modulus::error> {
auto error = std::error_code{};
auto absolute = std::filesystem::absolute(path, error);
if (error) {
return std::unexpected(filesystem_status(std::string{operation}, error).error());
}
auto normalized = std::filesystem::weakly_canonical(absolute, error);
if (error) {
return std::unexpected(filesystem_status(std::string{operation}, error).error());
}
return normalized.lexically_normal();
}
auto identity_for_path(std::filesystem::path const &path, std::string_view operation) -> std::expected<std::optional<file_identity>, modulus::error> {
struct stat data{};
auto text = path.string();
if (::stat(text.c_str(), &data) == 0) {
auto identity = file_identity{};
identity.device = data.st_dev;
identity.inode = data.st_ino;
return std::optional<file_identity>{identity};
}
if (errno == ENOENT || errno == ENOTDIR) {
return std::optional<file_identity>{};
}
return std::unexpected(errno_status(std::string{operation}, errno).error());
}
auto protected_path_for(std::string const &path, std::string_view operation) -> std::expected<protected_path, modulus::error> {
auto normalized = normalized_path(path, operation);
if (!normalized) {
return std::unexpected(normalized.error());
}
auto identity = identity_for_path(*normalized, operation);
if (!identity) {
return std::unexpected(identity.error());
}
return protected_path{
.normalized = std::move(*normalized),
.identity = std::move(*identity),
};
}
auto same_protected_path(protected_path const &left, protected_path const &right) -> std::bool_t {
if (left.normalized == right.normalized) {
return true;
}
return left.identity && right.identity && same_identity(*left.identity, *right.identity);
}
auto live_database_paths(sqlite3 *database) -> std::expected<std::vector<std::string>, modulus::error> {
auto *raw_statement = static_cast<sqlite3_stmt *>(nullptr);
auto prepared = prepare_statement(database, "PRAGMA database_list", &raw_statement, "prepare open database list");
if (!prepared) {
return failure_from<std::vector<std::string>>(prepared);
}
auto statement = std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)>{
raw_statement,
sqlite3_finalize,
};
auto paths = std::vector<std::string>{};
while (true) {
auto code = sqlite3_step(statement.get());
if (code == SQLITE_DONE) {
return paths;
}
if (code != SQLITE_ROW) {
return failure_from<std::vector<std::string>>(noto::result_from(code, database, "collect open database list"));
}
if (sqlite3_column_type(statement.get(), 2) != SQLITE_TEXT) {
continue;
}
auto const *text = reinterpret_cast<std::char_t const *>(sqlite3_column_text(statement.get(), 2));
auto bytes = sqlite3_column_bytes(statement.get(), 2);
auto path = text == nullptr ? std::string{} : std::string{
text,
static_cast<std::size_t>(bytes),
};
if (!path.empty()) {
paths.push_back(std::move(path));
}
}
}
auto validate_output_not_live_database(sqlite3 *database, std::string const &path, std::string_view label) -> std::expected<void, modulus::error> {
auto target = protected_path_for(path, std::string{"check "} + std::string{label});
if (!target) {
return std::unexpected(target.error());
}
auto live_paths = live_database_paths(database);
if (!live_paths) {
return std::unexpected(live_paths.error());
}
for (auto const &live_path : *live_paths) {
auto protected_live = protected_path_for(live_path, "check open database path");
if (!protected_live) {
return std::unexpected(protected_live.error());
}
if (same_protected_path(*target, *protected_live)) {
return invalid(std::string{label} + " must not target an open database file");
}
for (auto suffix : sqlite_sidecar_suffixes) {
auto protected_sidecar = protected_path_for(live_path + std::string{suffix}, "check open database sidecar path");
if (!protected_sidecar) {
return std::unexpected(protected_sidecar.error());
}
if (same_protected_path(*target, *protected_sidecar)) {
return invalid(std::string{label} + " must not target an open database sidecar");
}
}
}
return {};
}
}