// multipart/form-data — the only way a browser posts a file without JavaScript.
//
// ⛔ The whole risk here is byte fidelity. An image that round-trips through a UTF-8 string comes
// back corrupted, and the symptom is a broken picture rather than an error — so these tests use
// bytes that DO NOT survive a UTF-8 round trip, on purpose.
import test from 'brittle'
import b4a from 'b4a'
import { parse } from '../lib/multipart.js'
const B = '----favelaBoundary'
const CT = 'multipart/form-data; boundary=' + B
// Assemble a body the way a browser does, from parts that may be binary.
const body = (parts) => {
const chunks = []
for (const p of parts) {
chunks.push(b4a.from('--' + B + '\r\n'))
chunks.push(b4a.from(p.headers + '\r\n\r\n'))
chunks.push(typeof p.body === 'string' ? b4a.from(p.body) : p.body)
chunks.push(b4a.from('\r\n'))
}
chunks.push(b4a.from('--' + B + '--\r\n'))
return b4a.concat(chunks)
}
test('text fields decode; the parser is not confused by two of them', (t) => {
const r = parse(body([
{ headers: 'Content-Disposition: form-data; name="name"', body: 'Ada Lovelace' },
{ headers: 'Content-Disposition: form-data; name="other"', body: 'x' }
]), CT)
t.is(r.fields.name, 'Ada Lovelace')
t.is(r.fields.other, 'x')
t.alike(Object.keys(r.files), [])
})
test('★ a file survives BYTE FOR BYTE, including bytes that are not valid UTF-8', (t) => {
// A real PNG header plus every byte value. If anything on the path decoded to a string, the
// high bytes come back as U+FFFD and this fails.
const raw = b4a.concat([
b4a.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
b4a.from(Array.from({ length: 256 }, (_, i) => i))
])
const r = parse(body([
{ headers: 'Content-Disposition: form-data; name="name"', body: 'Ada' },
{ headers: 'Content-Disposition: form-data; name="avatar"; filename="a.png"\r\nContent-Type: image/png', body: raw }
]), CT)
t.is(r.fields.name, 'Ada')
t.is(r.files.avatar.filename, 'a.png')
t.ok(b4a.equals(r.files.avatar.bytes, raw), 'every byte, unchanged')
t.is(r.files.avatar.bytes.length, raw.length)
})
test('⛔ "no file chosen" is not a zero-byte upload', (t) => {
// Browsers send the part regardless. Treating an empty filename as a file would clear the avatar
// every single time the settings form was saved with the name alone.
const r = parse(body([
{ headers: 'Content-Disposition: form-data; name="name"', body: 'Ada' },
{ headers: 'Content-Disposition: form-data; name="avatar"; filename=""\r\nContent-Type: application/octet-stream', body: '' }
]), CT)
t.is(r.fields.name, 'Ada')
t.absent(r.files.avatar, 'no file part at all, so the caller leaves the picture alone')
})
test('a body containing the boundary text inside a file does not split it', (t) => {
// The delimiter is CRLF + "--" + boundary; the bare string is ordinary content.
const raw = b4a.from('before ' + B + ' after')
const r = parse(body([
{ headers: 'Content-Disposition: form-data; name="avatar"; filename="a.bin"', body: raw }
]), CT)
t.ok(b4a.equals(r.files.avatar.bytes, raw), 'the file is whole')
})
test('the declared content-type is recorded but never authoritative', (t) => {
const r = parse(body([
{ headers: 'Content-Disposition: form-data; name="avatar"; filename="x.png"\r\nContent-Type: image/png', body: b4a.from('<svg/>') }
]), CT)
// ⛔ The daemon types the file from its magic bytes. This field only reports what was claimed.
t.is(r.files.avatar.contentType, 'image/png')
t.is(b4a.toString(r.files.avatar.bytes), '<svg/>', 'and the bytes are what actually arrived')
})
test('a non-multipart request is null, not an empty form', (t) => {
// Null so the caller can say "expected a form upload" rather than silently saving nothing.
t.is(parse(b4a.from('name=Ada'), 'application/x-www-form-urlencoded'), null)
t.is(parse(b4a.from(''), 'multipart/form-data'), null, 'multipart with no boundary is unusable')
})