///|
/// Per-part metadata for a caller-produced multipart/byteranges body.
pub struct MultipartPartPlan {
range : ConcreteRange
content_range : ContentRangeValue
content_length : Int64
} derive(Eq, Debug)
///|
pub struct MultipartPlan {
boundary : String
content_type : String
parts : Array[MultipartPartPlan]
} derive(Eq, Debug)
///|
pub fn plan_multipart_ranges(
ranges : Array[ConcreteRange],
representation_length : Int64,
boundary : String,
) -> Result[MultipartPlan, RangeError] {
if boundary.length() == 0 || boundary.length() > 70 {
return Err(
range_error(
Plan,
InvalidBoundary,
0,
"multipart boundary must contain 1 to 70 characters",
),
)
}
for c in boundary {
if c.to_int() <= 0x20 || c.to_int() >= 0x7F || c == '"' {
return Err(
range_error(
Plan,
InvalidBoundary,
0,
"multipart boundary must be visible ASCII without quotes",
),
)
}
}
if representation_length < 0L {
return Err(
range_error(
Plan,
InvalidRepresentationLength,
0,
"representation length cannot be negative",
),
)
}
let parts : Array[MultipartPartPlan] = []
for range in ranges {
if range.first() < 0L ||
range.last() < range.first() ||
range.last() >= representation_length {
return Err(
range_error(
Plan,
InvalidRange,
0,
"multipart range is outside the representation",
),
)
}
parts.push({
range,
content_range: Satisfied(
Bytes,
range.first(),
range.last(),
Some(representation_length),
),
content_length: range.length(),
})
}
Ok({ boundary, content_type: multipart_content_type(boundary), parts })
}
///|
pub fn multipart_content_type(boundary : String) -> String {
"multipart/byteranges; boundary=\{boundary}"
}
///|
pub fn MultipartPartPlan::range(self : MultipartPartPlan) -> ConcreteRange {
self.range
}
///|
pub fn MultipartPartPlan::content_range(
self : MultipartPartPlan,
) -> ContentRangeValue {
self.content_range
}
///|
pub fn MultipartPartPlan::content_length(self : MultipartPartPlan) -> Int64 {
self.content_length
}
///|
pub fn MultipartPlan::boundary(self : MultipartPlan) -> String {
self.boundary
}
///|
pub fn MultipartPlan::content_type(self : MultipartPlan) -> String {
self.content_type
}
///|
pub fn MultipartPlan::parts(self : MultipartPlan) -> Array[MultipartPartPlan] {
self.parts.copy()
}
///|
/// Estimate the exact byte length of the multipart/byteranges body framed per
/// RFC 9110 ยง14.6, given the per-part Content-Type value. Every delimiter line,
/// header line, CRLF pair, data byte, and the closing delimiter is counted, so
/// the value is suitable for a Content-Length decision when the per-part
/// Content-Type header matches the supplied string.
pub fn MultipartPlan::estimated_body_length(
self : MultipartPlan,
part_content_type : String,
) -> Int64 {
let delimiter_length = 2 + self.boundary.length() + 2 // "--" + boundary + CRLF
let content_type_length = "Content-Type: ".length() +
part_content_type.length() +
2
let mut total = 0L
for part in self.parts() {
let content_range_length = "Content-Range: ".length() +
serialize_content_range(part.content_range()).length() +
2
total = total +
delimiter_length.to_int64() +
content_type_length.to_int64() +
content_range_length.to_int64() +
2L + // blank line separating headers from data
part.content_length() +
2L // CRLF before the next delimiter
}
total + (2 + self.boundary.length() + 2 + 2).to_int64() // "--" + boundary + "--" + CRLF
}