code wiki / _hdl_build / nx_json_lib.nx

nx_json_lib.nx source

↩ module page · 340 lines · 16450 B

1// nx_json_lib.nx -- minimal SOVEREIGN JSON field reader (no jq, no shell, no 3rd-party). Built for 2// the work-journal: extract "transcript_path" from a Claude-Code hook's stdin JSON and scan tool_use 3// blocks out of the session transcript .jsonl. NOT a full JSON parser -- it linearly finds 4// "key":"value" string fields and literal anchors, which is exactly what capture needs and is robust 5// to the harness's compact (space-free) emission. De-escapes backslash sequences and neutralizes any 6// TAB/NL/CR so an extracted value can never tear the TSV work-journal line. 7// Sovereign: imports nx_syscalls only. license_tier: ORIGINAL 8import "nx_syscalls.nx" 9 10func jx_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 11 12// first index of NUL-term `pat` in buf[start..n), or -1. 13func jx_find(buf: *u8, n: i64, start: i64, pat: *u8) -> i64 { 14 let pl: i64 = jx_len(pat) 15 if pl == 0 { return 0 - 1 } 16 var i: i64 = start 17 if i < 0 { i = 0 } 18 while i + pl <= n { 19 var j: i64 = 0 20 var ok: i64 = 1 21 while j < pl { if buf[i + j] != pat[j] { ok = 0; j = pl } else { j = j + 1 } } 22 if ok == 1 { return i } 23 i = i + 1 24 } 25 return 0 - 1 26} 27 28// one hex digit at buf[at], or -1 29func jx_hexdig(buf: *u8, n: i64, at: i64) -> i64 { 30 if at >= n { return 0 - 1 } 31 let c: i64 = buf[at] as i64 32 if c >= 48 { if c <= 57 { return c - 48 } } // 0-9 33 if c >= 97 { if c <= 102 { return c - 87 } } // a-f 34 if c >= 65 { if c <= 70 { return c - 55 } } // A-F 35 return 0 - 1 36} 37 38// the 4 hex digits of a \uXXXX escape starting at `at`, or -1 if any digit is malformed 39func jx_hex4(buf: *u8, n: i64, at: i64) -> i64 { 40 var v: i64 = 0 41 var k: i64 = 0 42 while k < 4 { 43 let d: i64 = jx_hexdig(buf, n, at + k) 44 if d < 0 { return 0 - 1 } 45 v = (v * 16) + d 46 k = k + 1 47 } 48 return v 49} 50 51// append codepoint `cp` to out as UTF-8, bounded. Returns the new offset. 52func jx_emit_utf8(out: *u8, o0: i64, outcap: i64, cp: i64) -> i64 { 53 var o: i64 = o0 54 if cp < 128 { 55 if o < outcap - 1 { out[o] = cp as u8; o = o + 1 } 56 return o 57 } 58 if cp < 2048 { 59 if o < outcap - 2 { out[o] = (192 + (cp / 64)) as u8; o = o + 1; out[o] = (128 + (cp % 64)) as u8; o = o + 1 } 60 return o 61 } 62 if cp < 65536 { 63 if o < outcap - 3 { out[o] = (224 + (cp / 4096)) as u8; o = o + 1 64 out[o] = (128 + ((cp / 64) % 64)) as u8; o = o + 1 65 out[o] = (128 + (cp % 64)) as u8; o = o + 1 } 66 return o 67 } 68 if o < outcap - 4 { out[o] = (240 + (cp / 262144)) as u8; o = o + 1 69 out[o] = (128 + ((cp / 4096) % 64)) as u8; o = o + 1 70 out[o] = (128 + ((cp / 64) % 64)) as u8; o = o + 1 71 out[o] = (128 + (cp % 64)) as u8; o = o + 1 } 72 return o 73} 74 75// copy a JSON string value starting at vstart (the byte right after the opening quote) into out, 76// de-escaping backslashes and forcing TAB/NL/CR -> space (TSV-safe), bounded by outcap-1, NUL-term. 77// returns index just past the closing quote, or -1 if unterminated within buf. 78// ★ \uXXXX IS DECODED, NOT SKIPPED. The old "emit the char after the backslash" rule turned the very 79// common `&` into the literal text `u0026` -- MEASURED against the live 4chan boards API, which 80// renders "Anime & Manga" as "Anime u0026 Manga". It corrupts silently and only in the fields a human 81// reads, so it survives every structural test. Surrogate PAIRS are recombined so astral characters 82// (emoji) round-trip instead of landing as two replacement halves. 83// ONE DE-ESCAPER, TWO CONTRACTS (2026-08-25, LN11/nx_lsp). keep_ctl=0 is the TSV contract above -- 84// TAB/NL/CR forced to space so an extracted value cannot tear a work-journal line. keep_ctl=1 is the 85// DOCUMENT contract: control bytes are de-escaped to their real values, because an LSP textDocument 86// buffer whose newlines were flattened to spaces silently destroys every line number downstream -- 87// the same defect class as a caret under the wrong line. Adding a SECOND de-escaper for the second 88// contract would be the duplicate-ruler defect, so both wrappers below delegate here and can never 89// drift. Note keep_ctl=0 was never a de-escaper for backslash-t/n/r at all: it maps them to space 90// directly, so the raw arm is a genuinely different mapping, not merely a suppressed one. 91// Strict raw UTF-8 scalar validation for the existing string decoder; no allocation. 92func jx_utf8_scalar_width(buf: *u8, n: i64, at: i64) -> i64 { 93 let lead: i64 = buf[at] as i64 94 var width: i64 = 0 95 if lead >= 194 { if lead <= 223 { width = 2 } } 96 if lead >= 224 { if lead <= 239 { width = 3 } } 97 if lead >= 240 { if lead <= 244 { width = 4 } } 98 if width == 0 { return 0 - 1 } 99 if width > n - at { return 0 - 1 } 100 var j: i64 = 1 101 while j < width { 102 let c: i64 = buf[at + j] as i64 103 if c < 128 { return 0 - 1 }; if c > 191 { return 0 - 1 } 104 j = j + 1 105 } 106 let second: i64 = buf[at + 1] as i64 107 if lead == 224 { if second < 160 { return 0 - 1 } } 108 if lead == 237 { if second >= 160 { return 0 - 1 } } 109 if lead == 240 { if second < 144 { return 0 - 1 } } 110 if lead == 244 { if second > 143 { return 0 - 1 } } 111 return width 112} 113 114func jx_copy_str_ctl(buf: *u8, n: i64, vstart: i64, out: *u8, outcap: i64, keep_ctl: i64) -> i64 { 115 let strict: i64 = keep_ctl == 2 || keep_ctl == 3 116 if strict == 1 { 117 if n < 0 || vstart < 0 || vstart > n { return 0 - 1 } 118 if outcap <= 0 || out == (0 as *u8) { return 0 - 1 } 119 if n > 0 && buf == (0 as *u8) { return 0 - 1 } 120 } 121 var i: i64 = vstart 122 var o: i64 = 0 123 while i < n { 124 let c: i64 = buf[i] as i64 125 if c == 92 { // backslash escape 126 if i + 1 < n { 127 var e: i64 = buf[i + 1] as i64 128 if e == 117 { // \uXXXX 129 var cp: i64 = jx_hex4(buf, n, i + 2) 130 if cp < 0 { 131 if strict == 1 { return 0 - 1 } 132 // malformed escape: emit nothing for it and step past the 'u' rather than 133 // re-emitting the digits as text (that is exactly the old corruption). 134 i = i + 2 135 } else { 136 var adv: i64 = 6 137 if cp >= 55296 { if cp < 56320 { // high surrogate D800-DBFF 138 if i + 7 < n { if (buf[i + 6] as i64) == 92 { if (buf[i + 7] as i64) == 117 { 139 let lo: i64 = jx_hex4(buf, n, i + 8) 140 if lo >= 56320 { if lo < 57344 { 141 cp = 65536 + ((cp - 55296) * 1024) + (lo - 56320) 142 adv = 12 143 } } 144 } } } 145 } } 146 if strict == 1 { 147 if cp == 0 && keep_ctl == 2 { return 0 - 1 } 148 if cp >= 55296 { if cp < 57344 { return 0 - 1 } } 149 var need: i64 = 1 150 if cp >= 128 { need = 2 } 151 if cp >= 2048 { need = 3 } 152 if cp >= 65536 { need = 4 } 153 if need > outcap - 1 - o { return 0 - 1 } 154 } 155 if keep_ctl == 0 { 156 if cp == 9 { cp = 32 } 157 if cp == 10 { cp = 32 } 158 if cp == 13 { cp = 32 } 159 } 160 o = jx_emit_utf8(out, o, outcap, cp) 161 i = i + adv 162 } 163 } else { 164 if strict == 1 { 165 var valid: i64 = 0 166 if e == 34 { valid = 1 }; if e == 92 { valid = 1 }; if e == 47 { valid = 1 } 167 if e == 116 { valid = 1 }; if e == 110 { valid = 1 }; if e == 114 { valid = 1 } 168 if e == 98 { valid = 1 }; if e == 102 { valid = 1 } 169 if valid == 0 { return 0 - 1 } 170 if o >= outcap - 1 { return 0 - 1 } 171 } 172 if keep_ctl != 0 { 173 if e == 116 { e = 9 } // t -> TAB 174 if e == 110 { e = 10 } // n -> LF 175 if e == 114 { e = 13 } // r -> CR 176 if e == 98 { e = 8 } // b -> BS 177 if e == 102 { e = 12 } // f -> FF 178 } else { 179 if e == 116 { e = 32 } // t -> space 180 if e == 110 { e = 32 } // n -> space 181 if e == 114 { e = 32 } // r -> space 182 } 183 if o < outcap - 1 { out[o] = e as u8; o = o + 1 } 184 i = i + 2 185 } 186 } else { if strict == 1 { return 0 - 1 } i = i + 1 } 187 } else { 188 if c == 34 { 189 if keep_ctl == 3 { return 0 - 1 } // a span excludes its surrounding quotes 190 out[o] = 0 as u8; return i + 1 191 } 192 if strict == 1 { if c < 32 { return 0 - 1 } } 193 var width: i64 = 1 194 if strict == 1 { if c >= 128 { 195 width = jx_utf8_scalar_width(buf, n, i) 196 if width < 0 { return 0 - 1 } 197 } } 198 if strict == 1 { if width > outcap - 1 - o { return 0 - 1 } } 199 var cc: i64 = c 200 if keep_ctl == 0 { 201 if cc == 9 { cc = 32 } 202 if cc == 10 { cc = 32 } 203 if cc == 13 { cc = 32 } 204 } 205 if o < outcap - 1 { out[o] = cc as u8; o = o + 1 } 206 var rest: i64 = 1 207 while rest < width { out[o] = buf[i + rest]; o = o + 1; rest = rest + 1 } 208 i = i + width 209 } 210 } 211 out[o] = 0 as u8 212 if keep_ctl == 3 { return o } 213 return 0 - 1 214} 215// Length-delimited JSON string content: strict Unicode and escapes, embedded 216// escaped NUL allowed. Capacity includes a terminator; return decoded byte count 217// or -1. Failure may leave a decoded prefix, never a success or truncated ID. 218func jx_decode_span(buf:*u8,n:i64,out:*u8,outcap:i64)->i64 { 219 return jx_copy_str_ctl(buf,n,0,out,outcap,3) 220} 221// Native argv contract: full JSON escapes, strict Unicode, no NUL and no silent truncation. 222// Legacy TSV/raw wrappers retain their existing behavior through the same decoder loop. 223func jx_copy_str_argv(buf: *u8, n: i64, vstart: i64, out: *u8, outcap: i64) -> i64 { 224 return jx_copy_str_ctl(buf, n, vstart, out, outcap, 2) 225} 226 227func jx_copy_str(buf: *u8, n: i64, vstart: i64, out: *u8, outcap: i64) -> i64 { 228 return jx_copy_str_ctl(buf, n, vstart, out, outcap, 0) 229} 230func jx_copy_str_raw(buf: *u8, n: i64, vstart: i64, out: *u8, outcap: i64) -> i64 { 231 return jx_copy_str_ctl(buf, n, vstart, out, outcap, 1) 232} 233 234// skip JSON insignificant whitespace at/after `from`; returns the first non-ws index (or n). 235// Separate cursor + explicit run flag: a loop that exits by clobbering its own cursor cannot also 236// report where it stopped. 237func jx_skip_ws(buf: *u8, n: i64, from: i64) -> i64 { 238 var p: i64 = from 239 var run: i64 = 1 240 while run == 1 { 241 if p >= n { run = 0 } else { 242 let c: i64 = buf[p] as i64 243 if c == 32 { p = p + 1 } else { 244 if c == 9 { p = p + 1 } else { 245 if c == 10 { p = p + 1 } else { 246 if c == 13 { p = p + 1 } else { run = 0 } } } } 247 } 248 } 249 return p 250} 251 252// find `"key":` at/after `start` and read its NUMERIC value into out_val. Returns the index just past 253// the last digit, or -1 when the key is absent OR its value is not a number. 254// ★ -1 (not 0) on a non-numeric/absent field ON PURPOSE: "field missing" and "field is zero" are 255// different facts, and a reader that merges them reports a healthy default for a parse failure. 256// ⚠ BOUND THE SEARCH TO ONE RECORD by passing that record's END as `n` -- jx_find scans forward to `n`, 257// so on a record whose field is absent an unbounded call silently returns the NEXT record's value. 258func jx_get_int(buf: *u8, n: i64, start: i64, key: *u8, out_val: *i64) -> i64 { 259 out_val[0] = 0 260 let kl: i64 = jx_len(key) 261 let needle: *u8 = sys_mmap(kl + 8) 262 var o: i64 = 0 263 needle[o] = 34 as u8; o = o + 1 // " 264 var i: i64 = 0 265 while key[i] != (0 as u8) { needle[o] = key[i]; o = o + 1; i = i + 1 } 266 needle[o] = 34 as u8; o = o + 1 // " 267 needle[o] = 58 as u8; o = o + 1 // : 268 needle[o] = 0 as u8 269 let at: i64 = jx_find(buf, n, start, needle) 270 if at < 0 { return 0 - 1 } 271 var p: i64 = jx_skip_ws(buf, n, at + o) 272 if p >= n { return 0 - 1 } 273 var neg: i64 = 0 274 if buf[p] == (45 as u8) { neg = 1; p = p + 1 } // '-' 275 var got: i64 = 0 276 var v: i64 = 0 277 var run: i64 = 1 278 while run == 1 { 279 if p >= n { run = 0 } else { 280 let c: i64 = buf[p] as i64 281 if c >= 48 { if c <= 57 { v = (v * 10) + (c - 48); got = got + 1; p = p + 1 } else { run = 0 } } 282 else { run = 0 } 283 } 284 } 285 if got == 0 { return 0 - 1 } // a STRING value lands here -> honest "not a number" 286 if neg == 1 { v = 0 - v } 287 out_val[0] = v 288 return p 289} 290 291// find `"key":"` at/after `start`, copy its string value into out. returns index past the value's 292// closing quote, or -1 if not found. `key` is the bare name (no quotes); harness JSON is compact. 293func jx_get_str(buf: *u8, n: i64, start: i64, key: *u8, out: *u8, outcap: i64) -> i64 { 294 let kl: i64 = jx_len(key) 295 let needle: *u8 = sys_mmap(kl + 8) 296 var o: i64 = 0 297 needle[o] = 34 as u8; o = o + 1 // " 298 var i: i64 = 0 299 while key[i] != (0 as u8) { needle[o] = key[i]; o = o + 1; i = i + 1 } 300 needle[o] = 34 as u8; o = o + 1 // " 301 needle[o] = 58 as u8; o = o + 1 // : 302 needle[o] = 0 as u8 303 // WHITESPACE AFTER THE COLON IS LEGAL JSON AND THIS READER USED TO REQUIRE ITS ABSENCE. 304 // The needle ended `":"` so it matched ONLY compact producers; a pretty-printed `"k": "v"` -- what 305 // the GitHub contents API returns -- silently matched NOTHING, and a caller reading that miss as an 306 // absent field concluded the population was empty. jx_get_int ONE FUNCTION ABOVE already skipped 307 // whitespace at exactly this point, so the tolerance existed in one verb and not its siblings -- 308 // a law applied in half an API. Behaviour on compact JSON is UNCHANGED (jx_skip_ws consumes 309 // nothing there), so this is strictly additive for every existing consumer. 310 let at: i64 = jx_find(buf, n, start, needle) 311 if at < 0 { return 0 - 1 } 312 let vq: i64 = jx_skip_ws(buf, n, at + o) 313 if vq >= n { return 0 - 1 } 314 if buf[vq] != (34 as u8) { return 0 - 1 } // present but NOT a string -> honest miss 315 return jx_copy_str(buf, n, vq + 1, out, outcap) 316} 317 318// DOCUMENT-CONTRACT sibling of jx_get_str: same needle, raw de-escape. For any field whose value IS 319// text with structure (an LSP textDocument buffer), where a flattened newline is silent corruption. 320func jx_get_str_raw(buf: *u8, n: i64, start: i64, key: *u8, out: *u8, outcap: i64) -> i64 { 321 let kl: i64 = jx_len(key) 322 let needle: *u8 = sys_mmap(kl + 8) 323 var o: i64 = 0 324 needle[o] = 34 as u8; o = o + 1 325 var i: i64 = 0 326 while key[i] != (0 as u8) { needle[o] = key[i]; o = o + 1; i = i + 1 } 327 needle[o] = 34 as u8; o = o + 1 328 needle[o] = 58 as u8; o = o + 1 329 needle[o] = 0 as u8 330 // Same whitespace tolerance as jx_get_str, applied here in the SAME edit rather than left for a 331 // later reader to discover: a fix that lands in one verb and not its sibling is half a fix, and the 332 // half left undone is always the one that ships. Compact JSON is unaffected. 333 let at: i64 = jx_find(buf, n, start, needle) 334 if at < 0 { return 0 - 1 } 335 let vq: i64 = jx_skip_ws(buf, n, at + o) 336 if vq >= n { return 0 - 1 } 337 if buf[vq] != (34 as u8) { return 0 - 1 } 338 return jx_copy_str_raw(buf, n, vq + 1, out, outcap) 339} 340