/* test_json.c - JSON 解析器/序列化器 单元测试 */ #include "json.h" #include "util.h" #include #include #include static int failures = 0; static int checks = 0; #define CHECK(cond, ...) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ printf(" ✗ %s:%d: ", __FILE__, __LINE__); \ printf(__VA_ARGS__); \ printf("\n"); \ } \ } while (0) static void test_basic(void) { puts("test_basic"); char err[256]; JsonValue *v = json_parse("{\"a\": 1, \"b\": [true, false, null], \"c\": \"hi\"}", err, sizeof err); CHECK(v != NULL, "解析失败: %s", err); if (!v) return; CHECK(json_get_number(v, "a", -1) == 1, "a 应为 1"); CHECK(json_array_size(json_object_get(v, "b")) == 3, "b 应有 3 个元素"); CHECK(json_bool_value(json_array_get(json_object_get(v, "b"), 0), 0) == 1, "b[0] 应为 true"); CHECK(json_array_get(json_object_get(v, "b"), 2)->type == JSON_NULL, "b[2] 应为 null"); CHECK(strcmp(json_get_string(v, "c"), "hi") == 0, "c 应为 hi"); char *dump = json_dump(v); JsonValue *v2 = json_parse(dump, err, sizeof err); CHECK(v2 != NULL, "往返解析失败: %s", err); char *dump2 = json_dump(v2); CHECK(strcmp(dump, dump2) == 0, "往返不一致:\n %s\n %s", dump, dump2); free(dump); free(dump2); json_free(v); json_free(v2); } static void test_escapes(void) { puts("test_escapes"); char err[256]; JsonValue *v = json_parse("\"a\\nb\\t\\\"c\\\\d\\u4e2d\\u6587\\uD83D\\uDE00\"", err, sizeof err); CHECK(v != NULL, "解析失败: %s", err); if (!v) return; const char *s = json_string_value(v); CHECK(strcmp(s, "a\nb\t\"c\\d中文😀") == 0, "转义解码错误: [%s]", s); char *q = json_quote_string(s); JsonValue *v2 = json_parse(q, err, sizeof err); CHECK(v2 != NULL, "转义往返失败: %s / %s", q, err); CHECK(v2 && strcmp(json_string_value(v2), s) == 0, "转义往返内容不一致"); free(q); json_free(v); json_free(v2); } static void test_numbers(void) { puts("test_numbers"); char err[256]; JsonValue *v = json_parse("[0, -1, 3.5, 1e3, -2.5e-2, 123456789012]", err, sizeof err); CHECK(v != NULL, "解析失败: %s", err); if (!v) return; CHECK(json_number_value(json_array_get(v, 0), -1) == 0, "[0]"); CHECK(json_number_value(json_array_get(v, 1), 0) == -1, "[1]"); CHECK(json_number_value(json_array_get(v, 2), 0) == 3.5, "[2]"); CHECK(json_number_value(json_array_get(v, 3), 0) == 1000, "[3]"); CHECK(json_number_value(json_array_get(v, 4), 0) == -0.025, "[4]"); CHECK(json_number_value(json_array_get(v, 5), 0) == 123456789012.0, "[5]"); json_free(v); /* 严格按 RFC 8259 扫描数字:这些以前全都被接受 —— strtod 会把 "1e" 读成 1、 * "5." 读成 5,于是一个手写错的数字会被静默当成另一个数用下去(配置文件、 * 存档都是手写的,用户只会在很久之后从一个不相干的现象里发现它)。 */ const char *bad_nums[] = { "01", "-01", "5.", ".5", "+1", "1e", "1e+", "1e-", "1.2e", "-", "1_000", }; for (size_t i = 0; i < sizeof bad_nums / sizeof bad_nums[0]; i++) { JsonValue *b = json_parse(bad_nums[i], err, sizeof err); CHECK(b == NULL, "非法数字应被拒绝: [%s]", bad_nums[i]); json_free(b); } /* 边界上合法的写法不能被误伤 */ const char *ok_nums[] = { "0", "-0", "0.0", "10", "-1.5", "1e3", "1E3", "1e+3", "1e-3", "0.1e10", "123456789012345678901234567890", "1e308", }; for (size_t i = 0; i < sizeof ok_nums / sizeof ok_nums[0]; i++) { JsonValue *g = json_parse(ok_nums[i], err, sizeof err); CHECK(g != NULL && g->type == JSON_NUMBER, "合法数字被拒: [%s] (%s)", ok_nums[i], err); json_free(g); } } static void test_invalid(void) { puts("test_invalid"); char err[256]; const char *bad[] = { "{", "}", "[1,]", "{\"a\":}", "\"unterminated", "tru", "{\"a\" 1}", "[1 2]", "01abc", "", " ", "{\"a\":1}}", "[1,2] extra", }; for (size_t i = 0; i < sizeof bad / sizeof bad[0]; i++) { JsonValue *v = json_parse(bad[i], err, sizeof err); CHECK(v == NULL, "应当解析失败但成功了: [%s]", bad[i]); json_free(v); } /* 空字符串是合法 JSON 字符串 */ JsonValue *e = json_parse("\"\"", err, sizeof err); CHECK(e != NULL && strcmp(json_string_value(e), "") == 0, "空字符串应为合法"); json_free(e); } static void test_builders(void) { puts("test_builders"); JsonValue *o = json_new_object(); json_object_set_string(o, "model", "gpt-x"); json_object_set_number(o, "n", 3); json_object_set_bool(o, "ok", 1); JsonValue *arr = json_new_array(); json_array_push(arr, json_new_string("a")); json_array_push(arr, json_new_string("b")); json_object_set(o, "items", arr); json_object_set_string(o, "model", "gpt-y"); /* 覆盖 */ char *s = json_dump(o); CHECK(strcmp(s, "{\"model\":\"gpt-y\",\"n\":3,\"ok\":true,\"items\":[\"a\",\"b\"]}") == 0, "构造结果不对: %s", s); free(s); /* clone 独立性 */ JsonValue *c = json_clone(o); json_object_set_string(o, "model", "changed"); CHECK(strcmp(json_get_string(c, "model"), "gpt-y") == 0, "clone 应相互独立"); json_object_remove(o, "model"); CHECK(json_object_get(o, "model") == NULL, "remove 应删除键"); json_free(o); json_free(c); } static void test_nested_depth(void) { puts("test_nested_depth"); char err[256]; size_t n = 300; char *deep = xmalloc(n * 2 + 1); for (size_t i = 0; i < n; i++) deep[i] = '['; for (size_t i = 0; i < n; i++) deep[n + i] = ']'; deep[n * 2] = '\0'; JsonValue *v = json_parse(deep, err, sizeof err); CHECK(v == NULL, "超深嵌套应被拒绝"); free(deep); JsonValue *ok = json_parse("[[[1]]]", err, sizeof err); CHECK(ok != NULL, "正常嵌套应成功"); json_free(ok); } /* 报错必须能定位:只有"语法错了"而不知道错在哪,等于没有报错 */ static void test_error_location(void) { puts("test_error_location"); char err[256]; JsonValue *v; /* 尾随逗号:行号必须指向真正出问题的那一行 */ v = json_parse("{\n \"a\": 1,\n}\n", err, sizeof err); CHECK(v == NULL, "尾随逗号应报错"); CHECK(strstr(err, "第 3 行") != NULL, "应报出行号: %s", err); CHECK(strstr(err, "期望对象的键") != NULL, "应说明期望什么: %s", err); CHECK(strstr(err, "}") != NULL, "应回显出错处原文: %s", err); json_free(v); /* 未闭合的数组:位置应指向输入结束处 */ v = json_parse("[1, 2\n", err, sizeof err); CHECK(v == NULL, "未闭合数组应报错"); CHECK(strstr(err, "第 2 行") != NULL, "未闭合应指向结尾: %s", err); CHECK(strstr(err, "结束") != NULL, "应说明输入在此结束: %s", err); json_free(v); /* 缺冒号 */ v = json_parse("{\"a\" 1}", err, sizeof err); CHECK(v == NULL, "缺冒号应报错"); CHECK(strstr(err, "第 1 行") && strstr(err, "':'") != NULL, "缺冒号: %s", err); json_free(v); /* 尾部多余内容 */ v = json_parse("{\"a\": 1} extra", err, sizeof err); CHECK(v == NULL, "尾部多余内容应报错"); CHECK(strstr(err, "多余") != NULL, "应说明是多余内容: %s", err); json_free(v); /* 非法字符要被回显出来 */ v = json_parse("{\n \"a\": @\n}", err, sizeof err); CHECK(v == NULL, "非法字符应报错"); CHECK(strstr(err, "@") != NULL, "应回显非法字符: %s", err); CHECK(strstr(err, "第 2 行") != NULL, "行号应为第 2 行: %s", err); json_free(v); /* 控制字符不能被原样吐到终端上 */ v = json_parse("{\"a\": \x01\x02}", err, sizeof err); CHECK(v == NULL, "控制字符应报错"); CHECK(strchr(err, '\x01') == NULL, "错误信息里不应含原始控制字符"); json_free(v); /* 合法输入不受影响 */ v = json_parse("{\"a\": [1, {\"b\": null}]}", err, sizeof err); CHECK(v != NULL, "合法输入不应报错: %s", err); json_free(v); } /* 序列化出去的东西必须是**合法 UTF-8**。 * * 服务端会因为请求体里有非法 UTF-8 直接回 * 400 ... invalid unicode code point * 而那段坏数据一旦进了对话历史,之后每次请求都会失败 —— 除非丢掉整轮对话。 * 坏字节的来源很日常:某条命令在非 UTF-8 locale 下打出来的路径、被截断的多 * 字节字符、二进制垃圾。所以这条不能只靠"在入口清理",必须在序列化器这层兜住: * 出口是唯一能保证"发出去的永远合法"的地方。 * * 这个校验函数故意用 utf8_seq_len 自己走一遍,而不是信任 dump 的实现 —— * 断言的是**产物**的性质,不是实现的自述。 */ static int utf8_all_valid(const char *s) { size_t n = strlen(s), i = 0; while (i < n) { size_t adv = utf8_seq_len(s + i, n - i); if (adv == 0) return 0; i += adv; } return 1; } static void test_dump_utf8_safety(void) { puts("test_dump_utf8_safety"); /* utf8_seq_len:合法序列返回长度 */ CHECK(utf8_seq_len("\xe4\xb8\xad", 3) == 3, "合法 CJK 应是 3 字节"); CHECK(utf8_seq_len("A", 1) == 1, "ASCII 应是 1 字节"); CHECK(utf8_seq_len("\xf0\x9f\x98\x80", 4) == 4, "emoji 应是 4 字节"); CHECK(utf8_seq_len("\xc2\x80", 2) == 2, "U+0080 的 2 字节编码应合法"); /* 非法的一律返回 0 */ CHECK(utf8_seq_len("\x80", 1) == 0, "孤立续字节应非法"); CHECK(utf8_seq_len("\xe4\xb8", 2) == 0, "被截断的序列应非法"); CHECK(utf8_seq_len("\xc0\xaf", 2) == 0, "过长编码应非法"); CHECK(utf8_seq_len("\xed\xa0\x80", 3) == 0, "UTF-16 代理区应非法"); CHECK(utf8_seq_len("\xf5\x80\x80\x80", 4) == 0, "超出 U+10FFFF 应非法"); CHECK(utf8_seq_len("\xe4\x41\x42", 3) == 0, "续字节格式不对应非法"); /* 各种坏字节经 json_dump 之后,整份输出必须是合法 UTF-8 */ static const char *bad[] = { "a\x80" "b", /* 孤立续字节 */ "a\xe4\xb8" "b", /* 截断的 3 字节序列 */ "a\xc0\xaf" "b", /* 过长编码(历史上被用来夹带 '/') */ "a\xed\xa0\x80" "b", /* UTF-16 代理区 */ "a\xf5\x80\x80\x80" "b", /* 超出 U+10FFFF */ "\xff\xfe", /* 二进制垃圾 */ "\xe4\xb8\xad\x80\xe6", /* 合法中文里夹一个坏字节 */ }; for (size_t k = 0; k < sizeof bad / sizeof bad[0]; k++) { JsonValue *o = json_new_object(); json_object_set_string(o, "content", bad[k]); char *d = json_dump(o); CHECK(utf8_all_valid(d), "第 %zu 个坏输入序列化后仍含非法 UTF-8: %s", k, d); /* 而且我们的解析器(和服务端一样严格)必须收得下 */ char err[256]; JsonValue *back = json_parse(d, err, sizeof err); CHECK(back != NULL, "第 %zu 个输出无法解析回来: %s", k, err); if (back) { const char *got = json_get_string(back, "content"); CHECK(got && strstr(got, "\xef\xbf\xbd") != NULL, "第 %zu 个输出里应出现 U+FFFD 替换字符", k); json_free(back); } free(d); json_free(o); } /* 合法的多字节内容必须**原样保留** —— 别把好数据一起误伤 */ JsonValue *o = json_new_object(); json_object_set_string(o, "content", "中文 😀 tail"); char *d = json_dump(o); CHECK(strstr(d, "中文") != NULL, "合法中文不该被改动: %s", d); CHECK(strstr(d, "\xf0\x9f\x98\x80") != NULL, "合法 emoji 不该被改动: %s", d); CHECK(strstr(d, "\xef\xbf\xbd") == NULL, "合法输入不该出现替换字符: %s", d); free(d); json_free(o); } int main(void) { test_basic(); test_escapes(); test_numbers(); test_invalid(); test_builders(); test_nested_depth(); test_error_location(); test_dump_utf8_safety(); printf("\n%d 项检查,%d 项失败\n", checks, failures); return failures ? 1 : 0; }