From 199d16411e8bebaa55a436720a8181131ef943db Mon Sep 17 00:00:00 2001 From: dishmaker <141624503+dishmaker@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:18:54 +0100 Subject: [PATCH 1/5] feat: const DateTime::new --- der/src/datetime.rs | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/der/src/datetime.rs b/der/src/datetime.rs index 39dbd11ef..d2cd3cb39 100644 --- a/der/src/datetime.rs +++ b/der/src/datetime.rs @@ -11,6 +11,7 @@ use core::{fmt, str::FromStr, time::Duration}; #[cfg(feature = "std")] use std::time::{SystemTime, UNIX_EPOCH}; +use const_range::const_contains_u8; #[cfg(feature = "time")] use time::PrimitiveDateTime; @@ -71,16 +72,23 @@ impl DateTime { /// Create a new [`DateTime`] from the given UTC time components. // TODO(tarcieri): checked arithmetic #[allow(clippy::arithmetic_side_effects)] - pub fn new(year: u16, month: u8, day: u8, hour: u8, minutes: u8, seconds: u8) -> Result { + pub const fn new( + year: u16, + month: u8, + day: u8, + hour: u8, + minutes: u8, + seconds: u8, + ) -> Result { // Basic validation of the components. if year < MIN_YEAR - || !(1..=12).contains(&month) - || !(1..=31).contains(&day) - || !(0..=23).contains(&hour) - || !(0..=59).contains(&minutes) - || !(0..=59).contains(&seconds) + || !const_contains_u8(1..=12, month) + || !const_contains_u8(1..=31, day) + || !const_contains_u8(0..=23, hour) + || !const_contains_u8(0..=59, minutes) + || !const_contains_u8(0..=59, seconds) { - return Err(ErrorKind::DateTime.into()); + return Error::from_kind(ErrorKind::DateTime); } let leap_years = @@ -430,6 +438,22 @@ fn decode_year(year: &[u8; 4]) -> Result { Ok(u16::from(hi) * 100 + u16::from(lo)) } +mod const_range { + use core::ops::RangeInclusive; + + /// const [`RangeInclusive::contains`] + #[inline] + pub const fn const_contains_u8(range: RangeInclusive, item: u8) -> bool { + item >= *range.start() && item <= *range.end() + } + + /// const [`RangeInclusive::contains`] + #[inline] + pub const fn const_contains_u16(range: RangeInclusive, item: u16) -> bool { + item >= *range.start() && item <= *range.end() + } +} + #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { From 206651926ad9e8b16e66b8670d023d491eaf40e4 Mon Sep 17 00:00:00 2001 From: dishmaker <141624503+dishmaker@users.noreply.github.com> Date: Fri, 14 Mar 2025 09:58:45 +0100 Subject: [PATCH 2/5] der: const DateTime::new --- der/src/datetime.rs | 70 +++++++++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/der/src/datetime.rs b/der/src/datetime.rs index d2cd3cb39..95785bb4d 100644 --- a/der/src/datetime.rs +++ b/der/src/datetime.rs @@ -70,8 +70,6 @@ impl DateTime { }; /// Create a new [`DateTime`] from the given UTC time components. - // TODO(tarcieri): checked arithmetic - #[allow(clippy::arithmetic_side_effects)] pub const fn new( year: u16, month: u8, @@ -80,6 +78,25 @@ impl DateTime { minutes: u8, seconds: u8, ) -> Result { + match Self::from_ymd_hms(year, month, day, hour, minutes, seconds) { + Some(date) => Ok(date), + None => Err(Error::from_kind(ErrorKind::DateTime)), + } + } + + /// Create a new [`DateTime`] from the given UTC time components. + /// + /// Returns `None` if the value is outside the supported date range. + // TODO(tarcieri): checked arithmetic + #[allow(clippy::arithmetic_side_effects)] + pub(crate) const fn from_ymd_hms( + year: u16, + month: u8, + day: u8, + hour: u8, + minutes: u8, + seconds: u8, + ) -> Option { // Basic validation of the components. if year < MIN_YEAR || !const_contains_u8(1..=12, month) @@ -88,7 +105,7 @@ impl DateTime { || !const_contains_u8(0..=59, minutes) || !const_contains_u8(0..=59, seconds) { - return Error::from_kind(ErrorKind::DateTime); + return None; } let leap_years = @@ -110,28 +127,28 @@ impl DateTime { 10 => (273, 31), 11 => (304, 30), 12 => (334, 31), - _ => return Err(ErrorKind::DateTime.into()), + _ => return None, }; if day > mdays || day == 0 { - return Err(ErrorKind::DateTime.into()); + return None; } - ydays += u16::from(day) - 1; + ydays += day as u16 - 1; if is_leap_year && month > 2 { ydays += 1; } - let days = u64::from(year - 1970) * 365 + u64::from(leap_years) + u64::from(ydays); - let time = u64::from(seconds) + (u64::from(minutes) * 60) + (u64::from(hour) * 3600); + let days = ((year - 1970) as u64) * 365 + leap_years as u64 + ydays as u64; + let time = seconds as u64 + (minutes as u64 * 60) + (hour as u64 * 3600); let unix_duration = Duration::from_secs(time + days * 86400); - if unix_duration > MAX_UNIX_DURATION { - return Err(ErrorKind::DateTime.into()); + if unix_duration.as_secs() > MAX_UNIX_DURATION.as_secs() { + return None; } - Ok(Self { + Some(Self { year, month, day, @@ -144,7 +161,7 @@ impl DateTime { /// Compute a [`DateTime`] from the given [`Duration`] since the `UNIX_EPOCH`. /// - /// Returns `None` if the value is outside the supported date range. + /// Returns `Err` if the value is outside the supported date range. // TODO(tarcieri): checked arithmetic #[allow(clippy::arithmetic_side_effects)] pub fn from_unix_duration(unix_duration: Duration) -> Result { @@ -446,12 +463,6 @@ mod const_range { pub const fn const_contains_u8(range: RangeInclusive, item: u8) -> bool { item >= *range.start() && item <= *range.end() } - - /// const [`RangeInclusive::contains`] - #[inline] - pub const fn const_contains_u16(range: RangeInclusive, item: u16) -> bool { - item >= *range.start() && item <= *range.end() - } } #[cfg(test)] @@ -471,6 +482,29 @@ mod tests { assert!(!is_date_valid(2100, 2, 29, 0, 0, 0)); } + #[test] + fn invalid_dates() { + assert!(!is_date_valid(2, 3, 25, 0, 0, 0)); + + assert!(is_date_valid(1970, 1, 26, 0, 0, 0)); + assert!(!is_date_valid(1969, 1, 26, 0, 0, 0)); + assert!(!is_date_valid(1968, 1, 26, 0, 0, 0)); + assert!(!is_date_valid(1600, 1, 26, 0, 0, 0)); + + assert!(is_date_valid(2039, 2, 27, 0, 0, 0)); + assert!(!is_date_valid(2039, 2, 27, 255, 0, 0)); + assert!(!is_date_valid(2039, 2, 27, 0, 255, 0)); + assert!(!is_date_valid(2039, 2, 27, 0, 0, 255)); + + assert!(is_date_valid(2055, 12, 31, 0, 0, 0)); + assert!(is_date_valid(2055, 12, 31, 23, 0, 0)); + assert!(!is_date_valid(2055, 12, 31, 24, 0, 0)); + assert!(is_date_valid(2055, 12, 31, 0, 59, 0)); + assert!(!is_date_valid(2055, 12, 31, 0, 60, 0)); + assert!(is_date_valid(2055, 12, 31, 0, 0, 59)); + assert!(!is_date_valid(2055, 12, 31, 0, 0, 60)); + } + #[test] fn from_str() { let datetime = "2001-01-02T12:13:14Z".parse::().unwrap(); From 60d0deada444ca2292355aadf152e6e76da37b23 Mon Sep 17 00:00:00 2001 From: dishmaker <141624503+dishmaker@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:01:26 +0100 Subject: [PATCH 3/5] der: const GeneralizedTime::to_date_time --- der/src/asn1/generalized_time.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/der/src/asn1/generalized_time.rs b/der/src/asn1/generalized_time.rs index c3a0be004..384943857 100644 --- a/der/src/asn1/generalized_time.rs +++ b/der/src/asn1/generalized_time.rs @@ -40,7 +40,7 @@ impl GeneralizedTime { } /// Convert this [`GeneralizedTime`] into a [`DateTime`]. - pub fn to_date_time(&self) -> DateTime { + pub const fn to_date_time(&self) -> DateTime { self.0 } From 526479f575670feff7725d9e7b545a4d18d0ea3e Mon Sep 17 00:00:00 2001 From: dishmaker <141624503+dishmaker@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:26:12 +0100 Subject: [PATCH 4/5] test: add max valid/invalid generalized time --- der/src/asn1/generalized_time.rs | 48 ++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/der/src/asn1/generalized_time.rs b/der/src/asn1/generalized_time.rs index 384943857..015cacfd4 100644 --- a/der/src/asn1/generalized_time.rs +++ b/der/src/asn1/generalized_time.rs @@ -347,4 +347,52 @@ mod tests { utc_time.encode(&mut encoder).unwrap(); assert_eq!(example_bytes, encoder.finish().unwrap()); } + + #[test] + fn max_valid_generalized_time() { + let example_bytes = "\x18\x0f99991231235959Z".as_bytes(); + let utc_time = GeneralizedTime::from_der(&example_bytes).unwrap(); + assert_eq!(utc_time.to_unix_duration().as_secs(), 253402300799); + + let mut buf = [0u8; 128]; + let mut encoder = SliceWriter::new(&mut buf); + utc_time.encode(&mut encoder).unwrap(); + assert_eq!(example_bytes, encoder.finish().unwrap()); + } + + #[test] + fn invalid_year_generalized_time() { + let example_bytes = "\x18\x0f999@1231235959Z".as_bytes(); + assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + } + + #[test] + fn invalid_month_generalized_time() { + let example_bytes = "\x18\x0f99991331235959Z".as_bytes(); + assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + } + + #[test] + fn invalid_day_generalized_time() { + let example_bytes = "\x18\x0f99991232235959Z".as_bytes(); + assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + } + + #[test] + fn invalid_hour_generalized_time() { + let example_bytes = "\x18\x0f99991231245959Z".as_bytes(); + assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + } + + #[test] + fn invalid_minute_generalized_time() { + let example_bytes = "\x18\x0f99991231236059Z".as_bytes(); + assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + } + + #[test] + fn invalid_second_generalized_time() { + let example_bytes = "\x18\x0f99991231235960Z".as_bytes(); + assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + } } From 300a3ca65b1c22e89d7b9edbf6b55fc48555645a Mon Sep 17 00:00:00 2001 From: dishmaker <141624503+dishmaker@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:36:18 +0100 Subject: [PATCH 5/5] fix cargo clippy & --- der/src/asn1/generalized_time.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/der/src/asn1/generalized_time.rs b/der/src/asn1/generalized_time.rs index 015cacfd4..63305315b 100644 --- a/der/src/asn1/generalized_time.rs +++ b/der/src/asn1/generalized_time.rs @@ -351,7 +351,7 @@ mod tests { #[test] fn max_valid_generalized_time() { let example_bytes = "\x18\x0f99991231235959Z".as_bytes(); - let utc_time = GeneralizedTime::from_der(&example_bytes).unwrap(); + let utc_time = GeneralizedTime::from_der(example_bytes).unwrap(); assert_eq!(utc_time.to_unix_duration().as_secs(), 253402300799); let mut buf = [0u8; 128]; @@ -363,36 +363,36 @@ mod tests { #[test] fn invalid_year_generalized_time() { let example_bytes = "\x18\x0f999@1231235959Z".as_bytes(); - assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + assert!(GeneralizedTime::from_der(example_bytes).is_err()); } #[test] fn invalid_month_generalized_time() { let example_bytes = "\x18\x0f99991331235959Z".as_bytes(); - assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + assert!(GeneralizedTime::from_der(example_bytes).is_err()); } #[test] fn invalid_day_generalized_time() { let example_bytes = "\x18\x0f99991232235959Z".as_bytes(); - assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + assert!(GeneralizedTime::from_der(example_bytes).is_err()); } #[test] fn invalid_hour_generalized_time() { let example_bytes = "\x18\x0f99991231245959Z".as_bytes(); - assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + assert!(GeneralizedTime::from_der(example_bytes).is_err()); } #[test] fn invalid_minute_generalized_time() { let example_bytes = "\x18\x0f99991231236059Z".as_bytes(); - assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + assert!(GeneralizedTime::from_der(example_bytes).is_err()); } #[test] fn invalid_second_generalized_time() { let example_bytes = "\x18\x0f99991231235960Z".as_bytes(); - assert!(GeneralizedTime::from_der(&example_bytes).is_err()); + assert!(GeneralizedTime::from_der(example_bytes).is_err()); } }