code wiki / (root) / nx_multipart.nx

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}