nx_kv.nx source
↩ module page · 219 lines · 7506 B
1// nx_kv.nx -- key=value (.env / dotenv-style) parser.
2//
3// Parses input of the form:
4//
5// # comment
6// KEY=value
7// ANOTHER_KEY=other value
8// QUOTED="value with spaces"
9// SINGLE='also fine'
10//
11// Returns one entry at a time via a streaming iterator. No
12// multiline values, no variable interpolation, no escape sequences
13// in this commit; those land if a real .env consumer requires them.
14//
15// Used by:
16// - Service config loaders (NX_HOST=... NX_PORT=...)
17// - Build-tool .env file consumers
18// - Test-harness fixture loading
19
20// nx_safety_envelope:
21// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
22// sil_target: SIL1
23// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
24// verdict: NOT_YET_EVALUATED
25
26import "syscalls.nx"
27import "nx_lex.nx"
28
29struct NxKvIter {
30 src: *u8,
31 len: i64,
32 pos: i64,
33 // Last entry's spans into `src`. Both NUL-terminated via
34 // separate scratch buffers since src is read-only.
35 key_off: i64, key_len: i64,
36 val_off: i64, val_len: i64,
37}
38
39const NX_KVITER_BYTES: i64 = 56
40
41func nx_kv_new(src: *u8, len: i64) -> *NxKvIter {
42 let raw: *u8 = sys_mmap(NX_KVITER_BYTES)
43 let it: *NxKvIter = raw as *NxKvIter
44 it.src = src
45 it.len = len
46 it.pos = 0
47 it.key_off = 0; it.key_len = 0
48 it.val_off = 0; it.val_len = 0
49 return it
50}
51
52// is_intraline_ws canonical in nx_lex.nx (KV deliberately treats newline
53// as record separator, NOT whitespace -- distinct from nx_lex_is_ws).
54
55func nx_kv_is_eol(c: i64) -> i64 {
56 if c == 0x0A { return 1 }
57 if c == 0x0D { return 1 }
58 return 0
59}
60
61// Skip whitespace + blank lines + comments at current position.
62func nx_kv_skip_blanks(it: *NxKvIter) -> i64 {
63 var done: i64 = 0
64 while done == 0 {
65 if it.pos >= it.len { done = 1 }
66 else {
67 let c: i64 = it.src[it.pos]
68 if nx_lex_is_intraline_ws(c) == 1 { it.pos = it.pos + 1 }
69 else { if nx_kv_is_eol(c) == 1 { it.pos = it.pos + 1 }
70 else {
71 if c == 0x23 { // '#' comment -- skip to EOL
72 while it.pos < it.len {
73 if nx_kv_is_eol(it.src[it.pos]) == 1 { it.pos = it.len + 1 }
74 else { it.pos = it.pos + 1 }
75 }
76 if it.pos > it.len { it.pos = it.pos - 1 - it.len }
77 // recover index after sentinel break
78 var stop: i64 = 0
79 while stop == 0 {
80 if it.pos >= it.len { stop = 1 }
81 else {
82 if nx_kv_is_eol(it.src[it.pos]) == 1 { stop = 1 }
83 else { it.pos = it.pos + 1 }
84 }
85 }
86 } else {
87 done = 1
88 }
89 }
90 }
91 }
92 }
93 return 0
94}
95
96// Read next key=value entry. Returns:
97// 1 = entry available; key/val spans populated
98// 0 = end of input
99// -1 = malformed
100func nx_kv_next(it: *NxKvIter) -> i64 {
101 nx_kv_skip_blanks(it)
102 if it.pos >= it.len { return 0 }
103
104 // Key = identifier-ish chars until '='.
105 it.key_off = it.pos
106 var k_end: i64 = it.pos
107 var stop_k: i64 = 0
108 while stop_k == 0 {
109 if k_end >= it.len { return -1 }
110 let c: i64 = it.src[k_end]
111 if c == 0x3D { stop_k = 1 }
112 else { if nx_kv_is_eol(c) == 1 { return -1 }
113 else { k_end = k_end + 1 }
114 }
115 }
116 it.key_len = k_end - it.key_off
117 if it.key_len == 0 { return -1 }
118 it.pos = k_end + 1 // past '='
119
120 // Value: until EOL. If first char is ' or ", read until matching.
121 if it.pos >= it.len {
122 it.val_off = it.pos
123 it.val_len = 0
124 return 1
125 }
126 let first: i64 = it.src[it.pos]
127 if first == 0x22 {
128 it.pos = it.pos + 1
129 it.val_off = it.pos
130 var stop_v: i64 = 0
131 while stop_v == 0 {
132 if it.pos >= it.len { return -1 }
133 if it.src[it.pos] == 0x22 { stop_v = 1 }
134 else { it.pos = it.pos + 1 }
135 }
136 it.val_len = it.pos - it.val_off
137 it.pos = it.pos + 1 // past closing "
138 } else {
139 if first == 0x27 {
140 it.pos = it.pos + 1
141 it.val_off = it.pos
142 var stop_v2: i64 = 0
143 while stop_v2 == 0 {
144 if it.pos >= it.len { return -1 }
145 if it.src[it.pos] == 0x27 { stop_v2 = 1 }
146 else { it.pos = it.pos + 1 }
147 }
148 it.val_len = it.pos - it.val_off
149 it.pos = it.pos + 1
150 } else {
151 it.val_off = it.pos
152 var stop_v3: i64 = 0
153 while stop_v3 == 0 {
154 if it.pos >= it.len { stop_v3 = 1 }
155 else {
156 if nx_kv_is_eol(it.src[it.pos]) == 1 { stop_v3 = 1 }
157 else { it.pos = it.pos + 1 }
158 }
159 }
160 it.val_len = it.pos - it.val_off
161 }
162 }
163 return 1
164}
165
166// ---- self-test ---------------------------------------------------
167
168func main() -> i64 {
169 // Input:
170 // # config
171 // HOST=localhost
172 // PORT=8080
173 // NAME="quoted value"
174 //
175 let s: *u8 = sys_mmap(128)
176 s[0]=0x23; s[1]=0x20; s[2]=0x63; s[3]=0x66; s[4]=0x67; s[5]=0x0A
177 s[6]=0x48; s[7]=0x4F; s[8]=0x53; s[9]=0x54; s[10]=0x3D
178 s[11]=0x6C; s[12]=0x6F; s[13]=0x63; s[14]=0x61; s[15]=0x6C
179 s[16]=0x68; s[17]=0x6F; s[18]=0x73; s[19]=0x74; s[20]=0x0A
180 s[21]=0x50; s[22]=0x4F; s[23]=0x52; s[24]=0x54; s[25]=0x3D
181 s[26]=0x38; s[27]=0x30; s[28]=0x38; s[29]=0x30; s[30]=0x0A
182 s[31]=0x4E; s[32]=0x41; s[33]=0x4D; s[34]=0x45; s[35]=0x3D
183 s[36]=0x22; s[37]=0x71; s[38]=0x75; s[39]=0x6F; s[40]=0x74
184 s[41]=0x65; s[42]=0x64; s[43]=0x20; s[44]=0x76; s[45]=0x61
185 s[46]=0x6C; s[47]=0x75; s[48]=0x65; s[49]=0x22; s[50]=0x0A
186 let total: i64 = 51
187
188 let it: *NxKvIter = nx_kv_new(s, total)
189
190 // Entry 1: HOST=localhost
191 let r1: i64 = nx_kv_next(it)
192 if r1 != 1 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
193 if it.key_len != 4 { return __syscall(93, 2, 0, 0, 0, 0, 0) }
194 if s[it.key_off] != 0x48 { return __syscall(93, 3, 0, 0, 0, 0, 0) } // 'H'
195 if it.val_len != 9 { return __syscall(93, 4, 0, 0, 0, 0, 0) }
196 if s[it.val_off] != 0x6C { return __syscall(93, 5, 0, 0, 0, 0, 0) } // 'l'
197
198 // Entry 2: PORT=8080
199 let r2: i64 = nx_kv_next(it)
200 if r2 != 1 { return __syscall(93, 6, 0, 0, 0, 0, 0) }
201 if it.key_len != 4 { return __syscall(93, 7, 0, 0, 0, 0, 0) }
202 if s[it.key_off] != 0x50 { return __syscall(93, 8, 0, 0, 0, 0, 0) } // 'P'
203 if it.val_len != 4 { return __syscall(93, 9, 0, 0, 0, 0, 0) }
204 if s[it.val_off] != 0x38 { return __syscall(93, 10, 0, 0, 0, 0, 0) } // '8'
205
206 // Entry 3: NAME="quoted value" -- value should be 12 chars (no quotes)
207 let r3: i64 = nx_kv_next(it)
208 if r3 != 1 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
209 if it.key_len != 4 { return __syscall(93, 12, 0, 0, 0, 0, 0) }
210 if it.val_len != 12 { return __syscall(93, 13, 0, 0, 0, 0, 0) }
211 if s[it.val_off] != 0x71 { return __syscall(93, 14, 0, 0, 0, 0, 0) } // 'q'
212 if s[it.val_off + 6] != 0x20 { return __syscall(93, 15, 0, 0, 0, 0, 0) } // ' '
213
214 // EOI.
215 let r4: i64 = nx_kv_next(it)
216 if r4 != 0 { return __syscall(93, 16, 0, 0, 0, 0, 0) }
217
218 return 0
219}