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