diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index 90d0ec7a..845ae332 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -1,8 +1,6 @@ #include "db_manager.h" #include "../src/debug_ui.h" #include "../../lib/debug_config.h" -#include -#include #include #include #include @@ -36,12 +34,6 @@ QString DbManager::peersTableName(const QString& chId) const { return QStringLiteral("peers_") + sanitizeTableName(chId); } -uint64_t DbManager::computeDatahash(const QByteArray& data) { - uint8_t hash[32]; - SHA256((const unsigned char*)data.constData(), data.size(), hash); - uint64_t dh; memcpy(&dh, hash, 8); return dh; -} - /* ── lifecycle ── */ DbManager::DbManager(QObject* parent) @@ -50,22 +42,36 @@ DbManager::DbManager(QObject* parent) } DbManager::~DbManager() { - for (auto it = m_cursors.begin(); it != m_cursors.end(); ++it) - sqlite3_finalize(it.value()); - m_cursors.clear(); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "DbManager: destroyed"); } sqlite3_stmt* DbManager::prepareOrNull(const char* sql) const { + if (!m_db) { GUI_DEBUG("read skipped: database unavailable sql=%s", sql); return nullptr; } sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(m_db, sql, -1, &stmt, nullptr) != SQLITE_OK) { const char* err = sqlite3_errmsg(m_db); - if (!strstr(err, "no such table")) GUI_ERROR("prepare failed: %s — %s", sql, err); + if (strstr(err, "no such table")) GUI_DEBUG("optional table unavailable: %s — %s", sql, err); + else GUI_ERROR("prepare failed: %s — %s", sql, err); + sqlite3_finalize(stmt); return nullptr; } + if (!sqlite3_stmt_readonly(stmt)) { + GUI_ERROR("GUI write rejected: %s", sql); sqlite3_finalize(stmt); return nullptr; + } return stmt; } +/* Ошибки bind/step/finalize не превращаются в частичный успешный снимок. */ +static bool finishRead(sqlite3_stmt* statement, int result) { + sqlite3* db = sqlite3_db_handle(statement); + const QByteArray sql = sqlite3_sql(statement); + const bool ok = result == SQLITE_ROW || result == SQLITE_DONE; + if (!ok) GUI_ERROR("read failed: rc=%d sql=%s error=%s", result, sql.constData(), sqlite3_errmsg(db)); + const int final = sqlite3_finalize(statement); + if (final != SQLITE_OK) GUI_ERROR("read finalize failed: rc=%d sql=%s error=%s", final, sql.constData(), sqlite3_errmsg(db)); + return ok && final == SQLITE_OK; +} + QString DbManager::colText(sqlite3_stmt* stmt, int col) { return QString::fromUtf8( reinterpret_cast(sqlite3_column_text(stmt, col))); @@ -396,19 +402,6 @@ QMap DbManager::getLocalNicknames(const QString& chId) const { return m; } -void DbManager::cleanOrphanedPeers(const QString& chId) const { - if (!m_db) return; - QString sql = QStringLiteral( - "DELETE FROM \"%1\" WHERE node_id NOT IN (SELECT node_id FROM nodes)" - ).arg(peersTableName(chId)); - char* err = nullptr; - int rc = sqlite3_exec(m_db, sql.toUtf8().constData(), nullptr, nullptr, &err); - if (rc != SQLITE_OK) { - qWarning("cleanOrphanedPeers: %s", err ? err : "?"); - sqlite3_free(err); - } -} - QString DbManager::getDisplayName(quint64 nodeId) const { sqlite3_stmt* st = prepareOrNull( "SELECT display_name FROM accounts WHERE node_id=?"); @@ -432,60 +425,6 @@ QString DbManager::getDisplayName(quint64 nodeId) const { return QString("node_%1").arg(nodeId); } -QByteArray DbManager::loadNodeInfo(quint64 nodeId) const { - /* binary format: [x25519:32][ed25519:32][addr_count:1]([family:1][proto:1][addr_len:1][addr:4|16][port:2][rtt:2])... */ - sqlite3_stmt* stmt = prepareOrNull( - "SELECT x25519_pubkey, ed25519_pubkey FROM nodes WHERE node_id=?"); - if (!stmt) return {}; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); - if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return {}; } - QByteArray x25519 = colBlob(stmt, 0); - QByteArray ed25519 = colBlob(stmt, 1); - sqlite3_finalize(stmt); - - QByteArray result; - result.append(x25519.isEmpty() ? QByteArray(32, '\0') : x25519); - result.append(ed25519.isEmpty() ? QByteArray(32, '\0') : ed25519); - - /* запросить адреса */ - stmt = prepareOrNull( - "SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=?"); - if (!stmt) { result.append('\0'); return result; } - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); - QByteArray addrsPart; - uint8_t cnt = 0; - while (sqlite3_step(stmt) == SQLITE_ROW && cnt < 255) { - int family = sqlite3_column_int(stmt, 0); - int proto = sqlite3_column_int(stmt, 1); - QByteArray addr = colBlob(stmt, 2); - uint16_t port = (uint16_t)sqlite3_column_int(stmt, 3); - int16_t rtt = (int16_t)(sqlite3_column_int(stmt, 4)); - addrsPart.append((char)family); - addrsPart.append((char)proto); - addrsPart.append((char)addr.size()); - addrsPart.append(addr); - addrsPart.append((const char*)&port, 2); - addrsPart.append((const char*)&rtt, 2); - cnt++; - } - sqlite3_finalize(stmt); - result.append((char)cnt); - result.append(addrsPart); - return result; -} - -QList DbManager::getOnlinePeers() const { - QList peers; - /* без is_online: получаем всех кроме себя */ - sqlite3_stmt* stmt = prepareOrNull("SELECT node_id FROM nodes WHERE node_id != ?"); - if (!stmt) return peers; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)m_myNodeId); - while (sqlite3_step(stmt) == SQLITE_ROW) - peers.append((quint64)sqlite3_column_int64(stmt, 0)); - sqlite3_finalize(stmt); - return peers; -} - QByteArray DbManager::getNodeEdPub(quint64 nodeId) const { sqlite3_stmt* stmt = prepareOrNull("SELECT ed25519_pubkey FROM nodes WHERE node_id=?"); if (!stmt) return {}; @@ -496,30 +435,6 @@ QByteArray DbManager::getNodeEdPub(quint64 nodeId) const { return r; } -QList DbManager::getOnlinePeerAddresses(quint64 excludeNodeId) const { - QList list; - sqlite3_stmt* stmt = prepareOrNull( - "SELECT na.node_id, na.family, na.protocol, na.address, na.port, na.rtt, na.addr_type" - " FROM node_addresses na JOIN nodes n ON na.node_id = n.node_id" - " WHERE na.addr_type=0 AND na.node_id != ?" - " ORDER BY RANDOM()"); - if (!stmt) return list; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)excludeNodeId); - while (sqlite3_step(stmt) == SQLITE_ROW) { - NodeAddr a; - a.nodeId = (quint64)sqlite3_column_int64(stmt, 0); - a.family = sqlite3_column_int(stmt, 1); - a.protocol = sqlite3_column_int(stmt, 2); - a.address = colBlob(stmt, 3); - a.port = (quint16)sqlite3_column_int(stmt, 4); - a.rtt = sqlite3_column_int(stmt, 5); - a.addrType = sqlite3_column_int(stmt, 6); - list.append(a); - } - sqlite3_finalize(stmt); - return list; -} - /* ── accounts ── */ QList DbManager::getAccounts(bool contactsOnly) const { @@ -602,102 +517,6 @@ int DbManager::getUnreadCount(const QString& channelId, qint64 lastReadMsgId) co return count; } -/* ── sync operations (read-only, for gui_bridge) ── */ - -QByteArray DbManager::syncListChannels() const { - /* binary: [count:2]([chId_len:1][chId:var])... */ - QList chs = getChannels(); - QByteArray out; - uint16_t cnt = (uint16_t)chs.size(); - out.append((const char*)&cnt, 2); - for (auto& ch : chs) { - QByteArray id = ch.channelId.toUtf8(); - out.append((char)(uint8_t)id.size()); - out.append(id); - } - return out; -} - -QByteArray DbManager::syncCount(const QString& chId) const { - QString sql = QStringLiteral("SELECT COUNT(*) FROM \"%1\"").arg(msgTableName(chId)); - sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); - if (!stmt) return {}; - uint32_t cnt = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) cnt = (uint32_t)sqlite3_column_int64(stmt, 0); - sqlite3_finalize(stmt); - QByteArray out; out.append((const char*)&cnt, 4); return out; -} - -QByteArray DbManager::syncCursorOpen(const QString& chId) { - QString sql = QStringLiteral( - "SELECT timestamp, node_id, data, author_signature" - " FROM \"%1\" ORDER BY timestamp, author_signature ASC").arg(msgTableName(chId)); - sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); - if (!stmt) { QByteArray r(4, '\0'); return r; } - uint32_t id = m_nextCursorId++; - m_cursors[id] = stmt; - QByteArray r; r.append((const char*)&id, 4); return r; -} - -QByteArray DbManager::syncCursorNext(uint32_t cursorId) { - auto it = m_cursors.find(cursorId); - if (it == m_cursors.end()) return {}; - if (sqlite3_step(it.value()) != SQLITE_ROW) return {}; - - /* return: [ts:8][dh:8][nid:8][ct_len:1][ct:var][data_len:4][data:var] */ - qint64 ts = sqlite3_column_int64(it.value(), 0); - uint64_t nid = (uint64_t)sqlite3_column_int64(it.value(), 1); - QByteArray jdata = colBlob(it.value(), 2); - /* parse JSON from data column */ - auto jField = [](const QByteArray& j, const char* k) -> QByteArray { - QByteArray p = "\"" + QByteArray(k) + "\":\""; - int i = j.indexOf(p); if (i < 0) return {}; - i += p.size(); int e = i; - while (e < j.size() && !(j[e] == '"' && (e == i || j[e - 1] != '\\'))) e++; - return (e > i) ? QByteArray(j.constData() + i, e - i) : QByteArray(); - }; - QByteArray ct_raw = jField(jdata, "ct"); - QString ct = QString::fromUtf8(ct_raw); - if (ct.isEmpty()) ct = "text/plain"; - QByteArray ddata = jField(jdata, "d"); - /* compute datahash for wire format compat */ - uint8_t dh_buf[32]; SHA256((const uint8_t*)ddata.constData(), ddata.size(), dh_buf); - uint64_t dh; memcpy(&dh, dh_buf, 8); - - QByteArray out; - out.append((const char*)&ts, 8); - out.append((const char*)&dh, 8); - out.append((const char*)&nid, 8); - QByteArray ctb = ct.toUtf8(); - out.append((char)(uint8_t)ctb.size()); - out.append(ctb); - uint32_t dLen = (uint32_t)ddata.size(); - out.append((const char*)&dLen, 4); - out.append(ddata); - return out; -} - -void DbManager::syncCursorClose(uint32_t cursorId) { - auto it = m_cursors.find(cursorId); - if (it != m_cursors.end()) { sqlite3_finalize(it.value()); m_cursors.erase(it); } -} - -QByteArray DbManager::syncListPeers(const QString& chId) const { - /* binary: [count:2]([node_id:8])... */ - QString sql = QStringLiteral("SELECT node_id FROM \"%1\"").arg(peersTableName(chId)); - sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); - if (!stmt) { QByteArray r(2, '\0'); return r; } - QList ids; - while (sqlite3_step(stmt) == SQLITE_ROW) - ids.append((uint64_t)sqlite3_column_int64(stmt, 0)); - sqlite3_finalize(stmt); - QByteArray out; - uint16_t cnt = (uint16_t)ids.size(); - out.append((const char*)&cnt, 2); - for (auto id : ids) out.append((const char*)&id, 8); - return out; -} - /* ── integrity checks ── */ enum class PragmaMode { IntegrityCheck, Command }; diff --git a/tools/chatgui/db/db_manager.h b/tools/chatgui/db/db_manager.h index dd895a8f..ea3932a2 100644 --- a/tools/chatgui/db/db_manager.h +++ b/tools/chatgui/db/db_manager.h @@ -1,6 +1,6 @@ /* SQLite-представление данных для desktop UI: каналы/сообщения/узлы, локальные имена и диагностика БД. - * setDb заимствует handle ядра, destructor закрывает свои statement-курсоры, но не SQLite. - * Перед уничтожением БД закрыть курсоры и сбросить ссылку. Сетевые и подписанные изменения идут через chat_core; + * setDb заимствует handle ядра; каждый запрос закрывает свой statement до возврата, SQLite остаётся ядру. + * Перед уничтожением БД сбросить ссылку. Сетевые и подписанные изменения идут через chat_core; * методы чтения возвращают самостоятельные Qt-значения. */ #ifndef DB_MANAGER_H #define DB_MANAGER_H @@ -58,17 +58,6 @@ struct AccountRow { bool isContact = true; }; -struct NodeAddr { - quint64 nodeId = 0; - int family = 0; - int protocol = 0; // битмаска: 1=UDP, 2=TCP, 4=REALITY (6=TCP+REALITY) - QByteArray address; - quint16 port = 0; - int rtt = 0; - int addrType = 0; // ADDR_TYPE_* - int socketId = 0; // socket_id из ETCP -}; - /* Результат синхронной проверки/обслуживания SQLite; успех optimize не подтверждает целостность данных. */ struct IntegrityResult { bool ok = false; // Проверка: SQL успешен и получен ok без нарушений; optimize: SQL завершён успешно. @@ -124,16 +113,12 @@ public: QList getDmConversations() const; /* ── Участники канала (UI) ── */ - void cleanOrphanedPeers(const QString& chId) const; QString getDisplayName(quint64 nodeId) const; QMap getLocalNicknames(const QString& chId) const; bool isChannelMember(const QString& chId, quint64 nodeId) const; /* ── Узлы ── */ - QByteArray loadNodeInfo(quint64 nodeId) const; - QList getOnlinePeers() const; QByteArray getNodeEdPub(quint64 nodeId) const; - QList getOnlinePeerAddresses(quint64 excludeNodeId) const; /* ── Аккаунты ── */ QList getAccounts(bool contactsOnly = false) const; @@ -143,15 +128,6 @@ public: QString getUiState(const QString& key) const; int getUiStateInt(const QString& key, int defaultVal) const; - /* ── Sync-операции (read-only, для gui_bridge/inline) ── */ - QByteArray syncListChannels() const; - QByteArray syncCount(const QString& chId) const; - QByteArray syncCursorOpen(const QString& chId); - QByteArray syncCursorNext(uint32_t cursorId); - void syncCursorClose(uint32_t cursorId); - QByteArray syncListPeers(const QString& chId) const; - - sqlite3_stmt* prepareOrNull(const char* sql) const; static QString colText(sqlite3_stmt* stmt, int col); static QByteArray colBlob(sqlite3_stmt* stmt, int col); @@ -159,7 +135,6 @@ public: QString msgTableName(const QString& chId) const; QString peersTableName(const QString& chId) const; static QString sanitizeTableName(const QString& chId); - static uint64_t computeDatahash(const QByteArray& data); /* Синхронные PRAGMA на заимствованном handle; вызывающий сохраняет его живым до возврата, ошибки логируются в CONFIG. */ IntegrityResult integrityCheckQuick() const; // quick_check: требует ответ ok, нарушения в строках SQLite означают отказ. @@ -170,9 +145,6 @@ public: QString m_dbPath; quint64 m_myNodeId = 0; - /* Кэш открытых курсоров для sync-операций */ - mutable QMap m_cursors; - mutable uint32_t m_nextCursorId = 1; }; #endif