From a33666b7787e909ee2086b02729bb41d2a4d2195 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 10 Oct 2026 23:25:52 +0200 Subject: [PATCH] Add atomic updates for signed member profile options --- src/chat/chat_core.h | 6 +++++ src/chat/chat_profile.c | 60 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index 3850311f..5cf5b497 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -248,6 +248,12 @@ int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint6 int chat_core_name_userinfo(const char* name, char* out, size_t capacity); int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, char* out, size_t capacity, uint8_t signature[64], uint64_t* timestamp); +/* Одно подписанное обновление нескольких собственных строковых опций. NULL value удаляет ключ. + * Остальные поля сохраняются; имя/аватар меняются своими API. Ошибка не меняет БД/Merkle. + * Вызов только в uasync-потоке, вне внешней SQLite-транзакции. */ +struct chat_profile_option { const char* key; const char* value; }; +int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel, + const struct chat_profile_option* options, size_t count); struct chat_avatar_req { struct UTUN_INSTANCE* inst; uint64_t id, group, server; diff --git a/src/chat/chat_profile.c b/src/chat/chat_profile.c index a3a02d2e..3fa97e67 100644 --- a/src/chat/chat_profile.c +++ b/src/chat/chat_profile.c @@ -49,7 +49,13 @@ void chat_core_update_my_name_trampoline(void* arg) { /* ── Обновление своего мембера (userinfo + update_ts + подпись + синк). Адреса больше не здесь — их синхронизирует BGP/topo_group. ── */ -struct profile_json { char text[MS_USERINFO_MAX + 1]; size_t used; int avatar; }; +struct profile_json { + char text[MS_USERINFO_MAX + 1]; + size_t used; + int avatar; + const struct chat_profile_option* options; + size_t option_count; +}; static int profile_string(struct profile_json* p, const char* text) { if (p->used >= MS_USERINFO_MAX) return -1; @@ -82,6 +88,7 @@ static int profile_pair(struct profile_json* p, const char* key, const char* val static int profile_copy_pair(const char* key, const char* value, void* arg) { struct profile_json* p = arg; if (!strcmp(key, "name") || (p->avatar && (!strcmp(key, "avatar_server") || !strcmp(key, "avatar_file")))) return 0; + for (size_t i = 0; i < p->option_count; i++) if (!strcmp(key, p->options[i].key)) return 0; return profile_pair(p, key, value); } @@ -117,7 +124,9 @@ int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, ch return result; } -static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, int avatar, uint64_t server, const char* file) { +/* Обновить профиль целиком одной подписанной записью, сохранив не затронутые поля. */ +static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, int avatar, uint64_t server, const char* file, + const struct chat_profile_option* options, size_t option_count) { struct chat_core_ctx* cc = CC(inst); if (!cc || !cc->initialized || !cc->db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: update before initialization"); return -1; @@ -128,7 +137,7 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in if (topo_node_sqlite_member_get_join(cc->db, ch, myid, join_sig, &join_ts) != 0) { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: own signed member missing channel=%s", ch); return -1; } - struct profile_json profile = { .text = "{", .used = 1, .avatar = avatar }; + struct profile_json profile = { .text = "{", .used = 1, .avatar = avatar, .options = options, .option_count = option_count }; if (profile_pair(&profile, "name", inst->name) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: display name exceeds profile limit channel=%s", ch); return -1; } @@ -144,11 +153,23 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in } uint64_t local_uts = (uint64_t)sqlite3_column_int64(st, 0); const char* old = (const char*)sqlite3_column_text(st, 1); + char before[MS_USERINFO_MAX + 1] = ""; + if (old && strlen(old) > MS_USERINFO_MAX) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: stored attributes exceed limit channel=%s", ch); + sqlite3_finalize(st); return -1; + } + if (old) memcpy(before, old, strlen(old) + 1); if (old && old[0] && json_flat_parse(old, profile_copy_pair, &profile) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: cannot preserve attributes channel=%s", ch); sqlite3_finalize(st); return -1; } sqlite3_finalize(st); + for (size_t i = 0; i < option_count; i++) { + if (options[i].value && profile_pair(&profile, options[i].key, options[i].value) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: option exceeds profile limit channel=%s key=%s", ch, options[i].key); + return -1; + } + } if (avatar && server) { char id[17]; snprintf(id, sizeof(id), "%016llx", (unsigned long long)server); if (profile_pair(&profile, "avatar_server", id) < 0 || profile_pair(&profile, "avatar_file", file) < 0) { @@ -159,7 +180,12 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: attributes exceed profile limit channel=%s", ch); return -1; } profile.text[profile.used++] = '}'; profile.text[profile.used] = 0; - uint64_t now = (uint64_t)ntp_time_get_seconds(inst); + if (option_count && !strcmp(before, profile.text)) { + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "profile: options unchanged channel=%s", ch); return 0; + } + if (local_uts >= INT64_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: version exhausted channel=%s", ch); return -1; } + time_t seconds = ntp_time_get_seconds(inst); + uint64_t now = seconds > 0 ? (uint64_t)seconds : 0; uint64_t update_ts = now > local_uts ? now : local_uts + 1; uint8_t umsg[MS_USERINFO_MAX + 73], update_sig[64]; int ulen = member_sync_build_update_msg(join_sig, update_ts, profile.text, umsg, sizeof(umsg)); @@ -174,6 +200,28 @@ static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* ch, in return 0; } +/* Проверить заменяемые ключи до построения/подписи нового блока мембера. */ +int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel, + const struct chat_profile_option* options, size_t count) { + if (!inst || !channel || !channel[0] || !options || !count || count > MS_USERINFO_MAX / 4) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: invalid options update count=%zu", count); return -1; + } + if (!chat_core_get_db(inst) || !sqlite3_get_autocommit(chat_core_get_db(inst))) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: options update requires initialized DB without outer transaction"); return -1; + } + for (size_t i = 0; i < count; i++) { + const char* key = options[i].key; + if (!key || !key[0] || strlen(key) > MS_USERINFO_MAX || (options[i].value && strlen(options[i].value) > MS_USERINFO_MAX) + || !strcmp(key, "name") || !strcmp(key, "avatar_server") || !strcmp(key, "avatar_file")) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: invalid/reserved option index=%zu key=%s", i, key ? key : "null"); return -1; + } + for (size_t j = 0; j < i; j++) if (!strcmp(key, options[j].key)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: duplicate option key=%s", key); return -1; + } + } + return profile_update_channel(inst, channel, 0, 0, NULL, options, count); +} + void chat_core_update_my_member(struct UTUN_INSTANCE* inst) { struct chat_core_ctx* cc = CC(inst); if (!cc || !cc->initialized || !cc->db) return; @@ -183,7 +231,7 @@ void chat_core_update_my_member(struct UTUN_INSTANCE* inst) { } while (sqlite3_step(cs) == SQLITE_ROW) { const char* ch = (const char*)sqlite3_column_text(cs, 0); - if (ch) profile_update_channel(inst, ch, 0, 0, NULL); + if (ch) profile_update_channel(inst, ch, 0, 0, NULL, NULL, 0); } sqlite3_finalize(cs); } @@ -201,7 +249,7 @@ int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint6 DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: avatar server has no storage role channel=%s", ch_id); return -1; } } - int rc = profile_update_channel(inst, ch_id, 1, server, file); + int rc = profile_update_channel(inst, ch_id, 1, server, file, NULL, 0); if (!rc) DEBUG_INFO(DEBUG_CATEGORY_CHAT, "profile: avatar published channel=%s server=%016llx file=%s", ch_id, (unsigned long long)server, server ? file : "none"); return rc;