/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 | | } |