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}