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}