1use std::{
2 error, fmt,
3 io::{self, BufRead, Cursor},
4 num::ParseIntError,
5 str::ParseBoolError,
6};
7
8use chrono::{DateTime, NaiveDateTime, Utc};
9
10#[cfg(feature = "feature-cookie")]
11mod feature_cookie;
12
13const HTTP_ONLY_PREFIX: &str = "#HttpOnly_";
14
15#[derive(Debug, Clone)]
16pub struct Cookie {
17 pub http_only: bool,
18 pub domain: String,
19 pub include_subdomains: bool,
20 pub path: String,
21 pub secure: bool,
22 pub expires: CookieExpires,
23 pub name: String,
24 pub value: String,
25}
26
27#[derive(Debug, Clone)]
28pub enum CookieExpires {
29 Session,
30 DateTime(DateTime<Utc>),
31}
32
33#[derive(PartialEq, Debug)]
34pub enum ParseError {
35 IoError((io::ErrorKind, String)),
36 DomainMissing,
37 IncludeSubdomainsMissing,
38 IncludeSubdomainsInvalid(ParseBoolError),
39 PathMissing,
40 SecureMissing,
41 SecureInvalid(ParseBoolError),
42 ExpiresMissing,
43 ExpiresInvalid(ParseIntError),
44 NameMissing,
45 ValueMissing,
46 TooManyElements,
47}
48impl fmt::Display for ParseError {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 Self::IoError((kind, msg)) => write!(f, "IoError {:?} {}", kind, msg),
52 Self::DomainMissing => write!(f, "DomainMissing"),
53 Self::IncludeSubdomainsMissing => write!(f, "IncludeSubdomainsMissing"),
54 Self::IncludeSubdomainsInvalid(err) => write!(f, "IncludeSubdomainsInvalid {}", err),
55 Self::PathMissing => write!(f, "PathMissing"),
56 Self::SecureMissing => write!(f, "SecureMissing"),
57 Self::SecureInvalid(err) => write!(f, "SecureInvalid {}", err),
58 Self::ExpiresMissing => write!(f, "ExpiresMissing"),
59 Self::ExpiresInvalid(err) => write!(f, "ExpiresInvalid {}", err),
60 Self::NameMissing => write!(f, "NameMissing"),
61 Self::ValueMissing => write!(f, "ValueMissing"),
62 Self::TooManyElements => write!(f, "TooManyElements"),
63 }
64 }
65}
66impl error::Error for ParseError {}
67
68impl From<io::Error> for ParseError {
69 fn from(err: io::Error) -> Self {
70 Self::IoError((err.kind(), err.to_string()))
71 }
72}
73
74pub fn parse(bytes: &[u8]) -> Result<Vec<Cookie>, ParseError> {
75 let mut cursor = Cursor::new(bytes);
76 let mut buf = String::new();
77
78 let mut cookies: Vec<Cookie> = vec![];
79
80 loop {
81 buf.clear();
82 let n = match cursor.read_line(&mut buf)? {
83 0 => break,
84 1 => continue,
85 n => n - 1,
86 };
87
88 let mut s = &buf[..n];
89
90 let mut http_only = false;
91 if s.starts_with(HTTP_ONLY_PREFIX) {
92 http_only = true;
93 s = &buf[HTTP_ONLY_PREFIX.len()..n];
94 } else if s.starts_with('#') {
95 continue;
96 }
97
98 let mut split = s.split('\t');
99
100 let domain = split.next().ok_or(ParseError::DomainMissing)?;
101
102 let include_subdomains = split.next().ok_or(ParseError::IncludeSubdomainsMissing)?;
103 let include_subdomains: bool = include_subdomains
104 .to_ascii_lowercase()
105 .parse()
106 .map_err(ParseError::IncludeSubdomainsInvalid)?;
107
108 let path = split.next().ok_or(ParseError::PathMissing)?;
109
110 let secure = split.next().ok_or(ParseError::SecureMissing)?;
111 let secure: bool = secure
112 .to_ascii_lowercase()
113 .parse()
114 .map_err(ParseError::SecureInvalid)?;
115
116 let expires = split.next().ok_or(ParseError::ExpiresMissing)?;
117 let expires: u64 = expires.parse().map_err(ParseError::ExpiresInvalid)?;
118 let expires = if expires == 0 {
119 CookieExpires::Session
120 } else {
121 CookieExpires::DateTime(DateTime::<Utc>::from_utc(
122 NaiveDateTime::from_timestamp(expires as i64, 0),
123 Utc,
124 ))
125 };
126
127 let name = split.next().ok_or(ParseError::NameMissing)?;
128
129 let value = split.next().ok_or(ParseError::ValueMissing)?;
130
131 if split.next().is_some() {
132 return Err(ParseError::TooManyElements);
133 }
134
135 let cookie = Cookie {
136 http_only,
137 domain: domain.to_owned(),
138 include_subdomains,
139 path: path.to_owned(),
140 secure,
141 expires,
142 name: name.to_owned(),
143 value: value.to_owned(),
144 };
145
146 cookies.push(cookie);
147 }
148
149 Ok(cookies)
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 use std::fs;
157
158 #[test]
159 fn test_parse_demo() -> Result<(), String> {
160 let txt_content = fs::read_to_string("tests/files/demo_cookies.txt").unwrap();
161
162 let cookies = parse(txt_content.as_bytes()).map_err(|err| err.to_string())?;
163
164 println!("{:?}", cookies);
165
166 assert_eq!(cookies.len(), 5);
167
168 let cookie = cookies.last().unwrap();
169 assert_eq!(cookie.http_only, true);
170 assert_eq!(cookie.domain, ".github.com");
171 assert_eq!(cookie.include_subdomains, true);
172 assert_eq!(cookie.path, "/");
173 assert_eq!(cookie.secure, true);
174 match cookie.expires {
175 CookieExpires::Session => assert!(false),
176 CookieExpires::DateTime(dt) => {
177 assert_eq!(dt.naive_utc().timestamp(), 1640586740);
178 }
179 }
180 assert_eq!(cookie.name, "logged_in");
181 assert_eq!(cookie.value, "no");
182
183 Ok(())
184 }
185}