nx_ini.nx source
↩ module page · 233 lines · 7289 B
1// ini.nx -- parse INI-style configuration files.
2//
3// Classic [section] / key = value format. Simpler than TOML,
4// used by 30+ years of Windows config + a lot of C daemons
5// (php.ini, systemd unit files, mumble / xmms / qt config).
6//
7// Format:
8// [section_name] <- section header, any leading/trailing ws ok
9// key = value <- key/value, '=' or ':' separator
10// ; comment <- semicolon
11// # comment <- hash
12// (blank line)
13//
14// We return a flat array of (section_off, section_len, key_off,
15// key_len, val_off, val_len) tuples. The section fields are
16// zero if a key appears before any [section] header.
17//
18// Like query.nx, this is zero-alloc -- we emit offsets into the
19// caller's buffer and never copy. Whitespace around keys/values
20// is trimmed (not stored). Inline comments are NOT supported
21// (value runs to end-of-line).
22//
23// Invariants:
24// I1 Empty input -> zero entries.
25// I2 Keys with no '=' or ':' are silently dropped (warning
26// would be logged in a production parser; we stay quiet).
27// I3 Sections persist down until next [section] header.
28// I4 Duplicate keys: all are emitted; caller picks which wins.
29
30// nx_safety_envelope:
31// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
32// sil_target: SIL1
33// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
34// verdict: NOT_YET_EVALUATED
35
36import "nx_syscalls.nx"
37const INI_MAGIC_1024: i64 = 1024
38
39const INI_ERR_OVERFLOW: i64 = -1
40
41struct IniEntry {
42 section_off: i64, section_len: i64,
43 key_off: i64, key_len: i64,
44 val_off: i64, val_len: i64,
45}
46
47// Is byte ASCII whitespace?
48func ini_is_ws(b: i64) -> i64 {
49 if b == 0x20 { return 1 }
50 if b == 0x09 { return 1 }
51 if b == 0x0D { return 1 }
52 return 0
53}
54
55// Scan to end of line (LF or end of buffer). Returns position of
56// LF (or n if no LF before EOF).
57func ini_eol(buf: *u8, n: i64, off: i64) -> i64 {
58 var i: i64 = off
59 while i < n {
60 if buf[i] == 0x0A { return i }
61 i = i + 1
62 }
63 return n
64}
65
66// Trim trailing whitespace from [start, end). Returns new end.
67func ini_rtrim(buf: *u8, start: i64, end: i64) -> i64 {
68 var e: i64 = end
69 while e > start {
70 if ini_is_ws(buf[e - 1]) == 0 { return e }
71 e = e - 1
72 }
73 return e
74}
75
76// Trim leading whitespace starting at off, bounded by end.
77func ini_ltrim(buf: *u8, off: i64, end: i64) -> i64 {
78 var i: i64 = off
79 while i < end {
80 if ini_is_ws(buf[i]) == 0 { return i }
81 i = i + 1
82 }
83 return end
84}
85
86// Parse an INI file into caller's entry table. Returns number
87// of entries or INI_ERR_OVERFLOW if cap exhausted.
88func ini_parse(buf: *u8, n: i64,
89 entries: *IniEntry, cap: i64) -> i64 {
90 var count: i64 = 0
91 var line_start: i64 = 0
92 // Current section offset/length (inherited across lines).
93 var sec_off: i64 = 0
94 var sec_len: i64 = 0
95
96 while line_start < n {
97 let line_end: i64 = ini_eol(buf, n, line_start)
98 // Trim leading whitespace.
99 let c_start: i64 = ini_ltrim(buf, line_start, line_end)
100 let c_end: i64 = ini_rtrim(buf, c_start, line_end)
101
102 if c_start == c_end {
103 // Blank line -- advance past LF.
104 line_start = line_end + 1
105 continue
106 }
107 let first: i64 = buf[c_start]
108 if first == 0x3B {
109 // ';' comment.
110 line_start = line_end + 1
111 continue
112 }
113 if first == 0x23 {
114 // '#' comment.
115 line_start = line_end + 1
116 continue
117 }
118 if first == 0x5B {
119 // '[' section header. Find the matching ']'.
120 var j: i64 = c_start + 1
121 while j < c_end {
122 if buf[j] == 0x5D { break }
123 j = j + 1
124 }
125 sec_off = c_start + 1
126 sec_len = j - (c_start + 1)
127 line_start = line_end + 1
128 continue
129 }
130
131 // Otherwise: key = value
132 var eq_pos: i64 = -1
133 var k: i64 = c_start
134 while k < c_end {
135 if buf[k] == 0x3D {
136 eq_pos = k
137 break
138 }
139 if buf[k] == 0x3A {
140 eq_pos = k
141 break
142 }
143 k = k + 1
144 }
145 if eq_pos < 0 {
146 line_start = line_end + 1
147 continue
148 }
149
150 if count >= cap { return INI_ERR_OVERFLOW }
151 let e: *IniEntry = entries + count * 48
152 let key_end: i64 = ini_rtrim(buf, c_start, eq_pos)
153 let val_start: i64 = ini_ltrim(buf, eq_pos + 1, c_end)
154
155 e.section_off = sec_off
156 e.section_len = sec_len
157 e.key_off = c_start
158 e.key_len = key_end - c_start
159 e.val_off = val_start
160 e.val_len = c_end - val_start
161 count = count + 1
162
163 line_start = line_end + 1
164 }
165 return count
166}
167
168// Find first entry with matching section + key. Byte-exact
169// comparison; empty section matches entries before any [section].
170func ini_find(buf: *u8, entries: *IniEntry, count: i64,
171 section: *u8, section_len: i64,
172 key: *u8, key_len: i64) -> i64 {
173 var i: i64 = 0
174 while i < count {
175 let e: *IniEntry = entries + i * 48
176 if e.section_len == section_len {
177 if e.key_len == key_len {
178 var j: i64 = 0
179 var ok: i64 = 1
180 while j < section_len {
181 if buf[e.section_off + j] != section[j] {
182 ok = 0
183 break
184 }
185 j = j + 1
186 }
187 if ok == 1 {
188 j = 0
189 while j < key_len {
190 if buf[e.key_off + j] != key[j] {
191 ok = 0
192 break
193 }
194 j = j + 1
195 }
196 }
197 if ok == 1 { return i }
198 }
199 }
200 i = i + 1
201 }
202 return -1
203}
204
205// Compile-only smoke.
206func main() -> i64 {
207 let entries_raw: *u8 = sys_mmap(INI_MAGIC_1024)
208 let entries: *IniEntry = entries_raw as *IniEntry
209
210 let input: *u8 = "[db]\nhost = 10.0.0.1\nport = 5432\n; comment\n[log]\nlevel=info\n"
211 // length: "[db]"=4 + LF + "host = 10.0.0.1"=15 + LF +
212 // "port = 5432"=11 + LF + "; comment"=9 + LF +
213 // "[log]"=5 + LF + "level=info"=10 + LF = 58.
214 let n: i64 = ini_parse(input, 58, entries, 16)
215 if n != 3 { return 1 }
216
217 // entries[0] = section "db" key "host" val "10.0.0.1"
218 let e0: *IniEntry = entries
219 if e0.section_len != 2 { return 2 }
220 if e0.key_len != 4 { return 3 }
221 if e0.val_len != 8 { return 4 }
222 if input[e0.section_off] != 0x64 { return 5 } // 'd'
223 if input[e0.key_off] != 0x68 { return 6 } // 'h'
224 if input[e0.val_off] != 0x31 { return 7 } // '1'
225
226 // Lookup "[db]" "port".
227 let idx: i64 = ini_find(input, entries, n, "db", 2, "port", 4)
228 if idx != 1 { return 8 }
229
230 // Lookup miss.
231 if ini_find(input, entries, n, "db", 2, "nope", 4) != -1 { return 9 }
232 return 0
233}