316 lines
13 KiB
C
316 lines
13 KiB
C
/* 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;
|
|
}
|