const MAX_MULTIPART_BOUNDARY_LEN: usize = 70;
pub(crate) fn media_type(content_type: &str) -> &str {
content_type
.split(';')
.next()
.map(str::trim)
.unwrap_or_default()
}
pub(crate) fn has_media_type(content_type: &str, expected: &str) -> bool {
media_type(content_type).eq_ignore_ascii_case(expected)
}
pub(crate) fn is_sse(content_type: &str) -> bool {
has_media_type(content_type, "text/event-stream")
}
pub(crate) fn is_json(content_type: &str) -> bool {
let media_type = media_type(content_type).to_ascii_lowercase();
media_type == "application/json"
|| media_type.ends_with("+json")
|| media_type.ends_with("/json")
}
pub(crate) fn is_ndjson(content_type: &str) -> bool {
let media_type = media_type(content_type);
media_type.eq_ignore_ascii_case("application/x-ndjson")
|| media_type.eq_ignore_ascii_case("application/ndjson")
}
pub(crate) fn is_form_urlencoded(content_type: &str) -> bool {
has_media_type(content_type, "application/x-www-form-urlencoded")
}
pub(crate) fn is_multipart(content_type: &str) -> bool {
media_type(content_type)
.to_ascii_lowercase()
.starts_with("multipart/")
}
pub(crate) fn is_text(content_type: &str) -> bool {
media_type(content_type)
.to_ascii_lowercase()
.starts_with("text/")
}
pub(crate) fn multipart_boundary(content_type: &str) -> Option<String> {
if !is_multipart(content_type) {
return None;
}
let boundary = parameter(content_type, "boundary")?;
if is_valid_multipart_boundary(&boundary) {
Some(boundary)
} else {
None
}
}
pub(crate) fn is_valid_multipart_boundary(boundary: &str) -> bool {
let len = boundary.len();
(1..=MAX_MULTIPART_BOUNDARY_LEN).contains(&len)
&& boundary.bytes().all(is_valid_multipart_boundary_byte)
}
pub(crate) fn parameter(value: &str, parameter_name: &str) -> Option<String> {
for segment in header_parameter_segments(value)?.into_iter().skip(1) {
let Some((name, raw_value)) = segment.split_once('=') else {
continue;
};
if !name.trim().eq_ignore_ascii_case(parameter_name) {
continue;
}
return decode_header_parameter(raw_value.trim());
}
None
}
pub(crate) fn has_parameter_name(value: &str, parameter_name: &str) -> Option<bool> {
Some(
header_parameter_segments(value)?
.into_iter()
.skip(1)
.filter_map(|parameter| parameter.split_once('='))
.any(|(name, _)| name.trim().eq_ignore_ascii_case(parameter_name)),
)
}
fn is_valid_multipart_boundary_byte(byte: u8) -> bool {
matches!(
byte,
b'0'..=b'9'
| b'A'..=b'Z'
| b'a'..=b'z'
| b'\''
| b'('
| b')'
| b'+'
| b'_'
| b','
| b'-'
| b'.'
| b'/'
| b':'
| b'='
| b'?'
)
}
fn header_parameter_segments(value: &str) -> Option<Vec<&str>> {
let mut segments = Vec::new();
let mut start = 0;
let mut in_quote = false;
let mut escaped = false;
for (index, ch) in value.char_indices() {
if escaped {
escaped = false;
continue;
}
if in_quote && ch == '\\' {
escaped = true;
continue;
}
if ch == '"' {
in_quote = !in_quote;
continue;
}
if ch == ';' && !in_quote {
segments.push(value[start..index].trim());
start = index + ch.len_utf8();
}
}
if in_quote || escaped {
return None;
}
segments.push(value[start..].trim());
Some(segments)
}
fn decode_header_parameter(value: &str) -> Option<String> {
if !value.starts_with('"') {
return Some(value.trim().to_string());
}
if !value.ends_with('"') || value.len() < 2 {
return None;
}
let mut result = String::new();
let mut chars = value[1..value.len() - 1].chars();
while let Some(ch) = chars.next() {
let value = if ch == '\\' { chars.next()? } else { ch };
if value == '\r' || value == '\n' {
return None;
}
result.push(value);
}
Some(result)
}