Skip to main content

cookie_rs/cookie/
parse.rs

1//! Parsing utilities for `Cookie`.
2//!
3//! This module provides functionality for parsing `Cookie` instances from strings.
4//! It supports both strict and lenient parsing modes and handles attributes such as
5//! `Domain`, `Path`, `Max-Age`, `Secure`, and more.
6//!
7//! # Features
8//! - Flexible parsing with `parse` and `parse_strict` methods.
9//! - Detailed error handling using `ParseError` and `ParseSameSiteError`.
10//! - Support for common cookie attributes.
11//!
12//! # Example
13//! ```
14//! use cookie_rs::prelude::*;
15//!
16//! let cookie = Cookie::parse("session=abc123; Path=/; Secure").unwrap();
17//! assert_eq!(cookie.name(), "session");
18//! assert_eq!(cookie.value(), "abc123");
19//! assert_eq!(cookie.path(), Some("/"));
20//! assert_eq!(cookie.secure(), Some(true));
21//! ```
22use std::borrow::Cow;
23use std::time::Duration;
24
25use super::Cookie;
26use super::SameSite;
27use crate::StringPrison;
28
29pub use self::error::*;
30
31pub mod error;
32
33impl<'a> Cookie<'a> {
34    /// Parses a cookie from a string in a lenient mode.
35    ///
36    /// In lenient mode, unknown attributes are ignored.
37    ///
38    /// # Arguments
39    /// - `value`: The string representation of the cookie.
40    ///
41    /// # Returns
42    /// A `Result` containing the parsed `Cookie` or a `ParseError`.
43    ///
44    /// # Example
45    /// ```
46    /// use cookie_rs::prelude::*;
47    ///
48    /// let cookie = Cookie::parse("session=abc123; Secure").unwrap();
49    /// assert_eq!(cookie.name(), "session");
50    /// assert_eq!(cookie.value(), "abc123");
51    /// assert_eq!(cookie.secure(), Some(true));
52    /// ```
53    pub fn parse<V: Into<Cow<'a, str>>>(value: V) -> Result<Self, ParseError> {
54        Self::inner_parse(value.into(), false)
55    }
56
57    /// Parses a cookie from a string in a strict mode.
58    ///
59    /// In strict mode, unknown attributes cause an error.
60    ///
61    /// # Arguments
62    /// - `value`: The string representation of the cookie.
63    ///
64    /// # Returns
65    /// A `Result` containing the parsed `Cookie` or a `ParseError`.
66    ///
67    /// # Example
68    /// ```
69    /// use cookie_rs::prelude::*;
70    ///
71    /// let result = Cookie::parse_strict("session=abc123; UnknownAttr");
72    /// assert!(result.is_err());
73    /// ```
74    pub fn parse_strict<V: Into<Cow<'a, str>>>(value: V) -> Result<Self, ParseError> {
75        Self::inner_parse(value.into(), true)
76    }
77
78    pub(crate) fn inner_parse(value: Cow<'a, str>, strict: bool) -> Result<Self, ParseError> {
79        let prison = StringPrison::new(value);
80
81        // SAFETY: prison and slice owned by the same struct
82        let str = unsafe { prison.get() };
83
84        let mut cookie = parse_cookie(str, strict)?;
85        cookie.prison = Some(prison);
86
87        Ok(cookie)
88    }
89}
90
91fn parse_cookie(str: &str, strict: bool) -> Result<Cookie<'_>, ParseError> {
92    let mut attributes = str.split(';');
93
94    let (name, value) = attributes
95        .next()
96        .expect("Missing any attributes")
97        .split_once('=')
98        .ok_or(MissingPair::NameValue)?;
99
100    let (name, value) = (name.trim(), value.trim());
101
102    if name.is_empty() {
103        return Err(ParseError::EmptyName);
104    }
105
106    #[cfg(not(feature = "percent-encoding"))]
107    let mut cookie = Cookie::new(name, value);
108    #[cfg(feature = "percent-encoding")]
109    let mut cookie = Cookie::new(
110        name,
111        percent_encoding::percent_decode_str(value)
112            .decode_utf8()
113            .map_err(|_| ParseError::ParseDecodeError)?,
114    );
115
116    for attribute in attributes {
117        let mut pair = attribute.splitn(2, '=');
118
119        let (name, value) = (
120            pair.next().expect("missing any attribute name").trim(),
121            pair.next().map(|v| v.trim()),
122        );
123
124        match value {
125            domain if name.eq_ignore_ascii_case("Domain") => {
126                cookie.set_domain(domain.ok_or(MissingPair::Domain)?)
127            }
128            expires if name.eq_ignore_ascii_case("Expires") => {
129                cookie.set_expires(expires.ok_or(MissingPair::Expires)?)
130            }
131            _ if name.eq_ignore_ascii_case("HttpOnly") => cookie.set_http_only(true),
132            max_age if name.eq_ignore_ascii_case("Max-Age") => {
133                let secs: i64 = max_age.ok_or(MissingPair::MaxAge)?.parse()?;
134                cookie.set_max_age(Duration::from_secs(secs.max(0) as u64))
135            }
136            _ if name.eq_ignore_ascii_case("Partitioned") => cookie.set_partitioned(true),
137            path if name.eq_ignore_ascii_case("Path") => {
138                cookie.set_path(path.ok_or(MissingPair::Path)?)
139            }
140            _ if name.eq_ignore_ascii_case("Secure") => cookie.set_secure(true),
141            same_site if name.eq_ignore_ascii_case("SameSite") => {
142                cookie.set_same_site(same_site.ok_or(MissingPair::SameSite)?.parse()?)
143            }
144            _ if strict => return Err(ParseError::UnknownAttribute(name.to_owned())),
145            _ => continue,
146        }
147    }
148
149    Ok(cookie)
150}
151
152impl std::str::FromStr for SameSite {
153    type Err = ParseSameSiteError;
154
155    fn from_str(s: &str) -> Result<Self, Self::Err> {
156        if s.eq_ignore_ascii_case("strict") {
157            Ok(Self::Strict)
158        } else if s.eq_ignore_ascii_case("lax") {
159            Ok(Self::Lax)
160        } else if s.eq_ignore_ascii_case("none") {
161            Ok(Self::None)
162        } else {
163            Err(ParseSameSiteError::UnknownValue(s.to_owned()))
164        }
165    }
166}