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