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, Default)]
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 quoted
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    /// Converts this header value into a quoted header value
193    pub fn quote(mut self) -> HeaderValue {
194        self.quote = true;
195        self
196    }
197
198  /// JSON content type
199  pub fn json() -> HeaderValue {
200    HeaderValue {
201      value: "application/json".to_string(),
202      params: Default::default(),
203      quote: false
204    }
205  }
206}
207
208impl PartialEq<HeaderValue> for HeaderValue {
209    fn eq(&self, other: &HeaderValue) -> bool {
210        self.value == other.value && self.params == other.params
211    }
212}
213
214impl PartialEq<&HeaderValue> for HeaderValue {
215  fn eq(&self, other: &&HeaderValue) -> bool {
216    self == *other
217  }
218}
219
220
221impl PartialEq<String> for HeaderValue {
222    fn eq(&self, other: &String) -> bool {
223        self.value == *other
224    }
225}
226
227impl PartialEq<str> for HeaderValue {
228    fn eq(&self, other: &str) -> bool {
229        self.value == *other
230    }
231}
232
233impl Hash for HeaderValue {
234    fn hash<H: Hasher>(&self, state: &mut H) {
235        self.value.hash(state);
236        for (k, v) in self.params.clone() {
237            k.hash(state);
238            v.hash(state);
239        }
240    }
241}
242
243/// Simple macro to convert a string to a `HeaderValue` struct.
244#[macro_export]
245macro_rules! h {
246  ($e:expr) => (HeaderValue::parse_string($e.into()))
247}
248
249#[cfg(test)]
250mod tests {
251  use expectest::prelude::*;
252  use maplit::hashmap;
253
254  use super::*;
255
256  #[test]
257    fn parse_header_value_test() {
258        expect!(HeaderValue::parse_string("")).to(be_equal_to("".to_string()));
259        expect!(HeaderValue::parse_string("A B")).to(be_equal_to("A B".to_string()));
260        expect!(HeaderValue::parse_string("A; B")).to(be_equal_to(HeaderValue {
261            value: "A".to_string(),
262            params: hashmap!{ "B".to_string() => "".to_string() },
263            quote: false
264        }));
265        expect!(HeaderValue::parse_string("text/html;charset=utf-8")).to(be_equal_to(HeaderValue {
266            value: "text/html".to_string(),
267            params: hashmap!{ "charset".to_string() => "utf-8".to_string() },
268            quote: false
269        }));
270        expect!(HeaderValue::parse_string("text/html;charset=UTF-8")).to(be_equal_to(HeaderValue {
271            value: "text/html".to_string(),
272            params: hashmap!{ "charset".to_string() => "UTF-8".to_string() },
273            quote: false
274        }));
275        expect!(HeaderValue::parse_string("Text/HTML;Charset= \"utf-8\"")).to(be_equal_to(HeaderValue {
276            value: "Text/HTML".to_string(),
277            params: hashmap!{ "Charset".to_string() => "utf-8".to_string() },
278            quote: false
279        }));
280        expect!(HeaderValue::parse_string("text/html; charset = \" utf-8 \"")).to(be_equal_to(HeaderValue {
281            value: "text/html".to_string(),
282            params: hashmap!{ "charset".to_string() => " utf-8 ".to_string() },
283            quote: false
284        }));
285        expect!(HeaderValue::parse_string(";")).to(be_equal_to(HeaderValue {
286            value: "".to_string(),
287            params: hashmap!{},
288            quote: false
289        }));
290        expect!(HeaderValue::parse_string("A;b=c=d")).to(be_equal_to(HeaderValue {
291            value: "A".to_string(),
292            params: hashmap!{ "b".to_string() => "c=d".to_string() },
293            quote: false
294        }));
295        expect!(HeaderValue::parse_string("A;b=\"c;d\"")).to(be_equal_to(HeaderValue {
296            value: "A".to_string(),
297            params: hashmap!{ "b".to_string() => "c;d".to_string() },
298            quote: false
299        }));
300        expect!(HeaderValue::parse_string("A;b=\"c\\\"d\"")).to(be_equal_to(HeaderValue {
301            value: "A".to_string(),
302            params: hashmap!{ "b".to_string() => "c\"d".to_string() },
303            quote: false
304        }));
305        expect!(HeaderValue::parse_string("A;b=\"c,d\"")).to(be_equal_to(HeaderValue {
306            value: "A".to_string(),
307            params: hashmap!{ "b".to_string() => "c,d".to_string() },
308            quote: false
309        }));
310        expect!(HeaderValue::parse_string("en;q=0.0")).to(be_equal_to(HeaderValue {
311            value: "en".to_string(),
312            params: hashmap!{ "q".to_string() => "0.0".to_string() },
313            quote: false
314        }));
315    }
316
317    #[test]
318    fn parse_qouted_header_value_test() {
319        expect!(HeaderValue::parse_string("\"*\"")).to(be_equal_to(HeaderValue {
320            value: "*".to_string(),
321            params: hashmap!{},
322            quote: false
323        }));
324        expect!(HeaderValue::parse_string(" \"quoted; value\"")).to(be_equal_to(HeaderValue {
325            value: "quoted; value".to_string(),
326            params: hashmap!{},
327            quote: false
328        }));
329    }
330
331    #[test]
332    fn parse_etag_header_value_test() {
333        let etag = "\"1234567890\"";
334        let weak_etag = "W/\"1234567890\"";
335
336        let header = HeaderValue::parse_string(etag);
337        expect!(header.clone()).to(be_equal_to(HeaderValue {
338            value: "1234567890".to_string(),
339            params: hashmap!{},
340            quote: false
341        }));
342        expect!(header.weak_etag()).to(be_none());
343
344        let weak_etag_value = HeaderValue::parse_string(weak_etag);
345        expect!(weak_etag_value.clone()).to(be_equal_to(HeaderValue {
346            value: weak_etag.to_string(),
347            params: hashmap!{},
348            quote: false
349        }));
350        expect!(weak_etag_value.weak_etag()).to(be_some().value("1234567890"));
351    }
352}