webmachine_rust/
headers.rs

1//! The `headers` deals with parsing and formatting request and response headers
2
3use std::collections::HashMap;
4use std::hash::{Hash, Hasher};
5use std::iter::Peekable;
6use std::str::Chars;
7
8use itertools::Itertools;
9
10const SEPARATORS: [char; 10] = ['(', ')', '<', '>', '@', ',', ';', '=', '{', '}'];
11const VALUE_SEPARATORS: [char; 9] = ['(', ')', '<', '>', '@', ',', ';', '{', '}'];
12
13fn batch(values: &[String]) -> Vec<(String, String)> {
14  values.into_iter().batching(|it| {
15    match it.next() {
16     None => None,
17     Some(x) => match it.next() {
18       None => Some((x.to_string(), "".to_string())),
19       Some(y) => Some((x.to_string(), y.to_string())),
20     }
21    }
22  }).collect()
23}
24
25// value -> [^SEP]* | quoted-string
26fn header_value(chars: &mut Peekable<Chars>, seperators: &[char]) -> String {
27    let mut value = String::new();
28    skip_whitespace(chars);
29    if chars.peek().is_some() && chars.peek().unwrap() == &'"' {
30        chars.next();
31        while chars.peek().is_some() && chars.peek().unwrap() != &'"' {
32            let ch = chars.next().unwrap();
33            match ch {
34                '\\' => {
35                    if chars.peek().is_some() {
36                        value.push(chars.next().unwrap());
37                    } else {
38                        value.push(ch);
39                    }
40                },
41                _ => value.push(ch)
42            }
43        }
44        if chars.peek().is_some() {
45            chars.next();
46        }
47    } else {
48        while chars.peek().is_some() && !seperators.contains(chars.peek().unwrap()) {
49            value.push(chars.next().unwrap())
50        }
51    }
52    value.trim().to_string()
53}
54
55// header -> value [; parameters]
56fn parse_header(s: &str) -> Vec<String> {
57  let mut chars = s.chars().peekable();
58  let header_value = header_value(&mut chars, &VALUE_SEPARATORS);
59  let mut values = vec![header_value];
60  if chars.peek().is_some() && chars.peek().unwrap() == &';' {
61      chars.next();
62      parse_header_parameters(&mut chars, &mut values);
63  }
64  values
65}
66
67// parameters -> parameter [; parameters]
68fn parse_header_parameters(chars: &mut Peekable<Chars>, values: &mut Vec<String>) {
69    parse_header_parameter(chars, values);
70    if chars.peek().is_some() && chars.peek().unwrap() == &';' {
71        chars.next();
72        parse_header_parameters(chars, values);
73    }
74}
75
76// parameter -> attribute [= [value]]
77fn parse_header_parameter(chars: &mut Peekable<Chars>, values: &mut Vec<String>) {
78    values.push(header_value(chars, &SEPARATORS));
79    if chars.peek().is_some() && chars.peek().unwrap() == &'=' {
80        chars.next();
81        parse_header_parameter_value(chars, values);
82    }
83}
84
85// parameter_value -> value | quoted-string
86fn parse_header_parameter_value(chars: &mut Peekable<Chars>, values: &mut Vec<String>) {
87    skip_whitespace(chars);
88    if chars.peek().is_some() && chars.peek().unwrap() == &'"' {
89        chars.next();
90        let mut value = String::new();
91        while chars.peek().is_some() && chars.peek().unwrap() != &'"' {
92            let ch = chars.next().unwrap();
93            match ch {
94                '\\' => {
95                    if chars.peek().is_some() {
96                        value.push(chars.next().unwrap());
97                    } else {
98                        value.push(ch);
99                    }
100                },
101                _ => value.push(ch)
102            }
103        }
104        if chars.peek().is_some() {
105            chars.next();
106        }
107        values.push(value.to_string());
108    } else {
109        values.push(header_value(chars, &[';']));
110    }
111}
112
113fn skip_whitespace(chars: &mut Peekable<Chars>) {
114    while chars.peek().is_some() && chars.peek().unwrap().is_whitespace() {
115        chars.next();
116    }
117}
118
119
120/// Struct to represent a header value and a map of header value parameters
121#[derive(Debug, Clone, Eq)]
122pub struct HeaderValue {
123    /// Value of the header
124    pub value: String,
125    /// Map of header value parameters
126    pub params: HashMap<String, String>,
127    /// If the header should be qouted
128    pub quote: bool
129}
130
131impl HeaderValue {
132  /// Parses a header value string into a HeaderValue struct
133  pub fn parse_string(s: &str) -> HeaderValue {
134    let values = parse_header(s);
135    let (first, second) = values.split_first().unwrap();
136    if second.is_empty() {
137      HeaderValue::basic(first.as_str())
138    } else {
139      HeaderValue {
140        value: first.clone(),
141        params: batch(second).iter()
142          .fold(HashMap::new(), |mut map, params| {
143            if !params.0.is_empty() {
144              map.insert(params.0.clone(), params.1.clone());
145            }
146            map
147          }),
148        quote: false
149      }
150    }
151  }
152
153    /// Creates a basic header value that has no parameters
154    pub fn basic<S: Into<String>>(s: S) -> HeaderValue {
155      HeaderValue {
156        value: s.into(),
157        params: HashMap::new(),
158        quote: false
159      }
160    }
161
162    /// Converts this header value into a string representation
163    pub fn to_string(&self) -> String {
164        let sparams = self.params.iter()
165            .map(|(k, v)| format!("{}={}", k, v))
166            .join("; ");
167        if self.quote {
168            if sparams.is_empty() {
169                format!("\"{}\"", self.value)
170            } else {
171                format!("\"{}\"; {}", self.value, sparams)
172            }
173        } else {
174            if self.params.is_empty() {
175                self.value.clone()
176            } else {
177                format!("{}; {}", self.value, sparams)
178            }
179        }
180    }
181
182    /// Parses a weak ETag value. Weak etags are in the form W/<quoted-string>. Returns the
183    /// contents of the qouted string if it matches, otherwise returns None.
184    pub fn weak_etag(&self) -> Option<String> {
185      if self.value.starts_with("W/") {
186        Some(parse_header(&self.value[2..])[0].clone())
187      } else {
188        None
189      }
190    }
191
192    /// Convertes this header value into a quoted header value
193    pub fn quote(mut self) -> HeaderValue {
194        self.quote = true;
195        self
196    }
197}
198
199impl PartialEq<HeaderValue> for HeaderValue {
200    fn eq(&self, other: &HeaderValue) -> bool {
201        self.value == other.value && self.params == other.params
202    }
203}
204
205impl PartialEq<String> for HeaderValue {
206    fn eq(&self, other: &String) -> bool {
207        self.value == *other
208    }
209}
210
211impl PartialEq<str> for HeaderValue {
212    fn eq(&self, other: &str) -> bool {
213        self.value == *other
214    }
215}
216
217impl Hash for HeaderValue {
218    fn hash<H: Hasher>(&self, state: &mut H) {
219        self.value.hash(state);
220        for (k, v) in self.params.clone() {
221            k.hash(state);
222            v.hash(state);
223        }
224    }
225}
226
227/// Simple macro to convert a string to a `HeaderValue` struct.
228#[macro_export]
229macro_rules! h {
230  ($e:expr) => (HeaderValue::parse_string($e.into()))
231}
232
233#[cfg(test)]
234mod tests {
235  use expectest::prelude::*;
236  use maplit::hashmap;
237
238  use super::*;
239
240  #[test]
241    fn parse_header_value_test() {
242        expect!(HeaderValue::parse_string("")).to(be_equal_to("".to_string()));
243        expect!(HeaderValue::parse_string("A B")).to(be_equal_to("A B".to_string()));
244        expect!(HeaderValue::parse_string("A; B")).to(be_equal_to(HeaderValue {
245            value: "A".to_string(),
246            params: hashmap!{ "B".to_string() => "".to_string() },
247            quote: false
248        }));
249        expect!(HeaderValue::parse_string("text/html;charset=utf-8")).to(be_equal_to(HeaderValue {
250            value: "text/html".to_string(),
251            params: hashmap!{ "charset".to_string() => "utf-8".to_string() },
252            quote: false
253        }));
254        expect!(HeaderValue::parse_string("text/html;charset=UTF-8")).to(be_equal_to(HeaderValue {
255            value: "text/html".to_string(),
256            params: hashmap!{ "charset".to_string() => "UTF-8".to_string() },
257            quote: false
258        }));
259        expect!(HeaderValue::parse_string("Text/HTML;Charset= \"utf-8\"")).to(be_equal_to(HeaderValue {
260            value: "Text/HTML".to_string(),
261            params: hashmap!{ "Charset".to_string() => "utf-8".to_string() },
262            quote: false
263        }));
264        expect!(HeaderValue::parse_string("text/html; charset = \" utf-8 \"")).to(be_equal_to(HeaderValue {
265            value: "text/html".to_string(),
266            params: hashmap!{ "charset".to_string() => " utf-8 ".to_string() },
267            quote: false
268        }));
269        expect!(HeaderValue::parse_string(";")).to(be_equal_to(HeaderValue {
270            value: "".to_string(),
271            params: hashmap!{},
272            quote: false
273        }));
274        expect!(HeaderValue::parse_string("A;b=c=d")).to(be_equal_to(HeaderValue {
275            value: "A".to_string(),
276            params: hashmap!{ "b".to_string() => "c=d".to_string() },
277            quote: false
278        }));
279        expect!(HeaderValue::parse_string("A;b=\"c;d\"")).to(be_equal_to(HeaderValue {
280            value: "A".to_string(),
281            params: hashmap!{ "b".to_string() => "c;d".to_string() },
282            quote: false
283        }));
284        expect!(HeaderValue::parse_string("A;b=\"c\\\"d\"")).to(be_equal_to(HeaderValue {
285            value: "A".to_string(),
286            params: hashmap!{ "b".to_string() => "c\"d".to_string() },
287            quote: false
288        }));
289        expect!(HeaderValue::parse_string("A;b=\"c,d\"")).to(be_equal_to(HeaderValue {
290            value: "A".to_string(),
291            params: hashmap!{ "b".to_string() => "c,d".to_string() },
292            quote: false
293        }));
294        expect!(HeaderValue::parse_string("en;q=0.0")).to(be_equal_to(HeaderValue {
295            value: "en".to_string(),
296            params: hashmap!{ "q".to_string() => "0.0".to_string() },
297            quote: false
298        }));
299    }
300
301    #[test]
302    fn parse_qouted_header_value_test() {
303        expect!(HeaderValue::parse_string("\"*\"")).to(be_equal_to(HeaderValue {
304            value: "*".to_string(),
305            params: hashmap!{},
306            quote: false
307        }));
308        expect!(HeaderValue::parse_string(" \"quoted; value\"")).to(be_equal_to(HeaderValue {
309            value: "quoted; value".to_string(),
310            params: hashmap!{},
311            quote: false
312        }));
313    }
314
315    #[test]
316    fn parse_etag_header_value_test() {
317        let etag = "\"1234567890\"";
318        let weak_etag = "W/\"1234567890\"";
319
320        let header = HeaderValue::parse_string(etag);
321        expect!(header.clone()).to(be_equal_to(HeaderValue {
322            value: "1234567890".to_string(),
323            params: hashmap!{},
324            quote: false
325        }));
326        expect!(header.weak_etag()).to(be_none());
327
328        let weak_etag_value = HeaderValue::parse_string(weak_etag);
329        expect!(weak_etag_value.clone()).to(be_equal_to(HeaderValue {
330            value: weak_etag.to_string(),
331            params: hashmap!{},
332            quote: false
333        }));
334        expect!(weak_etag_value.weak_etag()).to(be_some().value("1234567890"));
335    }
336}