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 | |