diff options
Diffstat (limited to 'headers/src/header_components/media_type.rs')
-rw-r--r-- | headers/src/header_components/media_type.rs | 513 |
1 files changed, 513 insertions, 0 deletions
diff --git a/headers/src/header_components/media_type.rs b/headers/src/header_components/media_type.rs new file mode 100644 index 0000000..a636258 --- /dev/null +++ b/headers/src/header_components/media_type.rs @@ -0,0 +1,513 @@ +use std::{ + ops::Deref, + str::FromStr +}; +#[cfg(feature="serde")] +use std::fmt; + +#[cfg(feature="serde")] +use serde::{Serialize, Serializer, Deserialize, Deserializer, de::{self, Visitor}}; +use soft_ascii_string::{SoftAsciiStr,SoftAsciiChar}; + +use mime::{ + MediaType as _MediaType, + Name, AnyMediaType, + spec::{MimeSpec, Ascii, Internationalized, Modern} +}; +use internals::{ + error::EncodingError, + encoder::{EncodingWriter, EncodableInHeader} +}; + +use crate::{ + HeaderTryFrom, + error::ComponentCreationError +}; + + +#[derive(Debug, Clone)] +pub struct MediaType { + media_type: InternationalizedMediaType, + might_need_utf8: bool +} + +impl MediaType { + + pub fn parse(media_type: &str) -> Result<Self, ComponentCreationError> { + let media_type = InternationalizedMediaType + ::parse(media_type) + .map_err(|e| + ComponentCreationError + ::from_parent(e.to_owned(), "MediaType") + .with_str_context(media_type) + )?; + + Ok(media_type.into()) + } + + pub fn new<T, ST>(type_: T, subtype: ST) -> Result<Self, ComponentCreationError> + where T: AsRef<str>, ST: AsRef<str> + { + let media_type = AsciiMediaType + ::new(type_.as_ref(), subtype.as_ref()) + .map_err(|e| + ComponentCreationError + ::from_parent(e, "MediaType") + .with_str_context(format!("{}/{}", type_.as_ref(), subtype.as_ref())) + )?; + + Ok(media_type.into()) + } + + pub fn new_with_params<T, ST, I, IV, IN>(type_: T, subtype: ST, params: I) + -> Result<Self, ComponentCreationError> + where T: AsRef<str>, ST: AsRef<str>, + I: IntoIterator<Item=(IV, IN)>, + IV: AsRef<str>, IN: AsRef<str> + { + let media_type = InternationalizedMediaType + ::new_with_params(type_.as_ref(), subtype.as_ref(), params) + .map_err(|e| + ComponentCreationError + ::from_parent(e, "MediaType") + .with_str_context(format!("{}/{} <params-eluded>", + type_.as_ref(), subtype.as_ref())) + )?; + + Ok(media_type.into()) + } + + pub fn remove_param<N>(&mut self, name: N) -> bool + where N: for<'a> PartialEq<Name<'a>> + { + self.media_type.remove_param(name) + } + + pub fn set_param<N, V>(&mut self, name: N, value: V) + where N: AsRef<str>, V: AsRef<str> + { + self.media_type.set_param(name, value) + } +} + +impl FromStr for MediaType { + type Err = ComponentCreationError; + fn from_str(inp: &str) -> Result<Self, Self::Err> { + MediaType::parse(inp) + } +} + + +impl Deref for MediaType { + type Target = AnyMediaType; + + fn deref(&self) -> &Self::Target { + &self.media_type + } +} + +type AsciiMediaType = _MediaType<MimeSpec<Ascii, Modern>>; +type InternationalizedMediaType = _MediaType<MimeSpec<Internationalized, Modern>>; + +impl From<AsciiMediaType> for MediaType { + fn from(media_type: AsciiMediaType) -> Self { + MediaType { + media_type: media_type.into(), + might_need_utf8: false + } + } +} + +impl From<InternationalizedMediaType> for MediaType { + fn from(media_type: InternationalizedMediaType) -> Self{ + MediaType { + media_type: media_type, + might_need_utf8: true + } + } +} + +impl Into<AnyMediaType> for MediaType { + fn into(self) -> AnyMediaType { + self.media_type.into() + } +} + +impl Into<InternationalizedMediaType> for MediaType { + fn into(self) -> InternationalizedMediaType { + self.media_type + } +} + +impl HeaderTryFrom<AsciiMediaType> for MediaType { + fn try_from(mime: AsciiMediaType) -> Result<Self, ComponentCreationError> { + Ok(mime.into()) + } +} + +impl HeaderTryFrom<InternationalizedMediaType> for MediaType { + fn try_from(mime: InternationalizedMediaType) -> Result<Self, ComponentCreationError> { + Ok(mime.into()) + } +} + +impl<'a> HeaderTryFrom<&'a str> for MediaType { + fn try_from(media_type: &'a str) -> Result<Self, ComponentCreationError> { + Self::parse(media_type) + } +} + + +impl EncodableInHeader for MediaType { + + fn encode(&self, handle: &mut EncodingWriter) -> Result<(), EncodingError> { + let no_recheck_needed = handle.mail_type().is_internationalized() || !self.might_need_utf8; + + //type and subtype are always ascii + handle.write_str(SoftAsciiStr::from_unchecked(self.type_().as_ref()))?; + handle.write_char(SoftAsciiChar::from_unchecked('/'))?; + handle.write_str(SoftAsciiStr::from_unchecked(self.subtype().as_ref()))?; + for (name, value) in self.params() { + //FIXME for now do not split params at all + handle.mark_fws_pos(); + handle.write_char(SoftAsciiChar::from_unchecked(';'))?; + handle.write_fws(); + //names are always ascii + handle.write_str(SoftAsciiStr::from_unchecked(name.as_ref()))?; + + handle.write_char(SoftAsciiChar::from_unchecked('='))?; + if no_recheck_needed { + handle.write_str_unchecked(value.as_str_repr())?; + } else { + match SoftAsciiStr::from_str(value.as_str_repr()) { + Ok(soa) => handle.write_str(soa)?, + Err(_) => { + //TODO encode value ! then write it + unimplemented!(); + } + } + } + } + Ok(()) + } + + fn boxed_clone(&self) -> Box<EncodableInHeader> { + Box::new(self.clone()) + } +} + + +#[cfg(feature="serde")] +impl Serialize for MediaType { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where S: Serializer + { + serializer.serialize_str(self.as_str_repr()) + } +} + +#[cfg(feature="serde")] +impl<'de> Deserialize<'de> for MediaType { + + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where D: Deserializer<'de> + { + struct MediaTypeVisitor; + + impl<'de> Visitor<'de> for MediaTypeVisitor { + type Value = MediaType; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a string representing a MediaType") + } + + fn visit_str<E>(self, s: &str) -> Result<Self::Value, E> + where + E: de::Error, + { + let mt = s.parse() + .map_err(|err| E::custom(err))?; + + Ok(mt) + } + } + + deserializer.deserialize_str(MediaTypeVisitor) + } +} + +// +///// encodes all non ascii parts of a mime turning it into an ascii mime +///// +//fn encode_mime(mime: &MediaType, handle: &mut EncodingWriter) -> Result<()> { +// //TODO(upstream=mime): this can be simplified with upstem fixes to the mime crate +// handle.write_str(SoftAsciiStr::from_unchecked(mime.type_().as_str()))?; +// handle.write_char(SoftAsciiChar::from_unchecked('/'))?; +// handle.write_str(SoftAsciiStr::from_unchecked(mime.subtype().as_str()))?; +// +// let mail_type = handle.mail_type(); +// let mut split_params = HashMap::new(); +// +// for (name, value) in mime.params() { +// let (split_num, is_encoded) = get_split_num(&name)?; +// if let Some((name, section)) = split_num { +// // as the charset can only be set in the first of multiple splits and +// // the first does not have to be the first in the iteration we have to +// // delay the handling +// let old = split_params +// .entry(name) +// .or_insert(HashMap::new()) +// .insert(section, (value, is_encoded)); +// +// if old.is_some() { +// bail!(InvalidMime(mime.to_string())) +// } +// } else { +// handle.mark_fws_pos(); +// handle.write_char(SoftAsciiChar::from_unchecked(';'))?; +// handle.mark_fws_pos(); +// if is_encoded { +// //parameter names are ascii, values might not be ascii +// handle.write_str(SoftAsciiStr::from_unchecked(name.as_str()))?; +// handle.write_char(SoftAsciiChar::from_unchecked('='))?; +// if let Ok(asciied) = SoftAsciiStr::from_str(value.as_str()) { +// handle.write_str(asciied)?; +// } else { +// bail!(InvalidMime(mime.to_string())) +// } +// } else { +// // this whole reparsing can be storngly simplified if the mime crate would +// // returns either the content OR the underlying representation for as_str, +// // but it returns something in between... +// let mut token = true; +// let mut qtext = true; +// let mut had_slash = false; +// for ch in value.as_str().chars() { +// if token { token = is_token_char(ch) } +// if qtext { +// if had_slash { +// qtext = is_vchar(ch, mail_type) || is_ws(ch); +// had_slash = false; +// } else if ch == '\\' { +// had_slash = true; +// } else { +// qtext = is_qtext(ch, mail_type) || is_ws(ch) +// } +// } +// } +// qtext = qtext & !had_slash; +// +// if token || qtext { +// handle.write_str(SoftAsciiStr::from_unchecked(name.as_str()))?; +// handle.write_char(SoftAsciiChar::from_unchecked('='))?; +// if token { +// handle.write_str(SoftAsciiStr::from_unchecked(value.as_str()))?; +// } else if qtext { +// handle.write_char(SoftAsciiChar::from_unchecked('\"'))?; +// handle.write_str_unchecked(value.as_str())?; +// handle.write_char(SoftAsciiChar::from_unchecked('\"'))?; +// } +// } else { +// handle.write_str(SoftAsciiStr::from_unchecked(name.as_str()))?; +// handle.write_str(SoftAsciiStr::from_unchecked("*=utf8''"))?; +// let encoded = percent_encode_param_value(value.as_str()); +// handle.write_str(&*encoded)?; +// } +// } +// } +// } +// +// if !split_params.is_empty() { +// for (name, parts) in split_params.iter_mut() { +// let mut counter = 0; +// while let Some(&(val, is_enc)) = parts.get(&counter) { +// let val = val.as_str(); +// //TODO implement quoting/encoding of section parameters +// if is_enc { +// if val.len() == 0 || !is_token(val) { +// bail!(InvalidMime(mime.to_string())); +// } +// //FIXME as as_str is not the representation this will won't work +// } else if val.starts_with(r#"""#) { +// if !is_quoted_string(val, mail_type) { +// bail!(InvalidMimeRq(mime.to_string())); +// } +// } else { +// if !is_token(val) { +// bail!(InvalidMimeRq(mime.to_string())); +// } +// } +// counter += 1; +// } +// +// if counter as usize != parts.len() { +// bail!(InvalidMime(mime.to_string())) +// } +// +// for (section, &(val, is_enc)) in parts.iter() { +// handle.mark_fws_pos(); +// handle.write_char(SoftAsciiChar::from_unchecked(';'))?; +// handle.mark_fws_pos(); +// handle.write_str(SoftAsciiStr::from_unchecked(name))?; +// handle.write_char(SoftAsciiChar::from_unchecked('*'))?; +// //OPTIMIZE (have 3 byte scretch memory as to_string 1. is ascii 2. len <= 3 +// handle.write_str(SoftAsciiStr::from_unchecked(&*section.to_string()))?; +// if is_enc { +// handle.write_char(SoftAsciiChar::from_unchecked('*'))?; +// } +// handle.write_char(SoftAsciiChar::from_unchecked('='))?; +// handle.write_str_unchecked(val.as_str())?; +// } +// } +// } +// Ok(()) +//} +// +////FIXME we could use nom for it is's already imported anyway +//fn get_split_num<'a, 'b: 'a>(param_name: &'a EName<'b>) -> Result<(Option<(&'b str, u8)>, bool)> { +// let param_name = param_name.as_str(); +// let mut iter = param_name.chars(); +// let mut last = iter.next_back(); +// let (end_idx, is_encoded) = +// if Some('*') == last { +// last = iter.next_back(); +// (param_name.len() - 1, true) +// } else { +// (param_name.len(), false) +// }; +// let mut start_idx = end_idx; +// while let Some(ch) = last { +// //-=1 is ok as Mime already makes sure parameter names are ascii only +// // even more we break on any non ascii chars anyway so even if wrong data +// // is passed in this will not panic when slicing +// start_idx -= 1; +// if !ch.is_digit(10) { +// if ch == '*' { +// // do not include the section starting * e.g. abc*1* => (Some((abc,1)),true) +// let actual_name = ¶m_name[..start_idx]; +// let section: u8 = param_name[start_idx+1..end_idx] +// .parse() +// //we now it's a number, so the only error can be Overflow +// .map_err(|_| error!(MimeSectionOverflow))?; +// +// return Ok((Some((actual_name, section)), is_encoded)); +// } else { +// return Ok((None, is_encoded)); +// } +// } +// +// last = iter.next_back(); +// } +// return Ok((None, is_encoded)) +//} + + +#[cfg(test)] +mod test { + use super::*; + + ec_test!{ writing_encoded, { + MediaType::try_from("text/plain; arbitrary*=utf8''this%20is%it")? + } => ascii => [ + Text "text/plain", + MarkFWS, + Text ";", + MarkFWS, + Text " arbitrary*=utf8''this%20is%it" + ]} + + ec_test!{ writing_normal, { + MediaType::try_from("text/plain; a=abc")? + } => ascii => [ + Text "text/plain", + MarkFWS, + Text ";", + MarkFWS, + Text " a=abc" + ]} + + ec_test!{ writing_needless_quoted, { + MediaType::try_from("text/plain; a=\"abc\"")? + } => ascii => [ + Text "text/plain", + MarkFWS, + Text ";", + MarkFWS, + Text " a=\"abc\"" + ]} + + ec_test!{ writing_quoted, { + MediaType::try_from("text/plain; a=\"abc def\"")? + } => ascii => [ + Text "text/plain", + MarkFWS, + Text ";", + MarkFWS, + Text " a=\"abc def\"" + ]} + + ec_test!{ writing_quoted_with_escape, { + MediaType::try_from("text/plain; a=\"abc\\ def\"")? + } => ascii => [ + Text "text/plain", + MarkFWS, + Text ";", + MarkFWS, + Text " a=\"abc\\ def\"" + ]} + + ec_test!{ writing_quoted_utf8, { + MediaType::try_from("text/plain; a=\"←→\"")? + } => utf8 => [ + Text "text/plain", + MarkFWS, + Text ";", + MarkFWS, + Text " a=\"←→\"" + ]} + + ec_test!{ #[ignore] writing_quoted_needed_encoding, { + MediaType::try_from("text/plain; a=\"←→\"")? + } => ascii => [ + Text "text/plain", + MarkFWS, + Text ";", + MarkFWS, + Text " a*=utf8\'\'%E2%86%90%E2%86%92" + ]} + + ec_test!{ writing_parts_simple, { + MediaType::try_from("text/plain; a*0=abc; a*1=\" def\"")? + } => ascii => [ + Text "text/plain", + MarkFWS, + Text ";", + MarkFWS, + Text " a*0=abc", + MarkFWS, + Text ";", + MarkFWS, + Text " a*1=\" def\"" + ]} + + //TODO media type needs parts awareness + // i.e. currently it would do a*1=\"↓\"" => "a*1*=utf-8''%E2%86%93" which is wrong + // as it's not the first part and it does not know about parts + ec_test!{ #[ignore] writing_parts_needs_encoding_not_first, { + MediaType::try_from("text/plain; a*0=abc; a*1=\"↓\"")? + } => ascii => [ + Text "text/plain", + MarkFWS, + Text ";", + MarkFWS, + Text " a*0*=utf8''abc", + MarkFWS, + Text ";", + MarkFWS, + Text " a*1*=%E2%86%93" + ]} + + + + +}
\ No newline at end of file |