Skip to main content

media_type/
parse.rs

1//! Parsing of media types.
2
3use alloc::string::String;
4use core::{error::Error, fmt};
5
6use super::{MediaType, Source};
7
8/// An error returned when parsing a [`MediaType`] from a string.
9///
10/// The [`Display`](fmt::Display) implementation describes what went wrong and, when relevant, the
11/// position of the offending byte.
12#[derive(Debug)]
13pub struct FromStrError {
14    inner: ParseError,
15}
16
17impl FromStrError {
18    pub(crate) fn new(inner: ParseError) -> Self {
19        Self {
20            inner,
21        }
22    }
23}
24
25impl fmt::Display for FromStrError {
26    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(formatter, "media type parse error: {}", self.inner)
28    }
29}
30
31impl Error for FromStrError {}
32
33/// The reason a media type could not be parsed.
34#[derive(Debug)]
35pub(crate) enum ParseError {
36    /// A `/` was missing between the type and the subtype.
37    MissingSlash,
38    /// An `=` was missing between a parameter name and its value.
39    MissingEqual,
40    /// A closing `"` was missing from a parameter value.
41    MissingQuote,
42    /// An invalid byte was encountered at the given position.
43    InvalidToken {
44        /// The byte offset of the invalid token.
45        pos: usize,
46        /// The invalid byte.
47        byte: u8,
48    },
49}
50
51impl ParseError {
52    fn message(&self) -> &'static str {
53        match self {
54            ParseError::MissingSlash => "a slash (/) was missing between the type and subtype",
55            ParseError::MissingEqual => "an equals sign (=) was missing between a parameter and its value",
56            ParseError::MissingQuote => "a quote (\") was missing from a parameter value",
57            ParseError::InvalidToken {
58                ..
59            } => "an invalid token was encountered",
60        }
61    }
62}
63
64impl fmt::Display for ParseError {
65    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            ParseError::InvalidToken {
68                pos,
69                byte,
70            } => {
71                write!(formatter, "{}, {:X} at position {}", self.message(), byte, pos)
72            }
73            _ => formatter.write_str(self.message()),
74        }
75    }
76}
77
78impl Error for ParseError {}
79
80/// An iterator over the media types found in a string.
81///
82/// This is useful to parse header fields such as `Accept` or `Content-Type`, which may contain a
83/// comma-separated list of media types. Each item is either a parsed [`MediaType`] or the slice of
84/// input that could not be parsed.
85///
86/// # Examples
87///
88/// ```
89/// use media_type::MediaTypeIter;
90///
91/// let mut iter = MediaTypeIter::new("text/html, application/json, not-a-media-type, text/xml");
92///
93/// assert_eq!(iter.next().unwrap().unwrap(), media_type::TEXT_HTML);
94/// assert_eq!(iter.next().unwrap().unwrap(), media_type::APPLICATION_JSON);
95/// assert_eq!(iter.next().unwrap().unwrap_err(), "not-a-media-type");
96/// assert_eq!(iter.next().unwrap().unwrap(), media_type::TEXT_XML);
97/// assert!(iter.next().is_none());
98/// ```
99#[derive(Clone, Debug)]
100pub struct MediaTypeIter<'a> {
101    pos: usize,
102    source: &'a str,
103}
104
105impl<'a> MediaTypeIter<'a> {
106    /// Create a new iterator over the media types contained in `source`.
107    pub fn new(source: &'a str) -> Self {
108        Self {
109            pos: 0,
110            source,
111        }
112    }
113}
114
115impl<'a> Iterator for MediaTypeIter<'a> {
116    type Item = Result<MediaType, &'a str>;
117
118    fn next(&mut self) -> Option<Self::Item> {
119        let start = self.pos;
120        let len = self.source.len();
121
122        if start >= len {
123            return None;
124        }
125
126        // Try parsing the whole remaining slice.
127        match parse(&self.source[start..len]) {
128            Ok(value) => {
129                self.pos = len;
130                Some(Ok(value))
131            }
132            Err(ParseError::InvalidToken {
133                pos, ..
134            }) => {
135                // The first token is immediately wrong: skip it and retry.
136                if pos == 0 {
137                    self.pos += 1;
138                    return self.next();
139                }
140
141                // Try parsing the longest slice up to the first invalid token.
142                let slice = &self.source[start..start + pos];
143                match parse(slice) {
144                    Ok(media_type) => {
145                        self.pos = start + pos + 1;
146                        Some(Ok(media_type))
147                    }
148                    Err(_) => {
149                        if start + pos < len {
150                            // Report the invalid slice and continue after it.
151                            self.pos = start + pos;
152                            Some(Err(slice))
153                        } else {
154                            None
155                        }
156                    }
157                }
158            }
159            // A missing character means the remaining slice is malformed: stop there.
160            Err(_) => None,
161        }
162    }
163}
164
165/// Parse a media type.
166///
167/// The returned `MediaType` is stored in canonical form (lowercase type, subtype and parameter
168/// names; `; ` separators; unquoted values).
169pub(crate) fn parse(s: &str) -> Result<MediaType, ParseError> {
170    if s == "*/*" {
171        return Ok(MediaType::from_static("*/*"));
172    }
173
174    let bytes = s.as_bytes();
175    let len = bytes.len();
176
177    // Top-level type.
178    let mut i = 0;
179    loop {
180        if i >= len {
181            return Err(ParseError::MissingSlash);
182        }
183        let byte = bytes[i];
184        if is_token(byte) {
185            i += 1;
186        } else if byte == b'/' && i > 0 {
187            break;
188        } else {
189            return Err(ParseError::InvalidToken {
190                pos: i,
191                byte,
192            });
193        }
194    }
195
196    // Subtype.
197    let subtype_start = i + 1;
198    let semicolon;
199    loop {
200        if i + 1 >= len {
201            // No parameters: the whole string is the media type.
202            return Ok(MediaType {
203                source: Source::Dynamic(s.to_ascii_lowercase()),
204            });
205        }
206        i += 1;
207        let byte = bytes[i];
208        if byte == b';' && i > subtype_start {
209            semicolon = i;
210            break;
211        } else if is_token(byte) {
212            continue;
213        } else {
214            return Err(ParseError::InvalidToken {
215                pos: i,
216                byte,
217            });
218        }
219    }
220
221    // Parameters: start from the canonical essence.
222    let mut out = String::with_capacity(len + 2);
223    out.push_str(&s[..semicolon].to_ascii_lowercase());
224
225    let mut i = semicolon;
226    loop {
227        // Consume the ';' and any optional whitespace.
228        i += 1;
229        while i < len && (bytes[i] == b' ' || bytes[i] == b'\t') {
230            i += 1;
231        }
232        if i >= len {
233            break;
234        }
235
236        // Parameter name.
237        let name_start = i;
238        while i < len && is_token(bytes[i]) {
239            i += 1;
240        }
241        if i == name_start {
242            return Err(ParseError::InvalidToken {
243                pos: i,
244                byte: bytes.get(i).copied().unwrap_or(0),
245            });
246        }
247        if i >= len || bytes[i] != b'=' {
248            return Err(ParseError::MissingEqual);
249        }
250        let name_end = i;
251        i += 1;
252
253        // Parameter value: either a quoted-string or a token.
254        let value;
255        if i < len && bytes[i] == b'"' {
256            i += 1;
257            let value_start = i;
258            while i < len && bytes[i] != b'"' {
259                let byte = bytes[i];
260                if byte < 0x20 || byte == 0x7f {
261                    return Err(ParseError::InvalidToken {
262                        pos: i,
263                        byte,
264                    });
265                }
266                i += 1;
267            }
268            if i >= len {
269                return Err(ParseError::MissingQuote);
270            }
271            value = &s[value_start..i];
272            i += 1;
273        } else {
274            let value_start = i;
275            while i < len && is_token(bytes[i]) {
276                i += 1;
277            }
278            value = &s[value_start..i];
279        }
280
281        let name = &s[name_start..name_end];
282        out.push_str("; ");
283        out.push_str(&name.to_ascii_lowercase());
284        out.push('=');
285        if name.eq_ignore_ascii_case(super::CHARSET) {
286            out.push_str(&value.to_ascii_lowercase());
287        } else {
288            out.push_str(value);
289        }
290
291        // After the value, allow optional whitespace followed by a ';' or the end of the input.
292        while i < len && (bytes[i] == b' ' || bytes[i] == b'\t') {
293            i += 1;
294        }
295        if i >= len {
296            break;
297        }
298        if bytes[i] != b';' {
299            return Err(ParseError::InvalidToken {
300                pos: i,
301                byte: bytes[i],
302            });
303        }
304    }
305
306    Ok(MediaType {
307        source: Source::Dynamic(out),
308    })
309}
310
311// From [RFC 6838](http://tools.ietf.org/html/rfc6838#section-4.2) and
312// [RFC 7231](https://tools.ietf.org/html/rfc7231#section-3.1.1.1):
313//
314//     token  = 1*tchar
315//     tchar  = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
316//              "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
317macro_rules! byte_map {
318    ($($flag:expr,)*) => {
319        [$($flag != 0,)*]
320    };
321}
322
323static TOKEN_MAP: [bool; 256] = byte_map![
324    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1,
325    1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
326    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
327    1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
328    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
329    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
330    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
331];
332
333#[inline]
334fn is_token(byte: u8) -> bool {
335    TOKEN_MAP[byte as usize]
336}
337
338#[cfg(test)]
339mod tests {
340    use alloc::string::ToString;
341
342    use super::*;
343
344    #[test]
345    fn test_lookup_tables() {
346        for (i, &valid) in TOKEN_MAP.iter().enumerate() {
347            let i = i as u8;
348            let should = matches!(
349                i,
350                b'a'..=b'z'
351                    | b'A'..=b'Z'
352                    | b'0'..=b'9'
353                    | b'!'
354                    | b'#'
355                    | b'$'
356                    | b'%'
357                    | b'&'
358                    | b'\''
359                    | b'*'
360                    | b'+'
361                    | b'-'
362                    | b'.'
363                    | b'^'
364                    | b'_'
365                    | b'`'
366                    | b'|'
367                    | b'~'
368            );
369            assert_eq!(valid, should, "{:?} ({}) should be {}", i as char, i, should);
370        }
371    }
372
373    #[test]
374    fn test_parse_iterator() {
375        let mut iter = MediaTypeIter::new("application/json, application/json");
376        assert_eq!(iter.next().unwrap().unwrap(), "application/json");
377        assert_eq!(iter.next().unwrap().unwrap(), "application/json");
378        assert_eq!(iter.next(), None);
379
380        let mut iter = MediaTypeIter::new("application/json");
381        assert_eq!(iter.next().unwrap().unwrap(), "application/json");
382        assert_eq!(iter.next(), None);
383
384        let mut iter = MediaTypeIter::new("application/json;  ");
385        assert_eq!(iter.next().unwrap().unwrap(), "application/json");
386        assert_eq!(iter.next(), None);
387    }
388
389    #[test]
390    fn test_parse_iterator_invalid() {
391        let mut iter = MediaTypeIter::new("application/json, invalid, application/json");
392        assert_eq!(iter.next().unwrap().unwrap(), "application/json");
393        assert_eq!(iter.next().unwrap().unwrap_err(), "invalid");
394        assert_eq!(iter.next().unwrap().unwrap(), "application/json");
395        assert_eq!(iter.next(), None);
396    }
397
398    #[test]
399    fn test_parse_iterator_leading_invalid() {
400        let mut iter = MediaTypeIter::new(";;application/json");
401        assert_eq!(iter.next().unwrap().unwrap(), "application/json");
402        assert_eq!(iter.next(), None);
403    }
404
405    #[test]
406    fn test_parse_canonical_form() {
407        assert_eq!(
408            "TEXT/PLAIN; CHARSET=UTF-8; FOO=BAR"
409                .parse::<MediaType>()
410                .unwrap()
411                .to_string(),
412            "text/plain; charset=utf-8; foo=BAR"
413        );
414        assert_eq!(
415            "text/plain;charset=\"utf-8\"".parse::<MediaType>().unwrap(),
416            super::super::TEXT_PLAIN_UTF_8
417        );
418    }
419
420    #[test]
421    fn test_parse_errors() {
422        assert!("f o o / bar".parse::<MediaType>().is_err());
423        assert!("text\n/plain".parse::<MediaType>().is_err());
424        assert!("text\r/plain".parse::<MediaType>().is_err());
425        assert!("text/\r\nplain".parse::<MediaType>().is_err());
426        assert!("text/plain;\r\ncharset=utf-8".parse::<MediaType>().is_err());
427        assert!("text/plain; charset=\r\nutf-8".parse::<MediaType>().is_err());
428        assert!("text/plain; charset=\"\r\nutf-8\"".parse::<MediaType>().is_err());
429    }
430
431    #[test]
432    fn test_from_static_and_from_str_are_equal() {
433        let parsed = "application/json".parse::<MediaType>().unwrap();
434        assert_eq!(parsed, super::super::APPLICATION_JSON);
435    }
436}