json_emit.nx source
↩ module page · 273 lines · 9701 B
1// json_emit.nx -- RFC 8259 JSON writer (complement to json.nx parser).
2//
3// Used by: TLS session tickets, API responses, config serialisation,
4// structured logs, Nishi Pages content negotiation (Accept: json).
5//
6// Shape:
7// - Caller allocates an output buffer; we append into it.
8// - A JsonWriter struct tracks position + depth + "needs comma"
9// state so the caller doesn't manage separators manually.
10// - Begin/end array/object and emit_* primitives for scalars.
11// - String escaping per RFC 8259 §7 (\\, \", \/, \b, \f, \n,
12// \r, \t, \uXXXX for < 0x20 + > 0x7E). Handles UTF-8
13// passthrough for BMP + astral.
14//
15// Invariants:
16// JE1 Output is valid JSON for any sequence of begin/end +
17// emit calls that matches the grammar. Callers can
18// misuse (nest mismatched) and get garbage, but we never
19// emit syntax errors in-pattern.
20// JE2 String emission escapes ALL required characters per
21// RFC 8259 §7; no silent acceptance of control bytes
22// that would produce ambiguous output.
23// JE3 Buffer overruns return a negative error; no silent
24// truncation (malformed JSON tail is worse than a
25// visible error).
26// JE4 Emit is single-pass, no back-patching; total size can
27// be computed up-front from the structure if needed.
28
29import "syscalls.nx"
30
31const JE_ERR_OVERFLOW: i64 = -1
32const JE_MAX_DEPTH: i64 = 64
33
34// Writer state -- caller allocates + initialises via json_writer_init.
35struct JsonWriter {
36 buf: *u8,
37 pos: i64,
38 cap: i64,
39 depth: i64,
40 // Per-depth: "have we emitted an item at this level yet?"
41 // 0 = first item (no leading comma), 1 = subsequent (emit comma).
42 // Indexed [0..depth]. depth 0 is the top-level value.
43 prior: *u8,
44}
45
46func json_writer_init(w: *JsonWriter, buf: *u8, cap: i64) -> i64 {
47 let prior_raw: *u8 = sys_mmap(JE_MAX_DEPTH + 16)
48 var i: i64 = 0
49 while i < JE_MAX_DEPTH { prior_raw[i] = 0; i = i + 1 }
50 w.buf = buf
51 w.pos = 0
52 w.cap = cap
53 w.depth = 0
54 w.prior = prior_raw
55 return 0
56}
57
58// Raw byte append (bounds-checked).
59func je_put(w: *JsonWriter, b: i64) -> i64 {
60 if w.pos >= w.cap { return JE_ERR_OVERFLOW }
61 w.buf[w.pos] = b
62 w.pos = w.pos + 1
63 return 0
64}
65
66// Emit separator before a new item at the current depth. If
67// prior[depth] == 1, prepend a comma. Always sets prior[depth] = 1
68// on return.
69func je_sep(w: *JsonWriter) -> i64 {
70 let d: i64 = w.depth
71 if w.prior[d] == 1 {
72 let rc: i64 = je_put(w, 0x2C) // ','
73 if rc < 0 { return rc }
74 }
75 w.prior[d] = 1
76 return 0
77}
78
79// Begin an object. Emits `{` at the current value position.
80func json_begin_object(w: *JsonWriter) -> i64 {
81 let rc0: i64 = je_sep(w)
82 if rc0 < 0 { return rc0 }
83 let rc1: i64 = je_put(w, 0x7B) // '{'
84 if rc1 < 0 { return rc1 }
85 if w.depth + 1 >= JE_MAX_DEPTH { return JE_ERR_OVERFLOW }
86 w.depth = w.depth + 1
87 w.prior[w.depth] = 0
88 return 0
89}
90
91// End an object. `}` + depth--.
92func json_end_object(w: *JsonWriter) -> i64 {
93 let rc: i64 = je_put(w, 0x7D) // '}'
94 if rc < 0 { return rc }
95 w.depth = w.depth - 1
96 return 0
97}
98
99func json_begin_array(w: *JsonWriter) -> i64 {
100 let rc0: i64 = je_sep(w)
101 if rc0 < 0 { return rc0 }
102 let rc1: i64 = je_put(w, 0x5B) // '['
103 if rc1 < 0 { return rc1 }
104 if w.depth + 1 >= JE_MAX_DEPTH { return JE_ERR_OVERFLOW }
105 w.depth = w.depth + 1
106 w.prior[w.depth] = 0
107 return 0
108}
109
110func json_end_array(w: *JsonWriter) -> i64 {
111 let rc: i64 = je_put(w, 0x5D) // ']'
112 if rc < 0 { return rc }
113 w.depth = w.depth - 1
114 return 0
115}
116
117// String emission with RFC 8259 §7 escaping.
118func je_emit_string_raw(w: *JsonWriter, s: *u8, n: i64) -> i64 {
119 let rc0: i64 = je_put(w, 0x22) // '"'
120 if rc0 < 0 { return rc0 }
121 var i: i64 = 0
122 while i < n {
123 let b: i64 = s[i]
124 if b == 0x22 {
125 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 }
126 let r2: i64 = je_put(w, 0x22); if r2 < 0 { return r2 }
127 } else {
128 if b == 0x5C {
129 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 }
130 let r2: i64 = je_put(w, 0x5C); if r2 < 0 { return r2 }
131 } else {
132 if b == 0x0A {
133 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 }
134 let r2: i64 = je_put(w, 0x6E); if r2 < 0 { return r2 }
135 } else {
136 if b == 0x0D {
137 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 }
138 let r2: i64 = je_put(w, 0x72); if r2 < 0 { return r2 }
139 } else {
140 if b == 0x09 {
141 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 }
142 let r2: i64 = je_put(w, 0x74); if r2 < 0 { return r2 }
143 } else {
144 if b < 0x20 {
145 // \uXXXX for control chars we don't
146 // have a short escape for.
147 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 }
148 let r2: i64 = je_put(w, 0x75); if r2 < 0 { return r2 }
149 let r3: i64 = je_put(w, 0x30); if r3 < 0 { return r3 }
150 let r4: i64 = je_put(w, 0x30); if r4 < 0 { return r4 }
151 let hi: i64 = (b >> 4) & 0xF
152 let lo: i64 = b & 0xF
153 var hc: i64 = 0x30 + hi
154 if hi > 9 { hc = 0x61 + hi - 10 }
155 var lc: i64 = 0x30 + lo
156 if lo > 9 { lc = 0x61 + lo - 10 }
157 let r5: i64 = je_put(w, hc); if r5 < 0 { return r5 }
158 let r6: i64 = je_put(w, lc); if r6 < 0 { return r6 }
159 } else {
160 // Passthrough for printable ASCII
161 // and UTF-8 multibyte (>= 0x80).
162 let r: i64 = je_put(w, b); if r < 0 { return r }
163 }
164 }
165 }
166 }
167 }
168 }
169 i = i + 1
170 }
171 return je_put(w, 0x22) // closing '"'
172}
173
174// Emit a string as the next value (with separator).
175func json_emit_string(w: *JsonWriter, s: *u8, n: i64) -> i64 {
176 let rc: i64 = je_sep(w)
177 if rc < 0 { return rc }
178 return je_emit_string_raw(w, s, n)
179}
180
181// Emit an object key. Contract: must be followed by a value emit
182// before any further key or end_object. We don't set prior[depth]
183// because the key-value pair emits the separator via the value's
184// own je_sep -- this key call emits the key, ':', and leaves
185// prior unchanged (the VALUE sets prior).
186func json_emit_key(w: *JsonWriter, name: *u8, n: i64) -> i64 {
187 let rc0: i64 = je_sep(w)
188 if rc0 < 0 { return rc0 }
189 let rc1: i64 = je_emit_string_raw(w, name, n)
190 if rc1 < 0 { return rc1 }
191 let rc2: i64 = je_put(w, 0x3A) // ':'
192 if rc2 < 0 { return rc2 }
193 // The next value emitted should NOT prepend a comma; the key
194 // has already set prior[depth] = 1 via its own sep call. But
195 // we want the value itself to NOT emit a leading comma. We
196 // cheat: set prior[depth] back to 0 so the VALUE's je_sep
197 // doesn't add another comma.
198 w.prior[w.depth] = 0
199 return 0
200}
201
202// Emit integer as decimal.
203func json_emit_int(w: *JsonWriter, n: i64) -> i64 {
204 let rc: i64 = je_sep(w)
205 if rc < 0 { return rc }
206 if n == 0 { return je_put(w, 0x30) }
207 var v: i64 = n
208 if v < 0 {
209 let r: i64 = je_put(w, 0x2D) // '-'
210 if r < 0 { return r }
211 v = 0 - v
212 }
213 let scratch: *u8 = sys_mmap(32)
214 var k: i64 = 0
215 while v > 0 {
216 scratch[k] = 0x30 + (v % 10)
217 v = v / 10
218 k = k + 1
219 }
220 while k > 0 {
221 k = k - 1
222 let r: i64 = je_put(w, scratch[k])
223 if r < 0 { return r }
224 }
225 return 0
226}
227
228// Emit a null-terminated literal; caller guarantees bounds.
229func je_emit_literal(w: *JsonWriter, lit: *u8, n: i64) -> i64 {
230 var i: i64 = 0
231 while i < n {
232 let r: i64 = je_put(w, lit[i])
233 if r < 0 { return r }
234 i = i + 1
235 }
236 return 0
237}
238
239func json_emit_bool(w: *JsonWriter, b: i64) -> i64 {
240 let rc0: i64 = je_sep(w)
241 if rc0 < 0 { return rc0 }
242 if b != 0 { return je_emit_literal(w, "true", 4) }
243 return je_emit_literal(w, "false", 5)
244}
245
246func json_emit_null(w: *JsonWriter) -> i64 {
247 let rc: i64 = je_sep(w)
248 if rc < 0 { return rc }
249 return je_emit_literal(w, "null", 4)
250}
251
252// Compile-only smoke. Emit: {"name":"Nishi","year":2026,"active":true}.
253func main() -> i64 {
254 let buf: *u8 = sys_mmap(256)
255 let w_raw: *u8 = sys_mmap(128)
256 let w: *JsonWriter = w_raw as *JsonWriter
257 json_writer_init(w, buf, 256)
258
259 json_begin_object(w)
260 json_emit_key(w, "name", 4)
261 json_emit_string(w, "Nishi", 5)
262 json_emit_key(w, "year", 4)
263 json_emit_int(w, 2026)
264 json_emit_key(w, "active", 6)
265 json_emit_bool(w, 1)
266 json_end_object(w)
267
268 // First byte should be '{'.
269 if buf[0] != 0x7B { return 1 }
270 // w.pos should be > 0.
271 if w.pos <= 0 { return 2 }
272 return 0
273}