mime.rs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. use alloc::{borrow::ToOwned, string::String, vec::Vec};
  2. use core::fmt::{self, Write};
  3. use core::str::FromStr;
  4. /// <https://mimesniff.spec.whatwg.org/#mime-type-representation>
  5. #[derive(Clone, Debug, PartialEq, Eq)]
  6. pub struct Mime {
  7. pub type_: String,
  8. pub subtype: String,
  9. /// (name, value)
  10. pub parameters: Vec<(String, String)>,
  11. }
  12. impl Mime {
  13. /// Construct a new [`Mime`] with the given `type_` and `subtype` and an
  14. /// empty parameter list.
  15. pub fn new(type_: &str, subtype: &str) -> Self {
  16. Self {
  17. type_: type_.into(),
  18. subtype: subtype.into(),
  19. parameters: vec![],
  20. }
  21. }
  22. /// Return true if this [`Mime`] matches a given type and subtype, regardless
  23. /// of what parameters it has.
  24. pub fn matches(&self, type_: &str, subtype: &str) -> bool {
  25. self.type_ == type_ && self.subtype == subtype
  26. }
  27. pub fn get_parameter<P>(&self, name: &P) -> Option<&str>
  28. where
  29. P: ?Sized + PartialEq<str>,
  30. {
  31. self.parameters
  32. .iter()
  33. .find(|&(n, _)| name == &**n)
  34. .map(|(_, v)| &**v)
  35. }
  36. }
  37. #[derive(Debug)]
  38. pub struct MimeParsingError(());
  39. impl fmt::Display for MimeParsingError {
  40. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  41. write!(f, "invalid mime type")
  42. }
  43. }
  44. #[cfg(feature = "std")]
  45. impl std::error::Error for MimeParsingError {}
  46. /// <https://mimesniff.spec.whatwg.org/#parsing-a-mime-type>
  47. impl FromStr for Mime {
  48. type Err = MimeParsingError;
  49. fn from_str(s: &str) -> Result<Self, Self::Err> {
  50. parse(s).ok_or(MimeParsingError(()))
  51. }
  52. }
  53. fn parse(s: &str) -> Option<Mime> {
  54. let trimmed = s.trim_matches(http_whitespace);
  55. let (type_, rest) = split2(trimmed, '/');
  56. require!(only_http_token_code_points(type_) && !type_.is_empty());
  57. let (subtype, rest) = split2(rest?, ';');
  58. let subtype = subtype.trim_end_matches(http_whitespace);
  59. require!(only_http_token_code_points(subtype) && !subtype.is_empty());
  60. let mut parameters = Vec::new();
  61. if let Some(rest) = rest {
  62. parse_parameters(rest, &mut parameters)
  63. }
  64. Some(Mime {
  65. type_: type_.to_ascii_lowercase(),
  66. subtype: subtype.to_ascii_lowercase(),
  67. parameters,
  68. })
  69. }
  70. fn split2(s: &str, separator: char) -> (&str, Option<&str>) {
  71. let mut iter = s.splitn(2, separator);
  72. let first = iter.next().unwrap();
  73. (first, iter.next())
  74. }
  75. fn parse_parameters(s: &str, parameters: &mut Vec<(String, String)>) {
  76. let mut semicolon_separated = s.split(';');
  77. while let Some(piece) = semicolon_separated.next() {
  78. let piece = piece.trim_start_matches(http_whitespace);
  79. let (name, value) = split2(piece, '=');
  80. // We can not early return on an invalid name here, because the value
  81. // parsing later may consume more semicolon seperated pieces.
  82. let name_valid =
  83. !name.is_empty() && only_http_token_code_points(name) && !contains(parameters, name);
  84. if let Some(value) = value {
  85. let value = if let Some(stripped) = value.strip_prefix('"') {
  86. let max_len = stripped.len().saturating_sub(1); // without end quote
  87. let mut unescaped_value = String::with_capacity(max_len);
  88. let mut chars = stripped.chars();
  89. 'until_closing_quote: loop {
  90. while let Some(c) = chars.next() {
  91. match c {
  92. '"' => break 'until_closing_quote,
  93. '\\' => unescaped_value.push(chars.next().unwrap_or_else(|| {
  94. semicolon_separated
  95. .next()
  96. .map(|piece| {
  97. // A semicolon inside a quoted value is not a separator
  98. // for the next parameter, but part of the value.
  99. chars = piece.chars();
  100. ';'
  101. })
  102. .unwrap_or('\\')
  103. })),
  104. _ => unescaped_value.push(c),
  105. }
  106. }
  107. if let Some(piece) = semicolon_separated.next() {
  108. // A semicolon inside a quoted value is not a separator
  109. // for the next parameter, but part of the value.
  110. unescaped_value.push(';');
  111. chars = piece.chars()
  112. } else {
  113. break;
  114. }
  115. }
  116. if !name_valid || !valid_value(value) {
  117. continue;
  118. }
  119. unescaped_value
  120. } else {
  121. let value = value.trim_end_matches(http_whitespace);
  122. if value.is_empty() {
  123. continue;
  124. }
  125. if !name_valid || !valid_value(value) {
  126. continue;
  127. }
  128. value.to_owned()
  129. };
  130. parameters.push((name.to_ascii_lowercase(), value))
  131. }
  132. }
  133. }
  134. fn contains(parameters: &[(String, String)], name: &str) -> bool {
  135. parameters.iter().any(|(n, _)| n == name)
  136. }
  137. fn valid_value(s: &str) -> bool {
  138. s.chars().all(|c| {
  139. // <https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point>
  140. matches!(c, '\t' | ' '..='~' | '\u{80}'..='\u{FF}')
  141. })
  142. }
  143. /// <https://mimesniff.spec.whatwg.org/#serializing-a-mime-type>
  144. impl fmt::Display for Mime {
  145. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  146. f.write_str(&self.type_)?;
  147. f.write_str("/")?;
  148. f.write_str(&self.subtype)?;
  149. for (name, value) in &self.parameters {
  150. f.write_str(";")?;
  151. f.write_str(name)?;
  152. f.write_str("=")?;
  153. if only_http_token_code_points(value) && !value.is_empty() {
  154. f.write_str(value)?
  155. } else {
  156. f.write_str("\"")?;
  157. for c in value.chars() {
  158. if c == '"' || c == '\\' {
  159. f.write_str("\\")?
  160. }
  161. f.write_char(c)?
  162. }
  163. f.write_str("\"")?
  164. }
  165. }
  166. Ok(())
  167. }
  168. }
  169. fn http_whitespace(c: char) -> bool {
  170. matches!(c, ' ' | '\t' | '\n' | '\r')
  171. }
  172. fn only_http_token_code_points(s: &str) -> bool {
  173. s.bytes().all(|byte| IS_HTTP_TOKEN[byte as usize])
  174. }
  175. macro_rules! byte_map {
  176. ($($flag:expr,)*) => ([
  177. $($flag != 0,)*
  178. ])
  179. }
  180. // Copied from https://github.com/hyperium/mime/blob/v0.3.5/src/parse.rs#L293
  181. #[rustfmt::skip]
  182. static IS_HTTP_TOKEN: [bool; 256] = byte_map![
  183. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  184. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  185. 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0,
  186. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
  187. 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  188. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
  189. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  190. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0,
  191. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  192. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  193. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  194. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  195. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  196. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  197. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  198. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  199. ];
  200. #[test]
  201. fn test_basic_mime() {
  202. let mime = Mime::new("text", "plain");
  203. assert!(mime.matches("text", "plain"));
  204. let cloned = mime.clone();
  205. assert!(cloned.matches("text", "plain"));
  206. let mime = Mime {
  207. type_: "text".into(),
  208. subtype: "html".into(),
  209. parameters: vec![("one".into(), "two".into())],
  210. };
  211. assert!(mime.matches("text", "html"));
  212. }