#include "config.hpp" #include #include #include #include #include namespace ranczo { /// UWAGA: /// Funkcja zakłada, że pod adresem data()[size()] znajduje się legalna pamięć. /// To jest bezpieczne tylko, jeśli string_view pochodzi z: /// - C-stringa /// - std::string /// - innego bufora, który ma '\0' za końcem /// /// Jeśli string_view zawiera dokładnie taki fragment: /// "abc" → OK /// "abc\0xyz" → będzie wykrywać '\0' po size()==3 → OK /// Jeśli string_view powstał z danych binary/packed → UWAŻAJ. inline bool is_null_terminated(std::string_view sv) { return true; // if(sv.empty()) // return true; // pusty string_view jest "C-stringiem" // const char * ptr = sv.data(); // return ptr[sv.size()] == '\0'; } std::string SettingsStore::Value::to_db_text() const { using namespace boost::json; object o; std::visit( [&](auto const & v) { using U = std::decay_t< decltype(v) >; if constexpr(std::is_same_v< U, int64_t >) { o["type"] = "int"; o["value"] = static_cast< std::int64_t >(v); } else if constexpr(std::is_same_v< U, double >) { o["type"] = "double"; o["value"] = v; } else if constexpr(std::is_same_v< U, std::string >) { o["type"] = "string"; o["value"] = v; } else if constexpr(std::is_same_v< U, bool >) { o["type"] = "bool"; o["value"] = v; } else if constexpr(std::is_same_v< U, Json >) { o["type"] = "json"; o["value"] = v; } else { static_assert(sizeof(U) == 0, "Unhandled variant type"); } }, data_); return serialize(o); } SettingsStore::Value SettingsStore::Value::from_db_text(std::string_view s) { using namespace boost::json; value j; try { j = parse(s); } catch(const std::exception & e) { spdlog::error("Value::from_db_text parse error: {}", e.what()); // traktujemy to jako zwykły string return Value{s}; } if(!j.is_object()) { // traktujemy jako ogólny JSON return Value{j}; } object & o = j.as_object(); auto * t_it = o.if_contains("type"); auto * v_it = o.if_contains("value"); if(!t_it || !v_it || !t_it->is_string()) { // coś dziwnego -> traktuj jako ogólny JSON return Value{j}; } std::string type = std::string(t_it->as_string().c_str()); try { if(type == "int") { return Value{static_cast< int64_t >(v_it->as_int64())}; } else if(type == "double") { return Value{v_it->as_double()}; } else if(type == "string") { return Value{std::string(v_it->as_string().c_str())}; } else if(type == "bool") { return Value{v_it->as_bool()}; } else if(type == "json") { return Value{*v_it}; } else { // nieznany typ -> zachowaj jako cały JSON return Value{j}; } } catch(const std::exception & e) { spdlog::error("Value::from_db_text type conversion error: {}", e.what()); // fallback na ogólny json return Value{j}; } } SettingsStore::SettingsStore(std::string_view app_db_path, executor_type main_exec, std::size_t db_threads) : main_exec_(main_exec), db_pool_(db_threads), db_path_(app_db_path) { open_or_create(); prepare_schema(); } SettingsStore::~SettingsStore() { if(db_) { sqlite3_close(db_); db_ = nullptr; } db_pool_.join(); } bool SettingsStore::contains(std::string_view component, std::string_view key) const { BOOST_ASSERT(component.size() < 256); BOOST_ASSERT(key.size() < 256); BOOST_ASSERT(is_null_terminated(component)); BOOST_ASSERT(is_null_terminated(key)); spdlog::debug("SettingsStore::contains_sync({}, {})", component, key); const char * sql = "SELECT 1 FROM settings " "WHERE component = ?1 AND key = ?2 LIMIT 1"; sqlite3_stmt * stmt = nullptr; check_sqlite(sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr), "prepare SELECT contains"); auto stmt_guard = make_stmt_guard(stmt); check_sqlite(sqlite3_bind_text(stmt, 1, component.data(), -1, SQLITE_TRANSIENT), "bind component"); check_sqlite(sqlite3_bind_text(stmt, 2, key.data(), -1, SQLITE_TRANSIENT), "bind key"); int rc = sqlite3_step(stmt); if(rc == SQLITE_ROW) { return true; } if(rc == SQLITE_DONE) { return false; } check_sqlite(rc, "step SELECT contains"); return false; } awaitable< bool > SettingsStore::async_contains(std::string_view component, std::string_view key) const noexcept { BOOST_ASSERT(component.size() < 256); BOOST_ASSERT(key.size() < 256); BOOST_ASSERT(is_null_terminated(component)); BOOST_ASSERT(is_null_terminated(key)); auto caller_exec = co_await boost::asio::this_coro::executor; bool exists = false; // Przeskok do puli DB co_await boost::asio::post(db_pool_, boost::asio::use_awaitable); try { exists = contains(component, key); _log.debug("{}/{} is {}", component, key, exists ? "awailable" : "unawailable"); } catch(const std::exception & e) { spdlog::error("async_contains exception: {}", e.what()); exists = false; } // Z powrotem na executor wywołujący co_await boost::asio::post(caller_exec, boost::asio::use_awaitable); _log.debug("{}/{} is {}", component, key, exists ? "awailable" : "unawailable"); co_return exists; } void SettingsStore::save(std::string_view component, std::string_view key, const Value & value) { BOOST_ASSERT(component.size() < 256); BOOST_ASSERT(key.size() < 256); BOOST_ASSERT(is_null_terminated(component)); BOOST_ASSERT(is_null_terminated(key)); spdlog::debug("SettingsStore::save_sync({}, {})", component, key); const char * sql = "INSERT INTO settings(component, key, value) " "VALUES(?1, ?2, ?3) " "ON CONFLICT(component, key) " "DO UPDATE SET value = excluded.value"; sqlite3_stmt * stmt = nullptr; check_sqlite(sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr), "prepare INSERT/UPDATE"); auto stmt_guard = make_stmt_guard(stmt); check_sqlite(sqlite3_bind_text(stmt, 1, component.data(), static_cast(component.size()), SQLITE_TRANSIENT), "bind component"); check_sqlite(sqlite3_bind_text(stmt, 2, key.data(),static_cast(key.size()), SQLITE_TRANSIENT), "bind key"); std::string s = value.to_db_text(); check_sqlite(sqlite3_bind_text(stmt, 3, s.c_str(), static_cast(s.size()), SQLITE_TRANSIENT), "bind value"); int rc = sqlite3_step(stmt); check_sqlite(rc == SQLITE_DONE ? SQLITE_OK : rc, "step INSERT/UPDATE"); } awaitable_expected< void > SettingsStore::async_save(std::string_view component, std::string_view key, const Value & value) noexcept { BOOST_ASSERT(component.size() < 256); BOOST_ASSERT(key.size() < 256); BOOST_ASSERT(is_null_terminated(component)); BOOST_ASSERT(is_null_terminated(key)); auto caller_exec = co_await boost::asio::this_coro::executor; // do puli DB co_await boost::asio::post(db_pool_, boost::asio::use_awaitable); try { save(component, key, value); } catch(const std::exception & e) { spdlog::error("async_save exception: {}", e.what()); } // z powrotem co_await boost::asio::post(caller_exec, boost::asio::use_awaitable); co_return _void{}; } void SettingsStore::open_or_create() { int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX; // bezpieczne między wątkami int rc = sqlite3_open_v2(db_path_.c_str(), &db_, flags, nullptr); if(rc != SQLITE_OK) { std::string msg = sqlite3_errmsg(db_); sqlite3_close(db_); db_ = nullptr; throw std::runtime_error("Failed to open DB: " + msg); } spdlog::info("Opened SQLite DB: {}", db_path_); } void SettingsStore::prepare_schema() { // value jako TEXT (zapisujemy tam JSON) const char * sql = "CREATE TABLE IF NOT EXISTS settings (" " component TEXT NOT NULL," " key TEXT NOT NULL," " value TEXT," " PRIMARY KEY(component, key)" ");"; char * errmsg = nullptr; int rc = sqlite3_exec(db_, sql, nullptr, nullptr, &errmsg); if(rc != SQLITE_OK) { std::string msg = errmsg ? errmsg : "unknown error"; sqlite3_free(errmsg); throw std::runtime_error("Failed to create schema: " + msg); } } void SettingsStore::check_sqlite(int rc, const char * what) { if(rc != SQLITE_OK && rc != SQLITE_DONE && rc != SQLITE_ROW) { throw std::runtime_error(std::string("SQLite error in ") + what + ": " + sqlite3_errstr(rc)); } } awaitable_expected< std::optional< SettingsStore::Value > > SettingsStore::async_get(std::string_view component, std::string_view key) noexcept { BOOST_ASSERT(component.size() < 256); BOOST_ASSERT(key.size() < 256); BOOST_ASSERT(is_null_terminated(component)); BOOST_ASSERT(is_null_terminated(key)); auto caller_exec = co_await boost::asio::this_coro::executor; std::optional< Value > result; // przeskok do puli DB co_await boost::asio::post(db_pool_, boost::asio::use_awaitable); try { result = get(component, key); } catch(const std::exception & e) { spdlog::error("async_get exception: {}", e.what()); result = std::nullopt; } _log.debug("get({}, {}) done, make context switch", component, key); // z powrotem na executor wywołujący co_await boost::asio::post(caller_exec, boost::asio::use_awaitable); _log.debug("get({}, {}) returning valu", component, key); co_return result; } std::optional< SettingsStore::Value > SettingsStore::get(std::string_view component, std::string_view key) { BOOST_ASSERT(component.size() < 256); BOOST_ASSERT(key.size() < 256); BOOST_ASSERT(is_null_terminated(component)); BOOST_ASSERT(is_null_terminated(key)); _log.debug("get({}, {})", component, key); const char * sql = "SELECT value FROM settings " "WHERE component = ?1 AND key = ?2"; sqlite3_stmt * stmt = nullptr; check_sqlite(sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr), "prepare SELECT"); auto stmt_guard = make_stmt_guard(stmt); check_sqlite(sqlite3_bind_text(stmt, 1, component.data(), static_cast(component.size()), SQLITE_TRANSIENT), "bind component"); check_sqlite(sqlite3_bind_text(stmt, 2, key.data(), static_cast(key.size()), SQLITE_TRANSIENT), "bind key"); int rc = sqlite3_step(stmt); if(rc == SQLITE_ROW) { if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) { _log.debug("get({}, {}) return nullopt", component, key); return std::nullopt; } const unsigned char * text = sqlite3_column_text(stmt, 0); int len = sqlite3_column_bytes(stmt, 0); if(!text || len <= 0) { _log.debug("get({}, {}) return nullopt", component, key); return std::nullopt; } _log.debug("get({}, {}) return value", component, key); return Value::from_db_text({reinterpret_cast< const char * >(text), static_cast(len)}); } if(rc == SQLITE_DONE) { _log.debug("get({}, {}) not found", component, key); return std::nullopt; } check_sqlite(rc, "step SELECT"); return std::nullopt; // tu nie dojdziemy } SettingsStore::StmtGuard::~StmtGuard() { if(stmt) sqlite3_finalize(stmt); } } // namespace ranczo