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}