Skip to main content

rustauth_core/cookies/
parse.rs

1use std::collections::BTreeMap;
2
3use super::types::{CookieOptions, ParsedCookie};
4
5pub fn parse_cookies(cookie_header: &str) -> BTreeMap<String, String> {
6    let mut cookies = BTreeMap::new();
7    for pair in cookie_header.split("; ") {
8        if let Some((name, value)) = pair.split_once('=') {
9            cookies.insert(name.to_owned(), value.to_owned());
10        }
11    }
12    cookies
13}
14
15/// Read the session token cookie from a request `Cookie` header.
16pub fn parse_set_cookie_header(set_cookie: &str) -> BTreeMap<String, ParsedCookie> {
17    let mut cookies = BTreeMap::new();
18    for cookie in split_set_cookie_header(set_cookie) {
19        let parts = cookie.split(';').map(str::trim).collect::<Vec<_>>();
20        let Some(name_value) = parts.first() else {
21            continue;
22        };
23        let Some((name, value)) = name_value.split_once('=') else {
24            continue;
25        };
26        if name.is_empty() {
27            continue;
28        }
29
30        let mut parsed = ParsedCookie {
31            value: percent_decode(value),
32            ..ParsedCookie::default()
33        };
34        for attribute in parts.iter().skip(1) {
35            let (attribute_name, attribute_value) = attribute
36                .split_once('=')
37                .map_or((*attribute, ""), |(name, value)| (name, value));
38            match attribute_name.trim().to_ascii_lowercase().as_str() {
39                "max-age" => parsed.max_age = attribute_value.trim().parse::<u64>().ok(),
40                "expires" => parsed.expires = Some(attribute_value.trim().to_owned()),
41                "domain" => parsed.domain = Some(attribute_value.trim().to_owned()),
42                "path" => parsed.path = Some(attribute_value.trim().to_owned()),
43                "secure" => parsed.secure = Some(true),
44                "httponly" => parsed.http_only = Some(true),
45                "samesite" => parsed.same_site = Some(attribute_value.trim().to_ascii_lowercase()),
46                "partitioned" => parsed.partitioned = Some(true),
47                _ => {}
48            }
49        }
50        cookies.insert(name.to_owned(), parsed);
51    }
52    cookies
53}
54
55fn split_set_cookie_header(set_cookie: &str) -> Vec<String> {
56    if set_cookie.is_empty() {
57        return Vec::new();
58    }
59    let mut result = Vec::new();
60    let mut start = 0;
61    let mut index = 0;
62    let bytes = set_cookie.as_bytes();
63    while index < bytes.len() {
64        if bytes[index] == b',' {
65            let mut cursor = index + 1;
66            while cursor < bytes.len() && bytes[cursor] == b' ' {
67                cursor += 1;
68            }
69            while cursor < bytes.len()
70                && bytes[cursor] != b'='
71                && bytes[cursor] != b';'
72                && bytes[cursor] != b','
73            {
74                cursor += 1;
75            }
76            if cursor < bytes.len() && bytes[cursor] == b'=' {
77                let part = set_cookie[start..index].trim();
78                if !part.is_empty() {
79                    result.push(part.to_owned());
80                }
81                start = index + 1;
82                while start < bytes.len() && bytes[start] == b' ' {
83                    start += 1;
84                }
85                index = start;
86                continue;
87            }
88        }
89        index += 1;
90    }
91    let last = set_cookie[start..].trim();
92    if !last.is_empty() {
93        result.push(last.to_owned());
94    }
95    result
96}
97
98fn percent_decode(value: &str) -> String {
99    let bytes = value.as_bytes();
100    let mut output = Vec::with_capacity(bytes.len());
101    let mut index = 0;
102    while index < bytes.len() {
103        if bytes[index] == b'%' && index + 2 < bytes.len() {
104            if let (Some(hi), Some(lo)) = (from_hex(bytes[index + 1]), from_hex(bytes[index + 2])) {
105                output.push((hi << 4) | lo);
106                index += 3;
107                continue;
108            }
109        }
110        output.push(bytes[index]);
111        index += 1;
112    }
113    String::from_utf8(output).unwrap_or_else(|_| value.to_owned())
114}
115
116fn from_hex(byte: u8) -> Option<u8> {
117    match byte {
118        b'0'..=b'9' => Some(byte - b'0'),
119        b'a'..=b'f' => Some(byte - b'a' + 10),
120        b'A'..=b'F' => Some(byte - b'A' + 10),
121        _ => None,
122    }
123}
124
125pub fn to_cookie_options(attributes: &ParsedCookie) -> CookieOptions {
126    CookieOptions {
127        max_age: attributes.max_age,
128        expires: attributes.expires.clone(),
129        domain: attributes.domain.clone(),
130        path: attributes.path.clone(),
131        secure: attributes.secure,
132        http_only: attributes.http_only,
133        same_site: attributes.same_site.clone(),
134        partitioned: attributes.partitioned,
135    }
136}