diff --git a/src/mime/constants.rs b/src/mime/constants.rs index d9bac62e..f4298c84 100644 --- a/src/mime/constants.rs +++ b/src/mime/constants.rs @@ -1,5 +1,5 @@ -use super::ParamKind; use crate::Mime; +use std::borrow::Cow; macro_rules! utf8_mime_const { ($name:ident, $desc:expr, $base:expr, $sub:expr) => { @@ -9,18 +9,18 @@ macro_rules! utf8_mime_const { $desc, $base, $sub, - Some(ParamKind::Utf8), + true, ";charset=utf-8" ); }; } macro_rules! mime_const { ($name:ident, $desc:expr, $base:expr, $sub:expr) => { - mime_const!(with_params, $name, $desc, $base, $sub, None, ""); + mime_const!(with_params, $name, $desc, $base, $sub, false, ""); }; - (with_params, $name:ident, $desc:expr, $base:expr, $sub:expr, $params:expr, $doccomment:expr) => { - mime_const!(doc_expanded, $name, $desc, $base, $sub, $params, + (with_params, $name:ident, $desc:expr, $base:expr, $sub:expr, $is_utf8:expr, $doccomment:expr) => { + mime_const!(doc_expanded, $name, $desc, $base, $sub, $is_utf8, concat!( "Content-Type for ", $desc, @@ -29,16 +29,14 @@ macro_rules! mime_const { ); }; - (doc_expanded, $name:ident, $desc:expr, $base:expr, $sub:expr, $params:expr, $doccomment:expr) => { + (doc_expanded, $name:ident, $desc:expr, $base:expr, $sub:expr, $is_utf8:expr, $doccomment:expr) => { #[doc = $doccomment] pub const $name: Mime = Mime { - essence: String::new(), - basetype: String::new(), - subtype: String::new(), - params: $params, - static_essence: Some(concat!($base, "/", $sub)), - static_basetype: Some($base), - static_subtype: Some($sub), + essence: Cow::Borrowed(concat!($base, "/", $sub)), + basetype: Cow::Borrowed($base), + subtype: Cow::Borrowed($sub), + is_utf8: $is_utf8, + params: vec![], }; }; } diff --git a/src/mime/mod.rs b/src/mime/mod.rs index 8a5dc9d7..8e46d26d 100644 --- a/src/mime/mod.rs +++ b/src/mime/mod.rs @@ -28,15 +28,15 @@ use infer::Infer; /// ``` // NOTE: we cannot statically initialize Strings with values yet, so we keep dedicated static // fields for the static strings. -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq, Debug)] pub struct Mime { - pub(crate) essence: String, - pub(crate) basetype: String, - pub(crate) subtype: String, - pub(crate) static_essence: Option<&'static str>, - pub(crate) static_basetype: Option<&'static str>, - pub(crate) static_subtype: Option<&'static str>, - pub(crate) params: Option, + pub(crate) essence: Cow<'static, str>, + pub(crate) basetype: Cow<'static, str>, + pub(crate) subtype: Cow<'static, str>, + // NOTE(yosh): this is a hack because we can't populate vecs in const yet. + // This enables us to encode media types as utf-8 at compilation. + pub(crate) is_utf8: bool, + pub(crate) params: Vec<(ParamName, ParamValue)>, } impl Mime { @@ -68,87 +68,40 @@ impl Mime { /// According to the spec this method should be named `type`, but that's a reserved keyword in /// Rust so hence prefix with `base` instead. pub fn basetype(&self) -> &str { - if let Some(basetype) = self.static_basetype { - &basetype - } else { - &self.basetype - } + &self.basetype } /// Access the Mime's `subtype` value. pub fn subtype(&self) -> &str { - if let Some(subtype) = self.static_subtype { - &subtype - } else { - &self.subtype - } + &self.subtype } /// Access the Mime's `essence` value. pub fn essence(&self) -> &str { - if let Some(essence) = self.static_essence { - &essence - } else { - &self.essence - } + &self.essence } /// Get a reference to a param. pub fn param(&self, name: impl Into) -> Option<&ParamValue> { let name: ParamName = name.into(); - self.params - .as_ref() - .map(|inner| match inner { - ParamKind::Vec(v) => v - .iter() - .find_map(|(k, v)| if k == &name { Some(v) } else { None }), - ParamKind::Utf8 => match name { - ParamName(Cow::Borrowed("charset")) => Some(&ParamValue(Cow::Borrowed("utf8"))), - _ => None, - }, - }) - .flatten() + if name.as_str() == "charset" && self.is_utf8 { + return Some(&ParamValue(Cow::Borrowed("utf-8"))); + } + + self.params.iter().find(|(k, _)| k == &name).map(|(_, v)| v) } /// Remove a param from the set. Returns the `ParamValue` if it was contained within the set. pub fn remove_param(&mut self, name: impl Into) -> Option { let name: ParamName = name.into(); - let mut unset_params = false; - let ret = self - .params - .as_mut() - .map(|inner| match inner { - ParamKind::Vec(v) => match v.iter().position(|(k, _)| k == &name) { - Some(index) => Some(v.remove(index).1), - None => None, - }, - ParamKind::Utf8 => match name { - ParamName(Cow::Borrowed("charset")) => { - unset_params = true; - Some(ParamValue(Cow::Borrowed("utf8"))) - } - _ => None, - }, - }) - .flatten(); - if unset_params { - self.params = None; + if name.as_str() == "charset" && self.is_utf8 { + self.is_utf8 = false; + return Some(ParamValue(Cow::Borrowed("utf-8"))); } - ret - } -} - -impl PartialEq for Mime { - fn eq(&self, other: &Mime) -> bool { - let left = match self.static_essence { - Some(essence) => essence, - None => &self.essence, - }; - let right = match other.static_essence { - Some(essence) => essence, - None => &other.essence, - }; - left == right + self.params + .iter() + .position(|(k, _)| k == &name) + .map(|pos| self.params.remove(pos).1) } } @@ -158,15 +111,11 @@ impl Display for Mime { } } -impl Debug for Mime { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(essence) = self.static_essence { - Debug::fmt(essence, f) - } else { - Debug::fmt(&self.essence, f) - } - } -} +// impl Debug for Mime { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// Debug::fmt(&self.essence, f) +// } +// } impl FromStr for Mime { type Err = crate::Error; @@ -196,6 +145,7 @@ impl ToHeaderValues for Mime { Ok(header.to_header_values().unwrap()) } } + /// A parameter name. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ParamName(Cow<'static, str>); @@ -259,11 +209,3 @@ impl PartialEq for ParamValue { self.0 == other } } - -/// This is a hack that allows us to mark a trait as utf8 during compilation. We -/// can remove this once we can construct HashMap during compilation. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum ParamKind { - Utf8, - Vec(Vec<(ParamName, ParamValue)>), -} diff --git a/src/mime/parse.rs b/src/mime/parse.rs index a8b8c48e..4c02ec4a 100644 --- a/src/mime/parse.rs +++ b/src/mime/parse.rs @@ -1,6 +1,7 @@ +use std::borrow::Cow; use std::fmt; -use super::{Mime, ParamKind, ParamName, ParamValue}; +use super::{Mime, ParamName, ParamValue}; /// Parse a string into a mime type. /// Follows the [WHATWG MIME parsing algorithm](https://mimesniff.spec.whatwg.org/#parsing-a-mime-type) @@ -40,7 +41,8 @@ pub(crate) fn parse(input: &str) -> crate::Result { // 10. let basetype = basetype.to_ascii_lowercase(); let subtype = subtype.to_ascii_lowercase(); - let mut params = None; + let mut params = vec![]; + let mut is_utf8 = false; // 11. let mut input = input; @@ -91,13 +93,14 @@ pub(crate) fn parse(input: &str) -> crate::Result { }; // 10. - if !parameter_name.is_empty() + if parameter_name == "charset" && parameter_value == "utf-8" { + is_utf8 = true; + } else if !parameter_name.is_empty() && parameter_name.chars().all(is_http_token_code_point) && parameter_value .chars() .all(is_http_quoted_string_token_code_point) { - let params = params.get_or_insert_with(Vec::new); let name = ParamName(parameter_name.into()); let value = ParamValue(parameter_value.into()); if !params.iter().any(|(k, _)| k == &name) { @@ -107,13 +110,11 @@ pub(crate) fn parse(input: &str) -> crate::Result { } Ok(Mime { - essence: format!("{}/{}", &basetype, &subtype), - basetype, - subtype, - params: params.map(ParamKind::Vec), - static_essence: None, - static_basetype: None, - static_subtype: None, + essence: Cow::Owned(format!("{}/{}", &basetype, &subtype)), + basetype: basetype.into(), + subtype: subtype.into(), + params, + is_utf8, }) } @@ -205,39 +206,23 @@ fn collect_http_quoted_string(mut input: &str) -> (String, &str) { /// Implementation of [WHATWG MIME serialization algorithm](https://mimesniff.spec.whatwg.org/#serializing-a-mime-type) pub(crate) fn format(mime_type: &Mime, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(essence) = mime_type.static_essence { - write!(f, "{}", essence)? - } else { - write!(f, "{}", &mime_type.essence)? + write!(f, "{}", &mime_type.essence)?; + if mime_type.is_utf8 { + write!(f, ";charset=utf-8")?; } - if let Some(params) = &mime_type.params { - match params { - ParamKind::Utf8 => write!(f, ";charset=utf-8")?, - ParamKind::Vec(params) => { - for (name, value) in params { - if value.0.chars().all(is_http_token_code_point) && !value.0.is_empty() { - write!(f, ";{}={}", name, value)?; - } else { - write!( - f, - ";{}=\"{}\"", - name, - value - .0 - .chars() - .flat_map(|c| match c { - '"' | '\\' => EscapeMimeValue { - state: EscapeMimeValueState::Backslash(c) - }, - c => EscapeMimeValue { - state: EscapeMimeValueState::Char(c) - }, - }) - .collect::() - )?; - } - } - } + for (name, value) in mime_type.params.iter() { + if value.0.chars().all(is_http_token_code_point) && !value.0.is_empty() { + write!(f, ";{}={}", name, value)?; + } else { + let value = value + .0 + .chars() + .flat_map(|c| match c { + '"' | '\\' => EscapeMimeValue::backslash(c), + c => EscapeMimeValue::char(c), + }) + .collect::(); + write!(f, ";{}=\"{}\"", name, value)?; } } Ok(()) @@ -247,6 +232,20 @@ struct EscapeMimeValue { state: EscapeMimeValueState, } +impl EscapeMimeValue { + fn backslash(c: char) -> Self { + EscapeMimeValue { + state: EscapeMimeValueState::Backslash(c), + } + } + + fn char(c: char) -> Self { + EscapeMimeValue { + state: EscapeMimeValueState::Char(c), + } + } +} + #[derive(Clone, Debug)] enum EscapeMimeValueState { Done, diff --git a/tests/mime.rs b/tests/mime.rs index 6533435e..b6fbe195 100644 --- a/tests/mime.rs +++ b/tests/mime.rs @@ -44,4 +44,13 @@ mod tests { assert_eq!(res.content_type(), Some(mime::BYTE_STREAM)); Ok(()) } + + // #[test] + // fn match_mime_types() { + // let req = Request::get("https://example.com"); + // match req.content_type() { + // Some(mime::JSON) => {} + // _ => {} + // } + // } }