1use alloc::{borrow::ToOwned, string::String, vec::Vec};
2use core::fmt::{self, Write};
3use core::str::FromStr;
4
5#[derive(Clone, 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 new(type_: &str, subtype: &str) -> Self {
18 Self {
19 type_: type_.into(),
20 subtype: subtype.into(),
21 parameters: vec![],
22 }
23 }
24
25 pub fn matches(&self, type_: &str, subtype: &str) -> bool {
28 self.type_ == type_ && self.subtype == subtype
29 }
30
31 pub fn get_parameter<P>(&self, name: &P) -> Option<&str>
32 where
33 P: ?Sized + PartialEq<str>,
34 {
35 self.parameters
36 .iter()
37 .find(|&(n, _)| name == &**n)
38 .map(|(_, v)| &**v)
39 }
40}
41
42#[derive(Debug)]
43pub struct MimeParsingError(());
44
45impl fmt::Display for MimeParsingError {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 write!(f, "invalid mime type")
48 }
49}
50
51#[cfg(feature = "std")]
52impl std::error::Error for MimeParsingError {}
53
54impl FromStr for Mime {
56 type Err = MimeParsingError;
57
58 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 parse(s).ok_or(MimeParsingError(()))
60 }
61}
62
63fn parse(s: &str) -> Option<Mime> {
64 let trimmed = s.trim_matches(http_whitespace);
65
66 let (type_, rest) = split2(trimmed, '/');
67 require!(only_http_token_code_points(type_) && !type_.is_empty());
68
69 let (subtype, rest) = split2(rest?, ';');
70 let subtype = subtype.trim_end_matches(http_whitespace);
71 require!(only_http_token_code_points(subtype) && !subtype.is_empty());
72
73 let mut parameters = Vec::new();
74 if let Some(rest) = rest {
75 parse_parameters(rest, &mut parameters)
76 }
77
78 Some(Mime {
79 type_: type_.to_ascii_lowercase(),
80 subtype: subtype.to_ascii_lowercase(),
81 parameters,
82 })
83}
84
85fn split2(s: &str, separator: char) -> (&str, Option<&str>) {
86 let mut iter = s.splitn(2, separator);
87 let first = iter.next().unwrap();
88 (first, iter.next())
89}
90
91fn parse_parameters(s: &str, parameters: &mut Vec<(String, String)>) {
92 let mut semicolon_separated = s.split(';');
93
94 while let Some(piece) = semicolon_separated.next() {
95 let piece = piece.trim_start_matches(http_whitespace);
96 let (name, value) = split2(piece, '=');
97 let name_valid =
100 !name.is_empty() && only_http_token_code_points(name) && !contains(parameters, name);
101 if let Some(value) = value {
102 let value = if let Some(stripped) = value.strip_prefix('"') {
103 let max_len = stripped.len().saturating_sub(1); let mut unescaped_value = String::with_capacity(max_len);
105 let mut chars = stripped.chars();
106 'until_closing_quote: loop {
107 while let Some(c) = chars.next() {
108 match c {
109 '"' => break 'until_closing_quote,
110 '\\' => unescaped_value.push(chars.next().unwrap_or_else(|| {
111 semicolon_separated
112 .next()
113 .map(|piece| {
114 chars = piece.chars();
117 ';'
118 })
119 .unwrap_or('\\')
120 })),
121 _ => unescaped_value.push(c),
122 }
123 }
124 if let Some(piece) = semicolon_separated.next() {
125 unescaped_value.push(';');
128 chars = piece.chars()
129 } else {
130 break;
131 }
132 }
133 if !name_valid || !valid_value(value) {
134 continue;
135 }
136 unescaped_value
137 } else {
138 let value = value.trim_end_matches(http_whitespace);
139 if value.is_empty() {
140 continue;
141 }
142 if !name_valid || !valid_value(value) {
143 continue;
144 }
145 value.to_owned()
146 };
147 parameters.push((name.to_ascii_lowercase(), value))
148 }
149 }
150}
151
152fn contains(parameters: &[(String, String)], name: &str) -> bool {
153 parameters.iter().any(|(n, _)| n == name)
154}
155
156fn valid_value(s: &str) -> bool {
157 s.chars().all(|c| {
158 matches!(c, '\t' | ' '..='~' | '\u{80}'..='\u{FF}')
160 })
161}
162
163impl fmt::Display for Mime {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.write_str(&self.type_)?;
167 f.write_str("/")?;
168 f.write_str(&self.subtype)?;
169 for (name, value) in &self.parameters {
170 f.write_str(";")?;
171 f.write_str(name)?;
172 f.write_str("=")?;
173 if only_http_token_code_points(value) && !value.is_empty() {
174 f.write_str(value)?
175 } else {
176 f.write_str("\"")?;
177 for c in value.chars() {
178 if c == '"' || c == '\\' {
179 f.write_str("\\")?
180 }
181 f.write_char(c)?
182 }
183 f.write_str("\"")?
184 }
185 }
186 Ok(())
187 }
188}
189
190fn http_whitespace(c: char) -> bool {
191 matches!(c, ' ' | '\t' | '\n' | '\r')
192}
193
194fn only_http_token_code_points(s: &str) -> bool {
195 s.bytes().all(|byte| IS_HTTP_TOKEN[byte as usize])
196}
197
198macro_rules! byte_map {
199 ($($flag:expr,)*) => ([
200 $($flag != 0,)*
201 ])
202}
203
204#[rustfmt::skip]
206static IS_HTTP_TOKEN: [bool; 256] = byte_map![
207 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
208 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
209 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0,
210 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
211 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
212 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
213 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
214 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0,
215 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
216 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
217 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
218 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
219 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
220 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
221 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
222 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
223];
224
225#[test]
226fn test_basic_mime() {
227 let mime = Mime::new("text", "plain");
228 assert!(mime.matches("text", "plain"));
229
230 let cloned = mime.clone();
231 assert!(cloned.matches("text", "plain"));
232
233 let mime = Mime {
234 type_: "text".into(),
235 subtype: "html".into(),
236 parameters: vec![("one".into(), "two".into())],
237 };
238 assert!(mime.matches("text", "html"));
239}