From dfe666a773f4f3df59048c1d8c52615b4d8aa536 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 10 Apr 2025 12:48:10 +0200 Subject: [PATCH 1/2] Validate range of DateTime values - Add associated constants with min/max limits to `ua::DateTime` - Check min/max limits when converting `time::OffsetDateTime` into `ua::DateTime`. --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- src/ua/data_types/date_time.rs | 65 ++++++++++++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d70f406..a2a2c346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added - Add `ua::Guid`. +- Add associated constants with min/max limits to `ua::DateTime`. +- Check min/max limits when converting `time::OffsetDateTime` into `ua::DateTime`. ## [0.8.3] - 2025-03-27 diff --git a/Cargo.toml b/Cargo.toml index 7a070cbd..e99d68b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ paste = "1.0.14" serde = { version = "1.0.194", optional = true } serde_json = { version = "1.0.111", optional = true } thiserror = "2.0.3" -time = { version = "0.3.36", optional = true } +time = { version = "0.3.36", optional = true, features = ["macros"] } tokio = { version = "1.38.2", optional = true, features = [ "rt", "sync", diff --git a/src/ua/data_types/date_time.rs b/src/ua/data_types/date_time.rs index f8208b36..1d0ef4a8 100644 --- a/src/ua/data_types/date_time.rs +++ b/src/ua/data_types/date_time.rs @@ -1,11 +1,41 @@ crate::data_type!(DateTime); impl DateTime { + /// Minimum value. + /// + /// See also: + pub const MIN: Self = Self(0); + + /// Maximum value. + /// + /// See also: + pub const MAX: Self = Self(2_650_467_743_990_000_000); + + /// Minimum UTC timestamp. + /// + /// See also: + #[cfg(feature = "time")] + pub const MIN_UTC: time::OffsetDateTime = time::macros::datetime!(1601-01-01 00:00:00 UTC); + + /// Maximum UTC timestamp. + /// + /// See also: + #[cfg(feature = "time")] + pub const MAX_UTC: time::OffsetDateTime = time::macros::datetime!(9999-12-31 23:59:59 UTC); + + /// Converts the value to an UTC timestamp. + /// + /// The values [`MIN`](Self::MIN) and [`MAX`](Self::MAX) are converted as is and not + /// mapped to the corresponding limits of [`time::OffsetDateTime`]. #[cfg(feature = "time")] #[must_use] pub fn to_utc(&self) -> Option { use open62541_sys::{UA_DATETIME_UNIX_EPOCH, UA_DATETIME_USEC}; + // TODO: How to handle values that are out of range? + debug_assert!(*self >= Self::MIN, "DateTime exceeds minimum value"); + debug_assert!(*self <= Self::MAX, "DateTime exceeds maximum value"); + // OPC UA encodes `DateTime` as Windows file time: a 64-bit value that represents the number // of 100-nanosecond intervals that have elapsed since 12:00 A.M. January 1, 1601 (UTC). let ticks_ua = i128::from(self.0); @@ -34,7 +64,8 @@ impl TryFrom for DateTime { /// /// # Errors /// - /// The date/time must be valid and in range of the 64-bit representation of [`DateTime`]. + /// The date/time must be valid and in range of the 64-bit representation of [`DateTime`] + /// within the limits [`MIN`](Self::MIN) and [`MAX`](Self::MAX). fn try_from(from: time::OffsetDateTime) -> Result { use open62541_sys::{UA_DATETIME_UNIX_EPOCH, UA_DATETIME_USEC}; @@ -44,6 +75,14 @@ impl TryFrom for DateTime { let ticks_unix = nanos_unix / i128::from(1000 / UA_DATETIME_USEC); let ticks_ua = ticks_unix + i128::from(UA_DATETIME_UNIX_EPOCH); + // Limit to min/max values. + if ticks_ua < Self::MIN.0.into() { + return Err(crate::Error::internal("DateTime exceeds minimum value")); + } + if ticks_ua > Self::MAX.0.into() { + return Err(crate::Error::internal("DateTime exceeds maximum value")); + } + i64::try_from(ticks_ua) // Explicit module path to avoid linter errors when feature is not enable by `#[cfg()]`. .map_err(|_| crate::Error::internal("DateTime should be in range")) @@ -72,11 +111,33 @@ mod tests { let dt = time::macros::datetime!(2023-11-20 16:51:15.9876543 -2:00); assert_eq!(time::macros::offset!(-2:00), dt.offset()); assert_ne!(time::macros::offset!(UTC), dt.offset()); - let dt_ua = crate::ua::DateTime::try_from(dt).unwrap(); + let dt_ua = super::DateTime::try_from(dt).unwrap(); let dt_utc = dt_ua.to_utc().unwrap(); // Equal to the original timestamp, but the offset is now UTC. assert_eq!(time::macros::offset!(UTC), dt_utc.offset()); assert_ne!(dt.offset(), dt_utc.offset()); assert_eq!(dt, dt_utc); } + + #[cfg(feature = "time")] + #[test] + fn min_max_utc() { + let min_ua = super::DateTime::try_from(super::DateTime::MIN_UTC).unwrap(); + assert_eq!(min_ua, super::DateTime::MIN); + assert!(super::DateTime::try_from( + super::DateTime(super::DateTime::MIN.0 - 1) + .to_utc() + .unwrap() + ) + .is_err()); + + let max_ua = super::DateTime::try_from(super::DateTime::MAX_UTC).unwrap(); + assert_eq!(max_ua, super::DateTime::MAX); + assert!(super::DateTime::try_from( + super::DateTime(super::DateTime::MAX.0 + 1) + .to_utc() + .unwrap() + ) + .is_err()); + } } From 65b2752e74c4e13af0b4bd54c2a1afbc1308d54d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 10 Apr 2025 13:20:21 +0200 Subject: [PATCH 2/2] Comment out debug assertions to fix tests with dev profile --- src/ua/data_types/date_time.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ua/data_types/date_time.rs b/src/ua/data_types/date_time.rs index 1d0ef4a8..c1f34eb5 100644 --- a/src/ua/data_types/date_time.rs +++ b/src/ua/data_types/date_time.rs @@ -33,8 +33,8 @@ impl DateTime { use open62541_sys::{UA_DATETIME_UNIX_EPOCH, UA_DATETIME_USEC}; // TODO: How to handle values that are out of range? - debug_assert!(*self >= Self::MIN, "DateTime exceeds minimum value"); - debug_assert!(*self <= Self::MAX, "DateTime exceeds maximum value"); + //debug_assert!(*self >= Self::MIN, "DateTime exceeds minimum value"); + //debug_assert!(*self <= Self::MAX, "DateTime exceeds maximum value"); // OPC UA encodes `DateTime` as Windows file time: a 64-bit value that represents the number // of 100-nanosecond intervals that have elapsed since 12:00 A.M. January 1, 1601 (UTC).