// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A request body in the form the transport sends it.
priv enum Payload {
Whole(Bytes)
Pieces(Source)
}
///|
/// How many boundaries have been made in this process: with the clock, enough to keep two
/// bodies made at the same time apart.
let boundaries : Ref[Int64] = Ref(0)
///|
/// A boundary no part contains. The first choice is random enough not to be in anything; the
/// loop makes it certain rather than likely.
fn boundary(parts : Array[Part]) -> String {
boundaries.val += 1
let mut text = "moonfetch-\{boundaries.val}-\{@async.now()}"
for ;; {
guard parts.iter().any(p => contains(p.content, text)) else { break }
text = text + "x"
}
text
}
///|
fn contains(content : Bytes, text : String) -> Bool {
let needle = @utf8.encode(text)
if needle.length() > content.length() {
return false
}
let last = content.length() - needle.length()
let mut start = 0
while start <= last {
let mut same = true
for i in 0.. String {
let out = StringBuilder()
for c in text {
match c {
'"' => out.write_string("%22")
'\r' => out.write_string("%0D")
'\n' => out.write_string("%0A")
'%' => out.write_string("%25")
_ => out.write_char(c)
}
}
out.to_string()
}
///|
/// A `multipart/form-data` body (RFC 7578) and the content type that names its boundary.
fn multipart(parts : Array[Part]) -> (Bytes, String) {
let mark = boundary(parts)
let out = Buffer()
for part in parts {
out.write_bytes(@utf8.encode("--\{mark}\r\n"))
out.write_bytes(
@utf8.encode(
"Content-Disposition: form-data; name=\"\{parameter(part.name)}\"",
),
)
if part.filename is Some(name) {
out.write_bytes(@utf8.encode("; filename=\"\{parameter(name)}\""))
}
out.write_bytes(@utf8.encode("\r\n"))
if part.content_type is Some(kind) {
out.write_bytes(@utf8.encode("Content-Type: \{kind}\r\n"))
}
out.write_bytes(@utf8.encode("\r\n"))
out.write_bytes(part.content)
out.write_bytes(@utf8.encode("\r\n"))
}
out.write_bytes(@utf8.encode("--\{mark}--\r\n"))
(out.to_bytes(), "multipart/form-data; boundary=\{mark}")
}
///|
/// The octets, and the content type they go out under unless the caller names one.
fn Body::payload(self : Body) -> (Payload, String?) {
match self {
Empty => (Whole(b""), None)
Text(text) => (Whole(@utf8.encode(text)), Some("text/plain; charset=utf-8"))
Bytes(bytes) => (Whole(bytes), Some("application/octet-stream"))
Json(value) =>
(Whole(@utf8.encode(value.stringify())), Some("application/json"))
Form(pairs) =>
(
Whole(@utf8.encode(@moonurl.Query::new(pairs~).encode())),
Some("application/x-www-form-urlencoded"),
)
Multipart(parts) => {
let (bytes, kind) = multipart(parts)
(Whole(bytes), Some(kind))
}
Stream(source) => (Pieces(source), None)
}
}
///|
/// How many octets a whole payload holds.
fn Payload::length(self : Payload) -> Int64? {
match self {
Whole(bytes) => Some(bytes.length().to_int64())
Pieces(source) => source.length
}
}