diff --git a/src/chat/chat_core.c b/src/chat/chat_core.c index 0acb58c5..43d94d59 100644 --- a/src/chat/chat_core.c +++ b/src/chat/chat_core.c @@ -279,51 +279,61 @@ int chat_core_is_initialized(struct UTUN_INSTANCE* inst) { return (inst && CC(in /* ─── chat settings (GUI → uasync) ─── */ -void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value) { - chat_setting_set(inst, name, value); - if (name && strcmp(name, "group_autoconnect") == 0) chat_core_apply_group_autoconnect(inst); +int chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value) { + int rc = chat_setting_set(inst, name, value); + if (!rc && !strcmp(name, "group_autoconnect")) chat_core_apply_group_autoconnect(inst); + return rc; } void chat_core_set_setting_trampoline(void* arg) { struct chat_setting_arg* a = (struct chat_setting_arg*)arg; - const char* name = a->data; - const char* value = name + strlen(name) + 1; - chat_core_set_setting(a->inst, name, value); + if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "chat setting: null request"); return; } + const char* end = memchr(a->data, 0, sizeof(a->data)); + if (!end || end == a->data + sizeof(a->data) - 1 || !memchr(end + 1, 0, sizeof(a->data) - (size_t)(end + 1 - a->data))) + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "chat setting: unterminated name/value request"); + else chat_core_set_setting(a->inst, a->data, end + 1); u_free(arg); } /* ─── per-channel «play sound on new message» ─── */ int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id) { - struct chat_core_ctx* cc = CC(inst); - if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return 1; + struct chat_core_ctx* cc = inst ? CC(inst) : NULL; + if (!cc || !cc->initialized || !cc->db || !chat_channel_id(ch_id)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sound setting: invalid read context/channel"); return -1; + } char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch_id); sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) != SQLITE_OK) return 1; - sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC); - int on = 1; - if (sqlite3_step(st) == SQLITE_ROW) { + int rc = sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL), on = 1; + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc == SQLITE_ROW) { const char* v = (const char*)sqlite3_column_text(st, 0); - if (v) on = atoi(v) != 0; + if (sqlite3_column_type(st, 0) != SQLITE_TEXT || sqlite3_column_bytes(st, 0) != 1 || !v || (*v != '0' && *v != '1')) rc = SQLITE_CORRUPT; + else on = *v == '1'; + } + int final = sqlite3_finalize(st); + if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sound setting: read ch=%s rc=%d finalize=%d: %s", ch_id, rc, final, sqlite3_errmsg(cc->db)); return -1; } - sqlite3_finalize(st); return on; } -void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on) { - struct chat_core_ctx* cc = CC(inst); - if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return; +int chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on) { + if (!chat_channel_id(ch_id)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sound setting: invalid channel"); return -1; } char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch_id); char val[2]; snprintf(val, sizeof(val), "%d", on ? 1 : 0); - chat_core_save_ui_state(inst, key, val); + if (chat_core_save_ui_state(inst, key, val)) return -1; DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: sound_on_message ch=%s = %d", CC_ID, ch_id, on ? 1 : 0); chat_sync_broadcast_sound_setting(inst, ch_id, on ? 1 : 0); + return 0; } void chat_core_set_sound_on_message_trampoline(void* arg) { struct chat_sound_arg* a = (struct chat_sound_arg*)arg; - if (!a) return; - chat_core_set_sound_on_message(a->inst, a->ch_id, a->on); + if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sound setting: null request"); return; } + if (!memchr(a->ch_id, 0, sizeof(a->ch_id))) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sound setting: unterminated channel request"); + else chat_core_set_sound_on_message(a->inst, a->ch_id, a->on); u_free(arg); } diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index 364b32d5..fcf68327 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -190,14 +190,18 @@ void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst); * Адреса обновляются отдельно через topo_node; подпись member-блока их не содержит. */ void chat_core_update_my_member(struct UTUN_INSTANCE* inst); -// Сохранение key-value в ui_state (вызывается из uasync-потока) -void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value); +/* Сохранить key/value в uasync:0 — committed/no-op,-1 — отказ;key непустой,value допускает пустую строку. + * Собственная транзакция проверяет все SQL-этапы;внешняя запрещена,no-op не вызывает commit hook. + * Ошибка сохраняет прежнее значение;unrecovered rollback запрашивает owner stop/DB close. */ +int chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value); /* Per-channel «play sound on new message» (ui_state: ch__sound_on_message, default 1=on). - * set — сохраняет и рассылает пирам через chat_sync (троттлинг db_sync на сервере). */ + * get возвращает0/1,-1 при ошибке;отсутствие записи —1. set возвращает0/-1, + * после собственного commit/no-op рассылает желаемое значение через chat_sync best effort. + * SQL-отказ не рассылается,идентичный повтор повторяет доставку. ch_id — канонический ID канала. */ int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id); -void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on); +int chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on); struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; }; void chat_core_set_sound_on_message_trampoline(void* arg); @@ -206,7 +210,7 @@ struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; }; void chat_core_save_ui_state_trampoline(void* arg); // Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) -void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value); +int chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value); struct chat_setting_arg { struct UTUN_INSTANCE* inst; char data[256]; }; void chat_core_set_setting_trampoline(void* arg); diff --git a/src/chat/chat_profile.c b/src/chat/chat_profile.c index 496d491d..ff2d3b76 100644 --- a/src/chat/chat_profile.c +++ b/src/chat/chat_profile.c @@ -375,24 +375,60 @@ void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst) { chat_core_update_my_member(inst); } -void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value) { - struct chat_core_ctx* cc = CC(inst); - if (!cc || !cc->initialized || !key || !value) return; - sqlite3_stmt* st = NULL; - sqlite3_prepare_v2(cc->db, - "INSERT OR REPLACE INTO ui_state(key, value) VALUES(?, ?)", -1, &st, NULL); - if (st) { - sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC); - sqlite3_bind_text(st, 2, value, -1, SQLITE_STATIC); - sqlite3_step(st); sqlite3_finalize(st); +int chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value) { + struct chat_core_ctx* cc = inst ? CC(inst) : NULL; + if (!cc || !cc->initialized || !cc->db || !key || !key[0] || !value || strlen(key) > INT_MAX || strlen(value) > INT_MAX || + !sqlite3_get_autocommit(cc->db)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: invalid context/key/value or outer transaction"); return -1; } + int rc = sqlite3_exec(cc->db, "BEGIN IMMEDIATE /* ui_state */", NULL, NULL, NULL); + if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: begin rc=%d: %s", rc, sqlite3_errmsg(cc->db)); return -1; } + sqlite3_stmt* st = NULL; const char* stage = "prepare read"; int result = -1, same = 0; + rc = sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL); + if (rc == SQLITE_OK) { stage = "bind read"; rc = sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC); } + if (rc == SQLITE_OK) { stage = "step read"; rc = sqlite3_step(st); } + if (rc == SQLITE_ROW) { + int type = sqlite3_column_type(st, 0), bytes = sqlite3_column_bytes(st, 0); + const char* text = (const char*)sqlite3_column_text(st, 0); + if (type != SQLITE_NULL && (type != SQLITE_TEXT || !text || strlen(text) != (size_t)bytes)) { + stage = "stored value"; rc = SQLITE_CORRUPT; goto error; + } + same = text && !strcmp(text, value); + } else if (rc != SQLITE_DONE) goto error; + stage = "finalize read"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error; + if (same) { DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "UI state: unchanged key=%s", key); result = 0; goto done; } + stage = "prepare write"; + rc = sqlite3_prepare_v2(cc->db, "INSERT INTO ui_state(key,value) VALUES(?1,?2) ON CONFLICT(key) DO UPDATE SET value=excluded.value", -1, &st, NULL); + if (rc == SQLITE_OK) { stage = "bind key"; rc = sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC); } + if (rc == SQLITE_OK) { stage = "bind value"; rc = sqlite3_bind_text(st, 2, value, -1, SQLITE_STATIC); } + if (rc == SQLITE_OK) { stage = "step write"; rc = sqlite3_step(st); } + if (rc != SQLITE_DONE) goto error; + if (sqlite3_changes(cc->db) != 1) { stage = "changes != 1"; rc = SQLITE_CONSTRAINT; goto error; } + stage = "finalize write"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error; + stage = "commit"; rc = sqlite3_exec(cc->db, "COMMIT /* ui_state */", NULL, NULL, NULL); if (rc != SQLITE_OK) goto error; + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "UI state: committed key=%s bytes=%zu", key, strlen(value)); return 0; +error: + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: key=%s stage=%s rc=%d: %s", key, stage, rc, sqlite3_errmsg(cc->db)); +done: + rc = sqlite3_finalize(st); + if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: cleanup finalize rc=%d", rc); + if (!sqlite3_get_autocommit(cc->db)) { + rc = sqlite3_exec(cc->db, "ROLLBACK", NULL, NULL, NULL); + if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: rollback rc=%d: %s", rc, sqlite3_errmsg(cc->db)); result = -1; } + if (!sqlite3_get_autocommit(cc->db)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: transaction remains open;requesting owner stop/DB close"); + utun_instance_stop(inst); result = -1; + } + } + return result; } void chat_core_save_ui_state_trampoline(void* arg) { struct save_ui_state_arg* a = (struct save_ui_state_arg*)arg; - if (!a) return; - const char* key = a->data; - const char* value = key + strlen(key) + 1; - chat_core_save_ui_state(a->inst, key, value); + if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: null request"); return; } + const char* end = memchr(a->data, 0, sizeof(a->data)); + if (!end || end == a->data + sizeof(a->data) - 1 || !memchr(end + 1, 0, sizeof(a->data) - (size_t)(end + 1 - a->data))) + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: unterminated key/value request"); + else chat_core_save_ui_state(a->inst, a->data, end + 1); u_free(arg); }