topcoat_cookie/prefix.rs
1use cookie::Cookie;
2
3use crate::Cookies;
4
5/// A [RFC 6265bis] cookie name prefix.
6///
7/// A prefix asks the browser to enforce extra constraints on a cookie based on
8/// its name. For example, apply `__Host-` with [`Cookies::override_prefix_host`].
9///
10/// [RFC 6265bis]: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum Prefix {
13 /// The `__Host-` prefix: the cookie must be `Secure`, have `Path=/`, and no
14 /// `Domain`.
15 Host,
16 /// The `__Secure-` prefix: the cookie must be `Secure`.
17 Secure,
18}
19
20impl Prefix {
21 /// The literal prefix string prepended to cookie names.
22 #[must_use]
23 pub const fn as_str(self) -> &'static str {
24 match self {
25 Prefix::Host => "__Host-",
26 Prefix::Secure => "__Secure-",
27 }
28 }
29
30 /// Prepends the prefix to the cookie's name, unless it is already present.
31 fn apply_name(self, cookie: &mut Cookie<'static>) {
32 if !cookie.name().starts_with(self.as_str()) {
33 let name = format!("{}{}", self.as_str(), cookie.name());
34 cookie.set_name(name);
35 }
36 }
37
38 /// Strips the prefix from the cookie's name, if present.
39 fn strip_name(self, cookie: &mut Cookie<'static>) {
40 if let Some(stripped) = cookie.name().strip_prefix(self.as_str()) {
41 let stripped = stripped.to_owned();
42 cookie.set_name(stripped);
43 }
44 }
45
46 /// Applies the attributes the prefix requires.
47 ///
48 /// [`Conform::Override`] replaces attributes to meet the prefix's rules.
49 /// [`Conform::Default`] fills only unset attributes and preserves explicit
50 /// values, even when they conflict with those rules.
51 fn conform(self, cookie: &mut Cookie<'static>, mode: Conform) {
52 let force = matches!(mode, Conform::Override);
53
54 // Both prefixes require `Secure`.
55 if force || cookie.secure().is_none() {
56 cookie.set_secure(true);
57 }
58
59 // `__Host-` additionally requires `Path=/` and no `Domain`.
60 if self == Prefix::Host {
61 if force || cookie.path().is_none() {
62 cookie.set_path("/");
63 }
64 if force {
65 cookie.unset_domain();
66 }
67 }
68 }
69}
70
71/// Whether a prefix forces its required attributes or only fills them when
72/// unset. See [`Prefix::conform`].
73#[derive(Debug, Clone, Copy)]
74pub(crate) enum Conform {
75 /// Fill required attributes only when the cookie has not set them.
76 Default,
77 /// Force the required attributes, guaranteeing RFC compliance.
78 Override,
79}
80
81/// A [`Cookies`] adapter that scopes cookies to a name [`Prefix`].
82///
83/// On write it prepends the prefix and applies the prefix's required
84/// attributes; on read it looks the cookie up under its prefixed name and
85/// strips the prefix from the result. Created by
86/// [`Cookies::override_prefix_host`] and the related combinators.
87/// Names may be bare or already carry this adapter's prefix.
88#[derive(Debug, Clone, Copy)]
89pub struct Prefixed<J> {
90 inner: J,
91 prefix: Prefix,
92 conform: Conform,
93}
94
95impl<J> Prefixed<J> {
96 pub(crate) fn new(inner: J, prefix: Prefix, conform: Conform) -> Self {
97 Self {
98 inner,
99 prefix,
100 conform,
101 }
102 }
103
104 /// Prepends the prefix and applies its required attributes under `mode`.
105 fn prepare(&self, cookie: impl Into<Cookie<'static>>, mode: Conform) -> Cookie<'static> {
106 let mut cookie = cookie.into();
107 self.prefix.apply_name(&mut cookie);
108 self.prefix.conform(&mut cookie, mode);
109 cookie
110 }
111}
112
113impl<J: Cookies> Cookies for Prefixed<J> {
114 fn get(&self, name: &str) -> Option<Cookie<'static>> {
115 let mut cookie = if name.starts_with(self.prefix.as_str()) {
116 self.inner.get(name)?
117 } else {
118 self.inner
119 .get(&format!("{}{}", self.prefix.as_str(), name))?
120 };
121 self.prefix.strip_name(&mut cookie);
122 Some(cookie)
123 }
124
125 fn add<C: Into<Cookie<'static>>>(&self, cookie: C) {
126 self.inner.add(self.prepare(cookie, self.conform));
127 }
128
129 fn remove<C: Into<Cookie<'static>>>(&self, cookie: C) {
130 // A removal must force the required attributes (notably `Path=/` for
131 // `__Host-`) so the browser matches and clears it.
132 self.inner.remove(self.prepare(cookie, Conform::Override));
133 }
134}