// multipart/form-data — enough of it to carry one small file from a no-JS form.
//
// ⛔ Byte-exact throughout. An image is binary, so the body is never turned into a string and back;
// the boundary search, the header split and the part slicing all work on bytes. Decoding to UTF-8
// anywhere in that path silently corrupts every non-text upload, and the corruption looks like a
// broken image rather than an error.
//
// This is deliberately the smallest thing that works: no streaming, no nested multiparts, no
// transfer-encodings. It exists because a browser cannot post a file any other way without
// JavaScript, and the surface's whole point is that reading and writing need none.
import b4a from 'b4a'
const CRLF = b4a.from('\r\n')
const DASH = 0x2d
/**
* parse(body, contentType) -> { fields, files } | null
*
* fields name -> string (utf-8 decoded, because a form field IS text)
* files name -> { filename, contentType, bytes }
*
* Returns null when the content type is not multipart or carries no boundary — a caller should
* treat that as a bad request rather than as an empty form.
*/
export function parse (body, contentType) {
const m = /boundary=(?:"([^"]+)"|([^;]+))/i.exec(String(contentType || ''))
if (!m) return null
const boundary = (m[1] || m[2]).trim()
if (!boundary) return null
// Every delimiter in the body is `\r\n--boundary`, except the first, which has no leading CRLF.
// Prepending one makes the scan uniform instead of special-casing the head.
const buf = b4a.concat([CRLF, body])
const delim = b4a.concat([CRLF, b4a.from('--' + boundary)])
const out = { fields: {}, files: {} }
let pos = b4a.indexOf(buf, delim)
if (pos !== 0) return out // not our boundary; nothing to read rather than a guess
while (pos !== -1) {
let start = pos + delim.byteLength
// `--` after the boundary is the terminator.
if (buf[start] === DASH && buf[start + 1] === DASH) break
// Otherwise a CRLF, then the part.
if (buf[start] !== 0x0d || buf[start + 1] !== 0x0a) break
start += 2
const next = b4a.indexOf(buf, delim, start)
if (next === -1) break
readPart(buf.subarray(start, next), out)
pos = next
}
return out
}
function readPart (part, out) {
const sep = b4a.indexOf(part, b4a.concat([CRLF, CRLF]))
if (sep === -1) return
// Headers are ASCII by definition; the BODY is what must stay bytes.
const head = b4a.toString(part.subarray(0, sep), 'utf8')
const bytes = part.subarray(sep + 4)
const name = /name="([^"]*)"/i.exec(head)
if (!name) return
const filename = /filename="([^"]*)"/i.exec(head)
if (!filename) {
out.fields[name[1]] = b4a.toString(bytes, 'utf8')
return
}
// ⚠ An empty filename means "no file chosen" — every browser sends the part anyway. Treating it
// as a zero-byte upload would clear an avatar every time the form was saved.
if (!filename[1]) return
const type = /content-type:\s*([^\r\n]+)/i.exec(head)
out.files[name[1]] = {
filename: filename[1],
// ⛔ Recorded, never trusted. The daemon types the file from its magic bytes; this is only here
// so a caller can report what the browser claimed.
contentType: type ? type[1].trim() : null,
bytes
}
}
export default { parse }