1use alloc::{borrow::ToOwned, string::String, vec::Vec};
2use core::fmt::{self, Write};
3use core::str::FromStr;
4
5#[derive(Debug, PartialEq, Eq)]
7pub struct Mime {
8 pub type_: String,
9 pub subtype: String,
10 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, _)| name == &**n)
22 .map(|(_, v)| &**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
38impl FromStr for Mime {
40 type Err = MimeParsingError;
41
42 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 parse(s).ok_or(MimeParsingError(()))
44 }
45}
46
47fn parse(s: &str) -> Option<Mime> {
48 let trimmed = s.trim_matches(http_whitespace);
49
50 let (type_, rest) = split2(trimmed, '/');
51 require!(only_http_token_code_points(type_) && !type_.is_empty());
52
53 let (subtype, rest) = split2(rest?, ';');
54 let subtype = subtype.trim_end_matches(http_whitespace);
55 require!(only_http_token_code_points(subtype) && !subtype.is_empty());
56
57 let mut parameters = Vec::new();
58 if let Some(rest) = rest {
59 parse_parameters(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 = s.splitn(2, separator);
71 let first = 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 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); 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 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 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, _)| n == name)
138}
139
140fn valid_value(s: &str) -> bool {
141 s.chars().all(|c| {
142 matches!(c, '\t' | ' '..='~' | '\u{80}'..='\u{FF}')
144 })
145}
146
147impl 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| IS_HTTP_TOKEN[byte as usize])
180}
181
182macro_rules! byte_map {
183 ($($flag:expr,)*) => ([
184 $($flag != 0,)*
185 ])
186}
187
188#[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];