diff --git a/Cargo.toml b/Cargo.toml index ac6cc475..221550bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ signed = ["hmac", "sha2", "base64", "rand", "subtle"] key-expansion = ["sha2", "hkdf"] [dependencies] -time = { version = "0.3", default-features = false, features = ["std", "parsing", "formatting", "macros"] } percent-encoding = { version = "2.0", optional = true } # dependencies for secure (private/signed) functionality @@ -33,6 +32,18 @@ rand = { version = "0.8", optional = true } hkdf = { version = "0.12.0", optional = true } subtle = { version = "2.3", optional = true } +[dependencies.time] +version = "0.3" +default-features = false +features = ["std", "parsing", "formatting", "macros"] +optional = true + +[dependencies.chrono] +version = "0.4.30" +default-features = false +features = ["std", "clock"] +optional = true + [build-dependencies] version_check = "0.9.4" diff --git a/src/builder.rs b/src/builder.rs index 34505ac1..cfe38642 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,6 +1,7 @@ use std::borrow::{Cow, Borrow, BorrowMut}; use crate::{Cookie, SameSite, Expiration}; +use crate::time::Duration; /// Structure that follows the builder pattern for building `Cookie` structs. /// @@ -102,7 +103,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: Duration) -> Self { self.cookie.set_max_age(value); self } diff --git a/src/expiration.rs b/src/expiration.rs index 89eaf9fb..683ea563 100644 --- a/src/expiration.rs +++ b/src/expiration.rs @@ -1,4 +1,4 @@ -use time::OffsetDateTime; +use crate::time::DateTime; /// 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(DateTime), /// 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 @@ -91,7 +91,7 @@ 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) @@ -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(DateTime) -> DateTime { match self { Expiration::Session => Expiration::Session, @@ -126,7 +126,7 @@ impl Expiration { } } -impl>> From for Expiration { +impl>> From for Expiration { fn from(option: T) -> Self { match option.into() { Some(value) => Expiration::DateTime(value), diff --git a/src/lib.rs b/src/lib.rs index 640a45ef..e461d51f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,7 +72,7 @@ #![deny(missing_docs)] -pub use time; +pub mod time; mod builder; mod parse; @@ -91,7 +91,7 @@ use std::str::FromStr; #[allow(unused_imports, deprecated)] use std::ascii::AsciiExt; -use time::{Duration, OffsetDateTime, UtcOffset, macros::datetime}; +use time::{Duration, DateTime, InternalDateTime}; use crate::parse::parse_cookie; pub use crate::parse::ParseError; @@ -781,8 +781,9 @@ 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 { + todo!() + // self.expires.and_then(|e| e.datetime()) } /// Sets the name of `self` to `name`. @@ -931,7 +932,8 @@ impl<'c> Cookie<'c> { /// ``` #[inline] pub fn set_max_age>>(&mut self, value: D) { - self.max_age = value.into(); + todo!() + // self.max_age = value.into(); } /// Sets the `path` of `self` to `path`. @@ -1031,11 +1033,9 @@ 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); - // RFC 6265 requires dates not to exceed 9999 years. self.expires = Some(time.into() - .map(|time| std::cmp::min(time, MAX_DATETIME))); + .map(|time| std::cmp::min(time, DateTime::MAX))); } /// Unsets the `expires` of `self`. @@ -1081,7 +1081,7 @@ 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); + self.set_expires(DateTime::now() + twenty_years); } /// Make `self` a "removal" cookie by clearing its value, setting a max-age @@ -1107,8 +1107,8 @@ 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)); + self.set_max_age(Duration::ZERO); + self.set_expires(DateTime::now_utc() - Duration::days(365)); } fn fmt_parameters(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -1137,12 +1137,11 @@ impl<'c> Cookie<'c> { } if let Some(max_age) = self.max_age() { - write!(f, "; Max-Age={}", max_age.whole_seconds())?; + write!(f, "; Max-Age={}", max_age.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)?)?; + write!(f, "; Expires={}", time.expiration_format().ok_or(fmt::Error)?)?; } Ok(()) diff --git a/src/time/datetime/internal.rs b/src/time/datetime/internal.rs new file mode 100644 index 00000000..1f6cb9b9 --- /dev/null +++ b/src/time/datetime/internal.rs @@ -0,0 +1,81 @@ +use super::{DateTime, InternalDateTime}; + +impl InternalDateTime for DateTime { + const MAX: Self = { + #[cfg(feature = "time")] { + DateTime::Time(time::OffsetDateTime::MAX) + } + + #[cfg(not(feature = "time"))] { + DateTime::Chrono(chrono::DateTime::MAX) + } + }; + + fn now() -> Self { + #[cfg(feature = "time")] { + DateTime::Time(time::OffsetDateTime::now()) + } + + #[cfg(not(feature = "time"))] { + DateTime::Chrono(chrono::DateTime::now()) + } + } + + fn destruct(&self) -> (i32, u32, u32, i32, u32, u32, u32) { + match self { + #[cfg(feature = "time")] + DateTime::Time(inner) => inner.destruct(), + #[cfg(feature = "chrono")] + DateTime::Chrono(inner) => inner.destruct(), + } + } + + fn expiration_format(&self) -> Option { + match self { + #[cfg(feature = "time")] + DateTime::Time(inner) => inner.expiration_format(), + #[cfg(feature = "chrono")] + DateTime::Chrono(inner) => inner.expiration_format(), + } + } +} + +#[cfg(feature = "time")] +impl InternalDateTime for time::OffsetDateTime { + const MAX: Self = time::macros::datetime!(9999-12-31 23:59:59.999_999 UTC); + + fn now() -> Self { + time::OffsetDateTime::now_utc() + } + + fn destruct(&self) -> (i32, u32, u32, i32, u32, u32, u32) { + let (year, month, day) = self.to_calendar_date(); + let (hour, minute, second, nanos) = self.to_hms_nano(); + (year, month as u32, day.into(), hour.into(), minute.into(), second.into(), nanos) + } + + fn expiration_format(&self) -> Option { + self.format(&crate::parse::FMT1).ok() + } +} + +#[cfg(feature = "chrono")] +impl InternalDateTime for chrono::DateTime { + const MAX: Self = chrono::DateTime::from_naive_utc_and_offset( + chrono::NaiveDateTime::new( + chrono::NaiveDate::from_ymd_opt(9999, 12, 31).unwrap(), + chrono::NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() + ), chrono::Utc); + + fn now() -> Self { + chrono::Utc::now() + } + + fn destruct(&self) -> (i32, u32, u32, i32, u32, u32, u32) { + todo!() + } + + fn expiration_format(&self) -> Option { + todo!() + } +} diff --git a/src/time/datetime/mod.rs b/src/time/datetime/mod.rs new file mode 100644 index 00000000..3cb02b52 --- /dev/null +++ b/src/time/datetime/mod.rs @@ -0,0 +1,98 @@ +mod internal; + +#[derive(Debug, Clone, Copy, Eq)] +pub enum DateTime { + #[cfg(feature = "time")] + Time(time::OffsetDateTime), + #[cfg(feature = "chrono")] + Chrono(chrono::DateTime) +} + +/// API implemented for internal use. This is private. Everything else: public. +pub(crate) trait InternalDateTime: From + Into { + /// The max cookie date-time. + const MAX: Self; + + /// The datetime right now. + fn now() -> Self; + + /// UTC based (year, month, day, hour, minute, second, nanosecond). + /// * date is ISO 8601 calendar date + /// * month is 1-indexed + fn destruct(&self) -> (i32, u32, u32, i32, u32, u32, u32); + + /// The datetime as a string suitable for use as a cookie expiration. + fn expiration_format(&self) -> Option; +} + +impl PartialEq for DateTime { + fn eq(&self, other: &Self) -> bool { + self.destruct() == other.destruct() + } +} + +impl std::hash::Hash for DateTime { + fn hash(&self, state: &mut H) { + self.destruct().hash(state) + } +} + +impl PartialOrd for DateTime { + fn partial_cmp(&self, other: &Self) -> Option { + self.destruct().partial_cmp(&other.destruct()) + } +} + +impl Ord for DateTime { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.destruct().cmp(&other.destruct()) + } +} + +#[cfg(feature = "time")] +mod time_impl { + use super::*; + + impl From for time::OffsetDateTime { + fn from(value: DateTime) -> Self { + let (yr, mon, day, hr, min, sec, nano) = value.destruct(); + todo!() + } + } + + impl From for DateTime { + fn from(value: time::OffsetDateTime) -> Self { + DateTime::Time(value) + } + } + + impl PartialEq for DateTime { + fn eq(&self, other: &time::OffsetDateTime) -> bool { + self.destruct().eq(&other.destruct()) + } + } +} + +#[cfg(feature = "chrono")] +mod chrono_impl { + use super::*; + + impl From for chrono::DateTime { + fn from(value: DateTime) -> Self { + let (yr, mon, day, hr, min, sec, nano) = value.destruct(); + todo!() + } + } + + impl From> for DateTime { + fn from(value: chrono::DateTime) -> Self { + DateTime::Chrono(value) + } + } + + impl PartialEq> for DateTime { + fn eq(&self, other: &chrono::DateTime) -> bool { + self.destruct().eq(&other.destruct()) + } + } +} diff --git a/src/time/duration/internal.rs b/src/time/duration/internal.rs new file mode 100644 index 00000000..5f3bf42a --- /dev/null +++ b/src/time/duration/internal.rs @@ -0,0 +1,51 @@ +use super::{Duration, InternalDuration}; + +impl InternalDuration for Duration { + #[cfg(feature = "time")] + const ZERO: Self = Duration::Time(time::Duration::ZERO); + + #[cfg(not(feature = "time"))] + const ZERO: Self = Duration::Chrono(chrono::Duration::zero()); + + fn seconds(&self) -> i64 { + match self { + #[cfg(feature = "time")] + Duration::Time(v) => v.seconds(), + #[cfg(feature = "chrono")] + Duration::Chrono(v) => v.seconds(), + } + } + + fn milliseconds(&self) -> i128 { + match self { + #[cfg(feature = "time")] + Duration::Time(v) => v.milliseconds(), + #[cfg(feature = "chrono")] + Duration::Chrono(v) => v.milliseconds(), + } + } +} + +impl InternalDuration for time::Duration { + const ZERO: Self = time::Duration::ZERO; + + fn seconds(&self) -> i64 { + self.whole_seconds() + } + + fn milliseconds(&self) -> i128 { + self.whole_milliseconds() + } +} + +impl InternalDuration for chrono::Duration { + const ZERO: Self = chrono::Duration::zero(); + + fn seconds(&self) -> i64 { + self.num_seconds() + } + + fn milliseconds(&self) -> i128 { + self.num_milliseconds().into() + } +} diff --git a/src/time/duration/mod.rs b/src/time/duration/mod.rs new file mode 100644 index 00000000..260fe6fb --- /dev/null +++ b/src/time/duration/mod.rs @@ -0,0 +1,80 @@ +mod internal; + +#[derive(Debug, Clone, Copy)] +pub enum Duration { + #[cfg(feature = "time")] + Time(time::Duration), + #[cfg(feature = "chrono")] + Chrono(chrono::Duration) +} + +pub(crate) trait InternalDuration { + const ZERO: Self; + + fn seconds(&self) -> i64; + fn milliseconds(&self) -> i128; +} + +impl PartialEq for Duration { + fn eq(&self, other: &Self) -> bool { + self.milliseconds() == other.milliseconds() + } +} + +#[cfg(feature = "time")] +mod time_impl { + use super::*; + + impl From for time::Duration { + fn from(value: Duration) -> Self { + time::Duration::milliseconds(value.milliseconds() as i64) + } + } + + impl From for Duration { + fn from(value: time::Duration) -> Self { + Duration::Time(value) + } + } + + impl PartialEq for Duration { + fn eq(&self, other: &time::Duration) -> bool { + self.milliseconds().eq(&other.milliseconds()) + } + } + + impl PartialEq for time::Duration { + fn eq(&self, other: &Duration) -> bool { + self.milliseconds().eq(&other.milliseconds()) + } + } +} + +#[cfg(feature = "chrono")] +mod chrono_impl { + use super::*; + + impl From for chrono::Duration { + fn from(value: Duration) -> Self { + chrono::Duration::milliseconds(value.milliseconds() as i64) + } + } + + impl From for Duration { + fn from(value: chrono::Duration) -> Self { + Duration::Chrono(value) + } + } + + impl PartialEq for Duration { + fn eq(&self, other: &chrono::Duration) -> bool { + self.milliseconds().eq(&other.milliseconds()) + } + } + + impl PartialEq for chrono::Duration { + fn eq(&self, other: &Duration) -> bool { + self.milliseconds().eq(&other.milliseconds()) + } + } +} diff --git a/src/time/mod.rs b/src/time/mod.rs new file mode 100644 index 00000000..fcbec2a1 --- /dev/null +++ b/src/time/mod.rs @@ -0,0 +1,8 @@ +mod duration; +mod datetime; + +pub use duration::Duration; +pub use datetime::DateTime; + +pub(crate) use self::duration::InternalDuration; +pub(crate) use self::datetime::InternalDateTime;