Files
ligent/tests/test_json.c
T
2026-10-03 14:13:21 +08:00

316 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* test_json.c - JSON 解析器/序列化器 单元测试 */
#include "json.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}