code wiki / (root) / nx_json_auth_read_t1.nx

nx_json_auth_read_t1.nx source

↩ module page · 323 lines · 15046 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. 91func jx_copy_str_ctl(buf: *u8, n: i64, vstart: i64, out: *u8, outcap: i64, keep_ctl: i64) -> i64 { 92 var i: i64 = vstart 93 var o: i64 = 0 94 while i < n { 95 let c: i64 = buf[i] as i64 96 if c == 92 { // backslash escape 97 if i + 1 < n { 98 var e: i64 = buf[i + 1] as i64 99 if e == 117 { // \uXXXX 100 var cp: i64 = jx_hex4(buf, n, i + 2) 101 if cp < 0 { 102 // malformed escape: emit nothing for it and step past the 'u' rather than 103 // re-emitting the digits as text (that is exactly the old corruption). 104 i = i + 2 105 } else { 106 var adv: i64 = 6 107 if cp >= 55296 { if cp < 56320 { // high surrogate D800-DBFF 108 if i + 7 < n { if (buf[i + 6] as i64) == 92 { if (buf[i + 7] as i64) == 117 { 109 let lo: i64 = jx_hex4(buf, n, i + 8) 110 if lo >= 56320 { if lo < 57344 { 111 cp = 65536 + ((cp - 55296) * 1024) + (lo - 56320) 112 adv = 12 113 } } 114 } } } 115 } } 116 if keep_ctl == 0 { 117 if cp == 9 { cp = 32 } 118 if cp == 10 { cp = 32 } 119 if cp == 13 { cp = 32 } 120 } 121 o = jx_emit_utf8(out, o, outcap, cp) 122 i = i + adv 123 } 124 } else { 125 if keep_ctl == 1 { 126 if e == 116 { e = 9 } // t -> TAB 127 if e == 110 { e = 10 } // n -> LF 128 if e == 114 { e = 13 } // r -> CR 129 if e == 98 { e = 8 } // b -> BS 130 if e == 102 { e = 12 } // f -> FF 131 } else { 132 if e == 116 { e = 32 } // t -> space 133 if e == 110 { e = 32 } // n -> space 134 if e == 114 { e = 32 } // r -> space 135 } 136 if o < outcap - 1 { out[o] = e as u8; o = o + 1 } 137 i = i + 2 138 } 139 } else { i = i + 1 } 140 } else { 141 if c == 34 { out[o] = 0 as u8; return i + 1 } // closing quote 142 var cc: i64 = c 143 if keep_ctl == 0 { 144 if cc == 9 { cc = 32 } 145 if cc == 10 { cc = 32 } 146 if cc == 13 { cc = 32 } 147 } 148 if o < outcap - 1 { out[o] = cc as u8; o = o + 1 } 149 i = i + 1 150 } 151 } 152 out[o] = 0 as u8 153 return 0 - 1 154} 155func jx_copy_str(buf: *u8, n: i64, vstart: i64, out: *u8, outcap: i64) -> i64 { 156 return jx_copy_str_ctl(buf, n, vstart, out, outcap, 0) 157} 158// Decode an exact string-content span. Return byte length (including embedded 159// NUL bytes), or -1 for invalid escapes/insufficient capacity; never truncate. 160func jx_decode_span(buf: *u8, n: i64, out: *u8, outcap: i64) -> i64 { 161 var i: i64 = 0 162 var o: i64 = 0 163 while i < n { 164 let c: i64 = buf[i] as i64 165 if c < 32 { return -1 } 166 if c == 34 { return -1 } 167 if c != 92 { 168 if o >= outcap { return -1 } 169 out[o] = c as u8; o = o + 1; i = i + 1 170 } else { 171 if n - i < 2 { return -1 } 172 let e: i64 = buf[i+1] as i64 173 var cp: i64 = -1 174 var advance: i64 = 2 175 if e == 34 { cp = 34 } 176 if e == 92 { cp = 92 } 177 if e == 47 { cp = 47 } 178 if e == 98 { cp = 8 } 179 if e == 102 { cp = 12 } 180 if e == 110 { cp = 10 } 181 if e == 114 { cp = 13 } 182 if e == 116 { cp = 9 } 183 if e == 117 { 184 if n-i < 6 { return -1 } 185 cp = jx_hex4(buf,n,i+2) 186 if cp < 0 { return -1 } 187 advance = 6 188 if cp >= 55296 { if cp < 56320 { 189 if n-i < 12 { return -1 } 190 if buf[i+6] != (92 as u8) { return -1 } 191 if buf[i+7] != (117 as u8) { return -1 } 192 let lo: i64 = jx_hex4(buf,n,i+8) 193 if lo < 56320 { return -1 } 194 if lo >= 57344 { return -1 } 195 cp = 65536 + (cp-55296)*1024 + lo-56320 196 advance = 12 197 } } 198 if cp >= 56320 { if cp < 57344 { return -1 } } 199 } 200 if cp < 0 { return -1 } 201 var width: i64 = 1 202 if cp >= 128 { width = 2 } 203 if cp >= 2048 { width = 3 } 204 if cp >= 65536 { width = 4 } 205 // Shared encoder reserves a terminator byte; require it explicitly. 206 if outcap-o <= width { return -1 } 207 o = jx_emit_utf8(out,o,outcap,cp) 208 i = i + advance 209 } 210 } 211 return o 212} 213 214func jx_copy_str_raw(buf: *u8, n: i64, vstart: i64, out: *u8, outcap: i64) -> i64 { 215 return jx_copy_str_ctl(buf, n, vstart, out, outcap, 1) 216} 217 218// skip JSON insignificant whitespace at/after `from`; returns the first non-ws index (or n). 219// Separate cursor + explicit run flag: a loop that exits by clobbering its own cursor cannot also 220// report where it stopped. 221func jx_skip_ws(buf: *u8, n: i64, from: i64) -> i64 { 222 var p: i64 = from 223 var run: i64 = 1 224 while run == 1 { 225 if p >= n { run = 0 } else { 226 let c: i64 = buf[p] as i64 227 if c == 32 { p = p + 1 } else { 228 if c == 9 { p = p + 1 } else { 229 if c == 10 { p = p + 1 } else { 230 if c == 13 { p = p + 1 } else { run = 0 } } } } 231 } 232 } 233 return p 234} 235 236// find `"key":` at/after `start` and read its NUMERIC value into out_val. Returns the index just past 237// the last digit, or -1 when the key is absent OR its value is not a number. 238// ★ -1 (not 0) on a non-numeric/absent field ON PURPOSE: "field missing" and "field is zero" are 239// different facts, and a reader that merges them reports a healthy default for a parse failure. 240// ⚠ BOUND THE SEARCH TO ONE RECORD by passing that record's END as `n` -- jx_find scans forward to `n`, 241// so on a record whose field is absent an unbounded call silently returns the NEXT record's value. 242func jx_get_int(buf: *u8, n: i64, start: i64, key: *u8, out_val: *i64) -> i64 { 243 out_val[0] = 0 244 let kl: i64 = jx_len(key) 245 let needle: *u8 = sys_mmap(kl + 8) 246 var o: i64 = 0 247 needle[o] = 34 as u8; o = o + 1 // " 248 var i: i64 = 0 249 while key[i] != (0 as u8) { needle[o] = key[i]; o = o + 1; i = i + 1 } 250 needle[o] = 34 as u8; o = o + 1 // " 251 needle[o] = 58 as u8; o = o + 1 // : 252 needle[o] = 0 as u8 253 let at: i64 = jx_find(buf, n, start, needle) 254 if at < 0 { return 0 - 1 } 255 var p: i64 = jx_skip_ws(buf, n, at + o) 256 if p >= n { return 0 - 1 } 257 var neg: i64 = 0 258 if buf[p] == (45 as u8) { neg = 1; p = p + 1 } // '-' 259 var got: i64 = 0 260 var v: i64 = 0 261 var run: i64 = 1 262 while run == 1 { 263 if p >= n { run = 0 } else { 264 let c: i64 = buf[p] as i64 265 if c >= 48 { if c <= 57 { v = (v * 10) + (c - 48); got = got + 1; p = p + 1 } else { run = 0 } } 266 else { run = 0 } 267 } 268 } 269 if got == 0 { return 0 - 1 } // a STRING value lands here -> honest "not a number" 270 if neg == 1 { v = 0 - v } 271 out_val[0] = v 272 return p 273} 274 275// find `"key":"` at/after `start`, copy its string value into out. returns index past the value's 276// closing quote, or -1 if not found. `key` is the bare name (no quotes); harness JSON is compact. 277func jx_get_str(buf: *u8, n: i64, start: i64, key: *u8, out: *u8, outcap: i64) -> i64 { 278 let kl: i64 = jx_len(key) 279 let needle: *u8 = sys_mmap(kl + 8) 280 var o: i64 = 0 281 needle[o] = 34 as u8; o = o + 1 // " 282 var i: i64 = 0 283 while key[i] != (0 as u8) { needle[o] = key[i]; o = o + 1; i = i + 1 } 284 needle[o] = 34 as u8; o = o + 1 // " 285 needle[o] = 58 as u8; o = o + 1 // : 286 needle[o] = 0 as u8 287 // WHITESPACE AFTER THE COLON IS LEGAL JSON AND THIS READER USED TO REQUIRE ITS ABSENCE. 288 // The needle ended `":"` so it matched ONLY compact producers; a pretty-printed `"k": "v"` -- what 289 // the GitHub contents API returns -- silently matched NOTHING, and a caller reading that miss as an 290 // absent field concluded the population was empty. jx_get_int ONE FUNCTION ABOVE already skipped 291 // whitespace at exactly this point, so the tolerance existed in one verb and not its siblings -- 292 // a law applied in half an API. Behaviour on compact JSON is UNCHANGED (jx_skip_ws consumes 293 // nothing there), so this is strictly additive for every existing consumer. 294 let at: i64 = jx_find(buf, n, start, needle) 295 if at < 0 { return 0 - 1 } 296 let vq: i64 = jx_skip_ws(buf, n, at + o) 297 if vq >= n { return 0 - 1 } 298 if buf[vq] != (34 as u8) { return 0 - 1 } // present but NOT a string -> honest miss 299 return jx_copy_str(buf, n, vq + 1, out, outcap) 300} 301 302// DOCUMENT-CONTRACT sibling of jx_get_str: same needle, raw de-escape. For any field whose value IS 303// text with structure (an LSP textDocument buffer), where a flattened newline is silent corruption. 304func jx_get_str_raw(buf: *u8, n: i64, start: i64, key: *u8, out: *u8, outcap: i64) -> i64 { 305 let kl: i64 = jx_len(key) 306 let needle: *u8 = sys_mmap(kl + 8) 307 var o: i64 = 0 308 needle[o] = 34 as u8; o = o + 1 309 var i: i64 = 0 310 while key[i] != (0 as u8) { needle[o] = key[i]; o = o + 1; i = i + 1 } 311 needle[o] = 34 as u8; o = o + 1 312 needle[o] = 58 as u8; o = o + 1 313 needle[o] = 0 as u8 314 // Same whitespace tolerance as jx_get_str, applied here in the SAME edit rather than left for a 315 // later reader to discover: a fix that lands in one verb and not its sibling is half a fix, and the 316 // half left undone is always the one that ships. Compact JSON is unaffected. 317 let at: i64 = jx_find(buf, n, start, needle) 318 if at < 0 { return 0 - 1 } 319 let vq: i64 = jx_skip_ws(buf, n, at + o) 320 if vq >= n { return 0 - 1 } 321 if buf[vq] != (34 as u8) { return 0 - 1 } 322 return jx_copy_str_raw(buf, n, vq + 1, out, outcap) 323}