cookie_monster/cookie/
parse.rs

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