Skip to content

feat(errors): EAGER parse 失败返回首个被拒字节的 offset #23

@membphis

Description

@membphis

遇到的问题

qjson_parse 在遇到非法 JSON 时,通过旁路出参 err_out(int*)返回一个错误码。错误码本身已经足够细(TRAILING_CONTENT / INVALID_NUMBER / INVALID_STRING / INVALID_UTF8 / NESTING_TOO_DEEP …),但没有任何位置信息:调用方知道"JSON 坏了、为什么坏",却不知道坏在哪个字节,排查困难。

位置信息其实在 Phase 1 的每个失败点都已经存在,只是被签名 Result<(), qjson_err> 压成了一个纯错误码丢弃掉了:

  • scan() 内部跑 validate_brackets,返回 Err(offset)(括号/引号失衡的字节位),但在 src/doc.rs:31.map_err(|_| QJSON_PARSE_ERROR) 抹掉。
  • validate_trailing 知道 root_end(垃圾起点),只返回错误码。
  • validate_eager_values 在每个失败点都攥着 pos / scalar-gap 边界,只返回错误码。
  • validate_depth(LAZY)知道越界的 idx,只返回错误码。

解决思路

  1. 加宽 FFI 旁路通道。失败时 qjson_parse 返回 NULL,没有 doc 可挂数据,所以位置必须随错误码一起从 err_out 出来。把 err_outint* 升级成一个错误结构体 qjson_error { int code; size_t offset; }*,让 code 和 offset 物理绑定、不可错位。选结构体而非"多加一个出参"是为了可扩展:后续要加路径上下文(见 Out of scope)时,往同一个 struct 加字段即可,签名零改动。
  2. 让 4 个 pass 把已有的位置带出来。把相关 validate 函数的错误类型从 qjson_err 升级成携带 usize 偏移,一路冒泡到 parse_with_options → FFI。
  3. 统一 offset 语义契约(见下),保证 SIMD 与 scalar 两条 scan 路径给出一致的偏移。
  4. lua wrapper 把 offset 拼进错误消息:"JSON parse error at byte 1234"

Spec

Scope

EAGER 模式下,parse 失败时除错误码外,再返回首个被拒字节的偏移。覆盖 Phase 1 的全部四个失败点:scan / validate_trailing / validate_eager_values / validate_depth

API

typedef struct {
    int    code;     // qjson_err
    size_t offset;   // 首个被拒字节的偏移;无位置语义时为 SIZE_MAX
} qjson_error;

// err_out 由 int* 升级为 qjson_error*(破坏现有 ABI)
qjson_doc *qjson_parse(const uint8_t *buf, size_t len, qjson_error *err_out);
qjson_doc *qjson_parse_ex(const uint8_t *buf, size_t len,
                          const qjson_options *opts, qjson_error *err_out);

⚠️ 这是对 qjson_parse / qjson_parse_ex 的一次性 ABI 破坏。需同步更新 include/qjson.hlua/qjson.lua(cdef + err_box)。本库尚未做稳定承诺、唯一外部消费者是自带的 lua wrapper,代价可控,早破比晚破便宜。

offset 语义契约

  • offset = 首个被拒字节的偏移
  • value 级错误(数字 / 字符串 / UTF-8 非法)给出坏 token 的起点(scalar-gap 起点),不是坏 token 内部的具体坏字节——后者要继续往 validate_number / validate_string_span 下钻,本 issue 不做。
  • 与位置无关的错误码(空输入、QJSON_INVALID_ARGQJSON_OOM)= SIZE_MAX
  • scan 在未闭合开括号/未闭合字符串这类"扫到末尾才发现"的情况,offset = buf.len()

实现要点

  • src/scan/mod.rs:validate_brackets 已返回 Err(usize);让 scan()Err 把这个 offset 透传出来。
  • src/doc.rs:31:去掉 .map_err(|_| QJSON_PARSE_ERROR),改为保留 offset。
  • src/validate/mod.rs:validate_trailing / validate_eager_values / validate_depth 错误类型升级为携带 usizevalidate_eager_values 内约 15 处 return Err(...) 需逐一带上对应 pos / gap 起点。
  • src/error.rs / include/qjson.h / lua/qjson.lua 三处枚举与新 qjson_error struct 保持同步(CLAUDE.md 要求)。

Acceptance Criteria

  • qjson_error struct 落地,qjson_parse / qjson_parse_ex 改用之;header 与 lua cdef 同步。
  • 四个 Phase 1 失败点都按契约填充 offset
  • lua wrapper 错误消息包含 at byte N
  • busted 测试:截断输入、括号失衡、坏数字、坏 UTF-8、trailing garbage —— 各验 offset 正确。
  • 扩展 tests/scanner_crosscheck.rs 的 proptest:比对 SIMD vs scalar 两条 scan 路径给出一致的 offset(防 backend drift)。

Out of scope(各自单开新 issue)

  • 路径上下文 [42].foo:需给 eager validator 增加 breadcrumb 栈以保留 key/index 面包屑;qjson_error 预留了扩展空间但本 issue 不实现。
  • LAZY / Phase-2 decode 错误的 offset:qjson_get_* 在字段访问期返回的 DECODE_FAILED 等,触发时机与 Phase 1 不同,属另一条战线。

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions