code wiki / (root) / nx_env_v1.nx

nx_env_v1.nx source

↩ module page · 259 lines · 7866 B

1// env.nx -- parse .env / dotenv-style configuration files. 2// 3// The de facto standard for deployment config (Heroku, Vercel, 4// Docker Compose, direnv, 12-factor apps all use this format). 5// Line-oriented; each non-comment non-blank line is: 6// 7// KEY=value 8// KEY=\"value with spaces\" 9// KEY='single-quoted' 10// KEY= (explicit empty) 11// 12// Comments start with '#'. Blank lines ignored. Unquoted values 13// are trimmed of leading/trailing whitespace; quoted values are 14// taken verbatim between quote marks (no escape handling for 15// simplicity -- callers with \"dangerous\" values use JSON in the 16// .env + parse with json.nx). 17// 18// Keys must match [A-Za-z_][A-Za-z0-9_]* or the line is skipped. 19// 20// Used by: every Nishi service runtime for config, local dev 21// workflows, CI environment injection. 22// 23// Complements ini.nx (which has sections) + query.nx (URL encode) 24// by covering the linear-only case. 25// 26// Invariants: 27// E1 Zero-alloc: offsets into caller's buffer. 28// E2 Malformed lines silently skipped (caller logs if needed). 29// E3 Duplicate keys both emitted; caller-supplied last-wins 30// semantics if desired. 31// E4 CRLF and bare LF line endings both accepted. 32 33// nx_safety_envelope: 34// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 35// sil_target: SIL1 36// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 37// verdict: NOT_YET_EVALUATED 38 39import "nx_syscalls.nx" 40 41const ENV_ERR_OVERFLOW: i64 = -1 42 43struct EnvEntry { 44 key_off: i64, key_len: i64, 45 val_off: i64, val_len: i64, 46} 47 48func env_is_key_start(b: i64) -> i64 { 49 if b >= 0x41 { 50 if b <= 0x5A { return 1 } 51 } 52 if b >= 0x61 { 53 if b <= 0x7A { return 1 } 54 } 55 if b == 0x5F { return 1 } // '_' 56 return 0 57} 58 59func env_is_key_cont(b: i64) -> i64 { 60 if env_is_key_start(b) == 1 { return 1 } 61 if b >= 0x30 { 62 if b <= 0x39 { return 1 } 63 } 64 return 0 65} 66 67func env_is_ws(b: i64) -> i64 { 68 if b == 0x20 { return 1 } 69 if b == 0x09 { return 1 } 70 return 0 71} 72 73// Scan to end of line. Returns offset of LF (or n if no LF). 74func env_eol(buf: *u8, n: i64, off: i64) -> i64 { 75 var i: i64 = off 76 while i < n { 77 if buf[i] == 0x0A { return i } 78 i = i + 1 79 } 80 return n 81} 82 83// Parse a .env file into caller's entry table. 84func env_parse(buf: *u8, n: i64, 85 entries: *EnvEntry, cap: i64) -> i64 { 86 var count: i64 = 0 87 var line_start: i64 = 0 88 89 while line_start < n { 90 let line_end: i64 = env_eol(buf, n, line_start) 91 // Trim CR before LF if CRLF. 92 var eff_end: i64 = line_end 93 if eff_end > line_start { 94 if buf[eff_end - 1] == 0x0D { eff_end = eff_end - 1 } 95 } 96 // Skip leading whitespace. 97 var i: i64 = line_start 98 while i < eff_end { 99 if env_is_ws(buf[i]) == 0 { break } 100 i = i + 1 101 } 102 // Skip blank or comment lines. 103 if i >= eff_end { 104 line_start = line_end + 1 105 continue 106 } 107 if buf[i] == 0x23 { 108 line_start = line_end + 1 109 continue 110 } 111 // Optional \"export \" prefix (bash-style dotenv). 112 if i + 7 <= eff_end { 113 if buf[i] == 0x65 { 114 if buf[i + 1] == 0x78 { 115 if buf[i + 2] == 0x70 { 116 if buf[i + 3] == 0x6F { 117 if buf[i + 4] == 0x72 { 118 if buf[i + 5] == 0x74 { 119 if buf[i + 6] == 0x20 { 120 i = i + 7 121 while i < eff_end { 122 if env_is_ws(buf[i]) == 0 { break } 123 i = i + 1 124 } 125 } 126 } 127 } 128 } 129 } 130 } 131 } 132 } 133 134 // Key must start with [A-Za-z_]. 135 if env_is_key_start(buf[i]) == 0 { 136 line_start = line_end + 1 137 continue 138 } 139 let key_start: i64 = i 140 while i < eff_end { 141 if env_is_key_cont(buf[i]) == 0 { break } 142 i = i + 1 143 } 144 let key_end: i64 = i 145 146 // Expect '='. 147 if i >= eff_end { 148 line_start = line_end + 1 149 continue 150 } 151 if buf[i] != 0x3D { 152 line_start = line_end + 1 153 continue 154 } 155 i = i + 1 156 157 // Value: handle quoted vs unquoted. 158 var val_start: i64 = i 159 var val_end: i64 = eff_end 160 if i < eff_end { 161 if buf[i] == 0x22 { 162 // Double-quoted. 163 val_start = i + 1 164 var k: i64 = val_start 165 while k < eff_end { 166 if buf[k] == 0x22 { break } 167 k = k + 1 168 } 169 val_end = k 170 } else { 171 if buf[i] == 0x27 { 172 // Single-quoted. 173 val_start = i + 1 174 var k: i64 = val_start 175 while k < eff_end { 176 if buf[k] == 0x27 { break } 177 k = k + 1 178 } 179 val_end = k 180 } else { 181 // Unquoted: trim trailing whitespace. 182 var e: i64 = eff_end 183 while e > val_start { 184 if env_is_ws(buf[e - 1]) == 0 { break } 185 e = e - 1 186 } 187 val_end = e 188 } 189 } 190 } 191 192 if count >= cap { return ENV_ERR_OVERFLOW } 193 let ent: *EnvEntry = entries + count * 32 194 ent.key_off = key_start 195 ent.key_len = key_end - key_start 196 ent.val_off = val_start 197 ent.val_len = val_end - val_start 198 count = count + 1 199 200 line_start = line_end + 1 201 } 202 return count 203} 204 205// Find by key. Returns entry index or -1. 206func env_find(buf: *u8, entries: *EnvEntry, count: i64, 207 key: *u8, key_len: i64) -> i64 { 208 var i: i64 = 0 209 while i < count { 210 let e: *EnvEntry = entries + i * 32 211 if e.key_len == key_len { 212 var j: i64 = 0 213 var ok: i64 = 1 214 while j < key_len { 215 if buf[e.key_off + j] != key[j] { 216 ok = 0 217 break 218 } 219 j = j + 1 220 } 221 if ok == 1 { return i } 222 } 223 i = i + 1 224 } 225 return -1 226} 227 228// Compile-only smoke. 229func main() -> i64 { 230 let entries_raw: *u8 = sys_mmap(512) 231 let entries: *EnvEntry = entries_raw as *EnvEntry 232 233 let input: *u8 = "# Nishi deploy\nHOST=nishifamily.com\nPORT=443\nAPI_KEY=\"s e c r e t\"\nexport DB_URL=postgres://localhost/db\n" 234 // Length: 15 + 22 + 9 + 21 + 36 = 103, plus trailing \n 235 let n: i64 = env_parse(input, 104, entries, 16) 236 if n != 4 { return 1 } 237 238 // HOST key. 239 let e0: *EnvEntry = entries 240 if e0.key_len != 4 { return 2 } 241 if e0.val_len != 15 { return 3 } // "nishifamily.com" 242 if input[e0.key_off] != 0x48 { return 4 } // 'H' 243 244 // API_KEY = "s e c r e t" (quoted, verbatim inside quotes) 245 let e2: *EnvEntry = entries + 2 * 32 246 if e2.val_len != 11 { return 5 } 247 248 // Lookup. 249 let idx: i64 = env_find(input, entries, n, "PORT", 4) 250 if idx != 1 { return 6 } 251 let e_port: *EnvEntry = entries + idx * 32 252 if e_port.val_len != 3 { return 7 } 253 254 // export DB_URL= prefix stripped. 255 let idx_db: i64 = env_find(input, entries, n, "DB_URL", 6) 256 if idx_db != 3 { return 8 } 257 258 return 0 259}