#![warn(missing_docs)]
#![doc = include_str!("../README.md")]
use std::{
convert::TryFrom,
error::Error,
fmt::Display,
ops::{Deref, DerefMut},
};
const HTTP_ONLY: &str = "#HttpOnly_";
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Cookie {
pub domain: String,
pub include_subdomains: bool,
pub path: String,
pub https_only: bool,
pub http_only: bool,
pub expires: u64,
pub name: String,
pub value: String,
}
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Cookies(Vec<Cookie>);
impl Deref for Cookies {
type Target = Vec<Cookie>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Cookies {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<Vec<Cookie>> for Cookies {
fn from(value: Vec<Cookie>) -> Self {
Cookies(value)
}
}
impl From<Cookies> for Vec<Cookie> {
fn from(value: Cookies) -> Self {
value.0
}
}
#[cfg(any(feature = "cookie", test))]
impl From<Cookies> for Vec<cookie::Cookie<'_>> {
fn from(value: Cookies) -> Self {
value.iter().map(cookie::Cookie::from).collect()
}
}
#[derive(Debug, PartialEq)]
pub enum ParseError {
InvalidFormat(String),
InvalidValue(String),
Empty,
InternalError(String),
InvalidUrl,
}
impl Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseError::InvalidFormat(m) => write!(f, "{}", m),
ParseError::InvalidValue(m) => write!(f, "{}", m),
ParseError::Empty => write!(f, "Input does not contain cookie"),
ParseError::InternalError(m) => {
write!(f, "Internal error occured, report this: \"{}\"", m)
}
ParseError::InvalidUrl => {
write!(f, "The URL stored in the cookie could not be converted")
}
}
}
}
impl Error for ParseError {}
#[doc(hidden)]
fn parse_bool<T>(s: T) -> Result<bool, ParseError>
where
T: AsRef<str> + std::fmt::Debug,
{
let input: &str = s.as_ref();
match input.to_lowercase().as_ref() {
"true" => Ok(true),
"false" => Ok(false),
_ => Err(ParseError::InvalidValue(format!(
"Expected \"TRUE\" or \"FALSE\", got \"{:?}\"",
s
))),
}
}
#[doc(hidden)]
fn parse_u64<T>(s: T) -> Result<u64, ParseError>
where
T: AsRef<str>,
{
let input: &str = s.as_ref();
match input.parse::<u64>() {
Ok(v) => Ok(v),
Err(_) => Err(ParseError::InvalidValue(format!(
"Expected a value between {} and {}, got \"{}\"",
u64::MIN,
u64::MAX,
input
))),
}
}
impl TryFrom<&str> for Cookie {
type Error = ParseError;
fn try_from(input: &str) -> Result<Self, Self::Error> {
let mut input = input.trim();
let mut domain: String = Default::default();
let mut include_subdomains: bool = Default::default();
let mut path: String = Default::default();
let mut https_only: bool = Default::default();
let mut http_only: bool = Default::default();
let mut expires: u64 = Default::default();
let mut name: String = Default::default();
let mut value: String = Default::default();
if input.starts_with('#') && !input.starts_with(HTTP_ONLY) {
return Err(ParseError::Empty);
}
if input.starts_with(HTTP_ONLY) {
http_only = true;
input = if let Some(v) = input.strip_prefix(HTTP_ONLY) {
v.trim()
} else {
return Err(ParseError::InternalError(
"Could not strip HTTP_ONLY prefix, even though it is present".to_string(),
));
};
}
let splits = input.split('\t').enumerate();
if splits.clone().count() != 7 {
return Err(ParseError::Empty);
}
for (i, part) in splits {
let part = part.trim();
match i {
0 => domain = part.to_string(),
1 => include_subdomains = parse_bool(part)?,
2 => path = part.to_string(),
3 => https_only = parse_bool(part)?,
4 => expires = parse_u64(part)?,
5 => name = part.to_string(),
6 => value = part.to_string(),
v => {
return Err(ParseError::InvalidFormat(format!(
"Too many fields: {}, expected 7",
v
)))
}
}
}
Ok(Cookie {
domain,
include_subdomains,
path,
https_only,
http_only,
expires,
name,
value,
})
}
}
impl TryFrom<&str> for Cookies {
type Error = ParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.lines().peekable().peek().is_none() {
return Err(ParseError::Empty);
}
let mut cookies: Cookies = Cookies(vec![]);
for line in value.lines() {
let cookie = match Cookie::try_from(line) {
Ok(c) => c,
Err(ParseError::Empty) => continue,
e => e?,
};
cookies.push(cookie);
}
if cookies.is_empty() {
return Err(ParseError::Empty);
}
Ok(cookies)
}
}
#[cfg(any(feature = "cookie", test))]
impl From<&Cookie> for cookie::Cookie<'_> {
fn from(value: &Cookie) -> Self {
Self::build((value.clone().name, value.clone().value))
.domain(value.clone().domain)
.path(value.clone().path)
.secure(value.https_only)
.http_only(value.http_only)
.expires(cookie::Expiration::from(match value.expires {
0 => None,
v => time::OffsetDateTime::from_unix_timestamp(v as i64).ok(),
}))
.build()
}
}
#[cfg(any(feature = "thirtyfour", test))]
impl From<Cookie> for thirtyfour::Cookie<'_> {
fn from(value: Cookie) -> Self {
Self::build(value.name, value.value)
.domain(value.domain)
.path(value.path)
.secure(value.https_only)
.http_only(value.http_only)
.expires(thirtyfour::cookie::Expiration::from(match value.expires {
0 => None,
v => time::OffsetDateTime::from_unix_timestamp(v as i64).ok(),
}))
.finish()
}
}
#[cfg(any(feature = "cookie_store", test))]
impl Cookie {
pub fn into_cookie_store_cookie(
self,
domain: &str,
) -> Result<cookie_store::Cookie<'_>, ParseError> {
cookie_store::Cookie::try_from_raw_cookie(
&self.into(),
&url::Url::parse(domain).map_err(|_| ParseError::InvalidUrl)?,
)
.map_err(|e| ParseError::InternalError(e.to_string()))
}
}
#[cfg(any(feature = "cookie_store", test))]
impl From<Cookie> for cookie_store::RawCookie<'_> {
fn from(value: Cookie) -> Self {
Self::build((value.name, value.value))
.domain(value.domain)
.path(value.path)
.secure(value.https_only)
.http_only(value.http_only)
.expires(cookie::Expiration::from(match value.expires {
0 => None,
v => time::OffsetDateTime::from_unix_timestamp(v as i64).ok(),
}))
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
const COOKIE_TXT: &str = r#"
# Netscape HTTP Cookie File
# http://curl.haxx.se/rfc/cookie_spec.html
# This is a generated file! Do not edit.
.example.com TRUE / TRUE 0000000000 foo bar
.example.com TRUE / TRUE 1740743335 foo2 bar2
#HttpOnly_ .example.com TRUE / TRUE 1740743335 foo3 bar3
"#;
#[test]
fn parse_cookie_line() {
let input = r#"
.example.com TRUE / TRUE 1234567890 foo bar
"#;
assert_eq!(
Cookie::try_from(input),
Ok(Cookie {
domain: ".example.com".to_string(),
include_subdomains: true,
path: "/".to_string(),
https_only: true,
http_only: false,
expires: 1234567890,
name: "foo".to_string(),
value: "bar".to_string()
})
);
}
#[test]
fn parse_cookie_line_http_only() {
let input = r#"
#HttpOnly_ .example.com TRUE / TRUE 1234567890 foo bar
"#;
assert_eq!(
Cookie::try_from(input),
Ok(Cookie {
domain: ".example.com".to_string(),
include_subdomains: true,
path: "/".to_string(),
https_only: true,
http_only: true,
expires: 1234567890,
name: "foo".to_string(),
value: "bar".to_string()
})
);
}
#[test]
fn parse_empty_line() {
let input = "";
assert_eq!(Cookie::try_from(input), Err(ParseError::Empty))
}
#[test]
fn parse_comment() {
let input = "# hello world";
assert_eq!(Cookie::try_from(input), Err(ParseError::Empty))
}
#[test]
fn parse_cookie_txt() {
let exp = vec![
Cookie {
domain: ".example.com".to_string(),
include_subdomains: true,
path: "/".to_string(),
https_only: true,
http_only: false,
expires: 0,
name: "foo".to_string(),
value: "bar".to_string(),
},
Cookie {
domain: ".example.com".to_string(),
include_subdomains: true,
path: "/".to_string(),
https_only: true,
http_only: false,
expires: 1740743335,
name: "foo2".to_string(),
value: "bar2".to_string(),
},
Cookie {
domain: ".example.com".to_string(),
include_subdomains: true,
path: "/".to_string(),
https_only: true,
http_only: true,
expires: 1740743335,
name: "foo3".to_string(),
value: "bar3".to_string(),
},
];
let cookies: Vec<Cookie> = Cookies::try_from(COOKIE_TXT).unwrap().to_vec();
assert_eq!(cookies, exp);
}
#[cfg(any(feature = "cookie", test))]
#[test]
fn test_convert_to_cookie() {
let converted: Vec<cookie::Cookie> = Cookies::try_from(COOKIE_TXT).unwrap().into();
let exp = vec![
cookie::Cookie::build(("foo", "bar"))
.domain(".example.com")
.path("/")
.secure(true)
.http_only(false)
.expires(cookie::Expiration::Session)
.build(),
cookie::Cookie::build(("foo2", "bar2"))
.domain(".example.com")
.path("/")
.secure(true)
.http_only(false)
.expires(time::OffsetDateTime::from_unix_timestamp(1740743335).unwrap())
.build(),
cookie::Cookie::build(("foo3", "bar3"))
.domain(".example.com")
.path("/")
.secure(true)
.http_only(true)
.expires(time::OffsetDateTime::from_unix_timestamp(1740743335).unwrap())
.build(),
];
assert_eq!(converted, exp);
}
}