Skip to main content

http_type/cookie/
impl.rs

1use super::*;
2
3/// Implementation for `CookieBuilder`.
4impl CookieBuilder {
5    /// Creates a new cookie builder instance.
6    ///
7    /// # Arguments
8    ///
9    /// - `N` - The cookie name, which requires the `AsRef<str>` bound.
10    /// - `V` - The cookie value, which requires the `AsRef<str>` bound.
11    ///
12    /// # Returns
13    ///
14    /// - `CookieBuilder` - A new builder instance.
15    #[inline(always)]
16    pub fn new<N, V>(name: N, value: V) -> Self
17    where
18        N: AsRef<str>,
19        V: AsRef<str>,
20    {
21        Self {
22            name: name.as_ref().to_owned(),
23            value: value.as_ref().to_owned(),
24            expires: None,
25            max_age: None,
26            domain: None,
27            path: None,
28            secure: None,
29            http_only: None,
30            same_site: None,
31        }
32    }
33
34    /// Parses a `Set-Cookie` header string into a `CookieBuilder`.
35    ///
36    /// This method takes a `Set-Cookie` header string and extracts the various
37    /// attributes of a cookie, populating a `CookieBuilder` instance.
38    ///
39    /// A name that is not a token and a value that carries a control byte are
40    /// refused, because RFC 6265 Section 4.1.1 admits neither in a cookie
41    /// pair, and a name that carries a separator would turn the rest of the
42    /// name into an attribute. An attribute whose value carries a control byte
43    /// or a semicolon is skipped rather than kept, for the same reason.
44    ///
45    /// # Arguments
46    ///
47    /// - `C` - The `Set-Cookie` header string to parse, which requires the `AsRef<str>` bound.
48    ///
49    /// # Returns
50    ///
51    /// A `CookieBuilder` instance populated with the parsed cookie attributes.
52    pub fn parse<C>(cookie: C) -> Self
53    where
54        C: AsRef<str>,
55    {
56        let mut cookie_builder: Self = Self::default();
57        let parts: Vec<&str> = cookie.as_ref().split(SEMICOLON).collect();
58        if parts.is_empty() {
59            return cookie_builder;
60        }
61        if let Some(name_value_pair) = parts.first() {
62            let name_value_pair: &str = name_value_pair.trim();
63            if let Some((name, value)) = name_value_pair.split_once(EQUAL) {
64                let name: &str = name.trim();
65                let value: &str = value.trim();
66                if is_cookie_name_valid(name) && !value.bytes().any(is_control_byte) {
67                    cookie_builder.name = name.to_owned();
68                    cookie_builder.value = value.to_owned();
69                }
70            } else if is_cookie_name_valid(name_value_pair) {
71                cookie_builder.name = name_value_pair.to_owned();
72                cookie_builder.value = String::new();
73            }
74        }
75        for part in parts.iter().skip(1) {
76            let part: &str = part.trim();
77            if part.is_empty() {
78                continue;
79            }
80            if let Some((key, value)) = part.split_once(EQUAL) {
81                let key_lowercase: String = key.trim().to_lowercase();
82                let value: String = value.trim().to_string();
83                if !is_cookie_attribute_valid(&value) {
84                    continue;
85                }
86                match key_lowercase.as_str() {
87                    COOKIE_EXPIRES_LOWERCASE => {
88                        cookie_builder.set_expires(value);
89                    }
90                    COOKIE_MAX_AGE_LOWERCASE => {
91                        if let Ok(max_age_value) = value.parse::<i64>() {
92                            cookie_builder.set_max_age(max_age_value);
93                        }
94                    }
95                    COOKIE_DOMAIN_LOWERCASE => {
96                        cookie_builder.set_domain(value);
97                    }
98                    COOKIE_PATH_LOWERCASE => {
99                        cookie_builder.set_path(value);
100                    }
101                    COOKIE_SAME_SITE_LOWERCASE => {
102                        cookie_builder.set_same_site(value);
103                    }
104                    _ => {}
105                }
106            } else {
107                let attribute_lowercase: String = part.to_lowercase();
108                match attribute_lowercase.as_str() {
109                    COOKIE_SECURE_LOWERCASE => {
110                        cookie_builder.secure = Some(true);
111                    }
112                    COOKIE_HTTP_ONLY_LOWERCASE => {
113                        cookie_builder.http_only = Some(true);
114                    }
115                    _ => {}
116                }
117            }
118        }
119        cookie_builder
120    }
121
122    /// Sets the expiration date for the cookie.
123    ///
124    /// # Arguments
125    ///
126    /// - `E` - The expiration date, which requires the `AsRef<str>` bound.
127    ///
128    /// # Returns
129    ///
130    /// The `CookieBuilder` instance for method chaining.
131    #[inline(always)]
132    pub fn set_expires<E>(&mut self, expires: E) -> &mut Self
133    where
134        E: AsRef<str>,
135    {
136        self.expires = Some(expires.as_ref().to_owned());
137        self
138    }
139
140    /// Sets the maximum age for the cookie in seconds.
141    ///
142    /// # Arguments
143    ///
144    /// - `M` - The maximum age in seconds, which requires the `Into<i64>` bound.
145    ///
146    /// # Returns
147    ///
148    /// The `CookieBuilder` instance for method chaining.
149    #[inline(always)]
150    pub fn set_max_age<M>(&mut self, max_age: M) -> &mut Self
151    where
152        M: Into<i64>,
153    {
154        self.max_age = Some(max_age.into());
155        self
156    }
157
158    /// Sets the domain for the cookie.
159    ///
160    /// # Arguments
161    ///
162    /// - `D` - The domain for the cookie, which requires the `AsRef<str>` bound.
163    ///
164    /// # Returns
165    ///
166    /// The `CookieBuilder` instance for method chaining.
167    #[inline(always)]
168    pub fn set_domain<D>(&mut self, domain: D) -> &mut Self
169    where
170        D: AsRef<str>,
171    {
172        self.domain = Some(domain.as_ref().to_owned());
173        self
174    }
175
176    /// Sets the path for the cookie.
177    ///
178    /// # Arguments
179    ///
180    /// - `T` - The path for the cookie, which requires the `AsRef<str>` bound.
181    ///
182    /// # Returns
183    ///
184    /// The `CookieBuilder` instance for method chaining.
185    #[inline(always)]
186    pub fn set_path<T>(&mut self, path: T) -> &mut Self
187    where
188        T: AsRef<str>,
189    {
190        self.path = Some(path.as_ref().to_owned());
191        self
192    }
193
194    /// Sets the `Secure` flag for the cookie.
195    ///
196    /// This flag indicates that the cookie should only be transmitted over secure (HTTPS) connections.
197    ///
198    /// # Returns
199    ///
200    /// The `CookieBuilder` instance for method chaining.
201    #[inline(always)]
202    pub fn secure(&mut self) -> &mut Self {
203        *self.get_mut_secure() = Some(true);
204        self
205    }
206
207    /// Sets the `HttpOnly` flag for the cookie.
208    ///
209    /// This flag prevents client-side JavaScript from accessing the cookie.
210    ///
211    /// # Returns
212    ///
213    /// The `CookieBuilder` instance for method chaining.
214    #[inline(always)]
215    pub fn http_only(&mut self) -> &mut Self {
216        *self.get_mut_http_only() = Some(true);
217        self
218    }
219
220    /// Explicitly disables the `Secure` flag for the cookie.
221    ///
222    /// This method explicitly sets the `Secure` flag to `false`, which may be useful
223    /// for overriding default secure settings or for testing purposes.
224    ///
225    /// # Returns
226    ///
227    /// The `CookieBuilder` instance for method chaining.
228    #[inline(always)]
229    pub fn disable_secure(&mut self) -> &mut Self {
230        *self.get_mut_secure() = Some(false);
231        self
232    }
233
234    /// Explicitly disables the `HttpOnly` flag for the cookie.
235    ///
236    /// This method explicitly sets the `HttpOnly` flag to `false`, which may be useful
237    /// for overriding default HttpOnly settings or for testing purposes.
238    ///
239    /// # Returns
240    ///
241    /// The `CookieBuilder` instance for method chaining.
242    #[inline(always)]
243    pub fn disable_http_only(&mut self) -> &mut Self {
244        *self.get_mut_http_only() = Some(false);
245        self
246    }
247
248    /// Sets the `SameSite` policy for the cookie.
249    ///
250    /// # Arguments
251    ///
252    /// - `T` - The `SameSite` policy, which requires the `AsRef<str>` bound.
253    ///
254    /// # Returns
255    ///
256    /// The `CookieBuilder` instance for method chaining.
257    #[inline(always)]
258    pub fn set_same_site<T>(&mut self, same_site: T) -> &mut Self
259    where
260        T: AsRef<str>,
261    {
262        self.same_site = Some(same_site.as_ref().to_owned());
263        self
264    }
265
266    /// Builds the cookie string according to the `Set-Cookie` header format.
267    ///
268    /// The name, the value and every attribute are checked against the
269    /// grammar of RFC 6265 Section 4.1.1 first, so a value that carries a
270    /// control byte cannot end the header line and turn the rest of the
271    /// cookie into a second header. A cookie that fails the check is not
272    /// emitted at all, which is safer than emitting a truncated one.
273    ///
274    /// # Returns
275    ///
276    /// - `String` - A formatted cookie string ready to be sent in a `Set-Cookie` header.
277    pub fn build(&self) -> String {
278        self.try_build().unwrap_or_default()
279    }
280
281    /// Builds the cookie string, reporting why a cookie may not be built.
282    ///
283    /// The name, the value and every attribute are checked against the
284    /// grammar of RFC 6265 Section 4.1.1. A name that is not a token would
285    /// end early and turn the rest of it into an attribute, a value that
286    /// carries a control byte would end the header line, and an attribute
287    /// that carries a semicolon would start another attribute.
288    ///
289    /// # Returns
290    ///
291    /// - `Result<String, CookieError>` - The formatted cookie string, or the
292    ///   reason the cookie cannot be represented.
293    pub fn try_build(&self) -> Result<String, CookieError> {
294        if self.get_name().is_empty() {
295            return Err(CookieError::InvalidName);
296        }
297        if !is_cookie_name_valid(self.get_name()) {
298            return Err(CookieError::InvalidName);
299        }
300        if !is_cookie_value_valid(self.get_value()) {
301            return Err(CookieError::InvalidValue);
302        }
303        if let Some(expires_value) = self.try_get_expires()
304            && !is_cookie_attribute_valid(expires_value)
305        {
306            return Err(CookieError::InvalidAttribute);
307        }
308        if let Some(domain_value) = self.try_get_domain()
309            && !is_cookie_attribute_valid(domain_value)
310        {
311            return Err(CookieError::InvalidAttribute);
312        }
313        if let Some(path_value) = self.try_get_path()
314            && !is_cookie_attribute_valid(path_value)
315        {
316            return Err(CookieError::InvalidAttribute);
317        }
318        if let Some(same_site_value) = self.try_get_same_site()
319            && !is_cookie_attribute_valid(same_site_value)
320        {
321            return Err(CookieError::InvalidAttribute);
322        }
323        let mut cookie_string: String = format!("{}={}", self.get_name(), self.get_value());
324        if let Some(expires_value) = self.try_get_expires() {
325            cookie_string.push_str(COOKIE_EXPIRES_ATTRIBUTE_LOWERCASE);
326            cookie_string.push_str(expires_value);
327        }
328        if let Some(max_age_value) = self.try_get_max_age() {
329            cookie_string.push_str(COOKIE_MAX_AGE_ATTRIBUTE_LOWERCASE);
330            cookie_string.push_str(&max_age_value.to_string());
331        }
332        if let Some(domain_value) = self.try_get_domain() {
333            cookie_string.push_str(COOKIE_DOMAIN_ATTRIBUTE_LOWERCASE);
334            cookie_string.push_str(domain_value);
335        }
336        if let Some(path_value) = self.try_get_path() {
337            cookie_string.push_str(COOKIE_PATH_ATTRIBUTE_LOWERCASE);
338            cookie_string.push_str(path_value);
339        }
340        if let Some(true) = self.try_get_secure() {
341            cookie_string.push_str(COOKIE_SECURE_ATTRIBUTE_LOWERCASE);
342        }
343        if let Some(true) = self.try_get_http_only() {
344            cookie_string.push_str(COOKIE_HTTP_ONLY_ATTRIBUTE_LOWERCASE);
345        }
346        if let Some(same_site_value) = self.try_get_same_site() {
347            cookie_string.push_str(COOKIE_SAME_SITE_ATTRIBUTE_LOWERCASE);
348            cookie_string.push_str(same_site_value);
349        }
350        Ok(cookie_string)
351    }
352}
353
354/// Implementation for `Cookie`.
355impl Cookie {
356    /// Parses a `Cookie` header string into a collection of key-value pairs.
357    ///
358    /// This method takes a `Cookie` header string (typically from a `Cookie` request header)
359    /// and parses it into a map of cookie names to their values.
360    ///
361    /// A pair whose name is not a token and a pair whose value carries a
362    /// control byte are skipped, because RFC 6265 Section 4.1.1 admits
363    /// neither, and a name that carries a separator would let the rest of
364    /// the name be read as part of the value.
365    ///
366    /// # Arguments
367    ///
368    /// - `C` - The `Cookie` header string to parse, which requires the `AsRef<str>` bound.
369    ///
370    /// # Returns
371    ///
372    /// A `Cookies` collection (a hash map) containing all parsed cookie key-value pairs.
373    pub fn parse<C>(cookie: C) -> Cookies
374    where
375        C: AsRef<str>,
376    {
377        let cookie_ref: &str = cookie.as_ref();
378        let mut cookies: Cookies = hash_map_xx_hash3_64();
379        if cookie_ref.trim().is_empty() {
380            return cookies;
381        }
382        let parts: Vec<&str> = cookie_ref.split(SEMICOLON).collect();
383        for part in parts {
384            let part: &str = part.trim();
385            if part.is_empty() {
386                continue;
387            }
388            if let Some((name, value)) = part.split_once(EQUAL) {
389                let name: &str = name.trim();
390                let value: &str = value.trim();
391                if is_cookie_name_valid(name) && !value.bytes().any(is_control_byte) {
392                    cookies.insert(name.to_owned(), value.to_owned());
393                }
394            } else if is_cookie_name_valid(part) {
395                cookies.insert(part.to_owned(), String::new());
396            }
397        }
398        cookies
399    }
400}