Skip to main content

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