ini.nx source
↩ module page · 226 lines · 7218 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
30import "syscalls.nx"
31
32const INI_ERR_OVERFLOW: i64 = -1
33
34struct IniEntry {
35 section_off: i64, section_len: i64,
36 key_off: i64, key_len: i64,
37 val_off: i64, val_len: i64,
38}
39
40// Is byte ASCII whitespace?
41func ini_is_ws(b: i64) -> i64 {
42 if b == 0x20 { return 1 }
43 if b == 0x09 { return 1 }
44 if b == 0x0D { return 1 }
45 return 0
46}
47
48// Scan to end of line (LF or end of buffer). Returns position of
49// LF (or n if no LF before EOF).
50func ini_eol(buf: *u8, n: i64, off: i64) -> i64 {
51 var i: i64 = off
52 while i < n {
53 if buf[i] == 0x0A { return i }
54 i = i + 1
55 }
56 return n
57}
58
59// Trim trailing whitespace from [start, end). Returns new end.
60func ini_rtrim(buf: *u8, start: i64, end: i64) -> i64 {
61 var e: i64 = end
62 while e > start {
63 if ini_is_ws(buf[e - 1]) == 0 { return e }
64 e = e - 1
65 }
66 return e
67}
68
69// Trim leading whitespace starting at off, bounded by end.
70func ini_ltrim(buf: *u8, off: i64, end: i64) -> i64 {
71 var i: i64 = off
72 while i < end {
73 if ini_is_ws(buf[i]) == 0 { return i }
74 i = i + 1
75 }
76 return end
77}
78
79// Parse an INI file into caller's entry table. Returns number
80// of entries or INI_ERR_OVERFLOW if cap exhausted.
81func ini_parse(buf: *u8, n: i64,
82 entries: *IniEntry, cap: i64) -> i64 {
83 var count: i64 = 0
84 var line_start: i64 = 0
85 // Current section offset/length (inherited across lines).
86 var sec_off: i64 = 0
87 var sec_len: i64 = 0
88
89 while line_start < n {
90 let line_end: i64 = ini_eol(buf, n, line_start)
91 // Trim leading whitespace.
92 let c_start: i64 = ini_ltrim(buf, line_start, line_end)
93 let c_end: i64 = ini_rtrim(buf, c_start, line_end)
94
95 if c_start == c_end {
96 // Blank line -- advance past LF.
97 line_start = line_end + 1
98 continue
99 }
100 let first: i64 = buf[c_start]
101 if first == 0x3B {
102 // ';' comment.
103 line_start = line_end + 1
104 continue
105 }
106 if first == 0x23 {
107 // '#' comment.
108 line_start = line_end + 1
109 continue
110 }
111 if first == 0x5B {
112 // '[' section header. Find the matching ']'.
113 var j: i64 = c_start + 1
114 while j < c_end {
115 if buf[j] == 0x5D { break }
116 j = j + 1
117 }
118 sec_off = c_start + 1
119 sec_len = j - (c_start + 1)
120 line_start = line_end + 1
121 continue
122 }
123
124 // Otherwise: key = value
125 var eq_pos: i64 = -1
126 var k: i64 = c_start
127 while k < c_end {
128 if buf[k] == 0x3D {
129 eq_pos = k
130 break
131 }
132 if buf[k] == 0x3A {
133 eq_pos = k
134 break
135 }
136 k = k + 1
137 }
138 if eq_pos < 0 {
139 line_start = line_end + 1
140 continue
141 }
142
143 if count >= cap { return INI_ERR_OVERFLOW }
144 let e: *IniEntry = entries + count * 48
145 let key_end: i64 = ini_rtrim(buf, c_start, eq_pos)
146 let val_start: i64 = ini_ltrim(buf, eq_pos + 1, c_end)
147
148 e.section_off = sec_off
149 e.section_len = sec_len
150 e.key_off = c_start
151 e.key_len = key_end - c_start
152 e.val_off = val_start
153 e.val_len = c_end - val_start
154 count = count + 1
155
156 line_start = line_end + 1
157 }
158 return count
159}
160
161// Find first entry with matching section + key. Byte-exact
162// comparison; empty section matches entries before any [section].
163func ini_find(buf: *u8, entries: *IniEntry, count: i64,
164 section: *u8, section_len: i64,
165 key: *u8, key_len: i64) -> i64 {
166 var i: i64 = 0
167 while i < count {
168 let e: *IniEntry = entries + i * 48
169 if e.section_len == section_len {
170 if e.key_len == key_len {
171 var j: i64 = 0
172 var ok: i64 = 1
173 while j < section_len {
174 if buf[e.section_off + j] != section[j] {
175 ok = 0
176 break
177 }
178 j = j + 1
179 }
180 if ok == 1 {
181 j = 0
182 while j < key_len {
183 if buf[e.key_off + j] != key[j] {
184 ok = 0
185 break
186 }
187 j = j + 1
188 }
189 }
190 if ok == 1 { return i }
191 }
192 }
193 i = i + 1
194 }
195 return -1
196}
197
198// Compile-only smoke.
199func main() -> i64 {
200 let entries_raw: *u8 = sys_mmap(1024)
201 let entries: *IniEntry = entries_raw as *IniEntry
202
203 let input: *u8 = "[db]\nhost = 10.0.0.1\nport = 5432\n; comment\n[log]\nlevel=info\n"
204 // length: "[db]"=4 + LF + "host = 10.0.0.1"=15 + LF +
205 // "port = 5432"=11 + LF + "; comment"=9 + LF +
206 // "[log]"=5 + LF + "level=info"=10 + LF = 58.
207 let n: i64 = ini_parse(input, 58, entries, 16)
208 if n != 3 { return 1 }
209
210 // entries[0] = section "db" key "host" val "10.0.0.1"
211 let e0: *IniEntry = entries
212 if e0.section_len != 2 { return 2 }
213 if e0.key_len != 4 { return 3 }
214 if e0.val_len != 8 { return 4 }
215 if input[e0.section_off] != 0x64 { return 5 } // 'd'
216 if input[e0.key_off] != 0x68 { return 6 } // 'h'
217 if input[e0.val_off] != 0x31 { return 7 } // '1'
218
219 // Lookup "[db]" "port".
220 let idx: i64 = ini_find(input, entries, n, "db", 2, "port", 4)
221 if idx != 1 { return 8 }
222
223 // Lookup miss.
224 if ini_find(input, entries, n, "db", 2, "nope", 4) != -1 { return 9 }
225 return 0
226}