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}