Skip to main content

use_cookie/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4/// A cookie name-value pair from the `Cookie` header.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Cookie {
7    pub name: String,
8    pub value: String,
9}
10
11/// A lightweight view of a `Set-Cookie` header.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct SetCookie {
14    pub name: String,
15    pub value: String,
16    pub path: Option<String>,
17    pub domain: Option<String>,
18    pub max_age: Option<i64>,
19    pub secure: bool,
20    pub http_only: bool,
21    pub same_site: Option<String>,
22}
23
24/// Parses a `Cookie` header into cookie pairs.
25#[must_use]
26pub fn parse_cookie_header(input: &str) -> Vec<Cookie> {
27    input
28        .split(';')
29        .filter_map(|segment| {
30            let trimmed = segment.trim();
31            let (name, value) = trimmed.split_once('=')?;
32            let name = name.trim();
33            cookie_name_is_valid(name).then(|| Cookie {
34                name: name.to_string(),
35                value: value.trim().to_string(),
36            })
37        })
38        .collect()
39}
40
41/// Parses a lightweight `Set-Cookie` header.
42#[must_use]
43pub fn parse_set_cookie_basic(input: &str) -> Option<SetCookie> {
44    let mut parts = input.split(';');
45    let first = parts.next()?.trim();
46    let (name, value) = first.split_once('=')?;
47    let name = name.trim();
48    if !cookie_name_is_valid(name) {
49        return None;
50    }
51
52    let mut cookie = SetCookie {
53        name: name.to_string(),
54        value: value.trim().to_string(),
55        path: None,
56        domain: None,
57        max_age: None,
58        secure: false,
59        http_only: false,
60        same_site: None,
61    };
62
63    for attribute in parts {
64        let attribute = attribute.trim();
65        if attribute.eq_ignore_ascii_case("secure") {
66            cookie.secure = true;
67            continue;
68        }
69        if attribute.eq_ignore_ascii_case("httponly") {
70            cookie.http_only = true;
71            continue;
72        }
73
74        if let Some((key, raw_value)) = attribute.split_once('=') {
75            let key = key.trim();
76            let value = raw_value.trim();
77            if key.eq_ignore_ascii_case("path") {
78                cookie.path = Some(value.to_string());
79            } else if key.eq_ignore_ascii_case("domain") {
80                cookie.domain = Some(value.to_string());
81            } else if key.eq_ignore_ascii_case("max-age") {
82                cookie.max_age = value.parse().ok();
83            } else if key.eq_ignore_ascii_case("samesite") && !value.is_empty() {
84                cookie.same_site = Some(value.to_string());
85            }
86        }
87    }
88
89    Some(cookie)
90}
91
92/// Builds a `Cookie` header string from cookie pairs.
93#[must_use]
94pub fn build_cookie_header(cookies: &[Cookie]) -> String {
95    cookies
96        .iter()
97        .map(|cookie| format!("{}={}", cookie.name, cookie.value))
98        .collect::<Vec<_>>()
99        .join("; ")
100}
101
102/// Returns the first cookie value with the requested name.
103#[must_use]
104pub fn get_cookie(input: &str, name: &str) -> Option<String> {
105    parse_cookie_header(input)
106        .into_iter()
107        .find(|cookie| cookie.name == name)
108        .map(|cookie| cookie.value)
109}
110
111/// Returns `true` when the header contains the requested cookie.
112#[must_use]
113pub fn has_cookie(input: &str, name: &str) -> bool {
114    get_cookie(input, name).is_some()
115}
116
117/// Returns `true` when the cookie name uses a conservative HTTP token character set.
118#[must_use]
119pub fn cookie_name_is_valid(input: &str) -> bool {
120    let trimmed = input.trim();
121    !trimmed.is_empty() && trimmed.bytes().all(is_token_byte)
122}
123
124fn is_token_byte(byte: u8) -> bool {
125    byte.is_ascii_alphanumeric()
126        || matches!(
127            byte,
128            b'!' | b'#'
129                | b'$'
130                | b'%'
131                | b'&'
132                | b'\''
133                | b'*'
134                | b'+'
135                | b'-'
136                | b'.'
137                | b'^'
138                | b'_'
139                | b'`'
140                | b'|'
141                | b'~'
142        )
143}