1use alloc::{borrow::ToOwned, string::String, vec::Vec};
2use core::fmt::{self, Write};
3use core::str::FromStr;
4
5/// <https://mimesniff.spec.whatwg.org/#mime-type-representation>
6#[derive(Debug, PartialEq, Eq)]
7pub struct Mime {
8 pub type_: String,
9 pub subtype: String,
10 /// (name, value)
11 pub parameters: Vec<(String, String)>,
12}
13
14impl 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)]
27pub struct MimeParsingError(());
28
29impl 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")]
36impl std::error::Error for MimeParsingError {}
37
38/// <https://mimesniff.spec.whatwg.org/#parsing-a-mime-type>
39impl 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
47fn 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
69fn 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
75fn 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
136fn contains(parameters: &[(String, String)], name: &str) -> bool {
137 parameters.iter().any(|(n: &String, _)| n == name)
138}
139
140fn 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>
148impl 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
174fn http_whitespace(c: char) -> bool {
175 matches!(c, ' ' | '\t' | '\n' | '\r')
176}
177
178fn only_http_token_code_points(s: &str) -> bool {
179 s.bytes().all(|byte: u8| IS_HTTP_TOKEN[byte as usize])
180}
181
182macro_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]
190static 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