// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A multipart body as bytes. The page's helper: it turns a string literal
/// into the octets a request would carry.
fn body(text : String) -> Bytes {
let out : Array[Byte] = []
for i = 0; i < text.length(); i = i + 1 {
out.push((text.at(i).to_int() & 0xFF).to_byte())
}
Bytes::from_array(out)
}
///|
/// One body a browser sends for a form with two checkboxes sharing a name, a
/// text field, and a file with its own header.
let upload : String = "------WebKitFormBoundaryabc123\r\n" +
"Content-Disposition: form-data; name=\"title\"\r\n" +
"\r\n" +
"a report\r\n" +
"------WebKitFormBoundaryabc123\r\n" +
"Content-Disposition: form-data; name=\"tags\"\r\n" +
"\r\n" +
"one\r\n" +
"------WebKitFormBoundaryabc123\r\n" +
"Content-Disposition: form-data; name=\"tags\"\r\n" +
"\r\n" +
"two\r\n" +
"------WebKitFormBoundaryabc123\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"notes.txt\"\r\n" +
"Content-Type: text/plain\r\n" +
"X-Checksum: abc\r\n" +
"\r\n" +
"line one\r\nline two\r\n" +
"------WebKitFormBoundaryabc123--\r\n"
///|
/// The boundary is what follows the two leading dashes on each delimiter line.
let boundary : String = "----WebKitFormBoundaryabc123"
///|
/// Read the form, then drop one level to the raw parts it was built from.
fn main raise {
// The typed view: text fields by name, files by name.
let form = @moonmime.formdata(body(upload)[:], boundary)
println(form.field("title").unwrap_or(""))
// A set of checkboxes sends one name many times; `values` keeps them all.
for tag in form.values("tags") {
println(tag)
}
let file = form.file("file").unwrap()
println(file.filename)
println(file.content_type)
// Content is held whole, minus the CRLF that belongs to the delimiter.
println(file.size())
println(file.header("x-checksum").unwrap_or(""))
// The raw view: four parts, each with its own headers.
let parts = @moonmime.multipart(body(upload)[:], boundary)
println(parts.length())
println(parts[0].disposition().unwrap().param("name").unwrap_or(""))
println(parts[3].content_type().unwrap().essence)
// A bound: two parts is fewer than this body has, so it is refused whole.
try {
let limited = @moonmime.formdata(
body(upload)[:],
boundary,
limits=@moonmime.Limits::new(parts=2),
)
println(limited.is_empty())
} catch {
@moonmime.Parts(limit~) =>
println("refused: more than " + limit.to_string() + " parts")
_ => println("refused")
}
// `strict` turns a body that stops making sense into an error instead of
// returning the parts read so far.
let truncated = "------b\r\nContent-Disposition: form-data; name=\"a\"\r\n"
ignore(@moonmime.formdata(body(truncated)[:], "----b", strict=true)) catch {
@moonmime.Malformed(at~) => println("malformed at byte " + at.to_string())
_ => println("refused")
}
}