Skip to main content

ssi_jwt/datatype/
numeric_date.rs

1use std::str::FromStr;
2
3use chrono::{prelude::*, Duration, LocalResult};
4use ordered_float::NotNan;
5use serde::{Deserialize, Serialize, Serializer};
6
7/// JSON numeric value representing the number of seconds from
8/// 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap
9/// seconds.
10///
11/// See: <https://datatracker.ietf.org/doc/html/rfc7519#section-2>
12#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct NumericDate(#[serde(serialize_with = "interop_serialize")] NotNan<f64>);
14
15/// As many JWT libraries only accept integers, this serializer aims for a
16/// middle ground by serializing a date as an integer if it does not have
17/// fractional seconds. Otherwise a trailing `.0` is always present.
18fn interop_serialize<S>(x: &f64, s: S) -> Result<S::Ok, S::Error>
19where
20    S: Serializer,
21{
22    if x.fract() != 0.0 {
23        s.serialize_f64(*x)
24    } else {
25        s.serialize_i64(*x as i64)
26    }
27}
28
29#[derive(thiserror::Error, Debug)]
30pub enum NumericDateConversionError {
31    #[error("Not a number")]
32    Nan,
33
34    #[error("Invalid float literal")]
35    InvalidFloatLiteral,
36}
37
38impl From<ordered_float::FloatIsNan> for NumericDateConversionError {
39    fn from(_: ordered_float::FloatIsNan) -> Self {
40        Self::Nan
41    }
42}
43
44impl NumericDate {
45    /// Return the f64-valued number of seconds represented by this NumericDate.
46    pub fn as_seconds(self) -> f64 {
47        *self.0
48    }
49
50    /// Try to create NumericDate from a f64 value, returning error upon out-of-range.
51    pub fn try_from_seconds(seconds: f64) -> Result<Self, NumericDateConversionError> {
52        let seconds = NotNan::new(seconds)?;
53        Ok(NumericDate(seconds))
54    }
55
56    /// Decompose NumericDate for use in Utc.timestamp and Utc.timestamp_opt
57    fn into_whole_seconds_and_fractional_nanoseconds(self) -> (i64, u32) {
58        let whole_seconds = self.0.floor() as i64;
59        let fractional_nanoseconds = ((self.0 - self.0.floor()) * 1_000_000_000.0).floor() as u32;
60        assert!(fractional_nanoseconds < 1_000_000_000);
61        (whole_seconds, fractional_nanoseconds)
62    }
63}
64
65/// Note that this will panic if the addition goes out-of-range.
66impl std::ops::Add<Duration> for NumericDate {
67    type Output = NumericDate;
68    fn add(self, rhs: Duration) -> Self::Output {
69        let self_dtu: DateTime<Utc> = self.into();
70        Self::Output::from(self_dtu + rhs)
71    }
72}
73
74/// Note that this will panic if the addition goes out-of-range.
75impl std::ops::Sub<NumericDate> for NumericDate {
76    type Output = Duration;
77    fn sub(self, rhs: NumericDate) -> Self::Output {
78        let self_dtu: DateTime<Utc> = self.into();
79        let rhs_dtu: DateTime<Utc> = rhs.into();
80        self_dtu - rhs_dtu
81    }
82}
83
84/// Note that this will panic if the addition goes out-of-range.
85impl std::ops::Sub<Duration> for NumericDate {
86    type Output = NumericDate;
87    fn sub(self, rhs: Duration) -> Self::Output {
88        let self_dtu: DateTime<Utc> = self.into();
89        Self::Output::from(self_dtu - rhs)
90    }
91}
92
93impl From<i32> for NumericDate {
94    fn from(value: i32) -> Self {
95        Self(NotNan::new(value as f64).unwrap())
96    }
97}
98
99impl TryFrom<i64> for NumericDate {
100    type Error = NumericDateConversionError;
101
102    fn try_from(value: i64) -> Result<Self, Self::Error> {
103        Self::try_from_seconds(value as f64)
104    }
105}
106
107impl TryFrom<f64> for NumericDate {
108    type Error = NumericDateConversionError;
109
110    fn try_from(value: f64) -> Result<Self, Self::Error> {
111        Self::try_from_seconds(value)
112    }
113}
114
115impl From<DateTime<Utc>> for NumericDate {
116    fn from(dtu: DateTime<Utc>) -> Self {
117        // Have to take seconds and nanoseconds separately in order to get the full allowable
118        // range of microsecond-precision values as described above.
119        let whole_seconds = dtu.timestamp() as f64;
120        let fractional_seconds = match dtu.timestamp_nanos_opt() {
121            Some(nanos) => nanos.rem_euclid(1_000_000_000) as f64 * 1.0e-9,
122            None => dtu.timestamp_micros().rem_euclid(1_000_000) as f64 * 1.0e-6,
123        };
124
125        Self::try_from_seconds(whole_seconds + fractional_seconds)
126            // UNWRAP SAFETY: input value can't be NaN nor infinite.
127            .unwrap()
128    }
129}
130
131impl From<DateTime<FixedOffset>> for NumericDate {
132    fn from(dtfo: DateTime<FixedOffset>) -> Self {
133        DateTime::<Utc>::from(dtfo).into()
134    }
135}
136
137impl From<NumericDate> for DateTime<Utc> {
138    fn from(nd: NumericDate) -> Self {
139        let (whole_seconds, fractional_nanoseconds) =
140            nd.into_whole_seconds_and_fractional_nanoseconds();
141        // `timestamp` (deprecated) was already doing an unwrap
142        Utc.timestamp_opt(whole_seconds, fractional_nanoseconds)
143            .unwrap()
144    }
145}
146
147impl From<NumericDate> for LocalResult<DateTime<Utc>> {
148    fn from(nd: NumericDate) -> Self {
149        let (whole_seconds, fractional_nanoseconds) =
150            nd.into_whole_seconds_and_fractional_nanoseconds();
151        Utc.timestamp_opt(whole_seconds, fractional_nanoseconds)
152    }
153}
154
155impl FromStr for NumericDate {
156    type Err = NumericDateConversionError;
157
158    fn from_str(s: &str) -> Result<Self, Self::Err> {
159        let f: NotNan<f64> = s
160            .parse()
161            .map_err(|_| NumericDateConversionError::InvalidFloatLiteral)?;
162        Ok(Self(f))
163    }
164}