1use std::fmt;
20use std::str::FromStr;
21
22use crate::Error;
23
24pub(crate) type Parameters = Vec<(String, String)>;
25
26#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct MimeType {
33 pub(crate) essence: String,
34 pub(crate) parameters: Parameters,
35}
36
37impl MimeType {
38 pub fn parse(input: &str) -> Result<MimeType, Error> {
40 parse(input).ok_or(Error::InvalidMediaType)
41 }
42
43 pub fn essence(&self) -> &str {
45 &self.essence
46 }
47
48 pub fn main_type(&self) -> &str {
50 self.essence
51 .split_once('/')
52 .map_or("", |(main_type, _)| main_type)
53 }
54
55 pub fn subtype(&self) -> &str {
57 self.essence
58 .split_once('/')
59 .map_or("", |(_, subtype)| subtype)
60 }
61
62 pub fn parameters(&self) -> impl Iterator<Item = (&str, &str)> {
64 self.parameters
65 .iter()
66 .map(|(n, v)| (n.as_str(), v.as_str()))
67 }
68
69 pub fn parameter(&self, name: &str) -> Option<&str> {
71 self.parameters
72 .iter()
73 .find(|(n, _)| n.eq_ignore_ascii_case(name))
74 .map(|(_, v)| v.as_str())
75 }
76
77 pub fn charset(&self) -> Option<&str> {
79 self.parameter("charset")
80 }
81}
82
83impl fmt::Display for MimeType {
84 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
85 let mut output = self.essence.clone();
86 serialize_parameters(&mut output, &self.parameters);
87 f.write_str(&output)
88 }
89}
90
91impl FromStr for MimeType {
92 type Err = Error;
93
94 fn from_str(input: &str) -> Result<MimeType, Error> {
95 MimeType::parse(input)
96 }
97}
98
99impl TryFrom<&str> for MimeType {
100 type Error = Error;
101
102 fn try_from(input: &str) -> Result<MimeType, Error> {
103 MimeType::parse(input)
104 }
105}
106
107pub(crate) fn is_http_whitespace(c: char) -> bool {
108 matches!(c, '\t' | '\n' | '\r' | ' ')
109}
110
111pub(crate) fn is_token_char(c: char) -> bool {
112 c.is_ascii_alphanumeric() || "!#$%&'*+-.^_`|~".contains(c)
113}
114
115fn is_quoted_string_token_char(c: char) -> bool {
116 matches!(c, '\t' | ' '..='~' | '\u{80}'..='\u{ff}')
117}
118
119pub(crate) fn is_valid_essence(type_or_subtype: &str) -> bool {
120 !type_or_subtype.is_empty() && type_or_subtype.chars().all(is_token_char)
121}
122
123pub(crate) fn parse(input: &str) -> Option<MimeType> {
125 let s = input.trim_matches(is_http_whitespace);
126 let b = s.as_bytes();
127
128 let (top_level_type, rest) = s.split_once('/')?;
129 if !is_valid_essence(top_level_type) {
130 return None;
131 }
132
133 let subtype_end = rest.find(';').unwrap_or(rest.len());
134 let subtype = rest[..subtype_end].trim_end_matches(is_http_whitespace);
135 if !is_valid_essence(subtype) {
136 return None;
137 }
138
139 let mut mime_type = MimeType {
140 essence: format!(
141 "{}/{}",
142 top_level_type.to_ascii_lowercase(),
143 subtype.to_ascii_lowercase()
144 ),
145 parameters: Vec::new(),
146 };
147
148 let mut pos = top_level_type.len() + 1 + subtype_end;
150 while pos < b.len() {
151 pos += 1;
153 while pos < b.len() && is_http_whitespace(b[pos] as char) {
154 pos += 1;
155 }
156
157 let name_start = pos;
158 while pos < b.len() && b[pos] != b';' && b[pos] != b'=' {
159 pos += 1;
160 }
161 let name = s[name_start..pos].to_ascii_lowercase();
162
163 if pos < b.len() {
164 if b[pos] == b';' {
165 continue;
166 }
167 pos += 1; }
169 if pos >= b.len() {
170 break;
171 }
172
173 let value: String = if b[pos] == b'"' {
174 let (value, new_pos) = collect_http_quoted_string(s, pos);
175 pos = new_pos;
176 while pos < b.len() && b[pos] != b';' {
177 pos += 1;
178 }
179 value
180 } else {
181 let value_start = pos;
182 while pos < b.len() && b[pos] != b';' {
183 pos += 1;
184 }
185 let value = s[value_start..pos].trim_end_matches(is_http_whitespace);
186 if value.is_empty() {
187 continue;
188 }
189 value.to_string()
190 };
191
192 if !name.is_empty()
193 && name.chars().all(is_token_char)
194 && value.chars().all(is_quoted_string_token_char)
195 && !mime_type.parameters.iter().any(|(n, _)| *n == name)
196 {
197 mime_type.parameters.push((name, value));
198 }
199 }
200
201 Some(mime_type)
202}
203
204fn collect_http_quoted_string(s: &str, mut pos: usize) -> (String, usize) {
206 let b = s.as_bytes();
207 let mut value = String::new();
208
209 pos += 1; loop {
211 let start = pos;
212 while pos < b.len() && b[pos] != b'"' && b[pos] != b'\\' {
213 pos += 1;
214 }
215 value.push_str(&s[start..pos]);
216
217 if pos >= b.len() {
218 break;
219 }
220
221 let quote_or_backslash = b[pos];
222 pos += 1;
223 if quote_or_backslash == b'\\' {
224 match s[pos..].chars().next() {
225 Some(c) => {
226 value.push(c);
227 pos += c.len_utf8();
228 }
229 None => {
230 value.push('\\');
231 break;
232 }
233 }
234 } else {
235 break;
236 }
237 }
238
239 (value, pos)
240}
241
242pub(crate) fn serialize_parameters(output: &mut String, parameters: &[(String, String)]) {
243 for (name, value) in parameters {
244 output.push(';');
245 output.push_str(name);
246 output.push('=');
247 if !value.is_empty() && value.chars().all(is_token_char) {
248 output.push_str(value);
249 } else {
250 output.push('"');
251 for c in value.chars() {
252 if c == '"' || c == '\\' {
253 output.push('\\');
254 }
255 output.push(c);
256 }
257 output.push('"');
258 }
259 }
260}
261
262#[cfg(test)]
263#[path = "../tests/common/json.rs"]
264mod json;
265
266#[cfg(test)]
267mod tests {
268 use super::json::{self, Json};
269 use super::*;
270 use encoding_rs::Encoding;
271
272 fn run_wpt_mime_type_tests(fixture: &str) {
273 let mut count = 0;
274
275 for test in json::parse(fixture).as_array() {
276 if let Json::Object(_) = test {
278 let input = test.get("input").as_str();
279 let expected = test.get("output");
280
281 match parse(input) {
282 Some(mime_type) => {
283 let mut serialized = mime_type.essence.clone();
284 serialize_parameters(&mut serialized, &mime_type.parameters);
285 assert_eq!(
286 Some(serialized.as_str()),
287 expected.as_opt_str(),
288 "{input:?}"
289 );
290
291 if let Some(expected_encoding) = test.get_opt("encoding") {
293 let encoding = mime_type
294 .parameters
295 .iter()
296 .find(|(name, _)| name == "charset")
297 .and_then(|(_, value)| Encoding::for_label(value.as_bytes()))
298 .map(|e| e.name());
299 assert_eq!(encoding, expected_encoding.as_opt_str(), "{input:?}");
300 }
301 }
302 None => assert!(expected.is_null(), "{input:?} must not be parsed"),
303 }
304
305 count += 1;
306 }
307 }
308
309 assert!(count > 0);
310 }
311
312 #[test]
313 fn wpt_mime_types() {
314 run_wpt_mime_type_tests(include_str!("../tests/wpt/mime-types.json"));
315 }
316
317 #[test]
318 fn wpt_generated_mime_types() {
319 run_wpt_mime_type_tests(include_str!("../tests/wpt/generated-mime-types.json"));
320 }
321}