From 5039617493d493c815e26d261213cc5b5944146e Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Fri, 19 Dec 2025 10:02:52 +0100 Subject: [PATCH] tai64: add `TryFrom` for `SystemTime` to avoid panic --- tai64/src/lib.rs | 64 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/tai64/src/lib.rs b/tai64/src/lib.rs index cf8f1b0f4..f7b42c12e 100644 --- a/tai64/src/lib.rs +++ b/tai64/src/lib.rs @@ -203,7 +203,15 @@ impl Tai64N { } } - /// Convert `TAI64N`to `SystemTime`. + /// Convert `TAI64N` to `SystemTime`. + /// + /// # Panics + /// + /// Panics if the timestamp cannot be represented as `SystemTime`. This can + /// occur when the `Tai64N` value is outside the range representable by the + /// platform's `SystemTime` (typically backed by `i64` seconds from Unix epoch). + /// + /// For a non-panicking alternative, use `SystemTime::try_from(tai64n)`. #[cfg(feature = "std")] pub fn to_system_time(self) -> SystemTime { match self.duration_since(&Self::UNIX_EPOCH) { @@ -265,6 +273,19 @@ impl From for Tai64N { } } +#[cfg(feature = "std")] +impl TryFrom for SystemTime { + type Error = Error; + + fn try_from(tai: Tai64N) -> Result { + match tai.duration_since(&Tai64N::UNIX_EPOCH) { + Ok(d) => UNIX_EPOCH.checked_add(d), + Err(d) => UNIX_EPOCH.checked_sub(d), + } + .ok_or(Error::TimestampOverflow) + } +} + #[allow(clippy::suspicious_arithmetic_impl)] impl ops::Add for Tai64N { type Output = Self; @@ -320,6 +341,9 @@ pub enum Error { /// Nanosecond part must be <= 999999999. NanosInvalid, + + /// Timestamp cannot be represented as `SystemTime` (overflow/underflow). + TimestampOverflow, } impl fmt::Display for Error { @@ -327,6 +351,7 @@ impl fmt::Display for Error { let description = match self { Error::LengthInvalid => "length invalid", Error::NanosInvalid => "invalid number of nanoseconds", + Error::TimestampOverflow => "timestamp cannot be represented as SystemTime", }; write!(f, "{description}") @@ -337,6 +362,7 @@ impl fmt::Display for Error { impl std::error::Error for Error {} #[cfg(all(test, feature = "std"))] +#[allow(clippy::unwrap_used)] mod tests { use super::*; @@ -360,4 +386,40 @@ mod tests { assert_eq!(t, t1); } + + #[test] + #[should_panic(expected = "overflow when adding duration to instant")] + fn to_system_time_panics_from_slice() { + let malicious_timestamp: [u8; 12] = [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + ]; + + let timestamp = Tai64N::from_slice(&malicious_timestamp).unwrap(); + let _ = timestamp.to_system_time(); // panics here + } + + #[test] + fn try_into_system_time_success() { + let tai = Tai64N::now(); + let result: Result = tai.try_into(); + assert!(result.is_ok()); + } + + #[test] + fn try_into_system_time_overflow() { + let malicious: [u8; 12] = [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + ]; + let tai = Tai64N::from_slice(&malicious).unwrap(); + let result: Result = tai.try_into(); + assert_eq!(result, Err(Error::TimestampOverflow)); + } + + #[test] + fn try_into_system_time_roundtrip() { + let original = SystemTime::now(); + let tai = Tai64N::from(original); + let recovered: SystemTime = tai.try_into().expect("should be representable"); + assert_eq!(original, recovered); + } }