Coverage Report

Created: 2025-07-18 06:08

/rust/registry/src/index.crates.io-6f17d22bba15001f/der-0.7.10/src/datetime.rs
Line
Count
Source (jump to first uncovered line)
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
8
use crate::{Error, ErrorKind, Result, Tag, Writer};
9
use core::{fmt, str::FromStr, time::Duration};
10
11
#[cfg(feature = "std")]
12
use std::time::{SystemTime, UNIX_EPOCH};
13
14
#[cfg(feature = "time")]
15
use time::PrimitiveDateTime;
16
17
/// Minimum year allowed in [`DateTime`] values.
18
const 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
24
const 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)]
33
pub 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
58
impl 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
0
    pub fn new(year: u16, month: u8, day: u8, hour: u8, minutes: u8, seconds: u8) -> Result<Self> {
75
0
        // Basic validation of the components.
76
0
        if year < MIN_YEAR
77
0
            || !(1..=12).contains(&month)
78
0
            || !(1..=31).contains(&day)
79
0
            || !(0..=23).contains(&hour)
80
0
            || !(0..=59).contains(&minutes)
81
0
            || !(0..=59).contains(&seconds)
82
        {
83
0
            return Err(ErrorKind::DateTime.into());
84
0
        }
85
0
86
0
        let leap_years =
87
0
            ((year - 1) - 1968) / 4 - ((year - 1) - 1900) / 100 + ((year - 1) - 1600) / 400;
88
89
0
        let is_leap_year = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
90
91
0
        let (mut ydays, mdays): (u16, u8) = match month {
92
0
            1 => (0, 31),
93
0
            2 if is_leap_year => (31, 29),
94
0
            2 => (31, 28),
95
0
            3 => (59, 31),
96
0
            4 => (90, 30),
97
0
            5 => (120, 31),
98
0
            6 => (151, 30),
99
0
            7 => (181, 31),
100
0
            8 => (212, 31),
101
0
            9 => (243, 30),
102
0
            10 => (273, 31),
103
0
            11 => (304, 30),
104
0
            12 => (334, 31),
105
0
            _ => return Err(ErrorKind::DateTime.into()),
106
        };
107
108
0
        if day > mdays || day == 0 {
109
0
            return Err(ErrorKind::DateTime.into());
110
0
        }
111
0
112
0
        ydays += u16::from(day) - 1;
113
0
114
0
        if is_leap_year && month > 2 {
115
0
            ydays += 1;
116
0
        }
117
118
0
        let days = u64::from(year - 1970) * 365 + u64::from(leap_years) + u64::from(ydays);
119
0
        let time = u64::from(seconds) + (u64::from(minutes) * 60) + (u64::from(hour) * 3600);
120
0
        let unix_duration = Duration::from_secs(time + days * 86400);
121
0
122
0
        if unix_duration > MAX_UNIX_DURATION {
123
0
            return Err(ErrorKind::DateTime.into());
124
0
        }
125
0
126
0
        Ok(Self {
127
0
            year,
128
0
            month,
129
0
            day,
130
0
            hour,
131
0
            minutes,
132
0
            seconds,
133
0
            unix_duration,
134
0
        })
135
0
    }
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
0
    pub fn from_unix_duration(unix_duration: Duration) -> Result<Self> {
143
0
        if unix_duration > MAX_UNIX_DURATION {
144
0
            return Err(ErrorKind::DateTime.into());
145
0
        }
146
0
147
0
        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
0
        let days = i64::try_from(secs_since_epoch / 86400)? - LEAPOCH;
156
0
        let secs_of_day = secs_since_epoch % 86400;
157
0
158
0
        let mut qc_cycles = days / DAYS_PER_400Y;
159
0
        let mut remdays = days % DAYS_PER_400Y;
160
0
161
0
        if remdays < 0 {
162
0
            remdays += DAYS_PER_400Y;
163
0
            qc_cycles -= 1;
164
0
        }
165
166
0
        let mut c_cycles = remdays / DAYS_PER_100Y;
167
0
        if c_cycles == 4 {
168
0
            c_cycles -= 1;
169
0
        }
170
0
        remdays -= c_cycles * DAYS_PER_100Y;
171
0
172
0
        let mut q_cycles = remdays / DAYS_PER_4Y;
173
0
        if q_cycles == 25 {
174
0
            q_cycles -= 1;
175
0
        }
176
0
        remdays -= q_cycles * DAYS_PER_4Y;
177
0
178
0
        let mut remyears = remdays / 365;
179
0
        if remyears == 4 {
180
0
            remyears -= 1;
181
0
        }
182
0
        remdays -= remyears * 365;
183
0
184
0
        let mut year = 2000 + remyears + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles;
185
0
186
0
        let months = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
187
0
        let mut mon = 0;
188
0
        for mon_len in months.iter() {
189
0
            mon += 1;
190
0
            if remdays < *mon_len {
191
0
                break;
192
0
            }
193
0
            remdays -= *mon_len;
194
        }
195
0
        let mday = remdays + 1;
196
0
        let mon = if mon + 2 > 12 {
197
0
            year += 1;
198
0
            mon - 10
199
        } else {
200
0
            mon + 2
201
        };
202
203
0
        let second = secs_of_day % 60;
204
0
        let mins_of_day = secs_of_day / 60;
205
0
        let minute = mins_of_day % 60;
206
0
        let hour = mins_of_day / 60;
207
0
208
0
        Self::new(
209
0
            year.try_into()?,
210
0
            mon,
211
0
            mday.try_into()?,
212
0
            hour.try_into()?,
213
0
            minute.try_into()?,
214
0
            second.try_into()?,
215
        )
216
0
    }
