nx_http_form.nx source
↩ module page · 261 lines · 9478 B
1// nx_http_form.nx -- application/x-www-form-urlencoded body parser.
2//
3// HTTP form submission per WHATWG URL §5 + RFC 1866 §8.2.1:
4//
5// key1=value1&key2=value2&key3=value3
6//
7// Where:
8// - `+` decodes to space
9// - `%HH` decodes to a single byte (HH = uppercase or lowercase hex)
10// - Other bytes pass through
11// - Keys + values may be empty
12// - Final `&` is tolerated
13//
14// Per cardinal feedback-no-third-party-trust-native-or-nothing:
15// substrate's own parser; no querystring lib, no urlencoded-body
16// middleware. ~250 LOC; auditable.
17//
18// Per cardinal feedback-privacy-by-default-no-tracking:
19// substrate doesn't store the parsed form data anywhere; caller
20// supplies output buffers + decides retention.
21//
22// nx_capability_claims:
23// needs: [sealed_enum, byte_ops]
24// provides: [form_urlencoded_decode, named_field_extract,
25// pct_hex_decode, plus_to_space]
26// safety: [no_unchecked_deref, no_floating_point, no_syscall,
27// bounded_iteration, bit_equal_reproducible,
28// malformed_input_rejected]
29// verdict: [sealed_enum_5_state]
30// license: ORIGINAL
31// kind: racing_crew_specialist
32// layer: L2 (transform: bytes -> decoded key=value pairs)
33
34// ---- Sealed enum: parse verdict ----------------------------------
35
36const NXF_OK: i64 = 0
37const NXF_OOM_BUFFER: i64 = 1
38const NXF_BAD_PCT: i64 = 2
39const NXF_NOT_FOUND: i64 = 3
40const NXF_BAD_ARG: i64 = 4
41const NXF_VERDICT_N: i64 = 5
42
43func nxf_verdict_is_valid(v: i64) -> i64 {
44 if v < 0 { return 0 }
45 if v >= NXF_VERDICT_N { return 0 }
46 return 1
47}
48
49func nxf_verdict_name(v: i64) -> *u8 {
50 if v == NXF_OK { return "OK" as *u8 }
51 if v == NXF_OOM_BUFFER { return "OOM_BUFFER" as *u8 }
52 if v == NXF_BAD_PCT { return "BAD_PCT" as *u8 }
53 if v == NXF_NOT_FOUND { return "NOT_FOUND" as *u8 }
54 if v == NXF_BAD_ARG { return "BAD_ARG" as *u8 }
55 return "INVALID" as *u8
56}
57
58// ---- Hex nibble decode ------------------------------------------
59//
60// Returns 0..15 for '0'-'9'/'a'-'f'/'A'-'F'; -1 otherwise.
61
62func nxf_hex_nibble(c: i64) -> i64 {
63 if c >= 0x30 && c <= 0x39 { return c - 0x30 }
64 if c >= 0x61 && c <= 0x66 { return 10 + (c - 0x61) }
65 if c >= 0x41 && c <= 0x46 { return 10 + (c - 0x41) }
66 return -1
67}
68
69// ---- Decode a single URL-encoded chunk ---------------------------
70//
71// Reads from in[in_off..in_end] until either `&` or `=` or end.
72// Writes decoded bytes to out[*out_off..]. Advances in_off via
73// out_in_after.
74//
75// Returns NXF_OK on success or NXF_BAD_PCT / NXF_OOM_BUFFER.
76
77func nxf_decode_chunk(
78 in_bytes: *u8, in_end: i64, in_start: i64,
79 stop_a: i64, stop_b: i64,
80 out: *u8, out_off: *i64, out_cap: i64,
81 out_in_after: *i64) -> i64 {
82 var p: i64 = in_start
83 while p < in_end {
84 let b: i64 = in_bytes[p] as i64
85 if b == stop_a {
86 *out_in_after = p
87 return NXF_OK
88 }
89 if b == stop_b {
90 *out_in_after = p
91 return NXF_OK
92 }
93 if *out_off >= out_cap { return NXF_OOM_BUFFER }
94 if b == 0x2b {
95 // `+` -> space
96 out[*out_off] = 0x20 as u8
97 *out_off = *out_off + 1
98 p = p + 1
99 } else {
100 if b == 0x25 {
101 // `%HH` -- two hex digits required.
102 if p + 2 >= in_end { return NXF_BAD_PCT }
103 let hi: i64 = nxf_hex_nibble(in_bytes[p + 1] as i64)
104 if hi < 0 { return NXF_BAD_PCT }
105 let lo: i64 = nxf_hex_nibble(in_bytes[p + 2] as i64)
106 if lo < 0 { return NXF_BAD_PCT }
107 out[*out_off] = ((hi << 4) | lo) as u8
108 *out_off = *out_off + 1
109 p = p + 3
110 } else {
111 // Passthrough byte.
112 out[*out_off] = b as u8
113 *out_off = *out_off + 1
114 p = p + 1
115 }
116 }
117 }
118 *out_in_after = in_end
119 return NXF_OK
120}
121
122// ---- Find a named field's decoded value -------------------------
123//
124// Walks body as key=value&key=value pairs; for each pair, decodes
125// the key and compares to `field_name`; if match, decodes the value
126// into out_val and returns NXF_OK. Returns NXF_NOT_FOUND if absent.
127//
128// Caller supplies:
129// body, body_n urlencoded request body bytes
130// field_name, name_n plain (non-encoded) field name to match
131// (typically literal "username", "password",
132// "csrf_token", etc.)
133// out_val, out_cap decoded value destination
134// out_val_len bytes written to out_val
135//
136// The field name in the body is decoded before comparison so that
137// e.g., "user%20name" in the body matches field_name "user name".
138
139func nx_http_form_get_field(
140 body: *u8, body_n: i64,
141 field_name: *u8, field_name_n: i64,
142 out_val: *u8, out_cap: i64,
143 out_val_len: *i64) -> i64 {
144 if body == (0 as *u8) { return NXF_BAD_ARG }
145 if field_name == (0 as *u8) { return NXF_BAD_ARG }
146 if out_val == (0 as *u8) { return NXF_BAD_ARG }
147 if out_val_len == (0 as *i64) { return NXF_BAD_ARG }
148 if body_n < 0 { return NXF_BAD_ARG }
149 if field_name_n <= 0 { return NXF_BAD_ARG }
150 if out_cap <= 0 { return NXF_BAD_ARG }
151
152 var p: i64 = 0
153 while p < body_n {
154 // Decode key into a scratch buffer.
155 let key_buf: *u8 = sys_mmap(256)
156 let key_off: *i64 = sys_mmap(8) as *i64
157 key_off[0] = 0
158 let after_key: *i64 = sys_mmap(8) as *i64
159 after_key[0] = 0
160 let rc1: i64 = nxf_decode_chunk(body, body_n, p,
161 0x3d, 0x26,
162 key_buf, key_off, 256,
163 after_key)
164 if rc1 != NXF_OK { return rc1 }
165 // After key: either `=`, `&`, or end-of-body.
166 if after_key[0] >= body_n {
167 // Last pair with no `=` -> key with empty value.
168 if key_off[0] == field_name_n {
169 var eq: i64 = 1
170 var i: i64 = 0
171 while i < key_off[0] {
172 if key_buf[i] != field_name[i] { eq = 0; i = key_off[0] }
173 i = i + 1
174 }
175 if eq == 1 {
176 *out_val_len = 0
177 return NXF_OK
178 }
179 }
180 return NXF_NOT_FOUND
181 }
182 let sep: i64 = body[after_key[0]] as i64
183 if sep == 0x26 {
184 // No value; advance past `&`.
185 // Optionally match key here.
186 if key_off[0] == field_name_n {
187 var eq2: i64 = 1
188 var j: i64 = 0
189 while j < key_off[0] {
190 if key_buf[j] != field_name[j] { eq2 = 0; j = key_off[0] }
191 j = j + 1
192 }
193 if eq2 == 1 {
194 *out_val_len = 0
195 return NXF_OK
196 }
197 }
198 p = after_key[0] + 1
199 }
200 if sep == 0x3d {
201 // Match key first; if no match, decode value to advance.
202 var key_match: i64 = 0
203 if key_off[0] == field_name_n {
204 var eq3: i64 = 1
205 var k: i64 = 0
206 while k < key_off[0] {
207 if key_buf[k] != field_name[k] { eq3 = 0; k = key_off[0] }
208 k = k + 1
209 }
210 if eq3 == 1 { key_match = 1 }
211 }
212 // Decode value (key_match=1 -> into out_val; else scratch).
213 if key_match == 1 {
214 let v_off: *i64 = sys_mmap(8) as *i64
215 v_off[0] = 0
216 let after_val: *i64 = sys_mmap(8) as *i64
217 after_val[0] = 0
218 let rc2: i64 = nxf_decode_chunk(body, body_n, after_key[0] + 1,
219 0x26, 0x26,
220 out_val, v_off, out_cap,
221 after_val)
222 if rc2 != NXF_OK { return rc2 }
223 *out_val_len = v_off[0]
224 return NXF_OK
225 }
226 let scratch_val: *u8 = sys_mmap(256)
227 let sv_off: *i64 = sys_mmap(8) as *i64
228 sv_off[0] = 0
229 let after_val2: *i64 = sys_mmap(8) as *i64
230 after_val2[0] = 0
231 let rc3: i64 = nxf_decode_chunk(body, body_n, after_key[0] + 1,
232 0x26, 0x26,
233 scratch_val, sv_off, 256,
234 after_val2)
235 if rc3 != NXF_OK { return rc3 }
236 if after_val2[0] >= body_n { return NXF_NOT_FOUND }
237 p = after_val2[0] + 1
238 }
239 }
240 return NXF_NOT_FOUND
241}
242
243// ---- Count pairs in body -----------------------------------------
244//
245// Useful for caller pre-allocation. Counts the number of `key=val`
246// pairs (or just `key` pairs without `=`) in the body.
247
248func nx_http_form_count_pairs(body: *u8, body_n: i64) -> i64 {
249 if body == (0 as *u8) { return 0 }
250 if body_n <= 0 { return 0 }
251 var n_pairs: i64 = 1
252 var i: i64 = 0
253 while i < body_n {
254 if body[i] == 0x26 as u8 {
255 // Skip trailing `&` -- doesn't add a pair.
256 if i + 1 < body_n { n_pairs = n_pairs + 1 }
257 }
258 i = i + 1
259 }
260 return n_pairs
261}