nx_http_resp.nx source
↩ module page · 204 lines · 7294 B
1// http_resp.nx -- HTTP/1.1 response generator.
2//
3// Companion to http.nx (request parser). Assembles a valid HTTP
4// response into a caller-mmapped output buffer: status line,
5// headers, body. No internal allocation beyond tiny scratch.
6//
7// Response shape (RFC 7230 §3):
8// HTTP-version SP status-code SP reason-phrase CRLF
9// *header-field CRLF
10// CRLF
11// [ message-body ]
12//
13// Implementation notes:
14// - Content-Length automatically emitted from body_len. Chunked
15// transfer encoding is a future extension.
16// - Connection: close emitted by default (no keep-alive yet).
17// - Server: nishi emitted for sovereignty attribution.
18// - Date header NOT emitted -- caller can pre-compute via
19// timefmt.nx if clock sync matters for their use case.
20//
21// Invariants:
22// HR1 Output buffer bounds-checked at every append; overrun
23// returns a negative error rather than silent truncation.
24// HR2 No internal state; each response is built from scratch.
25// Thread-safe for the caller's own isolation concerns.
26// HR3 Status codes + reason phrases kept in a fixed table; no
27// arbitrary int -> string formatting on the response path.
28// HR4 Content-Length header reflects body_len exactly; no
29// mismatched framing that would confuse HTTP/1.1 clients.
30
31// nx_safety_envelope:
32// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
33// sil_target: SIL1
34// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
35// verdict: NOT_YET_EVALUATED
36
37import "nx_syscalls.nx"
38
39const HRR_ERR_OVERFLOW: i64 = -1
40
41// Common HTTP status codes + reason phrases. Callers pass the
42// numeric status; we map to a phrase string. Missing codes
43// fall through to "Unknown Status" which is technically legal
44// HTTP (RFC 7230 §3.1.2: reason phrase is "essentially ignored
45// by the recipient").
46
47func http_reason(code: i64) -> *u8 {
48 if code == 200 { return "OK" }
49 if code == 201 { return "Created" }
50 if code == 204 { return "No Content" }
51 if code == 301 { return "Moved Permanently" }
52 if code == 302 { return "Found" }
53 if code == 304 { return "Not Modified" }
54 if code == 400 { return "Bad Request" }
55 if code == 401 { return "Unauthorized" }
56 if code == 403 { return "Forbidden" }
57 if code == 404 { return "Not Found" }
58 if code == 405 { return "Method Not Allowed" }
59 if code == 413 { return "Payload Too Large" }
60 if code == 429 { return "Too Many Requests" }
61 if code == 500 { return "Internal Server Error" }
62 if code == 501 { return "Not Implemented" }
63 if code == 503 { return "Service Unavailable" }
64 return "Unknown Status"
65}
66
67// Append a null-terminated cstring to out at *pos. Bounds-check
68// against cap. Returns 0 on success, HRR_ERR_OVERFLOW on
69// overflow.
70func http_append_cstr(out: *u8, pos: *i64, cap: i64, s: *u8) -> i64 {
71 let p0: i64 = *pos
72 var i: i64 = 0
73 while s[i] != 0 {
74 if p0 + i >= cap { return HRR_ERR_OVERFLOW }
75 out[p0 + i] = s[i]
76 i = i + 1
77 }
78 *pos = p0 + i
79 return 0
80}
81
82// Append n bytes from src.
83func http_append_bytes(out: *u8, pos: *i64, cap: i64,
84 src: *u8, n: i64) -> i64 {
85 let p0: i64 = *pos
86 if p0 + n > cap { return HRR_ERR_OVERFLOW }
87 var i: i64 = 0
88 while i < n { out[p0 + i] = src[i]; i = i + 1 }
89 *pos = p0 + n
90 return 0
91}
92
93// Append a decimal-formatted i64 (non-negative).
94func http_append_dec(out: *u8, pos: *i64, cap: i64, n: i64) -> i64 {
95 let p0: i64 = *pos
96 if n == 0 {
97 if p0 >= cap { return HRR_ERR_OVERFLOW }
98 out[p0] = 0x30
99 *pos = p0 + 1
100 return 0
101 }
102 let scratch: *u8 = sys_mmap(32)
103 var v: i64 = n
104 var k: i64 = 0
105 while v > 0 {
106 scratch[k] = 0x30 + (v % 10)
107 v = v / 10
108 k = k + 1
109 }
110 if p0 + k > cap { return HRR_ERR_OVERFLOW }
111 var i: i64 = 0
112 while i < k {
113 out[p0 + i] = scratch[k - 1 - i]
114 i = i + 1
115 }
116 *pos = p0 + k
117 return 0
118}
119
120// Emit status line. "HTTP/1.1 <code> <reason>\r\n".
121func http_emit_status(out: *u8, pos: *i64, cap: i64, code: i64) -> i64 {
122 let rc0: i64 = http_append_cstr(out, pos, cap, "HTTP/1.1 ")
123 if rc0 < 0 { return rc0 }
124 let rc1: i64 = http_append_dec(out, pos, cap, code)
125 if rc1 < 0 { return rc1 }
126 let rc2: i64 = http_append_cstr(out, pos, cap, " ")
127 if rc2 < 0 { return rc2 }
128 let rc3: i64 = http_append_cstr(out, pos, cap, http_reason(code))
129 if rc3 < 0 { return rc3 }
130 let rc4: i64 = http_append_cstr(out, pos, cap, "\r\n")
131 return rc4
132}
133
134// Emit "Name: value\r\n". `value` is a null-terminated cstring.
135func http_emit_header(out: *u8, pos: *i64, cap: i64,
136 name: *u8, value: *u8) -> i64 {
137 let rc0: i64 = http_append_cstr(out, pos, cap, name)
138 if rc0 < 0 { return rc0 }
139 let rc1: i64 = http_append_cstr(out, pos, cap, ": ")
140 if rc1 < 0 { return rc1 }
141 let rc2: i64 = http_append_cstr(out, pos, cap, value)
142 if rc2 < 0 { return rc2 }
143 return http_append_cstr(out, pos, cap, "\r\n")
144}
145
146// Emit "Content-Length: N\r\n" with numeric value.
147func http_emit_content_length(out: *u8, pos: *i64, cap: i64,
148 n: i64) -> i64 {
149 let rc0: i64 = http_append_cstr(out, pos, cap, "Content-Length: ")
150 if rc0 < 0 { return rc0 }
151 let rc1: i64 = http_append_dec(out, pos, cap, n)
152 if rc1 < 0 { return rc1 }
153 return http_append_cstr(out, pos, cap, "\r\n")
154}
155
156// Build a complete response: status + minimal headers + body.
157// Emitted headers:
158// Server: nishi
159// Connection: close
160// Content-Type: <content_type> (caller-supplied cstring)
161// Content-Length: <body_len>
162// Returns total bytes written or HRR_ERR_OVERFLOW.
163func http_build_response(out: *u8, cap: i64,
164 status_code: i64,
165 content_type: *u8,
166 body: *u8, body_len: i64) -> i64 {
167 let pos_raw: *u8 = sys_mmap(16)
168 let pos: *i64 = pos_raw as *i64
169 *pos = 0
170
171 let r1: i64 = http_emit_status(out, pos, cap, status_code)
172 if r1 < 0 { return r1 }
173 let r2: i64 = http_emit_header(out, pos, cap, "Server", "nishi")
174 if r2 < 0 { return r2 }
175 let r3: i64 = http_emit_header(out, pos, cap, "Connection", "close")
176 if r3 < 0 { return r3 }
177 let r4: i64 = http_emit_header(out, pos, cap, "Content-Type",
178 content_type)
179 if r4 < 0 { return r4 }
180 let r5: i64 = http_emit_content_length(out, pos, cap, body_len)
181 if r5 < 0 { return r5 }
182 // Blank line terminates headers.
183 let r6: i64 = http_append_cstr(out, pos, cap, "\r\n")
184 if r6 < 0 { return r6 }
185 // Body.
186 if body_len > 0 {
187 let r7: i64 = http_append_bytes(out, pos, cap, body, body_len)
188 if r7 < 0 { return r7 }
189 }
190 return *pos
191}
192
193// Compile-only smoke.
194func main() -> i64 {
195 let buf: *u8 = sys_mmap(512)
196 let body: *u8 = "Hello, Nishi!"
197 let written: i64 = http_build_response(buf, 512, 200,
198 "text/plain", body, 13)
199 if written <= 0 { return 1 }
200 // First 9 bytes should be "HTTP/1.1 ".
201 if buf[0] != 0x48 { return 2 } // 'H'
202 if buf[5] != 0x2F { return 3 } // '/'
203 return 0
204}