///|
struct FormWriter {
writer : Writer
}
///|
/// Create a form writer with a boundary containing 192 bits of platform entropy.
pub fn FormWriter::FormWriter(
target : &@io.Writer,
) -> Self raise MultipartError {
guard @env.rand(24) is Some(bytes) else {
raise MultipartError(
"secure randomness is unavailable for multipart boundary",
)
}
{ writer: Writer(target, boundary=@hex.encode(bytes)), }
}
///|
/// The generated boundary, for constructing a matching reader or MIME header.
pub fn FormWriter::boundary(self : Self) -> String {
self.writer.boundary
}
///|
/// The HTTP Content-Type for the generated form body.
pub fn FormWriter::content_type(self : Self) -> String {
"multipart/form-data; boundary=\{self.writer.boundary}"
}
///|
/// Write a part with caller-supplied MIME headers.
pub async fn FormWriter::write_part(
self : Self,
headers~ : @http.Headers,
callback : Callback,
) -> Unit {
self.writer.write_part(headers~, callback)
}
///|
/// Write a UTF-8 text field, preserving the value's line endings.
pub async fn FormWriter::write_field(
self : Self,
name : String,
value : String,
) -> Unit {
let headers : @http.Headers = {
"Content-Disposition": form_content_disposition(name),
}
self.write_part(headers~, target => target.write(value))
}
///|
/// Write a JSON field with Content-Type application/json.
pub async fn FormWriter::write_json(
self : Self,
name : String,
value : Json,
) -> Unit {
let headers : @http.Headers = {
"Content-Disposition": form_content_disposition(name),
"Content-Type": "application/json",
}
self.write_part(headers~, target => target.write(value))
}
///|
/// Stream a file without closing its reader. An unspecified content type uses
/// application/octet-stream; filename is optional (RFC 7578 sections 4.2–4.4).
pub async fn FormWriter::write_file(
self : Self,
name : String,
filename? : String,
content_type? : String,
file : &@io.Reader,
) -> Unit {
let headers : @http.Headers = {
"Content-Disposition": form_content_disposition(name, filename?),
"Content-Type": content_type.unwrap_or("application/octet-stream"),
}
self.writer.write_reader(headers~, file)
}
///|
pub async fn FormWriter::finish(self : Self) -> Unit {
self.writer.finish()
}
///|
// MIME quoted-pair escaping preserves literal backslashes and quotes. CR/LF
// uses RFC 7578 section 2 percent encoding, as in Go's FileContentDisposition.
fn form_content_disposition(name : String, filename? : String) -> String {
fn quote(value : String) -> String {
value
.replace_all(old="\\", new="\\\\")
.replace_all(old="\"", new="\\\"")
.replace_all(old="\r", new="%0D")
.replace_all(old="\n", new="%0A")
}
let disposition = "form-data; name=\"\{quote(name)}\""
if filename is Some(filename) {
disposition + "; filename=\"\{quote(filename)}\""
} else {
disposition
}
}