use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use crate::validate::{
QuotedStringError, escape_quotes, is_token_char, is_valid_token, parse_quoted_string, trim_ows,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContentTypeError {
Empty,
InvalidMediaType,
InvalidParameter,
UnterminatedQuote,
}
impl fmt::Display for ContentTypeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContentTypeError::Empty => write!(f, "empty Content-Type"),
ContentTypeError::InvalidMediaType => write!(f, "invalid media type"),
ContentTypeError::InvalidParameter => write!(f, "invalid parameter"),
ContentTypeError::UnterminatedQuote => write!(f, "unterminated quote"),
}
}
}
impl core::error::Error for ContentTypeError {}
impl From<QuotedStringError> for ContentTypeError {
fn from(e: QuotedStringError) -> Self {
match e {
QuotedStringError::InvalidQdtext | QuotedStringError::InvalidQuotedPair => {
ContentTypeError::InvalidParameter
}
QuotedStringError::Unterminated => ContentTypeError::UnterminatedQuote,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentType {
media_type: String,
subtype: String,
parameters: Vec<(String, String)>,
}
impl ContentType {
pub fn parse(input: &str) -> Result<Self, ContentTypeError> {
let input = trim_ows(input);
if input.is_empty() {
return Err(ContentTypeError::Empty);
}
let (media_type_part, rest) = split_at_semicolon(input);
let (media_type, subtype) = parse_media_type(media_type_part)?;
let parameters = parse_parameters(rest)?;
Ok(ContentType {
media_type: media_type.to_ascii_lowercase(),
subtype: subtype.to_ascii_lowercase(),
parameters,
})
}
pub fn new(media_type: &str, subtype: &str) -> Self {
ContentType {
media_type: media_type.to_ascii_lowercase(),
subtype: subtype.to_ascii_lowercase(),
parameters: Vec::new(),
}
}
pub fn with_parameter(mut self, name: &str, value: &str) -> Self {
self.parameters
.push((name.to_ascii_lowercase(), value.to_string()));
self
}
pub fn media_type(&self) -> &str {
&self.media_type
}
pub fn subtype(&self) -> &str {
&self.subtype
}
pub fn mime_type(&self) -> String {
alloc::format!("{}/{}", self.media_type, self.subtype)
}
pub fn parameter(&self, name: &str) -> Option<&str> {
let name_lower = name.to_ascii_lowercase();
self.parameters
.iter()
.find(|(n, _)| n == &name_lower)
.map(|(_, v)| v.as_str())
}
pub fn parameters(&self) -> &[(String, String)] {
&self.parameters
}
pub fn charset(&self) -> Option<&str> {
self.parameter("charset")
}
pub fn boundary(&self) -> Option<&str> {
self.parameter("boundary")
}
pub fn is_text(&self) -> bool {
self.media_type == "text"
}
pub fn is_json(&self) -> bool {
self.media_type == "application" && self.subtype == "json"
}
pub fn is_multipart(&self) -> bool {
self.media_type == "multipart"
}
pub fn is_form_data(&self) -> bool {
self.media_type == "multipart" && self.subtype == "form-data"
}
pub fn is_form_urlencoded(&self) -> bool {
self.media_type == "application" && self.subtype == "x-www-form-urlencoded"
}
}
impl fmt::Display for ContentType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.media_type, self.subtype)?;
for (name, value) in &self.parameters {
if needs_quoting(value) {
write!(f, "; {}=\"{}\"", name, escape_quotes(value))?;
} else {
write!(f, "; {}={}", name, value)?;
}
}
Ok(())
}
}
fn split_at_semicolon(input: &str) -> (&str, &str) {
if let Some(pos) = input.find(';') {
(trim_ows(&input[..pos]), trim_ows(&input[pos + 1..]))
} else {
(trim_ows(input), "")
}
}
fn parse_media_type(input: &str) -> Result<(&str, &str), ContentTypeError> {
let input = trim_ows(input);
if input.is_empty() {
return Err(ContentTypeError::InvalidMediaType);
}
let slash_pos = input.find('/').ok_or(ContentTypeError::InvalidMediaType)?;
let media_type = trim_ows(&input[..slash_pos]);
let subtype = trim_ows(&input[slash_pos + 1..]);
if media_type.is_empty() || subtype.is_empty() {
return Err(ContentTypeError::InvalidMediaType);
}
if !is_valid_token(media_type) || !is_valid_token(subtype) {
return Err(ContentTypeError::InvalidMediaType);
}
Ok((media_type, subtype))
}
fn parse_parameters(input: &str) -> Result<Vec<(String, String)>, ContentTypeError> {
let mut parameters = Vec::new();
let mut rest = trim_ows(input);
while !rest.is_empty() {
rest = trim_ows(rest.trim_start_matches(';'));
if rest.is_empty() {
break;
}
let eq_pos = rest.find('=').ok_or(ContentTypeError::InvalidParameter)?;
let name = trim_ows(&rest[..eq_pos]);
if name.is_empty() || !is_valid_token(name) {
return Err(ContentTypeError::InvalidParameter);
}
rest = trim_ows(&rest[eq_pos + 1..]);
let (value, remaining) = if let Some(after_quote) = rest.strip_prefix('"') {
parse_quoted_string(after_quote)?
} else {
parse_token_value(rest)?
};
parameters.push((name.to_ascii_lowercase(), value));
rest = trim_ows(remaining.trim_start_matches(';'));
}
Ok(parameters)
}
fn parse_token_value(input: &str) -> Result<(String, &str), ContentTypeError> {
let end = input
.find(|c: char| c == ';' || c.is_whitespace())
.unwrap_or(input.len());
let token = &input[..end];
if !is_valid_token(token) {
return Err(ContentTypeError::InvalidParameter);
}
Ok((token.to_string(), &input[end..]))
}
fn needs_quoting(s: &str) -> bool {
s.is_empty() || s.bytes().any(|b| !is_token_char(b))
}