Skip to main content

cbor_core/
date_time.rs

1use std::time::{Duration, SystemTime};
2
3use crate::iso3339::Timestamp;
4use crate::{Error, Result, Value, tag};
5
6const LEAP_SECOND_DATES: [(u16, u8, u8); 27] = [
7    (1972, 6, 30),
8    (1972, 12, 31),
9    (1973, 12, 31),
10    (1974, 12, 31),
11    (1975, 12, 31),
12    (1976, 12, 31),
13    (1977, 12, 31),
14    (1978, 12, 31),
15    (1979, 12, 31),
16    (1981, 6, 30),
17    (1982, 6, 30),
18    (1983, 6, 30),
19    (1985, 6, 30),
20    (1987, 12, 31),
21    (1989, 12, 31),
22    (1990, 12, 31),
23    (1992, 6, 30),
24    (1993, 6, 30),
25    (1994, 6, 30),
26    (1995, 12, 31),
27    (1997, 6, 30),
28    (1998, 12, 31),
29    (2005, 12, 31),
30    (2008, 12, 31),
31    (2012, 6, 30),
32    (2015, 6, 30),
33    (2016, 12, 31),
34];
35
36/// Helper for validated date/time string construction.
37///
38/// Wraps an RFC 3339 (an ISO 8601 profile) UTC string suitable
39/// for CBOR tag 0. The string is validated on creation: the date
40/// must be within `0001-01-01T00:00:00Z` to `9999-12-31T23:59:59Z`,
41/// and follow RFC 3339 section 5.6 layout.
42///
43/// Whole-second timestamps omit the fractional part. Sub-second
44/// timestamps include only the necessary digits (1-9, no trailing
45/// zeros).
46///
47/// CBOR::Core references section 5.6 of RFC 3339, which allows
48/// leap seconds (second == 60). They are accepted only on the
49/// 27 currently known leap second dates.
50///
51/// However, trying to convert a date/time value containing a
52/// leap second to `SystemTime` will fail with `Err(InvalidValue)`.
53///
54/// Implements `TryFrom<SystemTime>` and `TryFrom<&str>`, so that
55/// [`Value::date_time`] can accept both through a single
56/// `TryInto<DateTime>` bound.
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
58pub struct DateTime(String);
59
60impl From<DateTime> for Value<'_> {
61    fn from(value: DateTime) -> Self {
62        Self::tag(tag::DATE_TIME, value.0)
63    }
64}
65
66/// Borrows the timestamp text rather than cloning it.
67///
68/// The resulting `Value::Tag` still requires a small allocation
69/// for the boxed tag content; only the timestamp string itself
70/// is borrowed.
71impl<'a> From<&'a DateTime> for Value<'a> {
72    fn from(value: &'a DateTime) -> Self {
73        Self::tag(tag::DATE_TIME, value.0.as_str())
74    }
75}
76
77impl TryFrom<SystemTime> for DateTime {
78    type Error = Error;
79
80    fn try_from(value: SystemTime) -> Result<Self> {
81        if let Ok(time) = value.duration_since(SystemTime::UNIX_EPOCH)
82            && time > Duration::from_secs(253402300799)
83        {
84            return Err(Error::InvalidValue);
85        }
86
87        let ts = Timestamp::try_new(value)?;
88        Ok(Self(ts.to_string()))
89    }
90}
91
92fn validate_date_time(s: &str) -> Result<()> {
93    let ts: Timestamp = s.parse()?;
94
95    if ts.year > 9999 {
96        return Err(Error::InvalidValue);
97    }
98
99    if ts.second == 60 {
100        if ts.hour != 23 || ts.minute != 59 {
101            return Err(Error::InvalidValue);
102        }
103
104        if !LEAP_SECOND_DATES.contains(&(ts.year, ts.month, ts.day)) {
105            return Err(Error::InvalidValue);
106        }
107    }
108
109    if ts.year < 9999 || ts.month < 12 || ts.day < 31 || ts.hour < 23 || ts.minute < 59 || ts.second < 59 {
110        Ok(())
111    } else if ts.second > 59 || (ts.nano_seconds > 0 && ts.offset == 0) || ts.offset < 0 {
112        Err(Error::InvalidValue)
113    } else {
114        Ok(())
115    }
116}
117
118impl TryFrom<&str> for DateTime {
119    type Error = Error;
120
121    fn try_from(value: &str) -> Result<Self> {
122        validate_date_time(value)?;
123        Ok(Self(value.to_string()))
124    }
125}
126
127impl TryFrom<String> for DateTime {
128    type Error = Error;
129
130    fn try_from(value: String) -> Result<Self> {
131        validate_date_time(&value)?;
132        Ok(Self(value))
133    }
134}
135
136impl TryFrom<&String> for DateTime {
137    type Error = Error;
138
139    fn try_from(value: &String) -> Result<Self> {
140        validate_date_time(value)?;
141        Ok(Self(value.clone()))
142    }
143}
144
145impl TryFrom<Box<str>> for DateTime {
146    type Error = Error;
147
148    fn try_from(value: Box<str>) -> Result<Self> {
149        validate_date_time(value.as_ref())?;
150        Ok(Self(value.to_string()))
151    }
152}