From 1cc60a3079cf2e017f6ec805c102fd4bdfafb46d Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 06:21:05 +0100 Subject: [PATCH] fix(json): decode Unicode escapes without losing subsequent profile fields --- lib/json_flat.c | 41 +++++++++++++++++++++++++++++++++++++++ lib/json_flat.h | 3 ++- tests/test_chat_profile.c | 13 +++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/json_flat.c b/lib/json_flat.c index f8c8e47c..d440a6f5 100644 --- a/lib/json_flat.c +++ b/lib/json_flat.c @@ -6,14 +6,54 @@ #define SKIP_WS(p, end) while ((p) < (end) && (*(p) == ' ' || *(p) == '\t' || *(p) == '\n' || *(p) == '\r')) (p)++ #define CHECK(p, end) if ((p) >= (end)) return -1 +static int json_unicode_unit(const char** text, const char* end, uint32_t* value) { + if (end - *text < 4) return -1; + uint32_t result = 0; + for (int i = 0; i < 4; i++) { + unsigned char c = (unsigned char)(*text)[i]; int digit; + if (c >= '0' && c <= '9') digit = c - '0'; + else if (c >= 'a' && c <= 'f') digit = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') digit = c - 'A' + 10; + else return -1; + result = (result << 4) | (uint32_t)digit; + } + *text += 4; *value = result; return 0; +} + static int json_read_string(const char** pp, const char* end, char* out, size_t out_sz) { const char* p = *pp; CHECK(p, end); if (*p != '"') return -1; p++; size_t n = 0; while (p < end && *p != '"') { + if ((unsigned char)*p < 32) return -1; char ch = *p; if (*p == '\\') { p++; CHECK(p, end); + if (*p == 'u') { + uint32_t code; p++; if (json_unicode_unit(&p, end, &code) < 0 || !code) return -1; + if (code >= 0xd800 && code <= 0xdbff) { + if (end - p < 2 || p[0] != '\\' || p[1] != 'u') return -1; + p += 2; uint32_t low; + if (json_unicode_unit(&p, end, &low) < 0 || low < 0xdc00 || low > 0xdfff) return -1; + code = 0x10000 + ((code - 0xd800) << 10) + low - 0xdc00; + } else if (code >= 0xdc00 && code <= 0xdfff) return -1; + uint8_t utf8[4]; size_t bytes; + if (code < 0x80) { utf8[0] = (uint8_t)code; bytes = 1; } + else if (code < 0x800) { + utf8[0] = (uint8_t)(0xc0 | (code >> 6)); utf8[1] = (uint8_t)(0x80 | (code & 63)); bytes = 2; + } else if (code < 0x10000) { + utf8[0] = (uint8_t)(0xe0 | (code >> 12)); utf8[1] = (uint8_t)(0x80 | ((code >> 6) & 63)); + utf8[2] = (uint8_t)(0x80 | (code & 63)); bytes = 3; + } else { + utf8[0] = (uint8_t)(0xf0 | (code >> 18)); utf8[1] = (uint8_t)(0x80 | ((code >> 12) & 63)); + utf8[2] = (uint8_t)(0x80 | ((code >> 6) & 63)); utf8[3] = (uint8_t)(0x80 | (code & 63)); bytes = 4; + } + if (out) { + if (n + bytes >= out_sz) return -1; + memcpy(out + n, utf8, bytes); n += bytes; + } + continue; + } switch (*p) { case '"': ch = '"'; break; case '\\': ch = '\\'; break; @@ -83,6 +123,7 @@ int json_flat_parse(const char* json, json_flat_cb cb, void* arg) { if (*p == '}') break; } if (scratch != local) u_free(scratch); + if (result < 0) DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "json_flat: invalid object/string bytes=%zu", capacity - 1); return result; } diff --git a/lib/json_flat.h b/lib/json_flat.h index 125ad0eb..8384343c 100644 --- a/lib/json_flat.h +++ b/lib/json_flat.h @@ -2,7 +2,8 @@ * @file json_flat.h * @brief Парсер плоского JSON без вложенности: {"key":"value",...} * - * Только строковые значения и обычные escapes; \uXXXX не поддерживается. + * Только строковые значения,обычные escapes и \uXXXX (surrogate pairs→UTF-8). + * Нулевой Unicode-символ,непарные суррогаты,неверный hex и сырые ASCII-control отклоняются. * Callback получает полные key/value, действительные только до его возврата. * parse возвращает 0 при успехе/остановке callback, -1 при ошибке; остановка не проверяет остаток ввода. * Синтаксис проверяется нестрого: допускаются пропущенные и завершающие запятые; diff --git a/tests/test_chat_profile.c b/tests/test_chat_profile.c index 4ecfa041..0ceaf917 100644 --- a/tests/test_chat_profile.c +++ b/tests/test_chat_profile.c @@ -4,10 +4,22 @@ #include "../src/chat/member_sync.h" #include "../lib/debug_config.h" #include "../lib/mem.h" +#include "../lib/json_flat.h" #include #include static int fail_read; + +static void unicode(void) { + const char* json = "{\"name\":\"\\u0410\\uD83D\\uDE00\",\"protect\":\"yes\",\"control\":\"\\u0001\"}"; + char value[64]; assert(!json_flat_get(json, "name", value, sizeof(value)) && !strcmp(value, "А😀")); + assert(!json_flat_get(json, "protect", value, sizeof(value)) && !strcmp(value, "yes")); + assert(!json_flat_get(json, "control", value, sizeof(value)) && value[0] == 1 && !value[1]); + const char* invalid[] = {"{\"a\":\"\\u0000\"}", "{\"a\":\"\\uD800\"}", "{\"a\":\"\\uDC00\"}", + "{\"a\":\"\\uD800\\u0001\"}", "{\"a\":\"\\uXX12\"}", "{\"a\":\"\\u123\"}", "{\"a\":\"raw\nline\"}"}; + for (unsigned i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) assert(json_flat_parse(invalid[i], NULL, NULL) < 0); + puts("PASS profile Unicode reader:UTF-8,surrogate pair,control,following policy field;invalid Unicode/NUL/raw control refused"); +} extern int __real_sqlite3_finalize(sqlite3_stmt*); int __wrap_sqlite3_finalize(sqlite3_stmt* st) { const char* prefix = "SELECT userinfo,update_sig,update_ts"; @@ -18,6 +30,7 @@ int __wrap_sqlite3_finalize(sqlite3_stmt* st) { int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); size_t baseline = u_get_allocated_count(); + unicode(); utun_instance_set_tun_init_enabled(0); struct UASYNC* ua = uasync_create(); assert(ua); struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, "[global]\nmy_node_name=profile test\n[ntp]\nenabled=no\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n");