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::{Encoder, Error, ErrorKind, Result, Tag};
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    /// Create a new [`DateTime`] from the given UTC time components.
60    pub fn new(year: u16, month: u8, day: u8, hour: u8, minutes: u8, seconds: u8) -> Result<Self> {
61        // Basic validation of the components.
62        if year < MIN_YEAR
63            || !(1..=12).contains(&month)
64            || !(1..=31).contains(&day)
65            || !(0..=23).contains(&hour)
66            || !(0..=59).contains(&minutes)
67            || !(0..=59).contains(&seconds)
68        {
69            return Err(ErrorKind::DateTime.into());
70        }
71
72        let leap_years =
73            ((year - 1) - 1968) / 4 - ((year - 1) - 1900) / 100 + ((year - 1) - 1600) / 400;
74
75        let is_leap_year = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
76
77        let (mut ydays, mdays): (u16, u8) = match month {
78            1 => (0, 31),
79            2 if is_leap_year => (31, 29),
80            2 => (31, 28),
81            3 => (59, 31),
82            4 => (90, 30),
83            5 => (120, 31),
84            6 => (151, 30),
85            7 => (181, 31),
86            8 => (212, 31),
87            9 => (243, 30),
88            10 => (273, 31),
89            11 => (304, 30),
90            12 => (334, 31),
91            _ => return Err(ErrorKind::DateTime.into()),
92        };
93
94        if day > mdays || day == 0 {
95            return Err(ErrorKind::DateTime.into());
96        }
97
98        ydays += day as u16 - 1;
99
100        if is_leap_year && month > 2 {
101            ydays += 1;
102        }
103
104        let days = (year - 1970) as u64 * 365 + leap_years as u64 + ydays as u64;
105        let time = seconds as u64 + (minutes as u64 * 60) + (hour as u64 * 3600);
106        let unix_duration = Duration::from_secs(time + days * 86400);
107
108        if unix_duration > MAX_UNIX_DURATION {
109            return Err(ErrorKind::DateTime.into());
110        }
111
112        Ok(Self {
113            year,
114            month,
115            day,
116            hour,
117            minutes,
118            seconds,
119            unix_duration,
120        })
121    }
122
123    /// Compute a [`DateTime`] from the given [`Duration`] since the `UNIX_EPOCH`.
124    ///
125    /// Returns `None` if the value is outside the supported date range.
126    pub fn from_unix_duration(unix_duration: Duration) -> Result<Self> {
127        if unix_duration > MAX_UNIX_DURATION {
128            return Err(ErrorKind::DateTime.into());
129        }
130
131        let secs_since_epoch = unix_duration.as_secs();
132
133        /// 2000-03-01 (mod 400 year, immediately after Feb 29)
134        const LEAPOCH: i64 = 11017;
135        const DAYS_PER_400Y: i64 = 365 * 400 + 97;
136        const DAYS_PER_100Y: i64 = 365 * 100 + 24;
137        const DAYS_PER_4Y: i64 = 365 * 4 + 1;
138
139        let days = (secs_since_epoch / 86400) as i64 - LEAPOCH;
140        let secs_of_day = secs_since_epoch % 86400;
141
142        let mut qc_cycles = days / DAYS_PER_400Y;
143        let mut remdays = days % DAYS_PER_400Y;
144
145        if remdays < 0 {
146            remdays += DAYS_PER_400Y;
147            qc_cycles -= 1;
148        }
149
150        let mut c_cycles = remdays / DAYS_PER_100Y;
151        if c_cycles == 4 {
152            c_cycles -= 1;
153        }
154        remdays -= c_cycles * DAYS_PER_100Y;
155
156        let mut q_cycles = remdays / DAYS_PER_4Y;
157        if q_cycles == 25 {
158            q_cycles -= 1;
159        }
160        remdays -= q_cycles * DAYS_PER_4Y;
161
162        let mut remyears = remdays / 365;
163        if remyears == 4 {
164            remyears -= 1;
165        }
166        remdays -= remyears * 365;
167
168        let mut year = 2000 + remyears + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles;
169
170        let months = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
171        let mut mon = 0;
172        for mon_len in months.iter() {
173            mon += 1;
174            if remdays < *mon_len {
175                break;
176            }
177            remdays -= *mon_len;
178        }
179        let mday = remdays + 1;
180        let mon = if mon + 2 > 12 {
181            year += 1;
182            mon - 10
183        } else {
184            mon + 2
185        };
186
187        let second = secs_of_day % 60;
188        let mins_of_day = secs_of_day / 60;
189        let minute = mins_of_day % 60;
190        let hour = mins_of_day / 60;
191
192        Self::new(
193            year as u16,
194            mon,
195            mday as u8,
196            hour as u8,
197            minute as u8,
198            second as u8,
199        )
200    }
201
202    /// Get the year.
203    pub fn year(&self) -> u16 {
204        self.year
205    }
206
207    /// Get the month.
208    pub fn month(&self) -> u8 {
209        self.month
210    }
211
212    /// Get the day.
213    pub fn day(&self) -> u8 {
214        self.day
215    }
216
217    /// Get the hour.
218    pub fn hour(&self) -> u8 {
219        self.hour
220    }
221
222    /// Get the minutes.
223    pub fn minutes(&self) -> u8 {
224        self.minutes
225    }
226
227    /// Get the seconds.
228    pub fn seconds(&self) -> u8 {
229        self.seconds
230    }
231
232    /// Compute [`Duration`] since `UNIX_EPOCH` from the given calendar date.
233    pub fn unix_duration(&self) -> Duration {
234        self.unix_duration
235    }
236
237    /// Instantiate from [`SystemTime`].
238    #[cfg(feature = "std")]
239    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
240    pub fn from_system_time(time: SystemTime) -> Result<Self> {
241        time.duration_since(UNIX_EPOCH)
242            .map_err(|_| ErrorKind::DateTime.into())
243            .and_then(Self::from_unix_duration)
244    }
245
246    /// Convert to [`SystemTime`].
247    #[cfg(feature = "std")]
248    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
249    pub fn to_system_time(&self) -> SystemTime {
250        UNIX_EPOCH + self.unix_duration()
251    }
252}
253
254impl FromStr for DateTime {
255    type Err = Error;
256
257    fn from_str(s: &str) -> Result<Self> {
258        match *s.as_bytes() {
259            [year1, year2, year3, year4, b'-', month1, month2, b'-', day1, day2, b'T', hour1, hour2, b':', min1, min2, b':', sec1, sec2, b'Z'] =>
260            {
261                let tag = Tag::GeneralizedTime;
262                let year = decode_decimal(tag, year1, year2).map_err(|_| ErrorKind::DateTime)?
263                    as u16
264                    * 100
265                    + decode_decimal(tag, year3, year4).map_err(|_| ErrorKind::DateTime)? as u16;
266                let month = decode_decimal(tag, month1, month2).map_err(|_| ErrorKind::DateTime)?;
267                let day = decode_decimal(tag, day1, day2).map_err(|_| ErrorKind::DateTime)?;
268                let hour = decode_decimal(tag, hour1, hour2).map_err(|_| ErrorKind::DateTime)?;
269                let minutes = decode_decimal(tag, min1, min2).map_err(|_| ErrorKind::DateTime)?;
270                let seconds = decode_decimal(tag, sec1, sec2).map_err(|_| ErrorKind::DateTime)?;
271                Self::new(year, month, day, hour, minutes, seconds)
272            }
273            _ => Err(ErrorKind::DateTime.into()),
274        }
275    }
276}
277
278impl fmt::Display for DateTime {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        write!(
281            f,
282            "{:02}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
283            self.year, self.month, self.day, self.hour, self.minutes, self.seconds
284        )
285    }
286}
287
288#[cfg(feature = "std")]
289#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
290impl From<DateTime> for SystemTime {
291    fn from(time: DateTime) -> SystemTime {
292        time.to_system_time()
293    }
294}
295
296#[cfg(feature = "std")]
297#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
298impl From<&DateTime> for SystemTime {
299    fn from(time: &DateTime) -> SystemTime {
300        time.to_system_time()
301    }
302}
303
304#[cfg(feature = "std")]
305#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
306impl TryFrom<SystemTime> for DateTime {
307    type Error = Error;
308
309    fn try_from(time: SystemTime) -> Result<DateTime> {
310        DateTime::from_system_time(time)
311    }
312}
313
314#[cfg(feature = "std")]
315#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
316impl TryFrom<&SystemTime> for DateTime {
317    type Error = Error;
318
319    fn try_from(time: &SystemTime) -> Result<DateTime> {
320        DateTime::from_system_time(*time)
321    }
322}
323
324#[cfg(feature = "time")]
325#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
326impl TryFrom<DateTime> for PrimitiveDateTime {
327    type Error = Error;
328
329    fn try_from(time: DateTime) -> Result<PrimitiveDateTime> {
330        let month = (time.month() as u8).try_into()?;
331        let date = time::Date::from_calendar_date(time.year() as i32, month, time.day())?;
332        let time = time::Time::from_hms(time.hour(), time.minutes(), time.seconds())?;
333
334        Ok(PrimitiveDateTime::new(date, time))
335    }
336}
337
338#[cfg(feature = "time")]
339#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
340impl TryFrom<PrimitiveDateTime> for DateTime {
341    type Error = Error;
342
343    fn try_from(time: PrimitiveDateTime) -> Result<DateTime> {
344        DateTime::new(
345            time.year() as u16,
346            time.month().into(),
347            time.day(),
348            time.hour(),
349            time.minute(),
350            time.second(),
351        )
352    }
353}
354
355/// Decode 2-digit decimal value
356pub(crate) fn decode_decimal(tag: Tag, hi: u8, lo: u8) -> Result<u8> {
357    if (b'0'..=b'9').contains(&hi) && (b'0'..=b'9').contains(&lo) {
358        Ok((hi - b'0') * 10 + (lo - b'0'))
359    } else {
360        Err(tag.value_error())
361    }
362}
363
364/// Encode 2-digit decimal value
365pub(crate) fn encode_decimal(encoder: &mut Encoder<'_>, tag: Tag, value: u8) -> Result<()> {
366    let hi_val = value / 10;
367
368    if hi_val >= 10 {
369        return Err(tag.value_error());
370    }
371
372    encoder.byte(hi_val + b'0')?;
373    encoder.byte((value % 10) + b'0')
374}
375
376#[cfg(test)]
377mod tests {
378    use super::DateTime;
379
380    /// Ensure a day is OK
381    fn is_date_valid(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> bool {
382        DateTime::new(year, month, day, hour, minute, second).is_ok()
383    }
384
385    #[test]
386    fn feb_leap_year_handling() {
387        assert!(is_date_valid(2000, 2, 29, 0, 0, 0));
388        assert!(!is_date_valid(2001, 2, 29, 0, 0, 0));
389        assert!(!is_date_valid(2100, 2, 29, 0, 0, 0));
390    }
391
392    #[test]
393    fn from_str() {
394        let datetime = "2001-01-02T12:13:14Z".parse::<DateTime>().unwrap();
395        assert_eq!(datetime.year(), 2001);
396        assert_eq!(datetime.month(), 1);
397        assert_eq!(datetime.day(), 2);
398        assert_eq!(datetime.hour(), 12);
399        assert_eq!(datetime.minutes(), 13);
400        assert_eq!(datetime.seconds(), 14);
401    }
402
403    #[cfg(feature = "alloc")]
404    #[test]
405    fn display() {
406        use alloc::string::ToString;
407        let datetime = DateTime::new(2001, 01, 02, 12, 13, 14).unwrap();
408        assert_eq!(&datetime.to_string(), "2001-01-02T12:13:14Z");
409    }
410}