asn1_rs/asn1_types/
generalizedtime.rs

1use crate::*;
2use alloc::format;
3#[cfg(not(feature = "std"))]
4use alloc::string::String;
5use core::convert::TryFrom;
6use core::fmt;
7#[cfg(feature = "datetime")]
8use time::OffsetDateTime;
9
10#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
11pub struct GeneralizedTime(pub ASN1DateTime);
12
13impl GeneralizedTime {
14    pub const fn new(datetime: ASN1DateTime) -> Self {
15        GeneralizedTime(datetime)
16    }
17
18    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
19        // X.680 section 42 defines a GeneralizedTime as a VisibleString restricted to:
20        //
21        // a) a string representing the calendar date, as specified in ISO 8601, with a four-digit representation of the
22        //    year, a two-digit representation of the month and a two-digit representation of the day, without use of
23        //    separators, followed by a string representing the time of day, as specified in ISO 8601, without separators
24        //    other than decimal comma or decimal period (as provided for in ISO 8601), and with no terminating Z (as
25        //    provided for in ISO 8601); or
26        // b) the characters in a) above followed by an upper-case letter Z ; or
27        // c) he characters in a) above followed by a string representing a local time differential, as specified in
28        //    ISO 8601, without separators.
29        let (year, month, day, hour, minute, rem) = match bytes {
30            [year1, year2, year3, year4, mon1, mon2, day1, day2, hour1, hour2, min1, min2, rem @ ..] =>
31            {
32                let year_hi = decode_decimal(Self::TAG, *year1, *year2)?;
33                let year_lo = decode_decimal(Self::TAG, *year3, *year4)?;
34                let year = (year_hi as u32) * 100 + (year_lo as u32);
35                let month = decode_decimal(Self::TAG, *mon1, *mon2)?;
36                let day = decode_decimal(Self::TAG, *day1, *day2)?;
37                let hour = decode_decimal(Self::TAG, *hour1, *hour2)?;
38                let minute = decode_decimal(Self::TAG, *min1, *min2)?;
39                (year, month, day, hour, minute, rem)
40            }
41            _ => return Err(Self::TAG.invalid_value("malformed time string (not yymmddhhmm)")),
42        };
43        if rem.is_empty() {
44            return Err(Self::TAG.invalid_value("malformed time string"));
45        }
46        // check for seconds
47        let (second, rem) = match rem {
48            [sec1, sec2, rem @ ..] => {
49                let second = decode_decimal(Self::TAG, *sec1, *sec2)?;
50                (second, rem)
51            }
52            _ => (0, rem),
53        };
54        if month > 12 || day > 31 || hour > 23 || minute > 59 || second > 59 {
55            // eprintln!("GeneralizedTime: time checks failed");
56            // eprintln!(" month:{}", month);
57            // eprintln!(" day:{}", day);
58            // eprintln!(" hour:{}", hour);
59            // eprintln!(" minute:{}", minute);
60            // eprintln!(" second:{}", second);
61            return Err(Self::TAG.invalid_value("time components with invalid values"));
62        }
63        if rem.is_empty() {
64            // case a): no fractional seconds part, and no terminating Z
65            return Ok(GeneralizedTime(ASN1DateTime::new(
66                year,
67                month,
68                day,
69                hour,
70                minute,
71                second,
72                None,
73                ASN1TimeZone::Undefined,
74            )));
75        }
76        // check for fractional seconds
77        let (millisecond, rem) = match rem {
78            [b'.' | b',', rem @ ..] => {
79                let mut fsecond = 0;
80                let mut rem = rem;
81                let mut digits = 0;
82                for idx in 0..=4 {
83                    if rem.is_empty() {
84                        if idx == 0 {
85                            // dot or comma, but no following digit
86                            return Err(Self::TAG.invalid_value(
87                                "malformed time string (dot or comma but no digits)",
88                            ));
89                        }
90                        digits = idx;
91                        break;
92                    }
93                    if idx == 4 {
94                        return Err(
95                            Self::TAG.invalid_value("malformed time string (invalid milliseconds)")
96                        );
97                    }
98                    match rem[0] {
99                        b'0'..=b'9' => {
100                            // cannot overflow, max 4 digits will be read
101                            fsecond = fsecond * 10 + (rem[0] - b'0') as u16;
102                        }
103                        b'Z' | b'+' | b'-' => {
104                            digits = idx;
105                            break;
106                        }
107                        _ => {
108                            return Err(Self::TAG.invalid_value(
109                                "malformed time string (invalid milliseconds/timezone)",
110                            ))
111                        }
112                    }
113                    rem = &rem[1..];
114                }
115                // fix fractional seconds depending on the number of digits
116                // for ex, date "xxxx.3" means 3000 milliseconds, not 3
117                let fsecond = match digits {
118                    1 => fsecond * 100,
119                    2 => fsecond * 10,
120                    _ => fsecond,
121                };
122                (Some(fsecond), rem)
123            }
124            _ => (None, rem),
125        };
126        // check timezone
127        if rem.is_empty() {
128            // case a): fractional seconds part, and no terminating Z
129            return Ok(GeneralizedTime(ASN1DateTime::new(
130                year,
131                month,
132                day,
133                hour,
134                minute,
135                second,
136                millisecond,
137                ASN1TimeZone::Undefined,
138            )));
139        }
140        let tz = match rem {
141            [b'Z'] => ASN1TimeZone::Z,
142            [b'+', h1, h2, m1, m2] => {
143                let hh = decode_decimal(Self::TAG, *h1, *h2)?;
144                let mm = decode_decimal(Self::TAG, *m1, *m2)?;
145                ASN1TimeZone::Offset(hh as i8, mm as i8)
146            }
147            [b'-', h1, h2, m1, m2] => {
148                let hh = decode_decimal(Self::TAG, *h1, *h2)?;
149                let mm = decode_decimal(Self::TAG, *m1, *m2)?;
150                ASN1TimeZone::Offset(-(hh as i8), mm as i8)
151            }
152            _ => return Err(Self::TAG.invalid_value("malformed time string: no time zone")),
153        };
154        Ok(GeneralizedTime(ASN1DateTime::new(
155            year,
156            month,
157            day,
158            hour,
159            minute,
160            second,
161            millisecond,
162            tz,
163        )))
164    }
165
166    /// Return a ISO 8601 combined date and time with time zone.
167    #[cfg(feature = "datetime")]
168    #[cfg_attr(docsrs, doc(cfg(feature = "datetime")))]
169    pub fn utc_datetime(&self) -> Result<OffsetDateTime> {
170        self.0.to_datetime()
171    }
172}
173
174impl<'a> TryFrom<Any<'a>> for GeneralizedTime {
175    type Error = Error;
176
177    fn try_from(any: Any<'a>) -> Result<GeneralizedTime> {
178        TryFrom::try_from(&any)
179    }
180}
181
182impl<'a, 'b> TryFrom<&'b Any<'a>> for GeneralizedTime {
183    type Error = Error;
184
185    fn try_from(any: &'b Any<'a>) -> Result<GeneralizedTime> {
186        any.tag().assert_eq(Self::TAG)?;
187        #[allow(clippy::trivially_copy_pass_by_ref)]
188        fn is_visible(b: &u8) -> bool {
189            0x20 <= *b && *b <= 0x7f
190        }
191        if !any.data.iter().all(is_visible) {
192            return Err(Error::StringInvalidCharset);
193        }
194
195        GeneralizedTime::from_bytes(any.data)
196    }
197}
198
199impl fmt::Display for GeneralizedTime {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        let dt = &self.0;
202        let fsec = match self.0.millisecond {
203            Some(v) => format!(".{}", v),
204            None => String::new(),
205        };
206        match dt.tz {
207            ASN1TimeZone::Undefined => write!(
208                f,
209                "{:04}-{:02}-{:02} {:02}:{:02}:{:02}{}",
210                dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, fsec
211            ),
212            ASN1TimeZone::Z => write!(
213                f,
214                "{:04}-{:02}-{:02} {:02}:{:02}:{:02}{}Z",
215                dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, fsec
216            ),
217            ASN1TimeZone::Offset(hh, mm) => {
218                let (s, hh) = if hh > 0 { ('+', hh) } else { ('-', -hh) };
219                write!(
220                    f,
221                    "{:04}-{:02}-{:02} {:02}:{:02}:{:02}{}{}{:02}{:02}",
222                    dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, fsec, s, hh, mm
223                )
224            }
225        }
226    }
227}
228
229impl CheckDerConstraints for GeneralizedTime {
230    fn check_constraints(any: &Any) -> Result<()> {
231        // X.690 section 11.7.1: The encoding shall terminate with a "Z"
232        if any.data.last() != Some(&b'Z') {
233            return Err(Error::DerConstraintFailed(DerConstraint::MissingTimeZone));
234        }
235        // X.690 section 11.7.2: The seconds element shall always be present.
236        // XXX
237        // X.690 section 11.7.4: The decimal point element, if present, shall be the point option "."
238        if any.data.iter().any(|&b| b == b',') {
239            return Err(Error::DerConstraintFailed(DerConstraint::MissingSeconds));
240        }
241        Ok(())
242    }
243}
244
245impl DerAutoDerive for GeneralizedTime {}
246
247impl Tagged for GeneralizedTime {
248    const TAG: Tag = Tag::GeneralizedTime;
249}
250
251#[cfg(feature = "std")]
252impl ToDer for GeneralizedTime {
253    fn to_der_len(&self) -> Result<usize> {
254        // data:
255        // - 8 bytes for YYYYMMDD
256        // - 6 for hhmmss in DER (X.690 section 11.7.2)
257        // - (variable) the fractional part, without trailing zeros, with a point "."
258        // - 1 for the character Z in DER (X.690 section 11.7.1)
259        // data length: 15 + fractional part
260        //
261        // thus, length will always be on 1 byte (short length) and
262        // class+structure+tag also on 1
263        //
264        // total: = 1 (class+constructed+tag) + 1 (length) + 15 + fractional
265        let num_digits = match self.0.millisecond {
266            None => 0,
267            Some(v) => 1 + v.to_string().len(),
268        };
269        Ok(2 + 15 + num_digits)
270    }
271
272    fn write_der_header(&self, writer: &mut dyn std::io::Write) -> SerializeResult<usize> {
273        // see above for length value
274        let num_digits = match self.0.millisecond {
275            None => 0,
276            Some(v) => 1 + v.to_string().len() as u8,
277        };
278        writer
279            .write(&[Self::TAG.0 as u8, 15 + num_digits])
280            .map_err(Into::into)
281    }
282
283    fn write_der_content(&self, writer: &mut dyn std::io::Write) -> SerializeResult<usize> {
284        let fractional = match self.0.millisecond {
285            None => "".to_string(),
286            Some(v) => format!(".{}", v),
287        };
288        let num_digits = fractional.len();
289        write!(
290            writer,
291            "{:04}{:02}{:02}{:02}{:02}{:02}{}Z",
292            self.0.year,
293            self.0.month,
294            self.0.day,
295            self.0.hour,
296            self.0.minute,
297            self.0.second,
298            fractional,
299        )?;
300        // write_fmt returns (), see above for length value
301        Ok(15 + num_digits)
302    }
303}