diff --git a/src/chat/chat_member.c b/src/chat/chat_member.c index 151bc171..09e46b8b 100644 --- a/src/chat/chat_member.c +++ b/src/chat/chat_member.c @@ -21,10 +21,12 @@ #include "../../lib/platform_compat.h" #include +#include /* ─── live-онлайн: тот же живой путь, которым пользуется маршрутизатор ─── */ int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) { + if (!inst) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member online: null instance"); return 0; } struct chat_core_ctx* cc = CC(inst); if (!cc || !cc->inst || !ch_id || !ch_id[0]) return 0; if (node_id == cc->my_node_id) return 1; /* self всегда онлайн (local_node вне group->nodes) */ @@ -186,9 +188,16 @@ static void member_from_row(struct UTUN_INSTANCE* inst, const char* ch_id, sqlit else snprintf(m->name, sizeof(m->name), "0x%04X", (unsigned)(m->node_id & 0xFFFF)); } +/* Онлайн выше offline, затем uint64 node_id по возрастанию без вычитания/знакового переполнения. */ +static int compare_members(const void* left, const void* right) { + const struct chat_member_display *a = left, *b = right; + if (a->online != b->online) return a->online > b->online ? -1 : 1; + return a->node_id == b->node_id ? 0 : a->node_id < b->node_id ? -1 : 1; +} + static int member_query(struct UTUN_INSTANCE* inst, const char* ch_id, int single, uint64_t node_id, uint8_t** out, int* count) { - struct chat_core_ctx* cc = CC(inst); + struct chat_core_ctx* cc = inst ? CC(inst) : NULL; *out = NULL; *count = 0; if (!cc || !cc->initialized || !ch_id || !ch_id[0] || !cc->db || !cc->inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: invalid instance/channel"); return -1; @@ -211,11 +220,19 @@ static int member_query(struct UTUN_INSTANCE* inst, const char* ch_id, int singl sqlite3_finalize(st); return -1; } struct chat_member_display* members = NULL; + size_t limit = UINT32_MAX / sizeof(*members); // u_realloc принимает uint32_t число байтов + if (limit > INT_MAX) limit = INT_MAX; int cap = 0, cnt = 0; while ((rc = sqlite3_step(st)) == SQLITE_ROW) { if (cnt == cap) { - cap = cap ? cap * 2 : (single ? 1 : 32); - struct chat_member_display* tmp = u_realloc(members, (size_t)cap * sizeof(*members)); + if ((size_t)cap >= limit) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: allocation limit ch=%s count=%d max=%zu", ch_id, cnt, limit); + u_free(members); sqlite3_finalize(st); return -1; + } + size_t next = cap ? (size_t)cap * 2 : (single ? 1 : 32); + if (next > limit) next = limit; + cap = (int)next; + struct chat_member_display* tmp = u_realloc(members, (uint32_t)(next * sizeof(*members))); if (!tmp) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: allocation ch=%s count=%d", ch_id, cnt); u_free(members); sqlite3_finalize(st); return -1; @@ -229,15 +246,7 @@ static int member_query(struct UTUN_INSTANCE* inst, const char* ch_id, int singl DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: read ch=%s rc=%d: %s", ch_id, rc, sqlite3_errmsg(cc->db)); u_free(members); return -1; } - for (int i = 1; i < cnt; i++) { - struct chat_member_display key = members[i]; - int j = i - 1; - while (j >= 0 && (members[j].online < key.online || - (members[j].online == key.online && members[j].node_id > key.node_id))) { - members[j + 1] = members[j]; j--; - } - members[j + 1] = key; - } + if (cnt > 1) qsort(members, (size_t)cnt, sizeof(*members), compare_members); if (cnt) { *out = u_malloc((size_t)cnt * CHAT_MEMBER_DISPLAY_SIZE); if (!*out) { @@ -300,7 +309,9 @@ void chat_core_toggle_member_radio_mute_trampoline(void* arg) { void chat_core_request_member_list_trampoline(void* arg) { struct chat_member_list_req* req = (struct chat_member_list_req*)arg; - if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; } + if (!req || !req->inst || !CC(req->inst) || !CC(req->inst)->initialized) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member list request rejected: chat unavailable"); u_free(arg); return; + } struct UTUN_INSTANCE* inst = req->inst; char ch_id[64]; strncpy(ch_id, req->ch_id, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0'; u_free(arg); @@ -310,6 +321,10 @@ void chat_core_request_member_list_trampoline(void* arg) { if (chat_core_get_member_list(inst, ch_id, &members, &count) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: cannot build list ch=%s", ch_id); return; } + if (count < 0 || count > UINT16_MAX) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member list event rejected ch=%s count=%d wire_limit=%u", ch_id, count, (unsigned)UINT16_MAX); + u_free(members); return; + } size_t ch_len = strlen(ch_id); size_t evt_sz = 1 + ch_len + 2 + (size_t)count * CHAT_MEMBER_DISPLAY_SIZE; @@ -335,19 +350,27 @@ void chat_core_request_member_list_trampoline(void* arg) { void chat_core_request_member_rtt_trampoline(void* arg) { struct chat_member_list_req* req = (struct chat_member_list_req*)arg; - if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; } + if (!req || !req->inst || !CC(req->inst) || !CC(req->inst)->initialized) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member RTT request rejected: chat unavailable"); u_free(arg); return; + } struct UTUN_INSTANCE* inst = req->inst; char ch_id[64]; strncpy(ch_id, req->ch_id, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0'; u_free(arg); uint8_t* members = NULL; int count = 0; - if (chat_core_get_member_list(inst, ch_id, &members, &count) != 0 || !members) return; + if (chat_core_get_member_list(inst, ch_id, &members, &count) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: cannot build RTT snapshots ch=%s", ch_id); return; + } + if (!count) { u_free(members); return; } size_t ch_len = strlen(ch_id); size_t evt_sz = 1 + ch_len + 1 + CHAT_MEMBER_DISPLAY_SIZE; uint8_t* evt = u_malloc(evt_sz); - if (!evt) { u_free(members); return; } + if (!evt) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: RTT event allocation failed ch=%s count=%d", ch_id, count); + u_free(members); return; + } /* шлём мемберов по одному (RTT мог измениться у каждого) */ for (int i = 0; i < count; i++) { diff --git a/src/chat/chat_member.h b/src/chat/chat_member.h index 53907841..66402c72 100644 --- a/src/chat/chat_member.h +++ b/src/chat/chat_member.h @@ -48,7 +48,10 @@ extern "C" { #define CHAT_MEMBER_DISPLAY_SIZE 164 -/* Получить всех мемберов канала. out = u_malloc-массив, count = количество. */ +/* Получить всех мемберов канала в uasync. out/count обязательны; 0 — успех, -1 с логом. + * out — собственный u_malloc-массив count*CHAT_MEMBER_DISPLAY_SIZE, освобождается u_free; + * пустой список даёт NULL/0. Ошибка сохраняет NULL/0. Порядок: online первыми, затем uint64 node_id. + * Это API не ограничено 16-битным count GUI-события; рост массива ограничен размером u_realloc. */ int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count); /* Получить одного мембера по node_id. out = буфер размером CHAT_MEMBER_DISPLAY_SIZE. */ @@ -81,13 +84,14 @@ struct chat_member_display { void chat_core_serialize_member(const struct chat_member_display* src, uint8_t* dst); int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display* dst); -/* Трамплин для gui_bridge: GUI запрашивает полный список мемберов канала. - * arg = struct chat_member_list_req*. Результат: CHAT_EVT_MEMBER_LIST. */ +/* Трамплин для gui_bridge, только uasync: принимает собственный u_malloc-блок arg и освобождает его. + * ch_id — NUL-строка до 63 байт. Успех: CHAT_EVT_MEMBER_LIST с uint16 native count (0..65535). + * Непредставимый count/ошибка чата/памяти логируются, события нет; ложный/усечённый снимок не отправляется. */ struct chat_member_list_req { struct UTUN_INSTANCE* inst; char ch_id[64]; }; void chat_core_request_member_list_trampoline(void* arg); -/* Трамплин для gui_bridge: GUI запрашивает RTT мемберов канала. - * arg = struct chat_member_list_req*. Результат: CHAT_EVT_MEMBER_UPDATED с RTT-обновлениями. */ +/* Владение/поток/аргумент как у list trampoline. Для каждого участника — один полный + * CHAT_EVT_MEMBER_UPDATED, включая остальные live-поля. Пустой список не даёт событий. */ void chat_core_request_member_rtt_trampoline(void* arg); /* Локальный переключатель рации; каждый запрос меняет актуальное состояние в uasync. */ diff --git a/tests/test_member_adapter.c b/tests/test_member_adapter.c index 8cfb8de5..541a6aac 100644 --- a/tests/test_member_adapter.c +++ b/tests/test_member_adapter.c @@ -1,5 +1,7 @@ /* Реальный адаптер, SQLite и транзакции; без копии алгоритма и без сетевых таймингов. */ #include "../src/chat/member_sync.c" +#include "../src/chat/chat_member.h" +#include "../src/member_files/protocol.h" #include void test_member_purge(struct UASYNC* ua); @@ -18,6 +20,88 @@ static void put(struct UTUN_INSTANCE* inst, uint64_t id) { assert(member_sync_put(inst, "1001", id, key, key, NULL, 0, NULL, 0, "{}", NULL, NULL, 0, 0, NULL) >= 0); } +static int snapshot_list_events, snapshot_updates, snapshot_bad; +static uint64_t snapshot_first, snapshot_last; + +/* Событие проверяем синхронно, не сохраняем заимствованный буфер и не печатаем тысячи записей. */ +static void snapshot_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + (void)inst; + if (type == CHAT_EVT_MEMBER_UPDATED) { snapshot_updates++; return; } + if (type != CHAT_EVT_MEMBER_LIST) return; + snapshot_list_events++; + if (!data || len < 7 || data[0] != 4 || memcmp(data + 1, "1001", 4)) { snapshot_bad++; return; } + uint16_t count; + memcpy(&count, data + 5, sizeof(count)); + if ((size_t)len != 7 + (size_t)count * CHAT_MEMBER_DISPLAY_SIZE) { snapshot_bad++; return; } + snapshot_first = count ? mf_get64(data + 7) : 0; + snapshot_last = count ? mf_get64(data + 7 + (size_t)(count - 1) * CHAT_MEMBER_DISPLAY_SIZE) : 0; +} + +/* Настоящие SQLite-запрос/сериализатор/trampoline: count не должен оборачиваться и чистить GUI ложным снимком. */ +static int test_snapshot_event_bounds(struct UASYNC* ua) { + struct UTUN_INSTANCE inst; setup(&inst, ua); + struct chat_core_ctx cc = { .inst = &inst, .db = inst.topo_sqlite_db, .initialized = 1 }; + inst.chat_core = &cc; + chat_event_set_handler(&inst, snapshot_event); + const int sizes[] = {0, 1, UINT16_MAX, UINT16_MAX + 1}; + int result = 0; + for (unsigned test = 0; test < sizeof(sizes) / sizeof(sizes[0]); test++) { + const int count = sizes[test]; inst.node_id = count; cc.my_node_id = count; + assert(sqlite3_exec(cc.db, "DELETE FROM peers_1001", NULL, NULL, NULL) == SQLITE_OK); + if (count) { + char sql[512]; + snprintf(sql, sizeof(sql), "WITH RECURSIVE n(id) AS (VALUES(1) UNION ALL SELECT id+1 FROM n WHERE id<%d) " + "INSERT INTO peers_1001(node_id,x25519_pubkey,ed25519_pubkey,join_sig,join_ts,source) " + "SELECT id,zeroblob(32),zeroblob(32),zeroblob(64),0,1 FROM n", count); + assert(sqlite3_exec(cc.db, sql, NULL, NULL, NULL) == SQLITE_OK); + } + snapshot_list_events = snapshot_updates = snapshot_bad = 0; snapshot_first = snapshot_last = 0; + const size_t before = u_get_allocated_count(); + struct chat_member_list_req* request = u_calloc(1, sizeof(*request)); assert(request); + request->inst = &inst; strcpy(request->ch_id, "1001"); + chat_core_request_member_list_trampoline(request); + if (snapshot_bad || snapshot_list_events != (count <= UINT16_MAX) || + (count <= UINT16_MAX && count && (snapshot_first != (uint64_t)count || snapshot_last != (uint64_t)(count > 1 ? count - 1 : 1)))) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member event boundary failed count=%d events=%d malformed=%d first=%llu last=%llu", + count, snapshot_list_events, snapshot_bad, (unsigned long long)snapshot_first, (unsigned long long)snapshot_last); + result = -1; + } + assert(u_get_allocated_count() == before); + if (count <= 1) { + request = u_calloc(1, sizeof(*request)); assert(request); + request->inst = &inst; strcpy(request->ch_id, "1001"); + chat_core_request_member_rtt_trampoline(request); + assert(snapshot_updates == count && u_get_allocated_count() == before); + } + } + if (!result) { + uint8_t* wire = NULL; int count = -1; + assert(chat_core_get_member_list(&inst, "1001", &wire, &count) == 0 && count == UINT16_MAX + 1); + u_free(wire); wire = NULL; + assert(sqlite3_exec(cc.db, "DELETE FROM peers_1001;" + "WITH n(id) AS (VALUES(1),(2),(-1),(-9223372036854775808)) " + "INSERT INTO peers_1001(node_id,x25519_pubkey,ed25519_pubkey,join_sig,join_ts,source) " + "SELECT id,zeroblob(32),zeroblob(32),zeroblob(64),0,1 FROM n", NULL, NULL, NULL) == SQLITE_OK); + inst.node_id = cc.my_node_id = 2; + assert(chat_core_get_member_list(&inst, "1001", &wire, &count) == 0 && count == 4); + const uint64_t order[] = {2, 1, UINT64_C(1) << 63, UINT64_MAX}; + for (int i = 0; i < 4; i++) { + assert(mf_get64(wire + i * CHAT_MEMBER_DISPLAY_SIZE) == order[i]); + assert(wire[i * CHAT_MEMBER_DISPLAY_SIZE + 8] == (i == 0)); + } + u_free(wire); wire = NULL; + assert(chat_core_get_member_list(NULL, "1001", &wire, &count) < 0 && !wire && count == 0); + struct chat_member_list_req* request = u_calloc(1, sizeof(*request)); assert(request); + chat_core_request_member_list_trampoline(request); + request = u_calloc(1, sizeof(*request)); assert(request); + chat_core_request_member_rtt_trampoline(request); + } + inst.chat_core = NULL; + member_sync_destroy(&inst); + assert(sqlite3_close(inst.topo_sqlite_db) == SQLITE_OK); + return result; +} + static void root(struct UTUN_INSTANCE* inst, uint8_t hash[32]) { assert(merkle_sync_read_hash(inst, MT_DATA_MEMBERS, "1001", 0, 0, hash) == 0); } @@ -196,6 +280,7 @@ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); size_t baseline = u_get_allocated_count(); struct UASYNC* ua = uasync_create(); assert(ua); + if (test_snapshot_event_bounds(ua) < 0) { uasync_destroy(ua, 0); return 1; } test_member_purge(ua); test_invitation_certificate(ua); test_owner_sizes(ua);