cookie_monster/cookie/
parse.rs

1use std::borrow::Cow;
2
3use crate::{Cookie, error::Error, util::TinyStr};
4
5impl Cookie {
6    /// Parses the given cookie header value. Errors when:
7    /// * No '=' is found.
8    /// * The name is empty.
9    /// * The name contains an invalid character.
10    /// * The cookie value contains an invalid character.
11    ///
12    /// Since this only parses a cookie header value, it does not parse any cookie attributes.
13    pub fn parse_cookie(string: impl Into<Box<str>>) -> crate::Result<Cookie> {
14        Self::parse_inner(string.into(), |name, value| {
15            Ok((Cow::Borrowed(name), Cow::Borrowed(value)))
16        })
17    }
18
19    /// Parses a percent encoded cookie value. Errors when:
20    /// * No '=' is found.
21    /// * The name is empty.
22    /// * The name contains an invalid character.
23    /// * The cookie value contains an invalid character.
24    ///
25    /// Since this only parses a cookie header value, it does not parse any cookie attributes.
26    #[cfg(feature = "percent-encode")]
27    pub fn parse_cookie_encoded(string: impl Into<Box<str>>) -> crate::Result<Cookie> {
28        use crate::cookie::encoding;
29
30        Self::parse_inner(string.into(), encoding::decode_name_value)
31    }
32
33    fn parse_inner(
34        mut string: Box<str>,
35        callback: impl for<'a> Fn(&'a str, &'a str) -> crate::Result<(Cow<'a, str>, Cow<'a, str>)>,
36    ) -> Result<Cookie, Error> {
37        let mut parts = SplitMut::new(&mut string);
38
39        let name_value = parts.next().expect("First split always returns something");
40
41        // 2.  If the name-value-pair string lacks a %x3D ("=") character,
42        //     ignore the set-cookie-string entirely.
43        let Some(index) = name_value.find('=') else {
44            return Err(Error::EqualsNotFound);
45        };
46
47        // 4.  Remove any leading or trailing WSP characters from the name
48        //     string and the value string.
49        let name = name_value[..index].trim();
50        let mut value = name_value[(index + 1)..].trim();
51
52        // 5.  If the name string is empty, ignore the set-cookie-string entirely.
53        if name.is_empty() {
54            return Err(Error::NameEmpty);
55        } else if let Some(token) = invalid_token(name) {
56            return Err(Error::InvalidName(token));
57        }
58
59        // Remove optional brackets.
60        value = trim_quotes(value);
61
62        if let Some(invalid_char) = find_invalid_cookie_value(value) {
63            return Err(Error::InvalidValue(invalid_char));
64        }
65
66        // Optionally decode the name and value.
67        let (name, value) = callback(name, value)?;
68
69        let name = TinyStr::from_cow_ref(name, parts.ptr);
70        let value = TinyStr::from_cow_ref(value, parts.ptr);
71
72        let mut cookie = Cookie::new_inner(name, value);
73        cookie.raw_value = Some(string);
74        Ok(cookie)
75    }
76}
77
78struct SplitMut<'s> {
79    haystack: Option<&'s mut str>,
80    ptr: *const u8,
81}
82
83impl<'s> SplitMut<'s> {
84    pub fn new(haystack: &'s mut str) -> SplitMut<'s> {
85        SplitMut {
86            ptr: haystack.as_ptr(),
87            haystack: Some(haystack),
88        }
89    }
90}
91
92impl<'s> Iterator for SplitMut<'s> {
93    type Item = &'s mut str;
94
95    fn next(&mut self) -> Option<Self::Item> {
96        let remainder = self.haystack.take()?;
97
98        match remainder.find(';') {
99            Some(index) => {
100                let (current, rest) = remainder.split_at_mut(index);
101
102                if !rest.is_empty() {
103                    self.haystack = Some(&mut rest[1..]);
104                } else {
105                    self.haystack = Some(&mut rest[..]);
106                }
107
108                Some(current)
109            }
110            None => Some(remainder),
111        }
112    }
113}
114
115#[inline]
116fn invalid_cookie_value_char(val: &char) -> bool {
117    match val {
118        ' ' | '"' | ',' | ';' | '\\' => true,
119        control if control.is_ascii_control() => true,
120        _ => false,
121    }
122}
123
124pub fn trim_quotes(value: &str) -> &str {
125    if value.len() > 1 && value.starts_with('"') && value.ends_with('"') {
126        &value[1..(value.len() - 1)]
127    } else {
128        value
129    }
130}
131
132#[inline]
133pub fn invalid_token(val: &str) -> Option<char> {
134    val.chars().find(|c| match c {
135        '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|'
136        | '~' => false,
137        c if c.is_alphanumeric() => false,
138        _ => true,
139    })
140}
141
142#[inline]
143pub fn find_invalid_cookie_value(val: &str) -> Option<char> {
144    val.chars().find(invalid_cookie_value_char)
145}