Files
HomeAgent/csrc/src/ha_json_scan.c
JianFeeeee 4d3962a845 feat(csrc): 第二刀 —— 零分配 JSON 扫描/取值层 ha_json_scan(含黄金对照)
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 不复用;
下一刀即协议编解码层。
2026-09-26 10:07:27 +08:00

826 lines
29 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.

/*
* 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);
}