netscape_cookie/
lib.rs

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}