data_url/
mime.rs

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(Clone, 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    /// Construct a new [`Mime`] with the given `type_` and `subtype` and an
16    /// empty parameter list.
17    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    /// Return true if this [`Mime`] matches a given type and subtype, regardless
26    /// of what parameters it has.
27    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
54/// <https://mimesniff.spec.whatwg.org/#parsing-a-mime-type>
55impl 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        // We can not early return on an invalid name here, because the value
98        // parsing later may consume more semicolon seperated pieces.
99        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); // without end quote
104                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                                        // A semicolon inside a quoted value is not a separator
115                                        // for the next parameter, but part of the value.
116                                        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                        // A semicolon inside a quoted value is not a separator
126                        // for the next parameter, but part of the value.
127                        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        // <https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point>
159        matches!(c, '\t' | ' '..='~' | '\u{80}'..='\u{FF}')
160    })
161}
162
163/// <https://mimesniff.spec.whatwg.org/#serializing-a-mime-type>
164impl 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// Copied from https://github.com/hyperium/mime/blob/v0.3.5/src/parse.rs#L293
205#[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}