Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions src/mime/constants.rs
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -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,
Expand All @@ -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![],
};
};
}
Expand Down
116 changes: 29 additions & 87 deletions src/mime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ParamKind>,
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 {
Expand Down Expand Up @@ -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<ParamName>) -> 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<ParamName>) -> Option<ParamValue> {
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<Mime> 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)
}
}

Expand All @@ -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;
Expand Down Expand Up @@ -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>);
Expand Down Expand Up @@ -259,11 +209,3 @@ impl PartialEq<str> 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)>),
}
85 changes: 42 additions & 43 deletions src/mime/parse.rs
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -40,7 +41,8 @@ pub(crate) fn parse(input: &str) -> crate::Result<Mime> {
// 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;
Expand Down Expand Up @@ -91,13 +93,14 @@ pub(crate) fn parse(input: &str) -> crate::Result<Mime> {
};

// 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) {
Expand All @@ -107,13 +110,11 @@ pub(crate) fn parse(input: &str) -> crate::Result<Mime> {
}

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,
})
}

Expand Down Expand Up @@ -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::<String>()
)?;
}
}
}
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::<String>();
write!(f, ";{}=\"{}\"", name, value)?;
}
}
Ok(())
Expand All @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions tests/mime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {}
// _ => {}
// }
// }
}