1#[cfg(not(target_arch = "wasm32"))]
8use chrono::offset::Utc;
9#[cfg(not(target_arch = "wasm32"))]
10use chrono::NaiveDateTime;
11#[cfg(target_arch = "wasm32")]
12use js_sys::Date;
13use std::borrow::Cow;
14use std::collections::HashMap;
15use std::time::Duration;
16use urlencoding::FromUrlEncodingError;
17#[cfg(target_arch = "wasm32")]
18use wasm_bindgen::JsValue;
19
20#[derive(Debug)]
22pub enum AllDecodeError {
23 Key(String, FromUrlEncodingError),
28
29 Value(String, FromUrlEncodingError),
34}
35
36fn process_key_value_str(key_value_str: &str) -> Result<(&str, &str), ()> {
37 match key_value_str.split_once('=') {
38 Some((key, value)) => Ok((key.trim(), value.trim())),
39 None => Err(()),
40 }
41}
42
43pub fn all_iter_raw(cookie_string: &str) -> impl Iterator<Item = (&str, &str)> {
45 cookie_string.split(';').filter_map(|key_value_str| {
46 match process_key_value_str(key_value_str) {
47 Ok((key, value)) => Some((key, value)),
48 Err(_) => None,
49 }
50 })
51}
52
53pub fn all_iter(
57 cookie_string: &str,
58) -> impl Iterator<Item = Result<(String, String), AllDecodeError>> + '_ {
59 all_iter_raw(cookie_string).map(|(key, value)| match urlencoding::decode(key) {
60 Ok(key) => match urlencoding::decode(value) {
61 Ok(value) => Ok((key, value)),
62 Err(error) => Err(AllDecodeError::Value(key, error)),
63 },
64
65 Err(error) => Err(AllDecodeError::Key(key.to_owned(), error)),
66 })
67}
68
69pub fn all_raw(cookie_string: &str) -> HashMap<String, String> {
71 all_iter_raw(cookie_string)
72 .map(|(key, value)| (key.to_owned(), value.to_owned()))
73 .collect()
74}
75
76pub fn all(cookie_string: &str) -> Result<HashMap<String, String>, AllDecodeError> {
80 all_iter(cookie_string).collect()
81}
82
83pub fn get_raw(cookie_string: &str, name: &str) -> Option<String> {
85 cookie_string
86 .split(';')
87 .find_map(|key_value_str| match process_key_value_str(key_value_str) {
88 Ok((key, value)) => {
89 if key == name {
90 Some(value.to_owned())
91 } else {
92 None
93 }
94 }
95
96 Err(_) => None,
97 })
98}
99
100pub fn get(cookie_string: &str, name: &str) -> Option<Result<String, FromUrlEncodingError>> {
104 let name = urlencoding::encode(name);
105
106 cookie_string
107 .split(';')
108 .find_map(|key_value_str| match process_key_value_str(key_value_str) {
109 Ok((key, value)) => {
110 if key == name {
111 Some(urlencoding::decode(value))
112 } else {
113 None
114 }
115 }
116
117 Err(_) => None,
118 })
119}
120
121#[derive(Default, Clone, Debug)]
125pub struct CookieOptions<'a> {
126 pub path: Option<&'a str>,
128
129 pub domain: Option<&'a str>,
131
132 pub expires: Option<Cow<'a, str>>,
135
136 pub secure: bool,
139
140 pub same_site: SameSite,
143}
144
145impl<'a> CookieOptions<'a> {
146 pub fn with_path(mut self, path: &'a str) -> Self {
149 self.path = Some(path);
150 self
151 }
152
153 pub fn with_domain(mut self, domain: &'a str) -> Self {
156 self.domain = Some(domain);
157 self
158 }
159
160 pub fn expires_at_date(mut self, date: &'a str) -> Self {
166 self.expires = Some(Cow::Borrowed(date));
167 self
168 }
169
170 pub fn expires_at_timestamp(mut self, timestamp: i64) -> Self {
173 #[cfg(target_arch = "wasm32")]
174 let date: String = Date::new(&JsValue::from_f64(timestamp as f64))
175 .to_utc_string()
176 .into();
177
178 #[cfg(not(target_arch = "wasm32"))]
179 let date = NaiveDateTime::from_timestamp_millis(timestamp)
180 .unwrap()
181 .format("%a, %d %b %Y %T GMT")
182 .to_string();
183
184 self.expires = Some(Cow::Owned(date));
185 self
186 }
187
188 pub fn expires_after(self, duration: Duration) -> Self {
191 #[cfg(target_arch = "wasm32")]
192 let now = Date::now() as i64;
193 #[cfg(not(target_arch = "wasm32"))]
194 let now = Utc::now().timestamp_millis();
195 self.expires_at_timestamp(now + duration.as_millis() as i64)
196 }
197
198 pub fn secure(mut self) -> Self {
200 self.secure = true;
201 self
202 }
203
204 pub fn with_same_site(mut self, same_site: SameSite) -> Self {
208 self.same_site = same_site;
209 self
210 }
211}
212
213#[derive(Clone, Debug)]
218pub enum SameSite {
219 Lax,
223
224 Strict,
227
228 None,
231}
232
233impl Default for SameSite {
234 fn default() -> Self {
235 Self::Lax
236 }
237}
238
239impl SameSite {
240 fn cookie_string_value(&self) -> &'static str {
241 match self {
242 SameSite::Lax => "lax",
243 SameSite::Strict => "strict",
244 SameSite::None => "none",
245 }
246 }
247}
248
249pub fn set_raw(name: &str, value: &str, options: &CookieOptions) -> String {
251 let mut cookie_string = name.to_owned();
252 cookie_string.push('=');
253 cookie_string.push_str(value);
254
255 if let Some(path) = options.path {
256 cookie_string.push_str(";path=");
257 cookie_string.push_str(path);
258 }
259
260 if let Some(domain) = options.domain {
261 cookie_string.push_str(";domain=");
262 cookie_string.push_str(domain);
263 }
264
265 if let Some(expires_str) = &options.expires {
266 cookie_string.push_str(";expires=");
267 cookie_string.push_str(expires_str);
268 }
269
270 if options.secure {
271 cookie_string.push_str(";secure");
272 }
273
274 cookie_string.push_str(";samesite=");
275 cookie_string.push_str(options.same_site.cookie_string_value());
276
277 cookie_string
278}
279
280pub fn set(name: &str, value: &str, options: &CookieOptions) -> String {
283 set_raw(
284 &urlencoding::encode(name),
285 &urlencoding::encode(value),
286 options,
287 )
288}
289
290pub fn delete_raw(name: &str) -> String {
292 format!("{}=;expires=Thu, 01 Jan 1970 00:00:00 GMT", name)
293}
294
295pub fn delete(name: &str) -> String {
297 delete_raw(&urlencoding::encode(name))
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_all_raw() {
306 let cookies = all_raw(" key1=value1;key2=value2 ; key3 = value3");
307 assert_eq!(cookies.len(), 3);
308 assert_eq!(cookies["key1"], "value1");
309 assert_eq!(cookies["key2"], "value2");
310 assert_eq!(cookies["key3"], "value3");
311
312 let cookies = all_raw("");
313 assert!(cookies.is_empty());
314 }
315
316 #[test]
317 fn test_all() {
318 let cookies =
319 all("key%20%251=value%20%251 ;key%20%252=value%20%252;key%20%253 = value%20%253")
320 .unwrap();
321 assert_eq!(cookies.len(), 3);
322 assert_eq!(cookies["key %1"], "value %1");
323 assert_eq!(cookies["key %2"], "value %2");
324 assert_eq!(cookies["key %3"], "value %3");
325
326 let cookies = all("").unwrap();
327 assert!(cookies.is_empty());
328
329 let error = all("key1=value1;key2%AA=value2").unwrap_err();
330
331 match error {
332 AllDecodeError::Key(raw_key, _) => assert_eq!(raw_key, "key2%AA"),
333 _ => panic!(),
334 }
335
336 let error = all("key1=value1;key%202=value2%AA").unwrap_err();
337
338 match error {
339 AllDecodeError::Value(key, _) => assert_eq!(key, "key 2"),
340 _ => panic!(),
341 }
342 }
343
344 #[test]
345 fn test_get_raw() {
346 assert_eq!(
347 get_raw("key1=value1 ; key2= value2;key3=value3", "key2"),
348 Some("value2".to_owned())
349 );
350
351 assert_eq!(
352 get_raw("key1=value1 ; key2= value2;key3=value3", "key4"),
353 None
354 );
355 }
356
357 #[test]
358 fn test_get() {
359 assert_eq!(
360 get("key1=value1 ; key%202= value%202=;key3=value3", "key 2")
361 .map(|result| result.unwrap()),
362 Some("value 2=".to_owned())
363 );
364
365 assert!(get("key1=value1 ; key2= value2;key3=value3", "key4").is_none());
366 assert!(get("key1=value1 ; key2= value2%AA;key3=value3", "key2")
367 .unwrap()
368 .is_err());
369 }
370
371 #[test]
372 fn test_set_raw() {
373 assert_eq!(
374 set_raw("key", "value", &CookieOptions::default()),
375 "key=value;samesite=lax"
376 );
377
378 assert_eq!(
379 set_raw("key", "value", &CookieOptions::default().with_path("/path")),
380 "key=value;path=/path;samesite=lax"
381 );
382
383 assert_eq!(
384 set_raw(
385 "key",
386 "value",
387 &CookieOptions::default()
388 .with_path("/path")
389 .with_domain("example.com")
390 ),
391 "key=value;path=/path;domain=example.com;samesite=lax"
392 );
393
394 assert_eq!(
395 set_raw(
396 "key",
397 "value",
398 &CookieOptions::default()
399 .with_path("/path")
400 .with_domain("example.com")
401 .secure()
402 ),
403 "key=value;path=/path;domain=example.com;secure;samesite=lax"
404 );
405
406 assert_eq!(
407 set_raw(
408 "key",
409 "value",
410 &CookieOptions::default().expires_at_timestamp(1100000000000),
411 ),
412 "key=value;expires=Tue, 09 Nov 2004 11:33:20 GMT;samesite=lax",
413 );
414 }
415}