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(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, _)| 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
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(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        // 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, _)| n == name)
138}
139
140fn valid_value(s: &str) -> bool {
141    s.chars().all(|c| {
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| 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];