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