///|
/// Resolves Content-Length and Transfer-Encoding, or `None` when neither is
/// present (RFC 9112, section 6).
fn framing_fields(headers : @mttp_core.Headers) -> Framing? raise ParseError {
let lengths = headers.get_all("content-length")
let encodings = headers.get_all("transfer-encoding")
if !lengths.is_empty() && !encodings.is_empty() {
raise ParseError(
kind=Smuggling,
detail="Content-Length with Transfer-Encoding",
)
}
// Stricter than RFC 9112, section 6.3, which lets a recipient accept a list
// or repetition of one identical value: any repetition is refused.
if lengths.length() > 1 {
raise malformed("duplicate Content-Length")
}
if lengths is [value] {
return Some(Length(content_length(value)))
}
if encodings.is_empty() {
return None
}
let codings = []
for value in encodings {
for part in value.split(",") {
let coding = ows(part).to_lower()
// Empty list elements are refused rather than skipped, so no
// reading of `chunked,` or `, chunked` can differ between parsers.
if coding.is_empty() {
raise malformed("empty transfer coding")
}
codings.push(coding)
}
}
if codings.filter(c => c == "chunked").length() > 1 {
raise malformed("chunked applied more than once")
}
if codings is ["chunked"] {
return Some(Chunked)
}
raise ParseError(kind=Unsupported, detail="unsupported transfer coding")
}
///|
/// `Content-Length = 1*DIGIT`, refusing values past `Int64`.
fn content_length(value : String) -> Int64 raise ParseError {
let value = ows(value)
if value.is_empty() {
raise malformed("empty Content-Length")
}
let mut length = 0L
for c in value {
guard c is ('0'..<='9') else { raise malformed("invalid Content-Length") }
let digit = (c.to_int() - '0'.to_int()).to_int64()
if length > (9223372036854775807L - digit) / 10L {
raise malformed("Content-Length overflow")
}
length = length * 10L + digit
}
length
}
///|
/// Resolves the framing of a request body: `Length` from one valid
/// Content-Length, `Chunked` from a Transfer-Encoding of exactly `chunked`,
/// and `NoBody` when neither field is present. Refuses Content-Length with
/// Transfer-Encoding as smuggling, any duplicate or invalid Content-Length as
/// malformed, and any other transfer coding as unsupported.
///
/// ```mbt check
/// test {
/// let chunked = @mttp_core.Headers::of([("Transfer-Encoding", "chunked")])
/// assert_eq(@h1.request_framing(chunked), Chunked)
/// let both = chunked.add("Content-Length", "5")
/// try @h1.request_framing(both) catch {
/// @h1.ParseError(kind~, ..) => assert_eq(kind, Smuggling)
/// } noraise {
/// _ => fail("CL+TE must be refused")
/// }
/// }
/// ```
pub fn request_framing(
headers : @mttp_core.Headers,
) -> Framing raise ParseError {
framing_fields(headers).unwrap_or(NoBody)
}
///|
/// Resolves the framing of a response body to `meth`. The fields are
/// checked as for `request_framing` even when the status has no body; then
/// a response to HEAD, a 1xx, 204 or 304, and a 2xx to CONNECT have
/// `NoBody`, and a response with neither field reads `UntilClose`. After a
/// successful CONNECT the caller hands the remaining bytes to the tunnel.
pub fn response_framing(
headers : @mttp_core.Headers,
status~ : Int32,
meth~ : @mttp_core.Method = Get,
) -> Framing raise ParseError {
let framing = framing_fields(headers)
if has_no_body(status, meth) {
return NoBody
}
framing.unwrap_or(UntilClose)
}
///|
/// Whether a response never has a body, whatever its fields say (RFC 9112,
/// section 6.3): a response to HEAD, a 1xx, 204 or 304, and a 2xx to
/// CONNECT.
fn has_no_body(status : Int32, meth : @mttp_core.Method) -> Bool {
meth == Head ||
status / 100 == 1 ||
status == 204 ||
status == 304 ||
(meth == Connect && status / 100 == 2)
}