Skip to main content

mime/
mime.rs

1use omnom::prelude::*;
2use std::collections::HashMap;
3use std::io::{BufRead, Cursor, Read};
4
5fn main() {
6    assert_eq!(
7        parse_mime("text/html").unwrap(),
8        Mime {
9            base_type: "text".to_string(),
10            sub_type: "html".to_string(),
11            parameters: None,
12        }
13    );
14
15    let mut parameters = HashMap::new();
16    parameters.insert("charset".to_string(), "utf-8".to_string());
17    assert_eq!(
18        parse_mime("text/html; charset=utf-8;").unwrap(),
19        Mime {
20            base_type: "text".to_string(),
21            sub_type: "html".to_string(),
22            parameters: Some(parameters),
23        }
24    );
25}
26
27#[derive(Eq, PartialEq, Debug)]
28pub struct Mime {
29    base_type: String,
30    sub_type: String,
31    parameters: Option<HashMap<String, String>>,
32}
33
34fn parse_mime(s: &str) -> Option<Mime> {
35    // parse the "type"
36    //
37    // ```txt
38    // text/html; charset=utf-8;
39    // ^^^^^
40    // ```
41    let mut s = Cursor::new(s);
42    let mut base_type = vec![];
43    match s.read_until(b'/', &mut base_type).unwrap() {
44        0 => return None,
45        _ => base_type.pop(),
46    };
47    validate_code_points(&base_type)?;
48
49    // parse the "subtype"
50    //
51    // ```txt
52    // text/html; charset=utf-8;
53    //      ^^^^^
54    // ```
55    let mut sub_type = vec![];
56    s.read_until(b';', &mut sub_type).unwrap();
57    if let Some(b';') = sub_type.last() {
58        sub_type.pop();
59    }
60    validate_code_points(&sub_type)?;
61
62    // instantiate our mime struct
63    let mut mime = Mime {
64        base_type: String::from_utf8(base_type).unwrap(),
65        sub_type: String::from_utf8(sub_type).unwrap(),
66        parameters: None,
67    };
68
69    // parse parameters into a hashmap
70    //
71    // ```txt
72    // text/html; charset=utf-8;
73    //           ^^^^^^^^^^^^^^^
74    // ```
75    loop {
76        // Stop parsing if there's no more bytes to consume.
77        if s.fill_buf().unwrap().len() == 0 {
78            break;
79        }
80
81        // Trim any whitespace.
82        //
83        // ```txt
84        // text/html; charset=utf-8;
85        //           ^
86        // ```
87        s.skip_while(is_http_whitespace_char).ok()?;
88
89        // Get the param name.
90        //
91        // ```txt
92        // text/html; charset=utf-8;
93        //            ^^^^^^^
94        // ```
95        let mut param_name = vec![];
96        s.read_while(&mut param_name, |b| b != b';' && b != b'=')
97            .ok()?;
98        validate_code_points(&param_name)?;
99        let mut param_name = String::from_utf8(param_name).ok()?;
100        param_name.make_ascii_lowercase();
101
102        // Ignore param names without values.
103        //
104        // ```txt
105        // text/html; charset=utf-8;
106        //                   ^
107        // ```
108        let mut token = vec![0; 1];
109        s.read_exact(&mut token).unwrap();
110        if token[0] == b';' {
111            continue;
112        }
113
114        // Get the param value.
115        //
116        // ```txt
117        // text/html; charset=utf-8;
118        //                    ^^^^^^
119        // ```
120        let mut param_value = vec![];
121        s.read_until(b';', &mut param_value).ok()?;
122        if let Some(b';') = param_value.last() {
123            param_value.pop();
124        }
125        validate_code_points(&param_value)?;
126        let mut param_value = String::from_utf8(param_value).ok()?;
127        param_value.make_ascii_lowercase();
128
129        // Insert attribute pair into hashmap.
130        if let None = mime.parameters {
131            mime.parameters = Some(HashMap::new());
132        }
133        mime.parameters.as_mut()?.insert(param_name, param_value);
134    }
135
136    Some(mime)
137}
138
139fn validate_code_points(buf: &[u8]) -> Option<()> {
140    let all = buf.iter().all(|b| match b {
141        b'-' | b'!' | b'#' | b'$' | b'%' => true,
142        b'&' | b'\'' | b'*' | b'+' | b'.' => true,
143        b'^' | b'_' | b'`' | b'|' | b'~' => true,
144        b'A'..=b'Z' => true,
145        b'a'..=b'z' => true,
146        b'0'..=b'9' => true,
147        _ => false,
148    });
149
150    if all {
151        Some(())
152    } else {
153        None
154    }
155}
156
157fn is_http_whitespace_char(b: u8) -> bool {
158    match b {
159        b' ' | b'\t' | b'\n' | b'\r' => true,
160        _ => false,
161    }
162}