#include "sqlite_support.hpp" #include #include #include #include #include #include #include #include namespace noto::detail { constexpr auto sqlite_sidecar_suffixes = std::array{"-wal", "-shm", "-journal"}; auto errno_status(std::string operation, std::int32_t code) -> std::expected { 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 { 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 { 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 { 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(::close(fd)); return status; } return close_fd(fd, operation); } auto sync_parent_directory(std::string const &path, std::string_view operation) -> std::expected { 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(::close(fd)); return status; } return close_fd(fd, operation); } auto make_temporary_path(std::string const &target, std::string_view operation) -> std::expected { 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{ 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(::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(::unlink(path.c_str())); } } auto remove_file_if_exists(std::string const &path, std::string_view operation) -> std::expected { 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 { 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 { 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 { auto const *tail = static_cast(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 { 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, 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{identity}; } if (errno == ENOENT || errno == ENOTDIR) { return std::optional{}; } return std::unexpected(errno_status(std::string{operation}, errno).error()); } auto protected_path_for(std::string const &path, std::string_view operation) -> std::expected { 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, modulus::error> { auto *raw_statement = static_cast(nullptr); auto prepared = prepare_statement(database, "PRAGMA database_list", &raw_statement, "prepare open database list"); if (!prepared) { return failure_from>(prepared); } auto statement = std::unique_ptr{ raw_statement, sqlite3_finalize, }; auto paths = std::vector{}; while (true) { auto code = sqlite3_step(statement.get()); if (code == SQLITE_DONE) { return paths; } if (code != SQLITE_ROW) { return failure_from>(noto::result_from(code, database, "collect open database list")); } if (sqlite3_column_type(statement.get(), 2) != SQLITE_TEXT) { continue; } auto const *text = reinterpret_cast(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(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 { 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 {}; } }