nx_multipart.nx source
↩ module page · 199 lines · 6910 B
1// multipart.nx -- parse multipart/form-data bodies (RFC 7578).
2//
3// HTML <form enctype="multipart/form-data"> encodes each field as
4// a separate "part" delimited by a boundary string announced in
5// the Content-Type header:
6//
7// Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
8//
9// Body structure (MIME):
10// --BOUNDARY\r\n
11// Content-Disposition: form-data; name="field1"\r\n
12// \r\n
13// value bytes\r\n
14// --BOUNDARY\r\n
15// Content-Disposition: form-data; name="file"; filename="a.txt"\r\n
16// Content-Type: text/plain\r\n
17// \r\n
18// file bytes\r\n
19// --BOUNDARY--\r\n (closing delimiter)
20//
21// We split the body into parts and surface each part's header
22// block + body byte range. Callers parse the Content-Disposition
23// line with content_type.nx to extract field name + filename.
24//
25// Invariants:
26// MP1 Boundary string (without leading --) comes from the
27// Content-Type parse; caller supplies it here.
28// MP2 Final "--BOUNDARY--" delimiter stops parsing; parts
29// after are ignored.
30// MP3 Parts are surfaced with offsets into the caller's
31// buffer; zero copies.
32// MP4 \r\n between header and body must be present; malformed
33// parts (missing blank line) are skipped.
34
35// nx_safety_envelope:
36// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
37// sil_target: SIL1
38// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
39// verdict: NOT_YET_EVALUATED
40
41import "nx_syscalls.nx"
42
43const MP_ERR_FORMAT: i64 = -1
44const MP_ERR_OVERFLOW: i64 = -2
45
46struct MultipartPart {
47 headers_off: i64, headers_len: i64,
48 body_off: i64, body_len: i64,
49}
50
51// Find the next occurrence of `needle[0..needle_len]` in
52// `buf[start..n]`. Returns offset or -1. Naive O(nm); good
53// enough for form uploads where boundaries are <80 bytes and
54// parts are <few MB.
55func mp_find(buf: *u8, start: i64, n: i64,
56 needle: *u8, needle_len: i64) -> i64 {
57 if needle_len == 0 { return start }
58 var i: i64 = start
59 while i <= n - needle_len {
60 var j: i64 = 0
61 var ok: i64 = 1
62 while j < needle_len {
63 if buf[i + j] != needle[j] {
64 ok = 0
65 break
66 }
67 j = j + 1
68 }
69 if ok == 1 { return i }
70 i = i + 1
71 }
72 return -1
73}
74
75// Parse the multipart body. `boundary` is the raw boundary
76// string (WITHOUT leading --); the parser prefixes -- internally.
77// Fills parts[] + returns count or MP_ERR_*.
78func multipart_parse(buf: *u8, n: i64,
79 boundary: *u8, boundary_len: i64,
80 parts: *MultipartPart, cap: i64) -> i64 {
81 // Build "--BOUNDARY" in scratch.
82 let delim_buf: *u8 = sys_mmap(256)
83 delim_buf[0] = 0x2D
84 delim_buf[1] = 0x2D
85 var i: i64 = 0
86 while i < boundary_len {
87 delim_buf[2 + i] = boundary[i]
88 i = i + 1
89 }
90 let delim_len: i64 = 2 + boundary_len
91
92 // Find the first delimiter -- body should start with it
93 // (optional preamble bytes before allowed per RFC, skipped).
94 var cur: i64 = mp_find(buf, 0, n, delim_buf, delim_len)
95 if cur < 0 { return MP_ERR_FORMAT }
96
97 var count: i64 = 0
98
99 while cur < n {
100 // Advance past boundary.
101 cur = cur + delim_len
102
103 // Check for closing delimiter "--BOUNDARY--".
104 if cur + 2 <= n {
105 if buf[cur] == 0x2D {
106 if buf[cur + 1] == 0x2D {
107 // End of multipart body.
108 break
109 }
110 }
111 }
112
113 // Consume trailing \r\n after boundary.
114 if cur + 2 <= n {
115 if buf[cur] == 0x0D {
116 if buf[cur + 1] == 0x0A {
117 cur = cur + 2
118 }
119 }
120 }
121
122 // Find next boundary -- CRLF "--BOUNDARY". The preceding
123 // \r\n is part of the delimiter per RFC (so part body
124 // doesn't include trailing CRLF).
125 let crlf_delim: *u8 = sys_mmap(256)
126 crlf_delim[0] = 0x0D
127 crlf_delim[1] = 0x0A
128 var k: i64 = 0
129 while k < delim_len {
130 crlf_delim[2 + k] = delim_buf[k]
131 k = k + 1
132 }
133 let crlf_delim_len: i64 = 2 + delim_len
134 let next_boundary: i64 = mp_find(buf, cur, n,
135 crlf_delim, crlf_delim_len)
136 if next_boundary < 0 { return MP_ERR_FORMAT }
137
138 // Find end of headers -- "\r\n\r\n" within [cur, next_boundary).
139 let header_end_delim: *u8 = sys_mmap(8)
140 header_end_delim[0] = 0x0D
141 header_end_delim[1] = 0x0A
142 header_end_delim[2] = 0x0D
143 header_end_delim[3] = 0x0A
144 let hdr_end: i64 = mp_find(buf, cur, next_boundary,
145 header_end_delim, 4)
146 if hdr_end < 0 {
147 // Malformed; skip.
148 cur = next_boundary
149 continue
150 }
151
152 if count >= cap { return MP_ERR_OVERFLOW }
153 let p: *MultipartPart = parts + count * 32
154 p.headers_off = cur
155 p.headers_len = hdr_end - cur
156 p.body_off = hdr_end + 4
157 p.body_len = next_boundary - (hdr_end + 4)
158 count = count + 1
159
160 // next_boundary points at the "\r\n" PRECEDING the delimiter; skip that CRLF so the loop top's
161 // `cur += delim_len` lands exactly past "--BOUNDARY" (same alignment as the first, CRLF-less
162 // delimiter). Without the +2 the loop stepped 2 bytes short on every part after the first,
163 // mis-detecting the closing delimiter -> MP_ERR on ALL multi-part bodies (the self-test below was
164 // compile-only, never run, so it hid this). FIX 2026-07-06 (first real caller: docportal upload).
165 cur = next_boundary + 2
166 }
167 return count
168}
169
170// Compile-only smoke -- two-field form.
171func main() -> i64 {
172 let parts_raw: *u8 = sys_mmap(256)
173 let parts: *MultipartPart = parts_raw as *MultipartPart
174
175 let body: *u8 = "--XX\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nhello\r\n--XX\r\nContent-Disposition: form-data; name=\"b\"\r\n\r\nworld\r\n--XX--\r\n"
176 // Precomputed length:
177 // "--XX\r\n" = 6
178 // "Content-Disposition: form-data; name=\"a\"" = 40
179 // "\r\n\r\n" = 4
180 // "hello" = 5
181 // "\r\n--XX\r\n" = 8
182 // "Content-Disposition: form-data; name=\"b\"" = 40
183 // "\r\n\r\n" = 4
184 // "world" = 5
185 // "\r\n--XX--\r\n" = 10
186 // total = 122
187 let n: i64 = multipart_parse(body, 122, "XX", 2, parts, 16)
188 if n != 2 { return 1 }
189
190 let p0: *MultipartPart = parts
191 if p0.body_len != 5 { return 2 }
192 if body[p0.body_off] != 0x68 { return 3 } // 'h' of hello
193
194 let p1: *MultipartPart = parts + 32
195 if p1.body_len != 5 { return 4 }
196 if body[p1.body_off] != 0x77 { return 5 } // 'w' of world
197
198 return 0
199}