276 lines
9.8 KiB
C++
276 lines
9.8 KiB
C++
#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 {};
|
|
}
|
|
|
|
}
|