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/// Struct to represent a header value and a map of header value parameters
120#[derive(Debug, Clone, Eq, Default)]
121pub struct HeaderValue {
122    /// Value of the header
123    pub value: String,
124    /// Map of header value parameters
125    pub params: HashMap<String, String>,
126    /// If the header should be quoted
127    pub quote: bool
128}
129
130impl HeaderValue {
131  /// Parses a header value string into a HeaderValue struct
132  pub fn parse_string(s: &str) -> HeaderValue {
133    let values = parse_header(s);
134    let (first, second) = values.split_first().unwrap();
135    if second.is_empty() {
136      HeaderValue::basic(first.as_str())
137    } else {
138      HeaderValue {
139        value: first.clone(),
140        params: batch(second).iter()
141          .fold(HashMap::new(), |mut map, params| {
142            if !params.0.is_empty() {
143              map.insert(params.0.clone(), params.1.clone());
144            }
145            map
146          }),
147        quote: false
148      }
149    }
150  }
151
152    /// Creates a basic header value that has no parameters
153    pub fn basic<S: Into<String>>(s: S) -> HeaderValue {
154      HeaderValue {
155        value: s.into(),
156        params: HashMap::new(),
157        quote: false
158      }
159    }
160
161    /// Converts this header value into a string representation
162    pub fn to_string(&self) -> String {
163        let sparams = self.params.iter()
164            .map(|(k, v)| format!("{}={}", k, v))
165            .join("; ");
166        if self.quote {
167            if sparams.is_empty() {
168                format!("\"{}\"", self.value)
169            } else {
170                format!("\"{}\"; {}", self.value, sparams)
171            }
172        } else {
173            if self.params.is_empty() {
174                self.value.clone()
175            } else {
176                format!("{}; {}", self.value, sparams)
177            }
178        }
179    }
180
181    /// Parses a weak ETag value. Weak etags are in the form W/<quoted-string>. Returns the
182    /// contents of the qouted string if it matches, otherwise returns None.
183    pub fn weak_etag(&self) -> Option<String> {
184      if self.value.starts_with("W/") {
185        Some(parse_header(&self.value[2..])[0].clone())
186      } else {
187        None
188      }
189    }
190
191    /// Converts this header value into a quoted header value
192    pub fn quote(mut self) -> HeaderValue {
193        self.quote = true;
194        self
195    }
196
197  /// JSON content type
198  pub fn json() -> HeaderValue {
199    HeaderValue {
200      value: "application/json".to_string(),
201      params: Default::default(),
202      quote: false
203    }
204  }
205}
206
207impl PartialEq<HeaderValue> for HeaderValue {
208    fn eq(&self, other: &HeaderValue) -> bool {
209        self.value == other.value && self.params == other.params
210    }
211}
212
213impl PartialEq<&HeaderValue> for HeaderValue {
214  fn eq(&self, other: &&HeaderValue) -> bool {
215    self == *other
216  }
217}
218
219
220impl PartialEq<String> for HeaderValue {
221    fn eq(&self, other: &String) -> bool {
222        self.value == *other
223    }
224}
225
226impl PartialEq<str> for HeaderValue {
227    fn eq(&self, other: &str) -> bool {
228        self.value == *other
229    }
230}
231
232impl Hash for HeaderValue {
233    fn hash<H: Hasher>(&self, state: &mut H) {
234        self.value.hash(state);
235        for (k, v) in self.params.clone() {
236            k.hash(state);
237            v.hash(state);
238        }
239    }
240}
241
242/// Simple macro to convert a string to a `HeaderValue` struct.
243#[macro_export]
244macro_rules! h {
245  ($e:expr) => (HeaderValue::parse_string($e.into()))
246}
247
248#[cfg(test)]
249mod tests {
250  use expectest::prelude::*;
251  use maplit::hashmap;
252
253  use super::*;
254
255  #[test]
256    fn parse_header_value_test() {
257        expect!(HeaderValue::parse_string("")).to(be_equal_to("".to_string()));
258        expect!(HeaderValue::parse_string("A B")).to(be_equal_to("A B".to_string()));
259        expect!(HeaderValue::parse_string("A; B")).to(be_equal_to(HeaderValue {
260            value: "A".to_string(),
261            params: hashmap!{ "B".to_string() => "".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("text/html;charset=UTF-8")).to(be_equal_to(HeaderValue {
270            value: "text/html".to_string(),
271            params: hashmap!{ "charset".to_string() => "UTF-8".to_string() },
272            quote: false
273        }));
274        expect!(HeaderValue::parse_string("Text/HTML;Charset= \"utf-8\"")).to(be_equal_to(HeaderValue {
275            value: "Text/HTML".to_string(),
276            params: hashmap!{ "Charset".to_string() => "utf-8".to_string() },
277            quote: false
278        }));
279        expect!(HeaderValue::parse_string("text/html; charset = \" utf-8 \"")).to(be_equal_to(HeaderValue {
280            value: "text/html".to_string(),
281            params: hashmap!{ "charset".to_string() => " utf-8 ".to_string() },
282            quote: false
283        }));
284        expect!(HeaderValue::parse_string(";")).to(be_equal_to(HeaderValue {
285            value: "".to_string(),
286            params: hashmap!{},
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("A;b=\"c;d\"")).to(be_equal_to(HeaderValue {
295            value: "A".to_string(),
296            params: hashmap!{ "b".to_string() => "c;d".to_string() },
297            quote: false
298        }));
299        expect!(HeaderValue::parse_string("A;b=\"c\\\"d\"")).to(be_equal_to(HeaderValue {
300            value: "A".to_string(),
301            params: hashmap!{ "b".to_string() => "c\"d".to_string() },
302            quote: false
303        }));
304        expect!(HeaderValue::parse_string("A;b=\"c,d\"")).to(be_equal_to(HeaderValue {
305            value: "A".to_string(),
306            params: hashmap!{ "b".to_string() => "c,d".to_string() },
307            quote: false
308        }));
309        expect!(HeaderValue::parse_string("en;q=0.0")).to(be_equal_to(HeaderValue {
310            value: "en".to_string(),
311            params: hashmap!{ "q".to_string() => "0.0".to_string() },
312            quote: false
313        }));
314    }
315
316    #[test]
317    fn parse_qouted_header_value_test() {
318        expect!(HeaderValue::parse_string("\"*\"")).to(be_equal_to(HeaderValue {
319            value: "*".to_string(),
320            params: hashmap!{},
321            quote: false
322        }));
323        expect!(HeaderValue::parse_string(" \"quoted; value\"")).to(be_equal_to(HeaderValue {
324            value: "quoted; value".to_string(),
325            params: hashmap!{},
326            quote: false
327        }));
328    }
329
330    #[test]
331    fn parse_etag_header_value_test() {
332        let etag = "\"1234567890\"";
333        let weak_etag = "W/\"1234567890\"";
334
335        let header = HeaderValue::parse_string(etag);
336        expect!(header.clone()).to(be_equal_to(HeaderValue {
337            value: "1234567890".to_string(),
338            params: hashmap!{},
339            quote: false
340        }));
341        expect!(header.weak_etag()).to(be_none());
342
343        let weak_etag_value = HeaderValue::parse_string(weak_etag);
344        expect!(weak_etag_value.clone()).to(be_equal_to(HeaderValue {
345            value: weak_etag.to_string(),
346            params: hashmap!{},
347            quote: false
348        }));
349        expect!(weak_etag_value.weak_etag()).to(be_some().value("1234567890"));
350    }
351}