code wiki / (root) / multipart.nx

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}