217
218
    /// Get the year.
219
0
    pub fn year(&self) -> u16 {
220
0
        self.year
221
0
    }
222
223
    /// Get the month.
224
0
    pub fn month(&self) -> u8 {
225
0
        self.month
226
0
    }
227
228
    /// Get the day.
229
0
    pub fn day(&self) -> u8 {
230
0
        self.day
231
0
    }
232
233
    /// Get the hour.
234
0
    pub fn hour(&self) -> u8 {
235
0
        self.hour
236
0
    }
237
238
    /// Get the minutes.
239
0
    pub fn minutes(&self) -> u8 {
240
0
        self.minutes
241
0
    }
242
243
    /// Get the seconds.
244
0
    pub fn seconds(&self) -> u8 {
245
0
        self.seconds
246
0
    }
247
248
    /// Compute [`Duration`] since `UNIX_EPOCH` from the given calendar date.
249
0
    pub fn unix_duration(&self) -> Duration {
250
0
        self.unix_duration
251
0
    }
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
268
impl FromStr for DateTime {
269
    type Err = Error;
270
271
0
    fn from_str(s: &str) -> Result<Self> {
272
0
        match *s.as_bytes() {
273
0
            [year1, year2, year3, year4, b'-', month1, month2, b'-', day1, day2, b'T', hour1, hour2, b':', min1, min2, b':', sec1, sec2, b'Z'] =>
274
0
            {
275
0
                let tag = Tag::GeneralizedTime;
276
0
                let year = decode_year(&[year1, year2, year3, year4])?;
277
0
                let month = decode_decimal(tag, month1, month2).map_err(|_| ErrorKind::DateTime)?;
278
0
                let day = decode_decimal(tag, day1, day2).map_err(|_| ErrorKind::DateTime)?;
279
0
                let hour = decode_decimal(tag, hour1, hour2).map_err(|_| ErrorKind::DateTime)?;
280
0
                let minutes = decode_decimal(tag, min1, min2).map_err(|_| ErrorKind::DateTime)?;
281
0
                let seconds = decode_decimal(tag, sec1, sec2).map_err(|_| ErrorKind::DateTime)?;
282
0
                Self::new(year, month, day, hour, minutes, seconds)
283
            }
284
0
            _ => Err(ErrorKind::DateTime.into()),
285
        }
286
0
    }
287
}
288
289
impl fmt::Display for DateTime {
290
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291
0
        write!(
292
0
            f,
293
0
            "{:02}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
294
0
            self.year, self.month, self.day, self.hour, self.minutes, self.seconds
295
0
        )
296
0
    }
297
}
298
299
#[cfg(feature = "std")]
300
impl From<DateTime> for SystemTime {
301
    fn from(time: DateTime) -> SystemTime {
302
        time.to_system_time()
303
    }
304
}
305
306
#[cfg(feature = "std")]
307
impl From<&DateTime> for SystemTime {
308
    fn from(time: &DateTime) -> SystemTime {
309
        time.to_system_time()
310
    }
311
}
312
313
#[cfg(feature = "std")]
314
impl 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
impl 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")]
332
impl 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")]
345
impl 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")]
363
impl<'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)]
380
0
pub(crate) fn decode_decimal(tag: Tag, hi: u8, lo: u8) -> Result<u8> {
381
0
    if hi.is_ascii_digit() && lo.is_ascii_digit() {
382
0
        Ok((hi - b'0') * 10 + (lo - b'0'))
383
    } else {
384
0
        Err(tag.value_error())
385
    }
386
0
}
387
388
/// Encode 2-digit decimal value
389
0
pub(crate) fn encode_decimal<W>(writer: &mut W, tag: Tag, value: u8) -> Result<()>
390
0
where
391
0
    W: Writer + ?Sized,
392
0
{
393
0
    let hi_val = value / 10;
394
0
395
0
    if hi_val >= 10 {
396
0
        return Err(tag.value_error());
397
0
    }
398
0
399
0
    writer.write_byte(b'0'.checked_add(hi_val).ok_or(ErrorKind::Overflow)?)?;
400
0
    writer.write_byte(b'0'.checked_add(value % 10).ok_or(ErrorKind::Overflow)?)
401
0
}
402
403
/// Decode 4-digit year.
404
// TODO(tarcieri): checked arithmetic
405
#[allow(clippy::integer_arithmetic)]
406
0
fn decode_year(year: &[u8; 4]) -> Result<u16> {
407
0
    let tag = Tag::GeneralizedTime;
408
0
    let hi = decode_decimal(tag, year[0], year[1]).map_err(|_| ErrorKind::DateTime)?;
409
0
    let lo = decode_decimal(tag, year[2], year[3]).map_err(|_| ErrorKind::DateTime)?;
410
0
    Ok(u16::from(hi) * 100 + u16::from(lo))
411
0
}
412
413
#[cfg(test)]
414
mod 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
}