nx_static_io.nx source
↩ module page · 253 lines · 9342 B
1// nx_static_io.nx -- sovereign static-file serving over HTTP.
2//
3// Companion to nx_http_io that knows how to:
4// - Map a URL path to a filesystem path (with safety: no .. escape)
5// - Detect content-type from filename suffix
6// - stream the file body to a TCP socket
7//
8// Built for nishi-web: a NishiLang HTTP server that hosts a static
9// site (e.g. andelinwest.com) without nginx / apache / wordpress.
10//
11// genealogy_id: rfc_7230_http_1_1 + nginx_static_serve + caddy_file_server
12// lineage_id: nx_static_serve_sovereign_q10
13
14// nx_safety_envelope:
15// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
16// sil_target: SIL1
17// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
18// verdict: NOT_YET_EVALUATED
19
20// Unified syscalls (per-ISA via @ifdef TARGET_X86_64 macro, commit
21// 164209c9) -- avoids symbol collision with nx_http_io which also
22// imports nx_syscalls.nx.
23import "nx_syscalls.nx"
24
25// Content-type table. Caller writes the type bytes into `out` and
26// returns the byte length. We accept these suffixes:
27// .html / .htm -> text/html; charset=utf-8 (24 bytes)
28// .css -> text/css; charset=utf-8 (23 bytes)
29// .js -> application/javascript (22 bytes)
30// .json -> application/json (16 bytes)
31// .md -> text/markdown (13 bytes)
32// .png -> image/png (9 bytes)
33// .jpg / .jpeg -> image/jpeg (10 bytes)
34// .svg -> image/svg+xml (13 bytes)
35// .ico -> image/x-icon (12 bytes)
36// default -> application/octet-stream (24 bytes)
37
38func _ct_html(out: *u8) -> i64 {
39 out[0]=116;out[1]=101;out[2]=120;out[3]=116;out[4]=47 // text/
40 out[5]=104;out[6]=116;out[7]=109;out[8]=108 // html
41 out[9]=59;out[10]=32 // ; (space)
42 out[11]=99;out[12]=104;out[13]=97;out[14]=114;out[15]=115;out[16]=101;out[17]=116
43 out[18]=61 // =
44 out[19]=117;out[20]=116;out[21]=102;out[22]=45;out[23]=56 // utf-8
45 return 24
46}
47func _ct_css(out: *u8) -> i64 {
48 out[0]=116;out[1]=101;out[2]=120;out[3]=116;out[4]=47 // text/
49 out[5]=99;out[6]=115;out[7]=115 // css
50 out[8]=59;out[9]=32
51 out[10]=99;out[11]=104;out[12]=97;out[13]=114;out[14]=115;out[15]=101;out[16]=116
52 out[17]=61
53 out[18]=117;out[19]=116;out[20]=102;out[21]=45;out[22]=56
54 return 23
55}
56func _ct_js(out: *u8) -> i64 {
57 out[0]=97;out[1]=112;out[2]=112;out[3]=108;out[4]=105;out[5]=99;out[6]=97;out[7]=116;out[8]=105;out[9]=111;out[10]=110
58 out[11]=47 // /
59 out[12]=106;out[13]=97;out[14]=118;out[15]=97;out[16]=115;out[17]=99;out[18]=114;out[19]=105;out[20]=112;out[21]=116
60 return 22
61}
62func _ct_json(out: *u8) -> i64 {
63 out[0]=97;out[1]=112;out[2]=112;out[3]=108;out[4]=105;out[5]=99;out[6]=97;out[7]=116;out[8]=105;out[9]=111;out[10]=110
64 out[11]=47
65 out[12]=106;out[13]=115;out[14]=111;out[15]=110
66 return 16
67}
68func _ct_md(out: *u8) -> i64 {
69 out[0]=116;out[1]=101;out[2]=120;out[3]=116;out[4]=47 // text/
70 out[5]=109;out[6]=97;out[7]=114;out[8]=107;out[9]=100;out[10]=111;out[11]=119;out[12]=110
71 return 13
72}
73func _ct_png(out: *u8) -> i64 {
74 out[0]=105;out[1]=109;out[2]=97;out[3]=103;out[4]=101;out[5]=47;out[6]=112;out[7]=110;out[8]=103
75 return 9
76}
77func _ct_jpeg(out: *u8) -> i64 {
78 out[0]=105;out[1]=109;out[2]=97;out[3]=103;out[4]=101;out[5]=47;out[6]=106;out[7]=112;out[8]=101;out[9]=103
79 return 10
80}
81func _ct_svg(out: *u8) -> i64 {
82 out[0]=105;out[1]=109;out[2]=97;out[3]=103;out[4]=101;out[5]=47;out[6]=115;out[7]=118;out[8]=103;out[9]=43;out[10]=120;out[11]=109;out[12]=108
83 return 13
84}
85func _ct_octet(out: *u8) -> i64 {
86 out[0]=97;out[1]=112;out[2]=112;out[3]=108;out[4]=105;out[5]=99;out[6]=97;out[7]=116;out[8]=105;out[9]=111;out[10]=110
87 out[11]=47
88 out[12]=111;out[13]=99;out[14]=116;out[15]=101;out[16]=116;out[17]=45;out[18]=115;out[19]=116;out[20]=114;out[21]=101;out[22]=97;out[23]=109
89 return 24
90}
91
92// Pick a content-type for a path by walking the filename suffix.
93// Writes the type bytes into `out`, returns byte length.
94func nx_static_content_type(path: *u8, path_len: i64, out: *u8) -> i64 {
95 // Find the last '.' in the path (within the last component).
96 var dot: i64 = -1
97 var i: i64 = 0
98 while i < path_len {
99 if path[i] == 46 { dot = i } // '.'
100 i = i + 1
101 }
102 if dot < 0 { return _ct_octet(out) }
103
104 let suf_len: i64 = path_len - dot - 1
105 let s0: i64 = path[dot + 1]
106 // .html / .htm
107 if s0 == 104 {
108 if suf_len >= 3 {
109 if path[dot+2] == 116 {
110 if path[dot+3] == 109 {
111 return _ct_html(out)
112 }
113 }
114 }
115 }
116 // .css
117 if s0 == 99 {
118 if suf_len == 3 {
119 if path[dot+2] == 115 {
120 if path[dot+3] == 115 { return _ct_css(out) }
121 }
122 }
123 }
124 // .js / .json
125 if s0 == 106 {
126 if suf_len == 2 {
127 if path[dot+2] == 115 { return _ct_js(out) }
128 }
129 if suf_len == 4 {
130 if path[dot+2] == 115 {
131 if path[dot+3] == 111 {
132 if path[dot+4] == 110 { return _ct_json(out) }
133 }
134 }
135 }
136 }
137 // .md
138 if s0 == 109 {
139 if suf_len == 2 {
140 if path[dot+2] == 100 { return _ct_md(out) }
141 }
142 }
143 // .png
144 if s0 == 112 {
145 if suf_len == 3 {
146 if path[dot+2] == 110 {
147 if path[dot+3] == 103 { return _ct_png(out) }
148 }
149 }
150 }
151 // .jpg / .jpeg
152 if s0 == 106 {
153 if suf_len == 3 {
154 if path[dot+2] == 112 {
155 if path[dot+3] == 103 { return _ct_jpeg(out) }
156 }
157 }
158 }
159 // .svg
160 if s0 == 115 {
161 if suf_len == 3 {
162 if path[dot+2] == 118 {
163 if path[dot+3] == 103 { return _ct_svg(out) }
164 }
165 }
166 }
167 return _ct_octet(out)
168}
169
170// Read a file at `path` into a fresh buffer. Returns the buffer
171// pointer + writes byte count into *out_len. Cap-bounded; longer
172// files truncate. Returns NULL ptr if open failed.
173func nx_static_load_file(path: *u8, cap: i64, out_len: *i64) -> *u8 {
174 *out_len = 0
175 let fd: i64 = sys_openat_rd(path)
176 if fd < 0 { return 0 as *u8 }
177 let buf: *u8 = sys_mmap(cap + 16)
178 var off: i64 = 0
179 var keep_reading: i64 = 1
180 while keep_reading == 1 {
181 if off >= cap { keep_reading = 0 }
182 else {
183 let r: i64 = sys_read(fd, (buf as i64 + off) as *u8, cap - off)
184 if r <= 0 { keep_reading = 0 }
185 else { off = off + r }
186 }
187 }
188 sys_close(fd)
189 *out_len = off
190 return buf
191}
192
193// Detect dangerous path traversal. Returns 1 if path is safe (no ..
194// sequence, no double slashes, etc), 0 otherwise. Belt-and-braces;
195// callers should also constrain to a known DOC_ROOT.
196func nx_static_path_is_safe(path: *u8, path_len: i64) -> i64 {
197 if path_len <= 0 { return 0 }
198 if path[0] != 47 { return 0 } // must start with /
199 var i: i64 = 0
200 while i + 2 < path_len {
201 // Reject "/.." prefix
202 if path[i] == 47 {
203 if path[i+1] == 46 {
204 if path[i+2] == 46 { return 0 }
205 }
206 }
207 i = i + 1
208 }
209 return 1
210}
211
212// Build the filesystem path: DOC_ROOT + url_path. url_path is the
213// thing after the host, starts with '/'. If url_path ENDS in '/' --
214// the directory-index case -- we append "index.html" so /foo/ serves
215// /foo/index.html and / serves /index.html.
216//
217// out must point at >= DOC_ROOT_len + url_path_len + 16 bytes.
218// Returns the byte length written (NOT including a trailing NUL,
219// which IS written one past the returned length so the buffer is
220// usable as a POSIX C-string).
221func nx_static_join_path(
222 doc_root: *u8, doc_root_len: i64,
223 url_path: *u8, url_path_len: i64,
224 out: *u8
225) -> i64 {
226 var o: i64 = 0
227 var k: i64 = 0
228 while k < doc_root_len { out[o] = doc_root[k]; o = o + 1; k = k + 1 }
229 // url_path always starts with '/'. Append it.
230 var u: i64 = 0
231 while u < url_path_len { out[o] = url_path[u]; o = o + 1; u = u + 1 }
232 // Directory-index: if the URL ended in '/' (this includes the
233 // single-byte path "/", which is path_len==1).
234 var append_index: i64 = 0
235 if url_path_len == 1 { append_index = 1 }
236 if url_path_len > 1 {
237 if url_path[url_path_len - 1] == 47 { append_index = 1 }
238 }
239 if append_index == 1 {
240 out[o] = 105; o = o + 1 // i
241 out[o] = 110; o = o + 1 // n
242 out[o] = 100; o = o + 1 // d
243 out[o] = 101; o = o + 1 // e
244 out[o] = 120; o = o + 1 // x
245 out[o] = 46; o = o + 1 // .
246 out[o] = 104; o = o + 1 // h
247 out[o] = 116; o = o + 1 // t
248 out[o] = 109; o = o + 1 // m
249 out[o] = 108; o = o + 1 // l
250 }
251 out[o] = 0 // NUL terminator (caller-friendly)
252 return o
253}