From 588368328710b52a53374c16230d66371efbf7a3 Mon Sep 17 00:00:00 2001 From: ozpv <39195175+ozpv@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:04:53 -0600 Subject: [PATCH 1/2] add chrono support --- Cargo.toml | 2 + src/builder.rs | 2 +- src/expiration.rs | 18 +++---- src/jar.rs | 4 +- src/lib.rs | 133 ++++++++++++++++++++++++++-------------------- src/parse.rs | 112 +++++++++++++++++++++++--------------- 6 files changed, 158 insertions(+), 113 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b1d19441..b3fd20e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,9 +19,11 @@ secure = ["private", "signed", "key-expansion"] private = ["aes-gcm", "base64", "rand", "subtle"] signed = ["hmac", "sha2", "base64", "rand", "subtle"] key-expansion = ["sha2", "hkdf"] +chrono = ["dep:chrono"] [dependencies] time = { version = "0.3", default-features = false, features = ["std", "parsing", "formatting", "macros"] } +chrono = { version = "0.4", optional = true } percent-encoding = { version = "2.0", optional = true } # dependencies for secure (private/signed) functionality diff --git a/src/builder.rs b/src/builder.rs index 8d7e9d59..934594cd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -102,7 +102,7 @@ impl<'c> CookieBuilder<'c> { /// assert_eq!(c.inner().max_age(), Some(Duration::seconds(30 * 60))); /// ``` #[inline] - pub fn max_age(mut self, value: time::Duration) -> Self { + pub fn max_age(mut self, value: crate::Duration) -> Self { self.cookie.set_max_age(value); self } diff --git a/src/expiration.rs b/src/expiration.rs index 89eaf9fb..aba43a30 100644 --- a/src/expiration.rs +++ b/src/expiration.rs @@ -1,4 +1,4 @@ -use time::OffsetDateTime; +use crate::UtcDateTime; /// A cookie's expiration: either a date-time or session. /// @@ -25,7 +25,7 @@ use time::OffsetDateTime; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Expiration { /// Expiration for a "permanent" cookie at a specific date-time. - DateTime(OffsetDateTime), + DateTime(UtcDateTime), /// Expiration for a "session" cookie. Browsers define the notion of a /// "session" and will automatically expire session cookies when they deem /// the "session" to be over. This is typically, but need not be, when the @@ -51,7 +51,7 @@ impl Expiration { pub fn is_datetime(&self) -> bool { match self { Expiration::DateTime(_) => true, - Expiration::Session => false + Expiration::Session => false, } } @@ -72,7 +72,7 @@ impl Expiration { pub fn is_session(&self) -> bool { match self { Expiration::DateTime(_) => false, - Expiration::Session => true + Expiration::Session => true, } } @@ -91,10 +91,10 @@ impl Expiration { /// let expires = Expiration::from(now); /// assert_eq!(expires.datetime(), Some(now)); /// ``` - pub fn datetime(self) -> Option { + pub fn datetime(self) -> Option { match self { Expiration::Session => None, - Expiration::DateTime(v) => Some(v) + Expiration::DateTime(v) => Some(v), } } @@ -117,7 +117,7 @@ impl Expiration { /// assert_eq!(expires.map(|t| t + one_week).datetime(), None); /// ``` pub fn map(self, f: F) -> Self - where F: FnOnce(OffsetDateTime) -> OffsetDateTime + where F: FnOnce(UtcDateTime) -> UtcDateTime, { match self { Expiration::Session => Expiration::Session, @@ -126,11 +126,11 @@ impl Expiration { } } -impl>> From for Expiration { +impl>> From for Expiration { fn from(option: T) -> Self { match option.into() { Some(value) => Expiration::DateTime(value), - None => Expiration::Session + None => Expiration::Session, } } } diff --git a/src/jar.rs b/src/jar.rs index 29953103..bc321c86 100644 --- a/src/jar.rs +++ b/src/jar.rs @@ -601,8 +601,8 @@ impl<'a> Iterator for Delta<'a> { } } -use std::collections::hash_set::Difference; use std::collections::hash_map::RandomState; +use std::collections::hash_set::Difference; use std::iter::Chain; /// Iterator over all of the cookies in a jar. @@ -616,7 +616,7 @@ impl<'a> Iterator for Iter<'a> { fn next(&mut self) -> Option<&'a Cookie<'static>> { for cookie in self.delta_cookies.by_ref() { if !cookie.removed { - return Some(&*cookie); + return Some(cookie); } } diff --git a/src/lib.rs b/src/lib.rs index f666d2c3..7be5f550 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,10 @@ //! A meta-feature that simultaneously enables `signed`, `private`, and //! `key-expansion`. //! +//! * **`chrono`** +//! +//! Use chrono instead of time. +//! //! You can enable features via `Cargo.toml`: //! //! ```toml @@ -69,17 +73,17 @@ //! ``` #![cfg_attr(all(nightly, doc), feature(doc_cfg))] - #![deny(missing_docs)] -pub use time; +#[cfg(feature = "chrono")] pub use chrono; +#[cfg(not(feature = "chrono"))] pub use time; mod builder; -mod parse; -mod jar; mod delta; -mod same_site; mod expiration; +mod jar; +mod parse; +mod same_site; /// Implementation of [HTTP RFC6265 draft] cookie prefixes. /// @@ -94,17 +98,29 @@ use std::borrow::Cow; use std::fmt; use std::str::FromStr; -#[allow(unused_imports, deprecated)] -use std::ascii::AsciiExt; +#[allow(unused_imports, deprecated)] use std::ascii::AsciiExt; -use time::{Duration, OffsetDateTime, UtcOffset, macros::datetime}; +#[cfg(feature = "chrono")] use chrono::{DateTime, Utc}; +#[cfg(not(feature = "chrono"))] use time::{macros::datetime, OffsetDateTime, UtcOffset}; -use crate::parse::parse_cookie; -pub use crate::parse::ParseError; pub use crate::builder::CookieBuilder; +pub use crate::expiration::*; pub use crate::jar::{CookieJar, Delta, Iter}; +use crate::parse::parse_cookie; +pub use crate::parse::ParseError; pub use crate::same_site::*; -pub use crate::expiration::*; + +/// Alias of [`OffsetDateTime`](time::OffsetDateTime) +#[cfg(not(feature = "chrono"))] pub type UtcDateTime = time::OffsetDateTime; + +/// Alias of [`DateTime`](chrono::DateTime) +#[cfg(feature = "chrono")] pub type UtcDateTime = chrono::DateTime; + +/// Alias of [`Duration`](time::Duration) +#[cfg(not(feature = "chrono"))] pub type Duration = time::Duration; + +/// Alias of [`Duration`](chrono::Duration) +#[cfg(feature = "chrono")] pub type Duration = chrono::Duration; #[derive(Debug, Clone)] enum CookieStr<'c> { @@ -151,7 +167,7 @@ impl<'c> CookieStr<'c> { converting indexed str to str! (This is a module invariant.)"); &s[i..j] }, - CookieStr::Concrete(ref cstr) => &*cstr, + CookieStr::Concrete(ref cstr) => cstr, } } @@ -163,13 +179,13 @@ impl<'c> CookieStr<'c> { Cow::Borrowed(s) => Some(&s[i..j]), Cow::Owned(_) => None, } - }, + } CookieStr::Concrete(_) => None, } } fn into_owned(self) -> CookieStr<'static> { - use crate::CookieStr::*; + use crate::CookieStr::{Concrete, Indexed}; match self { Indexed(a, b) => Indexed(a, b), @@ -255,7 +271,7 @@ impl<'c> Cookie<'c> { /// ``` pub fn new(name: N, value: V) -> Self where N: Into>, - V: Into> + V: Into>, { Cookie { cookie_string: None, @@ -290,7 +306,7 @@ impl<'c> Cookie<'c> { /// ``` #[deprecated(since = "0.18.0", note = "use `Cookie::build(name)` or `Cookie::from(name)`")] pub fn named(name: N) -> Cookie<'c> - where N: Into> + where N: Into>, { Cookie::new(name, "") } @@ -342,7 +358,7 @@ impl<'c> Cookie<'c> { /// assert_eq!(c.secure(), None); /// ``` pub fn parse(s: S) -> Result, ParseError> - where S: Into> + where S: Into>, { parse_cookie(s.into(), false) } @@ -364,7 +380,8 @@ impl<'c> Cookie<'c> { #[cfg(feature = "percent-encode")] #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] pub fn parse_encoded(s: S) -> Result, ParseError> - where S: Into> + where + S: Into>, { parse_cookie(s.into(), true) } @@ -399,7 +416,7 @@ impl<'c> Cookie<'c> { /// ``` #[inline(always)] pub fn split_parse(string: S) -> SplitCookies<'c> - where S: Into> + where S: Into>, { SplitCookies { string: string.into(), @@ -440,7 +457,7 @@ impl<'c> Cookie<'c> { #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] #[inline(always)] pub fn split_parse_encoded(string: S) -> SplitCookies<'c> - where S: Into> + where S: Into>, { SplitCookies { string: string.into(), @@ -468,8 +485,8 @@ impl<'c> Cookie<'c> { value: self.value.into_owned(), expires: self.expires, max_age: self.max_age, - domain: self.domain.map(|s| s.into_owned()), - path: self.path.map(|s| s.into_owned()), + domain: self.domain.map(CookieStr::into_owned), + path: self.path.map(CookieStr::into_owned), secure: self.secure, http_only: self.http_only, same_site: self.same_site, @@ -553,7 +570,7 @@ impl<'c> Cookie<'c> { let bytes = s.as_bytes(); match (bytes.first(), bytes.last()) { (Some(b'"'), Some(b'"')) => &s[1..(s.len() - 1)], - _ => s + _ => s, } } @@ -779,7 +796,7 @@ impl<'c> Cookie<'c> { Some(ref c) => { let domain = c.to_str(self.cookie_string.as_ref()); domain.strip_prefix(".").or(Some(domain)) - }, + } None => None, } } @@ -828,8 +845,8 @@ impl<'c> Cookie<'c> { /// assert_eq!(c.expires_datetime().map(|t| t.year()), Some(2017)); /// ``` #[inline] - pub fn expires_datetime(&self) -> Option { - self.expires.and_then(|e| e.datetime()) + pub fn expires_datetime(&self) -> Option { + self.expires.and_then(Expiration::datetime) } /// Sets the name of `self` to `name`. @@ -846,7 +863,7 @@ impl<'c> Cookie<'c> { /// assert_eq!(c.name(), "foo"); /// ``` pub fn set_name>>(&mut self, name: N) { - self.name = CookieStr::Concrete(name.into()) + self.name = CookieStr::Concrete(name.into()); } /// Sets the value of `self` to `value`. @@ -863,7 +880,7 @@ impl<'c> Cookie<'c> { /// assert_eq!(c.value(), "bar"); /// ``` pub fn set_value>>(&mut self, value: V) { - self.value = CookieStr::Concrete(value.into()) + self.value = CookieStr::Concrete(value.into()); } /// Sets the value of `http_only` in `self` to `value`. If `value` is @@ -1115,7 +1132,8 @@ impl<'c> Cookie<'c> { /// assert_eq!(c.expires(), Some(Expiration::Session)); /// ``` pub fn set_expires>(&mut self, time: T) { - static MAX_DATETIME: OffsetDateTime = datetime!(9999-12-31 23:59:59.999_999 UTC); + #[cfg(feature = "chrono")] static MAX_DATETIME: UtcDateTime = DateTime::::MAX_UTC; + #[cfg(not(feature = "chrono"))] static MAX_DATETIME: OffsetDateTime = datetime!(9999-12-31 23:59:59.999_999 UTC); // RFC 6265 requires dates not to exceed 9999 years. self.expires = Some(time.into() @@ -1165,7 +1183,9 @@ impl<'c> Cookie<'c> { pub fn make_permanent(&mut self) { let twenty_years = Duration::days(365 * 20); self.set_max_age(twenty_years); - self.set_expires(OffsetDateTime::now_utc() + twenty_years); + + #[cfg(feature = "chrono")] self.set_expires(Utc::now() + twenty_years); + #[cfg(not(feature = "chrono"))] self.set_expires(UtcDateTime::now_utc() + twenty_years); } /// Make `self` a "removal" cookie by clearing its value, setting a max-age @@ -1192,7 +1212,9 @@ impl<'c> Cookie<'c> { pub fn make_removal(&mut self) { self.set_value(""); self.set_max_age(Duration::seconds(0)); - self.set_expires(OffsetDateTime::now_utc() - Duration::days(365)); + + #[cfg(feature = "chrono")] self.set_expires(Utc::now() - Duration::days(365)); + #[cfg(not(feature = "chrono"))] self.set_expires(UtcDateTime::now_utc() - Duration::days(365)); } fn fmt_parameters(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -1224,12 +1246,20 @@ impl<'c> Cookie<'c> { } if let Some(max_age) = self.max_age() { - write!(f, "; Max-Age={}", max_age.whole_seconds())?; + #[cfg(feature = "chrono")] write!(f, "; Max-Age={}", max_age.num_seconds())?; + #[cfg(not(feature = "chrono"))] write!(f, "; Max-Age={}", max_age.whole_seconds())?; } if let Some(time) = self.expires_datetime() { - let time = time.to_offset(UtcOffset::UTC); - write!(f, "; Expires={}", time.format(&crate::parse::FMT1).map_err(|_| fmt::Error)?)?; + #[cfg(feature = "chrono")] { + let time = time.to_utc(); + write!(f, "; Expires={}", time.format(crate::parse::FMT1))?; + } + + #[cfg(not(feature = "chrono"))] { + let time = time.to_offset(UtcOffset::UTC); + write!(f, "; Expires={}", time.format(&crate::parse::FMT1).map_err(|_| fmt::Error)?)?; + } } Ok(()) @@ -1364,7 +1394,7 @@ impl<'c> Cookie<'c> { (Some(domain), Some(string)) => match domain.to_raw_str(string) { Some(s) => s.strip_prefix(".").or(Some(s)), None => None, - } + }, _ => None, } } @@ -1437,18 +1467,17 @@ impl<'c> Iterator for SplitCookies<'c> { let i = self.last; let j = self.string[i..] .find(';') - .map(|k| i + k) - .unwrap_or(self.string.len()); + .map_or(self.string.len(), |k| i + k); self.last = j + 1; - if self.string[i..j].chars().all(|c| c.is_whitespace()) { + if self.string[i..j].chars().all(char::is_whitespace) { continue; } return Some(match self.string { Cow::Borrowed(s) => parse_cookie(s[i..j].trim(), self.decode), Cow::Owned(ref s) => parse_cookie(s[i..j].trim().to_owned(), self.decode), - }) + }); } None @@ -1460,19 +1489,10 @@ mod encoding { use percent_encoding::{AsciiSet, CONTROLS}; /// https://url.spec.whatwg.org/#fragment-percent-encode-set - const FRAGMENT: &AsciiSet = &CONTROLS - .add(b' ') - .add(b'"') - .add(b'<') - .add(b'>') - .add(b'`'); + const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); /// https://url.spec.whatwg.org/#path-percent-encode-set - const PATH: &AsciiSet = &FRAGMENT - .add(b'#') - .add(b'?') - .add(b'{') - .add(b'}'); + const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}'); /// https://url.spec.whatwg.org/#userinfo-percent-encode-set const USERINFO: &AsciiSet = &PATH @@ -1489,10 +1509,7 @@ mod encoding { .add(b'%'); /// https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1 + '(', ')' - const COOKIE: &AsciiSet = &USERINFO - .add(b'(') - .add(b')') - .add(b','); + const COOKIE: &AsciiSet = &USERINFO.add(b'(').add(b')').add(b','); /// Percent-encode a cookie name or value with the proper encoding set. pub fn encode(string: &str) -> impl std::fmt::Display + '_ { @@ -1547,7 +1564,7 @@ impl<'a, 'c: 'a> fmt::Display for Display<'a, 'c> { match self.strip { true => Ok(()), - false => self.cookie.fmt_parameters(f) + false => self.cookie.fmt_parameters(f), } } } @@ -1603,7 +1620,7 @@ impl FromStr for Cookie<'static> { type Err = ParseError; fn from_str(s: &str) -> Result, ParseError> { - Cookie::parse(s).map(|c| c.into_owned()) + Cookie::parse(s).map(Cookie::into_owned) } } @@ -1657,7 +1674,7 @@ impl<'a> From> for Cookie<'a> { impl<'a, N, V> From<(N, V)> for Cookie<'a> where N: Into>, - V: Into> + V: Into>, { fn from((name, value): (N, V)) -> Self { Cookie::new(name, value) @@ -1684,7 +1701,7 @@ impl<'a> AsMut> for Cookie<'a> { #[cfg(test)] mod tests { - use crate::{Cookie, SameSite, parse::parse_date}; + use crate::{parse::parse_date, Cookie, SameSite}; use time::{Duration, OffsetDateTime}; #[test] diff --git a/src/parse.rs b/src/parse.rs index b263d915..5e163fbc 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,25 +1,31 @@ use std::borrow::Cow; +use std::convert::From; use std::error::Error; -use std::convert::{From, TryFrom}; -use std::str::Utf8Error; use std::fmt; +use std::str::Utf8Error; -#[allow(unused_imports, deprecated)] -use std::ascii::AsciiExt; +#[allow(unused_imports, deprecated)] use std::ascii::AsciiExt; -#[cfg(feature = "percent-encode")] -use percent_encoding::percent_decode; -use time::{PrimitiveDateTime, Duration, OffsetDateTime}; -use time::{parsing::Parsable, macros::format_description, format_description::FormatItem}; +#[cfg(feature = "chrono")] use chrono::{DateTime, Utc}; +#[cfg(feature = "percent-encode")] use percent_encoding::percent_decode; +#[cfg(not(feature = "chrono"))] use time::{format_description::FormatItem, macros::format_description, parsing::Parsable}; +#[cfg(not(feature = "chrono"))] use time::{OffsetDateTime, PrimitiveDateTime}; +#[cfg(not(feature = "chrono"))] use std::convert::TryFrom; -use crate::{Cookie, SameSite, CookieStr}; +use crate::{Cookie, CookieStr, Duration, SameSite}; // The three formats spec'd in http://tools.ietf.org/html/rfc2616#section-3.3.1. // Additional ones as encountered in the real world. -pub static FMT1: &[FormatItem<'_>] = format_description!("[weekday repr:short], [day] [month repr:short] [year padding:none] [hour]:[minute]:[second] GMT"); -pub static FMT2: &[FormatItem<'_>] = format_description!("[weekday], [day]-[month repr:short]-[year repr:last_two] [hour]:[minute]:[second] GMT"); -pub static FMT3: &[FormatItem<'_>] = format_description!("[weekday repr:short] [month repr:short] [day padding:space] [hour]:[minute]:[second] [year padding:none]"); -pub static FMT4: &[FormatItem<'_>] = format_description!("[weekday repr:short], [day]-[month repr:short]-[year padding:none] [hour]:[minute]:[second] GMT"); +#[cfg(not(feature = "chrono"))] pub static FMT1: &[FormatItem<'_>] = format_description!("[weekday repr:short], [day] [month repr:short] [year padding:none] [hour]:[minute]:[second] GMT"); +#[cfg(not(feature = "chrono"))] pub static FMT2: &[FormatItem<'_>] = format_description!("[weekday], [day]-[month repr:short]-[year repr:last_two] [hour]:[minute]:[second] GMT"); +#[cfg(not(feature = "chrono"))] pub static FMT3: &[FormatItem<'_>] = format_description!("[weekday repr:short] [month repr:short] [day padding:space] [hour]:[minute]:[second] [year padding:none]"); +#[cfg(not(feature = "chrono"))] pub static FMT4: &[FormatItem<'_>] = format_description!("[weekday repr:short], [day]-[month repr:short]-[year padding:none] [hour]:[minute]:[second] GMT"); + +// the exact formatters above, but for chrono +#[cfg(feature = "chrono")] pub const FMT1: &'static str = "%a, %d %b %G %H:%M:%S GMT"; +#[cfg(feature = "chrono")] pub const FMT2: &'static str = "%A, %d-%b-%g %H:%M:%S GMT"; +#[cfg(feature = "chrono")] pub const FMT3: &'static str = "%a %b %e %H:%M:%S %G"; +#[cfg(feature = "chrono")] pub const FMT4: &'static str = "%a, %d-%b-%G %H:%M:%S GMT"; /// Enum corresponding to a parsing error. #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -67,13 +73,13 @@ impl Error for ParseError { #[cfg(feature = "percent-encode")] fn name_val_decoded( name: &str, - val: &str + val: &str, ) -> Result, CookieStr<'static>)>, ParseError> { let decoded_name = percent_decode(name.as_bytes()).decode_utf8()?; let decoded_value = percent_decode(val.as_bytes()).decode_utf8()?; if let (&Cow::Borrowed(_), &Cow::Borrowed(_)) = (&decoded_name, &decoded_value) { - Ok(None) + Ok(None) } else { let name = CookieStr::Concrete(Cow::Owned(decoded_name.into())); let val = CookieStr::Concrete(Cow::Owned(decoded_value.into())); @@ -84,7 +90,7 @@ fn name_val_decoded( #[cfg(not(feature = "percent-encode"))] fn name_val_decoded( _: &str, - _: &str + _: &str, ) -> Result, CookieStr<'static>)>, ParseError> { unreachable!("This function should never be called with 'percent-encode' disabled!") } @@ -100,7 +106,7 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { let key_value = attributes.next().expect("first str::split().next() returns Some"); let (name, value) = match key_value.find('=') { Some(i) => (key_value[..i].trim(), key_value[(i + 1)..].trim()), - None => return Err(ParseError::MissingPair) + None => return Err(ParseError::MissingPair), }; if name.is_empty() { @@ -119,7 +125,7 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { let (name, value) = if decode { match name_val_decoded(name, value)? { Some((name, value)) => (name, value), - None => indexed_names(s, name, value) + None => indexed_names(s, name, value), } } else { indexed_names(s, name, value) @@ -153,18 +159,23 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { v = &v[1..]; } - if !v.chars().all(|d| d.is_digit(10)) { - continue + if !v.chars().all(|d| d.is_ascii_digit()) { + continue; } // From RFC 6265 5.2.2: neg values indicate that the earliest // expiration should be used, so set the max age to 0 seconds. if is_negative { - Some(Duration::ZERO) + #[cfg(feature = "chrono")] { + Some(Duration::zero()) + } + + #[cfg(not(feature = "chrono"))] { + Some(Duration::ZERO) + } } else { - Some(v.parse::() - .map(Duration::seconds) - .unwrap_or_else(|_| Duration::seconds(i64::max_value()))) + Some(v.parse::().map_or_else( + |_| Duration::seconds(i64::max_value()), Duration::seconds)) } }, ("domain", Some(d)) if !d.is_empty() => { @@ -187,19 +198,32 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { // invalid value is passed in. The draft is at // http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-same-site.html. } - } + }, ("partitioned", _) => cookie.partitioned = Some(true), ("expires", Some(v)) => { - let tm = parse_date(v, &FMT1) - .or_else(|_| parse_date(v, &FMT2)) - .or_else(|_| parse_date(v, &FMT3)) - .or_else(|_| parse_date(v, &FMT4)); - // .or_else(|_| parse_date(v, &FMT5)); - - if let Ok(time) = tm { - cookie.expires = Some(time.into()) + #[cfg(feature = "chrono")] { + let tm = DateTime::parse_from_str(v, FMT1) + .or_else(|_| DateTime::parse_from_str(v, FMT2)) + .or_else(|_| DateTime::parse_from_str(v, FMT3)) + .or_else(|_| DateTime::parse_from_str(v, FMT4)); + + if let Ok(time) = tm { + cookie.expires = Some(Into::>::into(time).into()); + } } - } + + #[cfg(not(feature = "chrono"))] { + let tm = parse_date(v, &FMT1) + .or_else(|_| parse_date(v, &FMT2)) + .or_else(|_| parse_date(v, &FMT3)) + .or_else(|_| parse_date(v, &FMT4)); + // .or_else(|_| parse_date(v, &FMT5)); + + if let Ok(time) = tm { + cookie.expires = Some(time.into()); + } + } + }, _ => { // We're going to be permissive here. If we have no idea what // this is, then it's something nonstandard. We're not going to @@ -213,7 +237,7 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { } pub(crate) fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result, ParseError> - where S: Into> + where S: Into>, { let s = cow.into(); let mut cookie = parse_inner(&s, decode)?; @@ -221,6 +245,7 @@ pub(crate) fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result, Pa Ok(cookie) } +#[cfg(not(feature = "chrono"))] pub(crate) fn parse_date(s: &str, format: &impl Parsable) -> Result { // Parse. Handle "abbreviated" dates like Chromium. See cookie#162. let mut date = format.parse(s.as_bytes())?; @@ -244,25 +269,25 @@ mod tests { use time::Duration; macro_rules! assert_eq_parse { - ($string:expr, $expected:expr) => ( + ($string:expr, $expected:expr) => { let cookie = match Cookie::parse($string) { Ok(cookie) => cookie, - Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e) + Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e), }; assert_eq!(cookie, $expected); - ) + }; } macro_rules! assert_ne_parse { - ($string:expr, $expected:expr) => ( + ($string:expr, $expected:expr) => { let cookie = match Cookie::parse($string) { Ok(cookie) => cookie, - Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e) + Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e), }; assert_ne!(cookie, $expected); - ) + }; } #[test] @@ -288,6 +313,7 @@ mod tests { assert_eq_parse!("foo=bar; SameSite=nOne", expected); } + #[test] fn parse() { assert!(Cookie::parse("bar").is_err()); @@ -426,7 +452,7 @@ mod tests { assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", unexpected); } - + #[test] fn parse_abbreviated_years() { let cookie_str = "foo=bar; expires=Thu, 10-Sep-20 20:00:00 GMT"; @@ -497,7 +523,7 @@ mod tests { let expected = Cookie::new("foo", "b/r"); let cookie = match Cookie::parse_encoded("foo=b%2Fr") { Ok(cookie) => cookie, - Err(e) => panic!("Failed to parse: {:?}", e) + Err(e) => panic!("Failed to parse: {:?}", e), }; assert_eq!(cookie, expected); From cb3f88edac1afee88096f18e05446e6edc181a0b Mon Sep 17 00:00:00 2001 From: ozpv <39195175+ozpv@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:14:45 -0600 Subject: [PATCH 2/2] some chrono tests passing --- Cargo.toml | 2 +- src/jar.rs | 3 +- src/lib.rs | 49 +++++++++++++++---- src/parse.rs | 134 ++++++++++++++++++++++++++++++++++----------------- 4 files changed, 132 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b3fd20e0..226a283d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ chrono = ["dep:chrono"] [dependencies] time = { version = "0.3", default-features = false, features = ["std", "parsing", "formatting", "macros"] } -chrono = { version = "0.4", optional = true } +chrono = { version = "0.4.39", optional = true } percent-encoding = { version = "2.0", optional = true } # dependencies for secure (private/signed) functionality diff --git a/src/jar.rs b/src/jar.rs index bc321c86..42d5cdca 100644 --- a/src/jar.rs +++ b/src/jar.rs @@ -693,7 +693,8 @@ mod test { #[test] fn delta() { use std::collections::HashMap; - use time::Duration; + #[cfg(feature = "chrono")] use chrono::Duration; + #[cfg(not(feature = "chrono"))] use time::Duration; let mut c = CookieJar::new(); diff --git a/src/lib.rs b/src/lib.rs index 7be5f550..1645d680 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,7 +100,7 @@ use std::str::FromStr; #[allow(unused_imports, deprecated)] use std::ascii::AsciiExt; -#[cfg(feature = "chrono")] use chrono::{DateTime, Utc}; +#[cfg(feature = "chrono")] use chrono::{DateTime, NaiveDate, Utc}; #[cfg(not(feature = "chrono"))] use time::{macros::datetime, OffsetDateTime, UtcOffset}; pub use crate::builder::CookieBuilder; @@ -1132,7 +1132,10 @@ impl<'c> Cookie<'c> { /// assert_eq!(c.expires(), Some(Expiration::Session)); /// ``` pub fn set_expires>(&mut self, time: T) { - #[cfg(feature = "chrono")] static MAX_DATETIME: UtcDateTime = DateTime::::MAX_UTC; + #[cfg(feature = "chrono")] + static MAX_DATETIME: DateTime + = NaiveDate::from_ymd_opt(9999, 12, 31).unwrap().and_hms_nano_opt(23, 59, 59, 999_999).unwrap().and_utc(); + #[cfg(not(feature = "chrono"))] static MAX_DATETIME: OffsetDateTime = datetime!(9999-12-31 23:59:59.999_999 UTC); // RFC 6265 requires dates not to exceed 9999 years. @@ -1701,8 +1704,10 @@ impl<'a> AsMut> for Cookie<'a> { #[cfg(test)] mod tests { - use crate::{parse::parse_date, Cookie, SameSite}; - use time::{Duration, OffsetDateTime}; + use crate::{Cookie, SameSite}; + #[cfg(feature = "chrono")] use chrono::{Duration, DateTime, NaiveDate, NaiveDateTime}; + #[cfg(not(feature = "chrono"))] use time::{Duration, OffsetDateTime}; + #[cfg(not(feature = "chrono"))] use crate::parse::parse_date; #[test] fn format() { @@ -1730,11 +1735,22 @@ mod tests { let cookie = Cookie::build(("foo", "bar")).domain("rust-lang.org"); assert_eq!(&cookie.to_string(), "foo=bar; Domain=rust-lang.org"); - let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; - let expires = parse_date(time_str, &crate::parse::FMT1).unwrap(); - let cookie = Cookie::build(("foo", "bar")).expires(expires); - assert_eq!(&cookie.to_string(), - "foo=bar; Expires=Wed, 21 Oct 2015 07:28:00 GMT"); + #[cfg(feature = "chrono")] { + let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; + let expires = NaiveDateTime::parse_from_str(time_str, crate::parse::FMT1) + .unwrap().and_utc(); + let cookie = Cookie::build(("foo", "bar")).expires(expires); + assert_eq!(&cookie.to_string(), + "foo=bar; Expires=Wed, 21 Oct 2015 07:28:00 GMT"); + } + + #[cfg(not(feature = "chrono"))] { + let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; + let expires = parse_date(time_str, &crate::parse::FMT1).unwrap(); + let cookie = Cookie::build(("foo", "bar")).expires(expires); + assert_eq!(&cookie.to_string(), + "foo=bar; Expires=Wed, 21 Oct 2015 07:28:00 GMT"); + } let cookie = Cookie::build(("foo", "bar")).same_site(SameSite::Strict); assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Strict"); @@ -1766,6 +1782,21 @@ mod tests { assert_eq!(&c.to_string(), "foo=bar; SameSite=None; Secure"); } + #[cfg(feature = "chrono")] + #[test] + #[ignore] + fn format_date_wraps() { + let expires = DateTime::UNIX_EPOCH + Duration::MAX; + let cookie = Cookie::build(("foo", "bar")).expires(expires); + assert_eq!(&cookie.to_string(), "foo=bar; Expires=Fri, 31 Dec 9999 23:59:59 GMT"); + + let expires = NaiveDate::from_ymd_opt(9999, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap() + .and_utc() + Duration::days(1000); + let cookie = Cookie::build(("foo", "bar")).expires(expires); + assert_eq!(&cookie.to_string(), "foo=bar; Expires=Fri, 31 Dec 9999 23:59:59 GMT"); + } + + #[cfg(not(feature = "chrono"))] #[test] #[ignore] fn format_date_wraps() { diff --git a/src/parse.rs b/src/parse.rs index 5e163fbc..cd0a6985 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,7 +6,7 @@ use std::str::Utf8Error; #[allow(unused_imports, deprecated)] use std::ascii::AsciiExt; -#[cfg(feature = "chrono")] use chrono::{DateTime, Utc}; +#[cfg(feature = "chrono")] use chrono::NaiveDateTime; #[cfg(feature = "percent-encode")] use percent_encoding::percent_decode; #[cfg(not(feature = "chrono"))] use time::{format_description::FormatItem, macros::format_description, parsing::Parsable}; #[cfg(not(feature = "chrono"))] use time::{OffsetDateTime, PrimitiveDateTime}; @@ -22,10 +22,10 @@ use crate::{Cookie, CookieStr, Duration, SameSite}; #[cfg(not(feature = "chrono"))] pub static FMT4: &[FormatItem<'_>] = format_description!("[weekday repr:short], [day]-[month repr:short]-[year padding:none] [hour]:[minute]:[second] GMT"); // the exact formatters above, but for chrono -#[cfg(feature = "chrono")] pub const FMT1: &'static str = "%a, %d %b %G %H:%M:%S GMT"; -#[cfg(feature = "chrono")] pub const FMT2: &'static str = "%A, %d-%b-%g %H:%M:%S GMT"; -#[cfg(feature = "chrono")] pub const FMT3: &'static str = "%a %b %e %H:%M:%S %G"; -#[cfg(feature = "chrono")] pub const FMT4: &'static str = "%a, %d-%b-%G %H:%M:%S GMT"; +#[cfg(feature = "chrono")] pub const FMT1: &'static str = "%a, %d %b %Y %H:%M:%S GMT"; +#[cfg(feature = "chrono")] pub const FMT2: &'static str = "%A, %d-%b-%y %H:%M:%S GMT"; +#[cfg(feature = "chrono")] pub const FMT3: &'static str = "%a %b %_d %H:%M:%S %Y"; +#[cfg(feature = "chrono")] pub const FMT4: &'static str = "%a, %d-%b-%Y %H:%M:%S GMT"; /// Enum corresponding to a parsing error. #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -165,17 +165,24 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { // From RFC 6265 5.2.2: neg values indicate that the earliest // expiration should be used, so set the max age to 0 seconds. + #[cfg(feature = "chrono")] if is_negative { - #[cfg(feature = "chrono")] { - Some(Duration::zero()) - } + Some(Duration::zero()) + } else { + Some(v.parse::().ok() + .and_then(Duration::try_seconds) + .unwrap_or_else(|| { + // chrono limits seconds to i64::MAX / 1_000 + Duration::seconds(Duration::MAX.num_seconds()) + })) + } - #[cfg(not(feature = "chrono"))] { - Some(Duration::ZERO) - } + #[cfg(not(feature = "chrono"))] + if is_negative { + Some(Duration::ZERO) } else { - Some(v.parse::().map_or_else( - |_| Duration::seconds(i64::max_value()), Duration::seconds)) + Some(v.parse::() + .map_or_else(|_| Duration::seconds(i64::max_value()), Duration::seconds)) } }, ("domain", Some(d)) if !d.is_empty() => { @@ -202,13 +209,13 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { ("partitioned", _) => cookie.partitioned = Some(true), ("expires", Some(v)) => { #[cfg(feature = "chrono")] { - let tm = DateTime::parse_from_str(v, FMT1) - .or_else(|_| DateTime::parse_from_str(v, FMT2)) - .or_else(|_| DateTime::parse_from_str(v, FMT3)) - .or_else(|_| DateTime::parse_from_str(v, FMT4)); + let tm = NaiveDateTime::parse_from_str(v, FMT1) + .or_else(|_| NaiveDateTime::parse_from_str(v, FMT2)) + .or_else(|_| NaiveDateTime::parse_from_str(v, FMT3)) + .or_else(|_| NaiveDateTime::parse_from_str(v, FMT4)); if let Ok(time) = tm { - cookie.expires = Some(Into::>::into(time).into()); + cookie.expires = Some(time.and_utc().into()); } } @@ -251,8 +258,8 @@ pub(crate) fn parse_date(s: &str, format: &impl Parsable) -> Result 2000, - 69..=99 => 1900, + 0..=69 => 2000, + 70..=99 => 1900, _ => 0, }; @@ -264,9 +271,10 @@ pub(crate) fn parse_date(s: &str, format: &impl Parsable) -> Result { @@ -380,11 +388,22 @@ mod tests { assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); - expected.set_max_age(Duration::ZERO); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected); + + #[cfg(feature = "chrono")] { + expected.set_max_age(Duration::zero()); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected); + } + + #[cfg(not(feature = "chrono"))] { + expected.set_max_age(Duration::ZERO); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected); + } expected.set_max_age(Duration::minutes(1)); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=60", expected); @@ -440,17 +459,35 @@ mod tests { assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=FOO.COM", unexpected); - let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; - let expires = parse_date(time_str, &super::FMT1).unwrap(); - expected.set_expires(expires); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", expected); + #[cfg(feature = "chrono")] { + let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; + let expires = NaiveDateTime::parse_from_str(time_str, super::FMT1) + .unwrap().and_utc(); + expected.set_expires(expires); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", expected); + + unexpected.set_domain("foo.com"); + let bad_expires = NaiveDateTime::parse_from_str(time_str, super::FMT1) + .unwrap().and_utc(); + expected.set_expires(bad_expires); + assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", unexpected); + } - unexpected.set_domain("foo.com"); - let bad_expires = parse_date(time_str, &super::FMT1).unwrap(); - expected.set_expires(bad_expires); - assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", unexpected); + #[cfg(not(feature = "chrono"))] { + let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; + let expires = parse_date(time_str, &super::FMT1).unwrap(); + expected.set_expires(expires); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", expected); + + unexpected.set_domain("foo.com"); + let bad_expires = parse_date(time_str, &super::FMT1).unwrap(); + expected.set_expires(bad_expires); + assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", unexpected); + } } #[test] @@ -459,19 +496,19 @@ mod tests { let cookie = Cookie::parse(cookie_str).unwrap(); assert_eq!(cookie.expires_datetime().unwrap().year(), 2020); - let cookie_str = "foo=bar; expires=Thu, 10-Sep-68 20:00:00 GMT"; + let cookie_str = "foo=bar; expires=Mon, 10-Sep-68 20:00:00 GMT"; let cookie = Cookie::parse(cookie_str).unwrap(); assert_eq!(cookie.expires_datetime().unwrap().year(), 2068); - let cookie_str = "foo=bar; expires=Thu, 10-Sep-69 20:00:00 GMT"; + let cookie_str = "foo=bar; expires=Thu, 10-Sep-70 20:00:00 GMT"; let cookie = Cookie::parse(cookie_str).unwrap(); - assert_eq!(cookie.expires_datetime().unwrap().year(), 1969); + assert_eq!(cookie.expires_datetime().unwrap().year(), 1970); - let cookie_str = "foo=bar; expires=Thu, 10-Sep-99 20:00:00 GMT"; + let cookie_str = "foo=bar; expires=Fri, 10-Sep-99 20:00:00 GMT"; let cookie = Cookie::parse(cookie_str).unwrap(); assert_eq!(cookie.expires_datetime().unwrap().year(), 1999); - let cookie_str = "foo=bar; expires=Thu, 10-Sep-2069 20:00:00 GMT"; + let cookie_str = "foo=bar; expires=Tue, 10-Sep-2069 20:00:00 GMT"; let cookie = Cookie::parse(cookie_str).unwrap(); assert_eq!(cookie.expires_datetime().unwrap().year(), 2069); } @@ -490,8 +527,11 @@ mod tests { #[test] fn parse_very_large_max_ages() { + #[cfg(feature = "chrono")] let max_seconds = TimeDelta::seconds(TimeDelta::MAX.num_seconds()); + #[cfg(not(feature = "chrono"))] let max_seconds = time::Duration::seconds(i64::max_value()); + let mut expected = Cookie::build(("foo", "bar")) - .max_age(Duration::seconds(i64::max_value())) + .max_age(max_seconds) .build(); let string = format!("foo=bar; Max-Age={}", 1u128 << 100); @@ -507,7 +547,7 @@ mod tests { assert_eq_parse!(&string, expected); let string = format!("foo=bar; Max-Age={}", i64::max_value()); - expected.set_max_age(Duration::seconds(i64::max_value())); + expected.set_max_age(max_seconds); assert_eq_parse!(&string, expected); } @@ -531,7 +571,11 @@ mod tests { #[test] fn do_not_panic_on_large_max_ages() { - let max_seconds = Duration::MAX.whole_seconds(); + #[cfg(feature = "chrono")] + let max_seconds = TimeDelta::MAX.num_seconds(); + #[cfg(not(feature = "chrono"))] + let max_seconds = time::Duration::MAX.whole_seconds(); + let expected = Cookie::build(("foo", "bar")) .max_age(Duration::seconds(max_seconds));