multipart.nx source
↩ module page · 188 lines · 6320 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
35import "syscalls.nx"
36
37const MP_ERR_FORMAT: i64 = -1
38const MP_ERR_OVERFLOW: i64 = -2
39
40struct MultipartPart {
41 headers_off: i64, headers_len: i64,
42 body_off: i64, body_len: i64,
43}
44
45// Find the next occurrence of `needle[0..needle_len]` in
46// `buf[start..n]`. Returns offset or -1. Naive O(nm); good
47// enough for form uploads where boundaries are <80 bytes and
48// parts are <few MB.
49func mp_find(buf: *u8, start: i64, n: i64,
50 needle: *u8, needle_len: i64) -> i64 {
51 if needle_len == 0 { return start }
52 var i: i64 = start
53 while i <= n - needle_len {
54 var j: i64 = 0
55 var ok: i64 = 1
56 while j < needle_len {
57 if buf[i + j] != needle[j] {
58 ok = 0
59 break
60 }
61 j = j + 1
62 }
63 if ok == 1 { return i }
64 i = i + 1
65 }
66 return -1
67}
68
69// Parse the multipart body. `boundary` is the raw boundary
70// string (WITHOUT leading --); the parser prefixes -- internally.
71// Fills parts[] + returns count or MP_ERR_*.
72func multipart_parse(buf: *u8, n: i64,
73 boundary: *u8, boundary_len: i64,
74 parts: *MultipartPart, cap: i64) -> i64 {
75 // Build "--BOUNDARY" in scratch.
76 let delim_buf: *u8 = sys_mmap(256)
77 delim_buf[0] = 0x2D
78 delim_buf[1] = 0x2D
79 var i: i64 = 0
80 while i < boundary_len {
81 delim_buf[2 + i] = boundary[i]
82 i = i + 1
83 }
84 let delim_len: i64 = 2 + boundary_len
85
86 // Find the first delimiter -- body should start with it
87 // (optional preamble bytes before allowed per RFC, skipped).
88 var cur: i64 = mp_find(buf, 0, n, delim_buf, delim_len)
89 if cur < 0 { return MP_ERR_FORMAT }
90
91 var count: i64 = 0
92
93 while cur < n {
94 // Advance past boundary.
95 cur = cur + delim_len
96
97 // Check for closing delimiter "--BOUNDARY--".
98 if cur + 2 <= n {
99 if buf[cur] == 0x2D {
100 if buf[cur + 1] == 0x2D {
101 // End of multipart body.
102 break
103 }
104 }
105 }
106
107 // Consume trailing \r\n after boundary.
108 if cur + 2 <= n {
109 if buf[cur] == 0x0D {
110 if buf[cur + 1] == 0x0A {
111 cur = cur + 2
112 }
113 }
114 }
115
116 // Find next boundary -- CRLF "--BOUNDARY". The preceding
117 // \r\n is part of the delimiter per RFC (so part body
118 // doesn't include trailing CRLF).
119 let crlf_delim: *u8 = sys_mmap(256)
120 crlf_delim[0] = 0x0D
121 crlf_delim[1] = 0x0A
122 var k: i64 = 0
123 while k < delim_len {
124 crlf_delim[2 + k] = delim_buf[k]
125 k = k + 1
126 }
127 let crlf_delim_len: i64 = 2 + delim_len
128 let next_boundary: i64 = mp_find(buf, cur, n,
129 crlf_delim, crlf_delim_len)
130 if next_boundary < 0 { return MP_ERR_FORMAT }
131
132 // Find end of headers -- "\r\n\r\n" within [cur, next_boundary).
133 let header_end_delim: *u8 = sys_mmap(8)
134 header_end_delim[0] = 0x0D
135 header_end_delim[1] = 0x0A
136 header_end_delim[2] = 0x0D
137 header_end_delim[3] = 0x0A
138 let hdr_end: i64 = mp_find(buf, cur, next_boundary,
139 header_end_delim, 4)
140 if hdr_end < 0 {
141 // Malformed; skip.
142 cur = next_boundary
143 continue
144 }
145
146 if count >= cap { return MP_ERR_OVERFLOW }
147 let p: *MultipartPart = parts + count * 32
148 p.headers_off = cur
149 p.headers_len = hdr_end - cur
150 p.body_off = hdr_end + 4
151 p.body_len = next_boundary - (hdr_end + 4)
152 count = count + 1
153
154 cur = next_boundary
155 }
156 return count
157}
158
159// Compile-only smoke -- two-field form.
160func main() -> i64 {
161 let parts_raw: *u8 = sys_mmap(256)
162 let parts: *MultipartPart = parts_raw as *MultipartPart
163
164 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"
165 // Precomputed length:
166 // "--XX\r\n" = 6
167 // "Content-Disposition: form-data; name=\"a\"" = 40
168 // "\r\n\r\n" = 4
169 // "hello" = 5
170 // "\r\n--XX\r\n" = 8
171 // "Content-Disposition: form-data; name=\"b\"" = 40
172 // "\r\n\r\n" = 4
173 // "world" = 5
174 // "\r\n--XX--\r\n" = 10
175 // total = 122
176 let n: i64 = multipart_parse(body, 122, "XX", 2, parts, 16)
177 if n != 2 { return 1 }
178
179 let p0: *MultipartPart = parts
180 if p0.body_len != 5 { return 2 }
181 if body[p0.body_off] != 0x68 { return 3 } // 'h' of hello
182
183 let p1: *MultipartPart = parts + 32
184 if p1.body_len != 5 { return 4 }
185 if body[p1.body_off] != 0x77 { return 5 } // 'w' of world
186
187 return 0
188}