code wiki / (root) / env.nx

env.nx source

↩ module page · 253 lines · 7865 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 33import "syscalls.nx" 34 35const ENV_ERR_OVERFLOW: i64 = -1 36 37struct EnvEntry { 38 key_off: i64, key_len: i64, 39 val_off: i64, val_len: i64, 40} 41 42func env_is_key_start(b: i64) -> i64 { 43 if b >= 0x41 { 44 if b <= 0x5A { return 1 } 45 } 46 if b >= 0x61 { 47 if b <= 0x7A { return 1 } 48 } 49 if b == 0x5F { return 1 } // '_' 50 return 0 51} 52 53func env_is_key_cont(b: i64) -> i64 { 54 if env_is_key_start(b) == 1 { return 1 } 55 if b >= 0x30 { 56 if b <= 0x39 { return 1 } 57 } 58 return 0 59} 60 61func env_is_ws(b: i64) -> i64 { 62 if b == 0x20 { return 1 } 63 if b == 0x09 { return 1 } 64 return 0 65} 66 67// Scan to end of line. Returns offset of LF (or n if no LF). 68func env_eol(buf: *u8, n: i64, off: i64) -> i64 { 69 var i: i64 = off 70 while i < n { 71 if buf[i] == 0x0A { return i } 72 i = i + 1 73 } 74 return n 75} 76 77// Parse a .env file into caller's entry table. 78func env_parse(buf: *u8, n: i64, 79 entries: *EnvEntry, cap: i64) -> i64 { 80 var count: i64 = 0 81 var line_start: i64 = 0 82 83 while line_start < n { 84 let line_end: i64 = env_eol(buf, n, line_start) 85 // Trim CR before LF if CRLF. 86 var eff_end: i64 = line_end 87 if eff_end > line_start { 88 if buf[eff_end - 1] == 0x0D { eff_end = eff_end - 1 } 89 } 90 // Skip leading whitespace. 91 var i: i64 = line_start 92 while i < eff_end { 93 if env_is_ws(buf[i]) == 0 { break } 94 i = i + 1 95 } 96 // Skip blank or comment lines. 97 if i >= eff_end { 98 line_start = line_end + 1 99 continue 100 } 101 if buf[i] == 0x23 { 102 line_start = line_end + 1 103 continue 104 } 105 // Optional \"export \" prefix (bash-style dotenv). 106 if i + 7 <= eff_end { 107 if buf[i] == 0x65 { 108 if buf[i + 1] == 0x78 { 109 if buf[i + 2] == 0x70 { 110 if buf[i + 3] == 0x6F { 111 if buf[i + 4] == 0x72 { 112 if buf[i + 5] == 0x74 { 113 if buf[i + 6] == 0x20 { 114 i = i + 7 115 while i < eff_end { 116 if env_is_ws(buf[i]) == 0 { break } 117 i = i + 1 118 } 119 } 120 } 121 } 122 } 123 } 124 } 125 } 126 } 127 128 // Key must start with [A-Za-z_]. 129 if env_is_key_start(buf[i]) == 0 { 130 line_start = line_end + 1 131 continue 132 } 133 let key_start: i64 = i 134 while i < eff_end { 135 if env_is_key_cont(buf[i]) == 0 { break } 136 i = i + 1 137 } 138 let key_end: i64 = i 139 140 // Expect '='. 141 if i >= eff_end { 142 line_start = line_end + 1 143 continue 144 } 145 if buf[i] != 0x3D { 146 line_start = line_end + 1 147 continue 148 } 149 i = i + 1 150 151 // Value: handle quoted vs unquoted. 152 var val_start: i64 = i 153 var val_end: i64 = eff_end 154 if i < eff_end { 155 if buf[i] == 0x22 { 156 // Double-quoted. 157 val_start = i + 1 158 var k: i64 = val_start 159 while k < eff_end { 160 if buf[k] == 0x22 { break } 161 k = k + 1 162 } 163 val_end = k 164 } else { 165 if buf[i] == 0x27 { 166 // Single-quoted. 167 val_start = i + 1 168 var k: i64 = val_start 169 while k < eff_end { 170 if buf[k] == 0x27 { break } 171 k = k + 1 172 } 173 val_end = k 174 } else { 175 // Unquoted: trim trailing whitespace. 176 var e: i64 = eff_end 177 while e > val_start { 178 if env_is_ws(buf[e - 1]) == 0 { break } 179 e = e - 1 180 } 181 val_end = e 182 } 183 } 184 } 185 186 if count >= cap { return ENV_ERR_OVERFLOW } 187 let ent: *EnvEntry = entries + count * 32 188 ent.key_off = key_start 189 ent.key_len = key_end - key_start 190 ent.val_off = val_start 191 ent.val_len = val_end - val_start 192 count = count + 1 193 194 line_start = line_end + 1 195 } 196 return count 197} 198 199// Find by key. Returns entry index or -1. 200func env_find(buf: *u8, entries: *EnvEntry, count: i64, 201 key: *u8, key_len: i64) -> i64 { 202 var i: i64 = 0 203 while i < count { 204 let e: *EnvEntry = entries + i * 32 205 if e.key_len == key_len { 206 var j: i64 = 0 207 var ok: i64 = 1 208 while j < key_len { 209 if buf[e.key_off + j] != key[j] { 210 ok = 0 211 break 212 } 213 j = j + 1 214 } 215 if ok == 1 { return i } 216 } 217 i = i + 1 218 } 219 return -1 220} 221 222// Compile-only smoke. 223func main() -> i64 { 224 let entries_raw: *u8 = sys_mmap(512) 225 let entries: *EnvEntry = entries_raw as *EnvEntry 226 227 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" 228 // Length: 15 + 22 + 9 + 21 + 36 = 103, plus trailing \n 229 let n: i64 = env_parse(input, 104, entries, 16) 230 if n != 4 { return 1 } 231 232 // HOST key. 233 let e0: *EnvEntry = entries 234 if e0.key_len != 4 { return 2 } 235 if e0.val_len != 15 { return 3 } // "nishifamily.com" 236 if input[e0.key_off] != 0x48 { return 4 } // 'H' 237 238 // API_KEY = "s e c r e t" (quoted, verbatim inside quotes) 239 let e2: *EnvEntry = entries + 2 * 32 240 if e2.val_len != 11 { return 5 } 241 242 // Lookup. 243 let idx: i64 = env_find(input, entries, n, "PORT", 4) 244 if idx != 1 { return 6 } 245 let e_port: *EnvEntry = entries + idx * 32 246 if e_port.val_len != 3 { return 7 } 247 248 // export DB_URL= prefix stripped. 249 let idx_db: i64 = env_find(input, entries, n, "DB_URL", 6) 250 if idx_db != 3 { return 8 } 251 252 return 0 253}