Skip to content

[M0-CORE-07] Bounded JSON: JsonValue + parseJson/serializeJson + laige-fuzz json_parse target - #8

Merged
offdev merged 1 commit into
masterfrom
m0-core-07-json
Sep 11, 2026
Merged

offdev merged 1 commit into
masterfrom
m0-core-07-json

Conversation

@offdev

@offdev offdev commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Summary

Implements roadmap step M0-CORE-07 (ADR 0003, D-JSON): the engine bounded JSON parser + value type + serializer in laige-core, no new dependency, plus the minimal deterministic fuzz runner whose json_parse target the step's Verify gate requires.

  • laige::JsonValue + parseJson / serializeJson (src/laige-core/include/laige/json.h, json.cpp)
  • Recursive-descent parser with an RAII depth guard; JsonOptions bounds (defaults: 1 MiB document, depth 32, both inclusive) — no recursion blowup
  • Every parse failure (grammar, escapes, duplicate keys, raw control chars, invalid UTF-8, lone surrogate halves, over size/depth) is ErrorCode::MalformedInput — no new error codes (registry code 3 already names the ADR 0003 bounds)
  • Numbers → double via strtod; overflow tokens store ±inf (valid parse result; callers reject non-finite); integers beyond ±2^53 lose precision (documented)
  • JsonValue: deep copy, O(1) move (moved-from = Null), "owns exactly the payload its kind names" invariant; deep equality (objects order-insensitive, arrays order-sensitive, NaN != NaN)
  • Serializer: canonical compact ASCII, shortest-round-trip decimal numbers (%.*g search over p=1..17 — deliberately not std::to_chars, which AppleClang 15 lacks for floats)
  • tools/fuzz/laige-fuzz.cpp: minimal deterministic runner (Prng-seeded, default seed 0x1F055EED, --runs/--seed, mutate/truncate/random modes over a 19-document corpus); target json_parse; CTest entry fuzz_json_parse runs in every build tree (instrumented in the ASan tree). M0-TEST-01 extends it.
  • Docs: docs/api/json.md (full contract), building.md current status, tools/README.md, roadmap entry checked off with decision record

Verify

  • ctest -R config_json green — 25 GTest cases (ConfigJsonValid / ConfigJsonInvalid / ConfigJsonRoundTrip / ConfigJsonValue / ConfigJsonOptions); every invalid case asserts MalformedInput specifically
  • laige-fuzz json_parse --runs=1000 clean under ASan — instrumented fuzz_json_parse CTest entry in the ASan tree, plus a direct run with ASAN_OPTIONS=detect_leaks=1:halt_on_error=1

Verified locally (2026-09-11)

  • 14/14 ctest on GCC 16.2.1: build (static), build-shared (shared), build-asan (ASan+UBSan fatal), build-tsan (TSan halt_on_error=1); 14/14 on Clang 22.1.8 (build-clang)
  • Zero warnings under the NFR-8.10 policy (-Wall -Werror -fno-exceptions -fno-rtti)

Remaining for CI: MSVC (windows lane) and AppleClang (macos lane) compilation of the new sources.

Closes M0-CORE-07.

…e-fuzz json_parse

laige::JsonValue + parseJson/serializeJson (hand-rolled, bounded, no
dependency — ADR 0003): recursive-descent parser with an RAII depth
guard; JsonOptions defaults 1 MiB document / depth 32 (both
inclusive). Every parse failure — grammar, escapes, duplicate keys,
raw control characters, invalid UTF-8, lone surrogate halves, over
size/depth — is ErrorCode::MalformedInput (no new codes; the registry
entry for code 3 already names the ADR 0003 bounds). Numbers:
strtod; a well-formed overflow token (e.g. 1e999) stores +/-inf
(valid parse result, callers reject non-finite); integer literals
beyond +/-2^53 lose precision (documented policy). Strings: strict
UTF-8 (no overlong, no raw surrogates, <= U+10FFFF), two-character
escapes, \uXXXX with required surrogate pairs. JsonValue: deep copy
(O(size)), O(1) move with moved-from == Null, and the "owns exactly
the payload its kind names" invariant kept by clearPayload() on every
kind change and every assignment; deep equality (objects
order-insensitive, arrays order-sensitive, NaN != NaN); findMember
total on any value. Serializer: canonical compact ASCII (\uXXXX for
control characters and every codepoint above 0x7F, surrogate pairs
above U+FFFF), shortest correctly rounded decimal numbers via a
%.*g search over p = 1..17 (deliberately not std::to_chars, which
AppleClang 15 lacks for floats), -0.0 -> "0"; parse(serialize(v)) ==
v for all finite values; serializing a non-finite number is a
documented precondition violation.

laige-fuzz: minimal deterministic fuzz runner (this step's Verify
gate requires it; M0-TEST-01 extends it): Prng-seeded (default seed
0x1F055EED, --runs/--seed), three input modes (mutate / truncate /
random bytes) over a 19-document ASCII corpus; target json_parse
registered; the fuzz_json_parse CTest entry runs in every build tree
(instrumented in the ASan tree).

Tests (ctest -R config_json, 25 cases): ConfigJsonValid (six kinds;
DBL_MAX / denorm_min / +/-inf overflow / 2^53+1 rounding; escapes,
surrogate pairs, strict UTF-8, DEL; containers, document order,
depth-32 and 1 MiB boundary documents), ConfigJsonInvalid (trailing
data, truncation, bad numbers, bad escapes, lone surrogates, raw
controls, invalid UTF-8, duplicate keys, depth 33, 1 MiB+2 — every
case asserts MalformedInput, not merely an error), ConfigJsonRoundTrip
(parse -> serialize -> parse value stability plus serializer
idempotence; canonical forms pinned), ConfigJsonValue (deep copy,
moved-from Null, in-place replacement, kind transitions, deep
equality, total findMember, churn), and ConfigJsonOptions (maxDepth
1/2; maxDocumentBytes inclusive bound and 0).

Docs: docs/api/json.md (full API contract), building.md (current
status: config_json + fuzz_json_parse entries; laige-fuzz minimal
form from this step), tools/README.md, and the roadmap M0-CORE-07
entry checked off with the decision record.

Verified locally 2026-09-11: 14/14 ctest on GCC 16.2.1 (static,
shared, ASan+UBSan fatal, TSan halt_on_error=1) and Clang 22.1.8;
laige-fuzz json_parse --runs=1000 clean under ASan (direct run with
detect_leaks=1:halt_on_error=1); zero warnings under the NFR-8.10
policy. MSVC (windows lane) and AppleClang (macos lane) compilation
to be proven by CI.
@offdev
offdev merged commit 41938b0 into master Sep 11, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant