code wiki / (root) / ini.nx

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}