mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-28 13:23:03 +00:00
C 化第二刀:为协议编解码层铺 JSON 底座。**本刀只交付库 + 验收,
未改 Go 生产路径**(接线是独立一步,库先验完再换产线)。
为什么是它:SSE 单块解析(parseOpenAICompatibleStreamChunkFull)是每个流式
chunk 都要跑的最热路径,实测 1937ns/13allocs(content 块)、3122ns/21allocs
(toolcall 块),而纯字节扫描理论下限 133ns/1alloc —— 差距 15~23×。
一次 1 万块的会话 = 1~2 万次堆分配,正是 GC 抖动的来源。
为什么不复用 SDK 的 remotedevice/ha_json.c(实测三缺陷,不可直接复用):
① 无 \u 解码:\u4f60\u597d → ?0?d?d?0(非 ASCII 全靠转义时内容直接损坏)
② 只有 _get_int 无浮点:temperature:0.7 静默变 0
③ null 与「键缺失」不可区分
外加它是 DOM + malloc,与本层「不 malloc / 零拷贝 / 纯函数」正交。
设计:scan(结构,零分配零解码)+ extract(取值,按需解码)两段分离。
content 可能是很大的多模态数组,而 stringifyContent 只需要 text 字段拼起来;
若 scan 就解码并分配缓冲,等于把成本付给不需要它的调用方。
★ 被测试抓出 7 个真实缺陷(写 C 时同一逻辑我读三遍都认为正确):
1 代理对合成成功后未跳过 unconditionally 的 U+FFFD 发射(😀 → 两个 FFFD)
2 过长编码检查用了只含首字节位的 cp(「你」→ 6 个 FFFD)
3 members_next 只报值起点不消费值 → 游标停在值前(模糊测试第一轮抓到)
4 扫描阶段不校验转义字符合法性({"a":"\q"} C 判合法、json.Valid=false)
5 扫描阶段不校验 \u 后四位十六进制(同上)
6 get_int 接受前导零(007 / 00)
7 cgo 桥接把 C 结构体声明为 Go 局部变量 → 运行时 panic
(cgo argument has Go pointer to unpinned Go pointer)
其中 4 个是「静默分叉」——不崩、不报错,生产里表现为「内容少一个字符」
或「某些块被静默丢弃」,极难归因。这正是黄金对照不可省的理由。
★ 另纠正我自己两次错误的「真值」(比代码 bug 更危险,会变成错误规格):
第一版真值表里 content:{} 的花括号少了一层,把「我写错 JSON」误读成
「Go 对 content 严格」。修正后实测发现一对方向相反的语义:
content 走 interface{} 宽松({}→"{}"、true→"true"),
reasoning_content/usage/finish_reason 强类型严格(123 ⇒ 整块作废)。
照错误表写 C 会产出「比 Go 更严格」的实现,静默丢弃本该生效的块。
两个由缺陷倒逼的设计决定:
- members_next 返回**完整值 span** 并内部跳过 ⇒ 「返回 1」蕴含「成员良构」。
要求调用方自己推进游标的 API 是错的:忘一次就解析到上一个值且不报错。
- members_complete() 区分「正常扫到 }」与「输入畸形」,否则无法复刻 Go 严格性。
同时修两个基础设施目标对「多源文件/多测试」的适配:
- csrc-sanitize:每个契约测试各自链接(多个 main 合链会 multiple definition,
而报错被吞后会被误报成「本机无 sanitizer」——一个假的 SKIP)
- csrc-cross:多源文件改用 -fsyntax-only 逐文件(gcc 不支持多源单 -o)
实测(全部当场可复现):
- C 契约测试 119 项断言全过;黄金对照 5 组全过(语法/成员/解码/整数/随机字节)
- libFuzzer 4948 万次运行零崩溃(121s)
- ASan+UBSan PASS(两个契约测试各跑);gcc+clang 零告警;arm64 交叉编译 0 告警
- 全量 go test -count=1 ./... 0 FAIL;make build-linux-arm64 → ELF aarch64
- 纪律检查 SDK 公开接口 diff = 0 行(未触碰 SDK)
决策关闭(jianf 本轮裁决):C 实现留主仓 csrc/(它本就是替换内核 Go 实现,
SDK 从未被触碰,跨端复用才需进 SDK 而它们不调用本层);ha_json.c 不复用;
下一刀即协议编解码层。
826 lines
29 KiB
C
826 lines
29 KiB
C
/*
|
||
* ha_json_scan.c — HomeAgent 内核 LLM 协议层 JSON 扫描/取值(C 实现)
|
||
*
|
||
* ============================ 性能设计(勿回退) ============================
|
||
* 1. **不 malloc**:一切结果以 span 回传,Go 侧零拷贝切片
|
||
* 2. **不 strlen**:长度由调用方传入
|
||
* 3. **不预扫**:scan 只在需要时前进一步;「找键」靠 members 迭代单趟,
|
||
* 不先扫一遍收集全部键(那会缓存踩踏 + 二次遍历)
|
||
* 4. **整数不走 strtoll**:strtoll 要 NUL 结尾或处理 locale,
|
||
* 自写定点解析只认 JSON 整数语法,顺带把溢出判掉
|
||
* 5. **字符串不建索引**:不记录转义位置。需要时按需解码
|
||
*
|
||
* 参照第一刀的教训(docs/zh/c-core/llm-orchestration-c.md §7.1):
|
||
* 初版每次调用 C.CString(malloc+拷贝)+ C 侧 strlen,单这两项就吃掉
|
||
* 82% 的时间 —— 那不是 cgo 的固有成本,是自找的。本库从设计上排除这类开销。
|
||
*
|
||
* 语义必须与 Go `encoding/json` 一致,由黄金对照测试钉死
|
||
* (真值表见 docs/zh/c-core/sse-codec-c.md §二)。
|
||
*/
|
||
|
||
#include "ha_json_scan.h"
|
||
|
||
#include <string.h>
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* ABI 自述 */
|
||
/* ---------------------------------------------------------------- */
|
||
|
||
int ha_json_scan_abi_version(void) {
|
||
return HA_JSON_SCAN_ABI_VERSION;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* 基础工具 */
|
||
/* ---------------------------------------------------------------- */
|
||
|
||
/* JSON 空白:Go 的 encoding/json 只认这四个(不是 isspace)。
|
||
* 差一个字符就会与 Go 分叉,故显式列举而非用 ctype。 */
|
||
static int is_ws(unsigned char c) {
|
||
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
|
||
}
|
||
|
||
static unsigned char ascii_lower(unsigned char c) {
|
||
return (c >= 'A' && c <= 'Z') ? (unsigned char)(c + 32) : c;
|
||
}
|
||
|
||
void ha_json_scan_init(ha_json_scan *sc, const char *s, size_t n) {
|
||
if (sc == NULL) {
|
||
return;
|
||
}
|
||
sc->s = (s != NULL) ? s : "";
|
||
sc->n = (s != NULL) ? n : 0;
|
||
sc->i = 0;
|
||
}
|
||
|
||
int ha_json_scan_ws(ha_json_scan *sc) {
|
||
if (sc == NULL) {
|
||
return 1;
|
||
}
|
||
while (sc->i < sc->n && is_ws((unsigned char)sc->s[sc->i])) {
|
||
sc->i++;
|
||
}
|
||
return (sc->i < sc->n) ? 0 : 1;
|
||
}
|
||
|
||
int ha_json_scan_eof(const ha_json_scan *sc) {
|
||
if (sc == NULL) {
|
||
return 1;
|
||
}
|
||
return (sc->i >= sc->n) ? 1 : 0;
|
||
}
|
||
|
||
/* 当前字符;到结尾返回 '\0'(0)。调用方需先判 eof。 */
|
||
static char peek(const ha_json_scan *sc) {
|
||
return (sc->i < sc->n) ? sc->s[sc->i] : '\0';
|
||
}
|
||
|
||
/* 前进一字节;越界时不动(保持 eof 语义稳定)。 */
|
||
static void bump(ha_json_scan *sc) {
|
||
if (sc->i < sc->n) {
|
||
sc->i++;
|
||
}
|
||
}
|
||
|
||
static int expect(ha_json_scan *sc, char c) {
|
||
if (ha_json_scan_ws(sc) || peek(sc) != c) {
|
||
return 0;
|
||
}
|
||
bump(sc);
|
||
return 1;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* 值扫描(skip 一个完整值) */
|
||
/* ---------------------------------------------------------------- */
|
||
|
||
static int scan_value(ha_json_scan *sc, int depth);
|
||
static int hex_val(unsigned char c);
|
||
|
||
/* 扫描字符串(含引号),出原始内容 span。
|
||
* depth 传入是因为 scan_value 会递归;字符串本身不递归但需要限额。 */
|
||
static int scan_string_raw(ha_json_scan *sc, ha_span *raw, int depth) {
|
||
if (depth > 128) {
|
||
return 0; /* 深度保险,正常文档远小于此 */
|
||
}
|
||
if (ha_json_scan_ws(sc) || peek(sc) != '"') {
|
||
return 0;
|
||
}
|
||
bump(sc); /* 开引号 */
|
||
size_t start = sc->i;
|
||
while (sc->i < sc->n) {
|
||
char c = sc->s[sc->i];
|
||
if (c == '"') {
|
||
if (raw != NULL) {
|
||
raw->p = sc->s + start;
|
||
raw->len = sc->i - start;
|
||
}
|
||
bump(sc); /* 闭引号 */
|
||
return 1;
|
||
}
|
||
if (c == '\\') {
|
||
bump(sc);
|
||
if (sc->i >= sc->n) {
|
||
return 0; /* 末尾悬空反斜杠 */
|
||
}
|
||
/* ★ 必须校验转义字符本身合法:Go 的 unquoteBytes 对未知转义
|
||
* (\q、\x、单独 \p)返回错误 ⇒ 整个 Unmarshal 失败。
|
||
* 初版只 bump 不校验,于是 `{"a":"\q"}` 被 C 判为合法,
|
||
* 而 json.Valid=false —— 黄金对照当场抓到。
|
||
* (`\u` 的 4 位十六进制在解码阶段校验:那是**值**层面的
|
||
* 错误,与扫描阶段的「转义序列形状」是两回事。) */
|
||
char e = sc->s[sc->i];
|
||
if (e != '"' && e != '\\' && e != '/' && e != 'b' && e != 'f' &&
|
||
e != 'n' && e != 'r' && e != 't' && e != 'u') {
|
||
return 0;
|
||
}
|
||
/* ★ `\u` 必须紧跟 **4 位十六进制**,且这一校验属于**扫描**阶段:
|
||
* Go 的 json.Valid 会拒绝 `{"a":"\u00"}`(不足 4 位),
|
||
* 而初版把它留到解码阶段 ⇒ scan 判合法、json.Valid 判非法,
|
||
* 黄金对照当场抓到这条分叉。
|
||
* 校验放在扫描阶段还有一个好处:畸形的 wire 数据在
|
||
* 「找键」阶段就被拒,不必等到取值。 */
|
||
if (e == 'u') {
|
||
/* 用 size_t 递推偏移,避免 int 与 size_t 混算
|
||
* (-Wconversion/-Wsign-conversion 会拦下 sign-change)。 */
|
||
if (sc->n - sc->i < 5u) {
|
||
return 0; /* 位数不足:还需 'u' 之后 4 位 */
|
||
}
|
||
for (size_t k = 1; k <= 4u; k++) {
|
||
if (hex_val((unsigned char)sc->s[sc->i + k]) < 0) {
|
||
return 0; /* 非十六进制 */
|
||
}
|
||
}
|
||
}
|
||
bump(sc); /* 被转义的字符;\u 的 4 位十六进制由上面的循环覆盖 */
|
||
continue;
|
||
}
|
||
if ((unsigned char)c < 0x20) {
|
||
return 0; /* Go 拒绝字符串里的裸控制字符 */
|
||
}
|
||
bump(sc);
|
||
}
|
||
return 0; /* 未闭合 */
|
||
}
|
||
|
||
/* 扫描字面量:true / false / null。 */
|
||
static int scan_literal(ha_json_scan *sc) {
|
||
static const char kTrue[] = "true";
|
||
static const char kFalse[] = "false";
|
||
static const char kNull[] = "null";
|
||
size_t rest = sc->n - sc->i;
|
||
const char *p = sc->s + sc->i;
|
||
|
||
if (rest >= 4 && memcmp(p, kTrue, 4) == 0) {
|
||
sc->i += 4;
|
||
return 1;
|
||
}
|
||
if (rest >= 5 && memcmp(p, kFalse, 5) == 0) {
|
||
sc->i += 5;
|
||
return 1;
|
||
}
|
||
if (rest >= 4 && memcmp(p, kNull, 4) == 0) {
|
||
sc->i += 4;
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 数字:只校验**语法**(不求值)。求值由 ha_json_get_int / 调用方负责。
|
||
* 这与 Go 的分工一致:Go 在 unmarshal 时求值并做范围检查,
|
||
* 而本层的取整数是独立的一步。 */
|
||
static int scan_number(ha_json_scan *sc) {
|
||
size_t start = sc->i;
|
||
if (sc->i < sc->n && peek(sc) == '-') {
|
||
bump(sc);
|
||
}
|
||
/* 整数部分:0 或 [1-9][0-9]*(禁止前导零,与 Go 一致) */
|
||
if (sc->i >= sc->n) {
|
||
return 0;
|
||
}
|
||
if (peek(sc) == '0') {
|
||
bump(sc);
|
||
} else if (peek(sc) >= '1' && peek(sc) <= '9') {
|
||
while (sc->i < sc->n && peek(sc) >= '0' && peek(sc) <= '9') {
|
||
bump(sc);
|
||
}
|
||
} else {
|
||
return 0;
|
||
}
|
||
/* 小数部分 */
|
||
if (sc->i < sc->n && peek(sc) == '.') {
|
||
bump(sc);
|
||
if (sc->i >= sc->n || peek(sc) < '0' || peek(sc) > '9') {
|
||
return 0; /* "1." 与 "1.e3" 非法 */
|
||
}
|
||
while (sc->i < sc->n && peek(sc) >= '0' && peek(sc) <= '9') {
|
||
bump(sc);
|
||
}
|
||
}
|
||
/* 指数部分 */
|
||
if (sc->i < sc->n && (peek(sc) == 'e' || peek(sc) == 'E')) {
|
||
bump(sc);
|
||
if (sc->i < sc->n && (peek(sc) == '+' || peek(sc) == '-')) {
|
||
bump(sc);
|
||
}
|
||
if (sc->i >= sc->n || peek(sc) < '0' || peek(sc) > '9') {
|
||
return 0;
|
||
}
|
||
while (sc->i < sc->n && peek(sc) >= '0' && peek(sc) <= '9') {
|
||
bump(sc);
|
||
}
|
||
}
|
||
return (sc->i > start) ? 1 : 0;
|
||
}
|
||
|
||
/* 扫描数组/对象。用显式 depth 递归(不用堆栈,零分配)。 */
|
||
static int scan_container(ha_json_scan *sc, char open, char close, int depth) {
|
||
if (!expect(sc, open)) {
|
||
return 0;
|
||
}
|
||
if (ha_json_scan_ws(sc)) {
|
||
return 0; /* 未闭合 */
|
||
}
|
||
if (peek(sc) == close) {
|
||
bump(sc);
|
||
return 1; /* 空容器 */
|
||
}
|
||
for (;;) {
|
||
if (open == '{') {
|
||
ha_span k;
|
||
if (!scan_string_raw(sc, &k, depth + 1)) {
|
||
return 0;
|
||
}
|
||
if (!expect(sc, ':')) {
|
||
return 0;
|
||
}
|
||
}
|
||
if (!scan_value(sc, depth + 1)) {
|
||
return 0;
|
||
}
|
||
if (ha_json_scan_ws(sc)) {
|
||
return 0;
|
||
}
|
||
if (peek(sc) == ',') {
|
||
bump(sc);
|
||
continue;
|
||
}
|
||
if (peek(sc) == close) {
|
||
bump(sc);
|
||
return 1;
|
||
}
|
||
return 0; /* 缺 '}' 或多余的 ',' 之后没有键 */
|
||
}
|
||
}
|
||
|
||
static int scan_value(ha_json_scan *sc, int depth) {
|
||
if (depth > 128) {
|
||
return 0;
|
||
}
|
||
if (ha_json_scan_ws(sc)) {
|
||
return 0;
|
||
}
|
||
char c = peek(sc);
|
||
switch (c) {
|
||
case '{': return scan_container(sc, '{', '}', depth);
|
||
case '[': return scan_container(sc, '[', ']', depth);
|
||
case '"': {
|
||
ha_span tmp;
|
||
return scan_string_raw(sc, &tmp, depth);
|
||
}
|
||
case 't': case 'f': case 'n': return scan_literal(sc);
|
||
default:
|
||
if (c == '-' || (c >= '0' && c <= '9')) {
|
||
return scan_number(sc);
|
||
}
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
int ha_json_skip(ha_json_scan *sc) {
|
||
if (sc == NULL) {
|
||
return 0;
|
||
}
|
||
return scan_value(sc, 0);
|
||
}
|
||
|
||
int ha_json_scan_string(ha_json_scan *sc, ha_span *raw) {
|
||
if (sc == NULL) {
|
||
return 0;
|
||
}
|
||
return scan_string_raw(sc, raw, 0);
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* 顶层对象成员迭代 */
|
||
/* ---------------------------------------------------------------- */
|
||
|
||
int ha_json_members_init(ha_json_members *m, const char *s, size_t n) {
|
||
if (m == NULL) {
|
||
return 0;
|
||
}
|
||
ha_json_scan_init(&m->sc, s, n);
|
||
m->started = 0;
|
||
m->done = 0;
|
||
m->error = 0;
|
||
if (ha_json_scan_ws(&m->sc) || peek(&m->sc) != '{') {
|
||
return 0;
|
||
}
|
||
bump(&m->sc);
|
||
return 1;
|
||
}
|
||
|
||
int ha_json_members_next(ha_json_members *m, ha_span *key, ha_span *val) {
|
||
if (m == NULL || m->done) {
|
||
return 0;
|
||
}
|
||
if (ha_json_scan_ws(&m->sc)) {
|
||
m->done = 1;
|
||
m->error = 1; /* 未闭合 */
|
||
return 0;
|
||
}
|
||
if (peek(&m->sc) == '}') {
|
||
bump(&m->sc);
|
||
m->done = 1;
|
||
m->error = 0; /* 正常结束 */
|
||
return 0; /* 没有更多成员 */
|
||
}
|
||
/* ★ 不接受尾逗号:Go 的 decoder 在 ',' 之后要求必有下一个键。
|
||
* `{"a":1,}` 在 Go 侧是语法错误,故这里也必须拒绝。 */
|
||
if (m->started) {
|
||
if (peek(&m->sc) != ',') {
|
||
m->done = 1;
|
||
m->error = 1;
|
||
return 0;
|
||
}
|
||
bump(&m->sc);
|
||
if (ha_json_scan_ws(&m->sc)) {
|
||
m->done = 1;
|
||
m->error = 1;
|
||
return 0;
|
||
}
|
||
if (peek(&m->sc) == '}') {
|
||
m->done = 1;
|
||
m->error = 1; /* 尾逗号 */
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
ha_span k;
|
||
if (!scan_string_raw(&m->sc, &k, 0)) {
|
||
m->done = 1;
|
||
m->error = 1;
|
||
return 0;
|
||
}
|
||
if (!expect(&m->sc, ':')) {
|
||
m->done = 1;
|
||
m->error = 1;
|
||
return 0;
|
||
}
|
||
if (ha_json_scan_ws(&m->sc)) {
|
||
m->done = 1;
|
||
m->error = 1;
|
||
return 0;
|
||
}
|
||
|
||
/* ★ 就地完整跳过一个值,得到它的精确 span。
|
||
* 这样「返回 1」就蕴含「该成员良构」,且游标已推进到值之后。 */
|
||
size_t vstart = m->sc.i;
|
||
if (!scan_value(&m->sc, 0)) {
|
||
m->done = 1;
|
||
m->error = 1;
|
||
return 0;
|
||
}
|
||
size_t vend = m->sc.i;
|
||
|
||
m->started = 1;
|
||
if (key != NULL) {
|
||
*key = k;
|
||
}
|
||
if (val != NULL) {
|
||
val->p = m->sc.s + vstart;
|
||
val->len = vend - vstart;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
int ha_json_members_complete(const ha_json_members *m) {
|
||
if (m == NULL) {
|
||
return 0;
|
||
}
|
||
return (m->done && !m->error) ? 1 : 0;
|
||
}
|
||
|
||
int ha_json_key_eq(ha_span key, const char *name) {
|
||
if (name == NULL) {
|
||
return 0;
|
||
}
|
||
size_t nl = 0;
|
||
while (name[nl] != '\0') {
|
||
nl++;
|
||
}
|
||
if (key.len != nl) {
|
||
return 0;
|
||
}
|
||
for (size_t i = 0; i < nl; i++) {
|
||
if (ascii_lower((unsigned char)key.p[i]) !=
|
||
ascii_lower((unsigned char)name[i])) {
|
||
return 0;
|
||
}
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* 字符串解码 */
|
||
/* ---------------------------------------------------------------- */
|
||
|
||
/* U+FFFD 的 UTF-8 编码(Go 对非法字节的替换目标)。 */
|
||
static const char kReplacement[3] = { (char)0xEF, (char)0xBF, (char)0xBD };
|
||
|
||
/* 十六进制值;非十六进制返回 -1。 */
|
||
static int hex_val(unsigned char c) {
|
||
if (c >= '0' && c <= '9') return c - '0';
|
||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||
return -1;
|
||
}
|
||
|
||
/* 把码点编码成 UTF-8 写给 sink。返回写入字节数。 */
|
||
static size_t emit_rune(unsigned long cp, ha_json_sink sink, void *ctx) {
|
||
unsigned char buf[4];
|
||
size_t len;
|
||
if (cp < 0x80) {
|
||
buf[0] = (unsigned char)cp;
|
||
len = 1;
|
||
} else if (cp < 0x800) {
|
||
buf[0] = (unsigned char)(0xC0 | (cp >> 6));
|
||
buf[1] = (unsigned char)(0x80 | (cp & 0x3F));
|
||
len = 2;
|
||
} else if (cp < 0x10000) {
|
||
buf[0] = (unsigned char)(0xE0 | (cp >> 12));
|
||
buf[1] = (unsigned char)(0x80 | ((cp >> 6) & 0x3F));
|
||
buf[2] = (unsigned char)(0x80 | (cp & 0x3F));
|
||
len = 3;
|
||
} else {
|
||
buf[0] = (unsigned char)(0xF0 | (cp >> 18));
|
||
buf[1] = (unsigned char)(0x80 | ((cp >> 12) & 0x3F));
|
||
buf[2] = (unsigned char)(0x80 | ((cp >> 6) & 0x3F));
|
||
buf[3] = (unsigned char)(0x80 | (cp & 0x3F));
|
||
len = 4;
|
||
}
|
||
sink(ctx, (const char *)buf, len);
|
||
return len;
|
||
}
|
||
|
||
/* 解码一段 raw(已定位转义与续字节的边界)。
|
||
*
|
||
* 非法 UTF-8 语义必须与 Go 逐字节一致:
|
||
* Go 的 unquoteBytes 遇到非法序列时,把**能构成前缀的最长合法部分**先解出,
|
||
* 再对**第一个坏字节**产出单个 U+FFFD,然后从坏字节**之后**继续。
|
||
* 即:一个坏字节 = 一个 U+FFFD(不是整个序列变一个)。
|
||
* 典型:`\xff\xfe` → 两个 U+FFFD(真值表 §2.8 实测确认)。
|
||
*/
|
||
static size_t decode_body(ha_span raw, ha_json_sink sink, void *ctx, int *err) {
|
||
size_t out = 0;
|
||
size_t i = 0;
|
||
*err = 0;
|
||
|
||
while (i < raw.len) {
|
||
unsigned char c = (unsigned char)raw.p[i];
|
||
|
||
/* --- 转义 --- */
|
||
if (c == '\\') {
|
||
if (i + 1 >= raw.len) {
|
||
*err = 1;
|
||
return out;
|
||
}
|
||
unsigned char e = (unsigned char)raw.p[i + 1];
|
||
switch (e) {
|
||
case '"': sink(ctx, "\"", 1); out += 1; i += 2; continue;
|
||
case '\\': sink(ctx, "\\", 1); out += 1; i += 2; continue;
|
||
case '/': sink(ctx, "/", 1); out += 1; i += 2; continue;
|
||
case 'b': sink(ctx, "\b", 1); out += 1; i += 2; continue;
|
||
case 'f': sink(ctx, "\f", 1); out += 1; i += 2; continue;
|
||
case 'n': sink(ctx, "\n", 1); out += 1; i += 2; continue;
|
||
case 'r': sink(ctx, "\r", 1); out += 1; i += 2; continue;
|
||
case 't': sink(ctx, "\t", 1); out += 1; i += 2; continue;
|
||
case 'u': {
|
||
/* 需要 4 位十六进制:i+2 .. i+5 */
|
||
if (i + 6 > raw.len) {
|
||
*err = 1;
|
||
return out;
|
||
}
|
||
int h0 = hex_val((unsigned char)raw.p[i + 2]);
|
||
int h1 = hex_val((unsigned char)raw.p[i + 3]);
|
||
int h2 = hex_val((unsigned char)raw.p[i + 4]);
|
||
int h3 = hex_val((unsigned char)raw.p[i + 5]);
|
||
if (h0 < 0 || h1 < 0 || h2 < 0 || h3 < 0) {
|
||
*err = 1;
|
||
return out;
|
||
}
|
||
unsigned long cp = (unsigned long)((h0 << 12) | (h1 << 8) |
|
||
(h2 << 4) | h3);
|
||
size_t adv = 6;
|
||
if (cp >= 0xD800 && cp <= 0xDBFF) {
|
||
/* 高代理:尝试与紧随的 \uDC00-\uDFFF 合成 4 字节 rune。
|
||
*
|
||
* ★ 必须用 combined 标志,而不是「合成成功就直接落到底部」:
|
||
* 本块末尾有一段**无条件的** replacement 发射(处理合成
|
||
* 失败的情形)。若成功的分支只设 cp/adv 而不跳过那一段,
|
||
* 会先把合成好的码点丢掉、再发一个 U+FFFD ——
|
||
* 实测症状:`\ud83d\ude00`(😀)得到 `\xef\xbf\xbd\xef\xbf\xbd`。
|
||
* 这个 bug 只有**真的代理对**才会触发(`\u4f60` 这类
|
||
* 非代理码点根本不进本块),是黄金对照最容易漏的一类。
|
||
*
|
||
* 下界必须是 'i + 6 < raw.len'(而非一次判 i+12 <= len):
|
||
* 后者会连带拒绝「合法高代理位于字符串末尾」的正确输入。 */
|
||
int combined = 0;
|
||
if (i + 6 < raw.len && raw.p[i + 6] == '\\' &&
|
||
raw.p[i + 7] == 'u') {
|
||
int g0 = hex_val((unsigned char)raw.p[i + 8]);
|
||
int g1 = hex_val((unsigned char)raw.p[i + 9]);
|
||
int g2 = hex_val((unsigned char)raw.p[i + 10]);
|
||
int g3 = hex_val((unsigned char)raw.p[i + 11]);
|
||
if (g0 >= 0 && g1 >= 0 && g2 >= 0 && g3 >= 0) {
|
||
unsigned long lo = (unsigned long)(
|
||
(g0 << 12) | (g1 << 8) | (g2 << 4) | g3);
|
||
if (lo >= 0xDC00 && lo <= 0xDFFF) {
|
||
cp = 0x10000UL + ((cp - 0xD800UL) << 10) +
|
||
(lo - 0xDC00UL);
|
||
adv = 12;
|
||
combined = 1;
|
||
}
|
||
}
|
||
}
|
||
if (!combined) {
|
||
/* 高代理后面不是合法低代理:发一个 U+FFFD,
|
||
* 只消费掉这个 6 字节 \uXXXX,让后面的内容按原样
|
||
* 继续解析(与 Go unquote 的行为一致)。 */
|
||
sink(ctx, kReplacement, 3);
|
||
out += 3;
|
||
i += adv;
|
||
continue;
|
||
}
|
||
}
|
||
if (cp >= 0xDC00 && cp <= 0xDFFF) {
|
||
/* 孤立低代理 → U+FFFD */
|
||
sink(ctx, kReplacement, 3);
|
||
out += 3;
|
||
i += 6;
|
||
continue;
|
||
}
|
||
out += emit_rune(cp, sink, ctx);
|
||
i += adv;
|
||
continue;
|
||
}
|
||
default:
|
||
/* Go 对未知转义(如 \q)报错 */
|
||
*err = 1;
|
||
return out;
|
||
}
|
||
}
|
||
|
||
/* --- 普通字节 / 多字节序列 --- */
|
||
if (c < 0x80) {
|
||
char ch = (char)c;
|
||
sink(ctx, &ch, 1);
|
||
out += 1;
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
/* 尝试解析一个合法多字节序列。
|
||
*
|
||
* ★ 过长编码(overlong)检查**必须在续字节全部并入之后**做。
|
||
* 初版把它写在这里、只用首字节的 cp:
|
||
* else if ((b0 & 0xF0) == 0xE0) { need = 3; cp = b0 & 0x0Fu; }
|
||
* if (need == 3 && cp < 0x800) valid = 0; // ← 此时 cp 只有首字节的位
|
||
* 而 0xE4 恰好满足 0x0F 掩码 ⇒ cp = 4 ⇒ 4 < 0x800 ⇒ 误判非法
|
||
* ⇒ 正常的「你」(e4 bd a0)被逐字节换成 6 个 U+FFFD(实测症状)。
|
||
* 过长的真实判据是「完整码点 < 该长度的最小值」,
|
||
* 即 0xC0/0x80、0xE0 0x80、0xF0 0x80/0x90 这几类前缀。 */
|
||
size_t need;
|
||
unsigned long cp;
|
||
unsigned char b0 = c;
|
||
if ((b0 & 0xE0) == 0xC0) { need = 2; cp = b0 & 0x1Fu; }
|
||
else if ((b0 & 0xF0) == 0xE0) { need = 3; cp = b0 & 0x0Fu; }
|
||
else if ((b0 & 0xF8) == 0xF0) { need = 4; cp = b0 & 0x07u; }
|
||
else { need = 0; cp = 0; }
|
||
|
||
int valid = (need != 0);
|
||
if (valid) {
|
||
for (size_t k = 1; k < need; k++) {
|
||
if (i + k >= raw.len) { valid = 0; break; }
|
||
unsigned char nb = (unsigned char)raw.p[i + k];
|
||
if ((nb & 0xC0) != 0x80) { valid = 0; break; }
|
||
cp = (cp << 6) | (unsigned long)(nb & 0x3F);
|
||
}
|
||
}
|
||
if (valid) {
|
||
/* 过长编码:按**完整码点**比该长度的最小合法值
|
||
* (2B:0x80 / 3B:0x800 / 4B:0x10000) */
|
||
if (need == 2 && cp < 0x80) valid = 0;
|
||
if (need == 3 && cp < 0x800) valid = 0;
|
||
if (need == 4 && cp < 0x10000) valid = 0;
|
||
/* 代理区编码(CESU-8 / WTF-8)Go 判非法 */
|
||
if (cp >= 0xD800 && cp <= 0xDFFF) valid = 0;
|
||
if (cp > 0x10FFFF) valid = 0;
|
||
}
|
||
if (valid) {
|
||
sink(ctx, raw.p + i, need);
|
||
out += need;
|
||
i += need;
|
||
continue;
|
||
}
|
||
|
||
/* 非法:单个字节 → 一个 U+FFFD,然后继续(与 Go 逐字节一致) */
|
||
sink(ctx, kReplacement, 3);
|
||
out += 3;
|
||
i++;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
int ha_json_decode_string(ha_span raw, ha_json_sink sink, void *ctx,
|
||
size_t *out_len) {
|
||
if (sink == NULL) {
|
||
return 0;
|
||
}
|
||
int err = 0;
|
||
size_t n = decode_body(raw, sink, ctx, &err);
|
||
if (out_len != NULL) {
|
||
*out_len = n;
|
||
}
|
||
return err ? 0 : 1;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* 写入缓冲的 sink */
|
||
/* ---------------------------------------------------------------- */
|
||
|
||
typedef struct {
|
||
char *out;
|
||
size_t cap;
|
||
size_t len;
|
||
} buf_sink;
|
||
|
||
static void buf_write(void *ctx, const char *b, size_t n) {
|
||
buf_sink *s = (buf_sink *)ctx;
|
||
/* 缓冲不足时,**绝不再往后写**,并标记溢出(len > cap 即可辨认)。
|
||
*
|
||
* ★ 契约是「不越界写」,不是「一个字节都不写」:本函数是流式的,
|
||
* 写到这里才知道放不下,之前已写出的部分无法撤销。
|
||
* 调用方拿到 (size_t)-1 时**必须丢弃整个结果**(Go 侧就是这么做的)。
|
||
* 若真需要 all-or-nothing,调用方应先测得长度再分配(两趟)。
|
||
* 这个取舍是有意的:单趟更快,而丢弃结果对调用方是廉价的。 */
|
||
if (s->len + n > s->cap) {
|
||
s->len = s->cap + 1; /* 标记溢出 */
|
||
return;
|
||
}
|
||
memcpy(s->out + s->len, b, n);
|
||
s->len += n;
|
||
}
|
||
|
||
size_t ha_json_decode_string_into(ha_span raw, char *out, size_t out_cap) {
|
||
if (out == NULL || out_cap == 0) {
|
||
return (size_t)-1;
|
||
}
|
||
buf_sink s;
|
||
s.out = out;
|
||
s.cap = out_cap - 1; /* 留一位给结尾 NUL */
|
||
s.len = 0;
|
||
int err = 0;
|
||
(void)decode_body(raw, buf_write, &s, &err);
|
||
if (err || s.len > s.cap) {
|
||
return (size_t)-1;
|
||
}
|
||
out[s.len] = '\0';
|
||
return s.len;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* 整数 */
|
||
/* ---------------------------------------------------------------- */
|
||
|
||
int ha_json_get_int(ha_span raw, long long *out) {
|
||
if (raw.len == 0 || out == NULL) {
|
||
return 0;
|
||
}
|
||
size_t i = 0;
|
||
int neg = 0;
|
||
if (raw.p[0] == '-') {
|
||
neg = 1;
|
||
i = 1;
|
||
if (raw.len == 1) {
|
||
return 0;
|
||
}
|
||
}
|
||
/* 只接受 **JSON 整数语法**:可选 '-' + (0 | [1-9][0-9]*)。
|
||
* 小数点 / 指数一律判「不是整数」,由调用方按 Go 的
|
||
* 「类型不匹配 ⇒ 整块作废」语义处理。
|
||
*
|
||
* ★ 必须禁前导零:JSON 里 `007` / `00` 是**非法数字**,
|
||
* 而 strconv.ParseInt 会接受它。若这里跟着接受,
|
||
* 就会出现「C 认得、json.Unmarshal 报错」的分叉 ——
|
||
* 黄金对照当场抓到(实测分歧:"007"、"00")。
|
||
* 本层的职责是回答「这是不是 JSON 整数」,不是「能不能转成数字」。 */
|
||
if (raw.p[i] == '0' && raw.len - i > 1) {
|
||
return 0; /* 前导零:00 / 01 / 007 均非法 */
|
||
}
|
||
for (size_t k = i; k < raw.len; k++) {
|
||
if (raw.p[k] < '0' || raw.p[k] > '9') {
|
||
return 0;
|
||
}
|
||
}
|
||
unsigned long long acc = 0;
|
||
const unsigned long long limit =
|
||
neg ? 9223372036854775807ULL + 1ULL : 9223372036854775807ULL;
|
||
for (size_t k = i; k < raw.len; k++) {
|
||
unsigned d = (unsigned)(raw.p[k] - '0');
|
||
if (acc > (limit - d) / 10ULL) {
|
||
return 0; /* 溢出(与 Go 报错等价) */
|
||
}
|
||
acc = acc * 10ULL + d;
|
||
}
|
||
if (neg) {
|
||
*out = (acc == 9223372036854775808ULL)
|
||
? (-9223372036854775807LL - 1)
|
||
: -(long long)acc;
|
||
} else {
|
||
*out = (long long)acc;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- */
|
||
/* 便捷取值 */
|
||
/* ---------------------------------------------------------------- */
|
||
|
||
/* 在对象里定位键 name 的值 span。
|
||
*
|
||
* 找到返回 1 且 *val 覆盖该值的原始字节(未解码);未找到 / 语法错返回 0。
|
||
* 重复键取**最后一次**(与 Go 的后者胜一致)。
|
||
*
|
||
* 实现要点:members 迭代器只报「值的起始位置」,值本身由本函数用
|
||
* ha_json_skip 消费并算出 span —— 这样两种便捷取值共用同一套定位逻辑,
|
||
* 不会因各自实现而分叉。 */
|
||
static int find_value(ha_span obj, const char *name, ha_span *val) {
|
||
ha_json_members m;
|
||
if (!ha_json_members_init(&m, obj.p, obj.len)) {
|
||
return 0;
|
||
}
|
||
ha_span key;
|
||
ha_span v;
|
||
int found = 0;
|
||
ha_span last = { NULL, 0 };
|
||
|
||
while (ha_json_members_next(&m, &key, &v)) {
|
||
if (ha_json_key_eq(key, name)) {
|
||
last = v;
|
||
found = 1; /* 重复键后者胜:继续扫,只保留最后一次 */
|
||
}
|
||
}
|
||
/* ★ 严格性:畸形输入必须判「找不到键」——
|
||
* Go 侧语法错误会让 json.Unmarshal 失败、整块作废,
|
||
* 若这里放宽成「扫到哪算哪」,就会比 Go 宽松(见真值表 §2.2)。 */
|
||
if (!ha_json_members_complete(&m)) {
|
||
return 0;
|
||
}
|
||
if (found && val != NULL) {
|
||
*val = last;
|
||
}
|
||
return found;
|
||
}
|
||
|
||
size_t ha_json_object_get_string(ha_span obj, const char *name,
|
||
char *out, size_t out_cap) {
|
||
if (out == NULL || out_cap == 0) {
|
||
return (size_t)-1;
|
||
}
|
||
ha_span val;
|
||
if (!find_value(obj, name, &val)) {
|
||
return (size_t)-1;
|
||
}
|
||
/* 只接受字符串值;其他类型视为「取不到」(类型判断由调用方按
|
||
* Go 的 interface{}/强类型语义决定,见 sse-codec-c.md §2.2) */
|
||
ha_json_scan sc;
|
||
ha_json_scan_init(&sc, val.p, val.len);
|
||
ha_span raw;
|
||
if (!ha_json_scan_string(&sc, &raw)) {
|
||
return (size_t)-1;
|
||
}
|
||
return ha_json_decode_string_into(raw, out, out_cap);
|
||
}
|
||
|
||
int ha_json_object_get_int(ha_span obj, const char *name, long long *out) {
|
||
if (out == NULL) {
|
||
return 0;
|
||
}
|
||
ha_span val;
|
||
if (!find_value(obj, name, &val)) {
|
||
return 0;
|
||
}
|
||
return ha_json_get_int(val, out);
|
||
}
|