der/
datetime.rs

1//! Date and time functionality shared between various ASN.1 types
2//! (e.g. `GeneralizedTime`, `UTCTime`)
3
4// Adapted from the `humantime` crate.
5// Copyright (c) 2016 The humantime Developers
6// Released under the MIT OR Apache 2.0 licenses
7
8use crate::{Error, ErrorKind, Result, Tag, Writer};
9use core::{fmt, str::FromStr, time::Duration};
10
11#[cfg(feature = "std")]
12use std::time::{SystemTime, UNIX_EPOCH};
13
14#[cfg(feature = "time")]
15use time::PrimitiveDateTime;
16
17/// Minimum year allowed in [`DateTime`] values.
18const MIN_YEAR: u16 = 1970;
19
20/// Maximum duration since `UNIX_EPOCH` which can be represented as a
21/// [`DateTime`] (non-inclusive).
22///
23/// This corresponds to: 9999-12-31T23:59:59Z
24const MAX_UNIX_DURATION: Duration = Duration::from_secs(253_402_300_799);
25
26/// Date-and-time type shared by multiple ASN.1 types
27/// (e.g. `GeneralizedTime`, `UTCTime`).
28///
29/// Following conventions from RFC 5280, this type is always Z-normalized
30/// (i.e. represents a UTC time). However, it isn't named "UTC time" in order
31/// to prevent confusion with ASN.1 `UTCTime`.
32#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
33pub struct DateTime {
34    /// Full year (e.g. 2000).
35    ///
36    /// Must be >=1970 to permit positive conversions to Unix time.
37    year: u16,
38
39    /// Month (1-12)
40    month: u8,
41
42    /// Day of the month (1-31)
43    day: u8,
44
45    /// Hour (0-23)
46    hour: u8,
47
48    /// Minutes (0-59)
49    minutes: u8,
50
51    /// Seconds (0-59)
52    seconds: u8,
53
54    /// [`Duration`] since the Unix epoch.
55    unix_duration: Duration,
56}
57
58impl DateTime {
59    /// This is the maximum date represented by the [`DateTime`]
60    /// This corresponds to: 9999-12-31T23:59:59Z
61    pub const INFINITY: DateTime = DateTime {
62        year: 9999,
63        month: 12,
64        day: 31,
65        hour: 23,
66        minutes: 59,
67        seconds: 59,
68        unix_duration: MAX_UNIX_DURATION,
69    };
70
71    /// Create a new [`DateTime`] from the given UTC time components.
72    // TODO(tarcieri): checked arithmetic
73    #[allow(clippy::integer_arithmetic)]
74    pub fn new(year: u16, month: u8, day: u8, hour: u8, minutes: u8, seconds: u8) -> Result<Self> {
75        // Basic validation of the components.
76        if year < MIN_YEAR
77            || !(1..=12).contains(&month)
78            || !(1..=31).contains(&day)
79            || !(0..=23).contains(&hour)
80            || !(0..=59).contains(&minutes)
81            || !(0..=59).contains(&seconds)
82        {
83            return Err(ErrorKind::DateTime.into());
84        }
85
86        let leap_years =
87            ((year - 1) - 1968) / 4 - ((year - 1) - 1900) / 100 + ((year - 1) - 1600) / 400;
88
89        let is_leap_year = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
90
91        let (mut ydays, mdays): (u16, u8) = match month {
92            1 => (0, 31),
93            2 if is_leap_year => (31, 29),
94            2 => (31, 28),
95            3 => (59, 31),
96            4 => (90, 30),
97            5 => (120, 31),
98            6 => (151, 30),
99            7 => (181, 31),
100            8 => (212, 31),
101            9 => (243, 30),
102            10 => (273, 31),
103            11 => (304, 30),
104            12 => (334, 31),
105            _ => return Err(ErrorKind::DateTime.into()),
106        };
107
108        if day > mdays || day == 0 {
109            return Err(ErrorKind::DateTime.into());
110        }
111
112        ydays += u16::from(day) - 1;
113
114        if is_leap_year && month > 2 {
115            ydays += 1;
116        }
117
118        let days = u64::from(year - 1970) * 365 + u64::from(leap_years) + u64::from(ydays);
119        let time = u64::from(seconds) + (u64::from(minutes) * 60) + (u64::from(hour) * 3600);
120        let unix_duration = Duration::from_secs(time + days * 86400);
121
122        if unix_duration > MAX_UNIX_DURATION {
123            return Err(ErrorKind::DateTime.into());
124        }
125
126        Ok(Self {
127            year,
128            month,
129            day,
130            hour,
131            minutes,
132            seconds,
133            unix_duration,
134        })
135    }
136
137    /// Compute a [`DateTime`] from the given [`Duration`] since the `UNIX_EPOCH`.
138    ///
139    /// Returns `None` if the value is outside the supported date range.
140    // TODO(tarcieri): checked arithmetic
141    #[allow(clippy::integer_arithmetic)]
142    pub fn from_unix_duration(unix_duration: Duration) -> Result<Self> {
143        if unix_duration > MAX_UNIX_DURATION {
144            return Err(ErrorKind::DateTime.into());
145        }
146
147        let secs_since_epoch = unix_duration.as_secs();
148
149        /// 2000-03-01 (mod 400 year, immediately after Feb 29)
150        const LEAPOCH: i64 = 11017;
151        const DAYS_PER_400Y: i64 = 365 * 400 + 97;
152        const DAYS_PER_100Y: i64 = 365 * 100 + 24;
153        const DAYS_PER_4Y: i64 = 365 * 4 + 1;
154
155        let days = i64::try_from(secs_since_epoch / 86400)? - LEAPOCH;
156        let secs_of_day = secs_since_epoch % 86400;
157
158        let mut qc_cycles = days / DAYS_PER_400Y;
159        let mut remdays = days % DAYS_PER_400Y;
160
161        if remdays < 0 {
162            remdays += DAYS_PER_400Y;
163            qc_cycles -= 1;
164        }
165
166        let mut c_cycles = remdays / DAYS_PER_100Y;
167        if c_cycles == 4 {
168            c_cycles -= 1;
169        }
170        remdays -= c_cycles * DAYS_PER_100Y;
171
172        let mut q_cycles = remdays / DAYS_PER_4Y;
173        if q_cycles == 25 {
174            q_cycles -= 1;
175        }
176        remdays -= q_cycles * DAYS_PER_4Y;
177
178        let mut remyears = remdays / 365;
179        if remyears == 4 {
180            remyears -= 1;
181        }
182        remdays -= remyears * 365;
183
184        let mut year = 2000 + remyears + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles;
185
186        let months = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
187        let mut mon = 0;
188        for mon_len in months.iter() {
189            mon += 1;
190            if remdays < *mon_len {
191                break;
192            }
193            remdays -= *mon_len;
194        }
195        let mday = remdays + 1;
196        let mon = if mon + 2 > 12 {
197            year += 1;
198            mon - 10
199        } else {
200            mon + 2
201        };
202
203        let second = secs_of_day % 60;
204        let mins_of_day = secs_of_day / 60;
205        let minute = mins_of_day % 60;
206        let hour = mins_of_day / 60;
207
208        Self::new(
209            year.try_into()?,
210            mon,
211            mday.try_into()?,
212            hour.try_into()?,
213            minute.try_into()?,
214            second.try_into()?,
215        )
216    }
217
218    /// Get the year.
219    pub fn year(&self) -> u16 {
220        self.year
221    }
222
223    /// Get the month.
224    pub fn month(&self) -> u8 {
225        self.month
226    }
227
228    /// Get the day.
229    pub fn day(&self) -> u8 {
230        self.day
231    }
232
233    /// Get the hour.
234    pub fn hour(&self) -> u8 {
235        self.hour
236    }
237
238    /// Get the minutes.
239    pub fn minutes(&self) -> u8 {
240        self.minutes
241    }
242
243    /// Get the seconds.
244    pub fn seconds(&self) -> u8 {
245        self.seconds
246    }
247
248    /// Compute [`Duration`] since `UNIX_EPOCH` from the given calendar date.
249    pub fn unix_duration(&self) -> Duration {
250        self.unix_duration
251    }
252
253    /// Instantiate from [`SystemTime`].
254    #[cfg(feature = "std")]
255    pub fn from_system_time(time: SystemTime) -> Result<Self> {
256        time.duration_since(UNIX_EPOCH)
257            .map_err(|_| ErrorKind::DateTime.into())
258            .and_then(Self::from_unix_duration)
259    }
260
261    /// Convert to [`SystemTime`].
262    #[cfg(feature = "std")]
263    pub fn to_system_time(&self) -> SystemTime {
264        UNIX_EPOCH + self.unix_duration()
265    }
266}
267
268impl FromStr for DateTime {
269    type Err = Error;
270
271    fn from_str(s: &str) -> Result<Self> {
272        match *s.as_bytes() {
273            [year1, year2, year3, year4, b'-', month1, month2, b'-', day1, day2, b'T', hour1, hour2, b':', min1, min2, b':', sec1, sec2, b'Z'] =>
274            {
275                let tag = Tag::GeneralizedTime;
276                let year = decode_year(&[year1, year2, year3, year4])?;
277                let month = decode_decimal(tag, month1, month2).map_err(|_| ErrorKind::DateTime)?;
278                let day = decode_decimal(tag, day1, day2).map_err(|_| ErrorKind::DateTime)?;
279                let hour = decode_decimal(tag, hour1, hour2).map_err(|_| ErrorKind::DateTime)?;
280                let minutes = decode_decimal(tag, min1, min2).map_err(|_| ErrorKind::DateTime)?;
281                let seconds = decode_decimal(tag, sec1, sec2).map_err(|_| ErrorKind::DateTime)?;
282                Self::new(year, month, day, hour, minutes, seconds)
283            }
284            _ => Err(ErrorKind::DateTime.into()),
285        }
286    }
287}
288
289impl fmt::Display for DateTime {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        write!(
292            f,
293            "{:02}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
294            self.year, self.month, self.day, self.hour, self.minutes, self.seconds
295        )
296    }
297}
298
299#[cfg(feature = "std")]
300impl From<DateTime> for SystemTime {
301    fn from(time: DateTime) -> SystemTime {
302        time.to_system_time()
303    }
304}
305
306#[cfg(feature = "std")]
307impl From<&DateTime> for SystemTime {
308    fn from(time: &DateTime) -> SystemTime {
309        time.to_system_time()
310    }
311}
312
313#[cfg(feature = "std")]
314impl TryFrom<SystemTime> for DateTime {
315    type Error = Error;
316
317    fn try_from(time: SystemTime) -> Result<DateTime> {
318        DateTime::from_system_time(time)
319    }
320}
321
322#[cfg(feature = "std")]
323impl TryFrom<&SystemTime> for DateTime {
324    type Error = Error;
325
326    fn try_from(time: &SystemTime) -> Result<DateTime> {
327        DateTime::from_system_time(*time)
328    }
329}
330
331#[cfg(feature = "time")]
332impl TryFrom<DateTime> for PrimitiveDateTime {
333    type Error = Error;
334
335    fn try_from(time: DateTime) -> Result<PrimitiveDateTime> {
336        let month = time.month().try_into()?;
337        let date = time::Date::from_calendar_date(i32::from(time.year()), month, time.day())?;
338        let time = time::Time::from_hms(time.hour(), time.minutes(), time.seconds())?;
339
340        Ok(PrimitiveDateTime::new(date, time))
341    }
342}
343
344#[cfg(feature = "time")]
345impl TryFrom<PrimitiveDateTime> for DateTime {
346    type Error = Error;
347
348    fn try_from(time: PrimitiveDateTime) -> Result<DateTime> {
349        DateTime::new(
350            time.year().try_into().map_err(|_| ErrorKind::DateTime)?,
351            time.month().into(),
352            time.day(),
353            time.hour(),
354            time.minute(),
355            time.second(),
356        )
357    }
358}
359
360// Implement by hand because the derive would create invalid values.
361// Use the conversion from Duration to create a valid value.
362#[cfg(feature = "arbitrary")]
363impl<'a> arbitrary::Arbitrary<'a> for DateTime {
364    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
365        Self::from_unix_duration(Duration::new(
366            u.int_in_range(0..=MAX_UNIX_DURATION.as_secs().saturating_sub(1))?,
367            u.int_in_range(0..=999_999_999)?,
368        ))
369        .map_err(|_| arbitrary::Error::IncorrectFormat)
370    }
371
372    fn size_hint(depth: usize) -> (usize, Option<usize>) {
373        arbitrary::size_hint::and(u64::size_hint(depth), u32::size_hint(depth))
374    }
375}
376
377/// Decode 2-digit decimal value
378// TODO(tarcieri): checked arithmetic
379#[allow(clippy::integer_arithmetic)]
380pub(crate) fn decode_decimal(tag: Tag, hi: u8, lo: u8) -> Result<u8> {
381    if hi.is_ascii_digit() && lo.is_ascii_digit() {
382        Ok((hi - b'0') * 10 + (lo - b'0'))
383    } else {
384        Err(tag.value_error())
385    }
386}
387
388/// Encode 2-digit decimal value
389pub(crate) fn encode_decimal<W>(writer: &mut W, tag: Tag, value: u8) -> Result<()>
390where
391    W: Writer + ?Sized,
392{
393    let hi_val = value / 10;
394
395    if hi_val >= 10 {
396        return Err(tag.value_error());
397    }
398
399    writer.write_byte(b'0'.checked_add(hi_val).ok_or(ErrorKind::Overflow)?)?;
400    writer.write_byte(b'0'.checked_add(value % 10).ok_or(ErrorKind::Overflow)?)
401}
402
403/// Decode 4-digit year.
404// TODO(tarcieri): checked arithmetic
405#[allow(clippy::integer_arithmetic)]
406fn decode_year(year: &[u8; 4]) -> Result<u16> {
407    let tag = Tag::GeneralizedTime;
408    let hi = decode_decimal(tag, year[0], year[1]).map_err(|_| ErrorKind::DateTime)?;
409    let lo = decode_decimal(tag, year[2], year[3]).map_err(|_| ErrorKind::DateTime)?;
410    Ok(u16::from(hi) * 100 + u16::from(lo))
411}
412
413#[cfg(test)]
414mod tests {
415    use super::DateTime;
416
417    /// Ensure a day is OK
418    fn is_date_valid(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> bool {
419        DateTime::new(year, month, day, hour, minute, second).is_ok()
420    }
421
422    #[test]
423    fn feb_leap_year_handling() {
424        assert!(is_date_valid(2000, 2, 29, 0, 0, 0));
425        assert!(!is_date_valid(2001, 2, 29, 0, 0, 0));
426        assert!(!is_date_valid(2100, 2, 29, 0, 0, 0));
427    }
428
429    #[test]
430    fn from_str() {
431        let datetime = "2001-01-02T12:13:14Z".parse::<DateTime>().unwrap();
432        assert_eq!(datetime.year(), 2001);
433        assert_eq!(datetime.month(), 1);
434        assert_eq!(datetime.day(), 2);
435        assert_eq!(datetime.hour(), 12);
436        assert_eq!(datetime.minutes(), 13);
437        assert_eq!(datetime.seconds(), 14);
438    }
439
440    #[cfg(feature = "alloc")]
441    #[test]
442    fn display() {
443        use alloc::string::ToString;
444        let datetime = DateTime::new(2001, 01, 02, 12, 13, 14).unwrap();
445        assert_eq!(&datetime.to_string(), "2001-01-02T12:13:14Z");
446    }
447}