Skip to main content

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        // Strip a recognized `__Host-` / `__Secure-` prefix off the name and remember it.
70        let (prefix, name) = crate::cookie::prefix::split_prefix(name);
71
72        let name = TinyStr::from_cow_ref(name, parts.ptr);
73        let value = TinyStr::from_cow_ref(value, parts.ptr);
74
75        let mut cookie = Cookie::new_inner(name, value);
76        cookie.prefix = prefix;
77        cookie.raw_value = Some(string);
78        Ok(cookie)
79    }
80}
81
82struct SplitMut<'s> {
83    haystack: Option<&'s mut str>,
84    ptr: *const u8,
85}
86
87impl<'s> SplitMut<'s> {
88    pub fn new(haystack: &'s mut str) -> SplitMut<'s> {
89        SplitMut {
90            ptr: haystack.as_ptr(),
91            haystack: Some(haystack),
92        }
93    }
94}
95
96impl<'s> Iterator for SplitMut<'s> {
97    type Item = &'s mut str;
98
99    fn next(&mut self) -> Option<Self::Item> {
100        let remainder = self.haystack.take()?;
101
102        match remainder.find(';') {
103            Some(index) => {
104                let (current, rest) = remainder.split_at_mut(index);
105
106                if !rest.is_empty() {
107                    self.haystack = Some(&mut rest[1..]);
108                } else {
109                    self.haystack = Some(&mut rest[..]);
110                }
111
112                Some(current)
113            }
114            None => Some(remainder),
115        }
116    }
117}
118
119#[inline]
120fn invalid_cookie_value_char(val: &char) -> bool {
121    match val {
122        ' ' | '"' | ',' | ';' | '\\' => true,
123        control if control.is_ascii_control() => true,
124        _ => false,
125    }
126}
127
128pub fn trim_quotes(value: &str) -> &str {
129    if value.len() > 1 && value.starts_with('"') && value.ends_with('"') {
130        &value[1..(value.len() - 1)]
131    } else {
132        value
133    }
134}
135
136#[inline]
137pub fn invalid_token(val: &str) -> Option<char> {
138    val.chars().find(|c| match c {
139        '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|'
140        | '~' => false,
141        c if c.is_alphanumeric() => false,
142        _ => true,
143    })
144}
145
146#[inline]
147pub fn find_invalid_cookie_value(val: &str) -> Option<char> {
148    val.chars().find(invalid_cookie_value_char)
149}