rustauth_core/cookies/
parse.rs1use 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
15pub 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}