asn1_rs/asn1_types/
generalizedtime.rs

1use crate::*;
2use alloc::format;
3#[cfg(not(feature = "std"))]
4use alloc::string::String;
5use core::fmt;
6use nom::{AsBytes, Input as _};
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_tryfrom_any!(GeneralizedTime);
175
176impl<'i> BerParser<'i> for GeneralizedTime {
177    type Error = BerError<Input<'i>>;
178
179    fn from_ber_content(
180        header: &'_ Header<'i>,
181        input: Input<'i>,
182    ) -> IResult<Input<'i>, Self, Self::Error> {
183        // GeneralizedTime is encoded as a VisibleString (X.680: 42.3) and can be constructed
184        // TODO: constructed GeneralizedTime not supported
185        if header.is_constructed() {
186            return Err(BerError::nom_err_input(&input, InnerError::Unsupported));
187        }
188
189        fn is_visible(b: u8) -> bool {
190            (0x20..=0x7f).contains(&b)
191        }
192        if !input.iter_elements().all(is_visible) {
193            return Err(BerError::nom_err_input(
194                &input,
195                InnerError::StringInvalidCharset,
196            ));
197        }
198
199        let (rem, data) = input.take_split(input.len());
200        let time = GeneralizedTime::from_bytes(data.as_bytes2())
201            .map_err(|e| BerError::nom_err_input(&data, e.into()))?;
202        Ok((rem, time))
203    }
204}
205
206impl<'i> DerParser<'i> for GeneralizedTime {
207    type Error = BerError<Input<'i>>;
208
209    fn from_der_content(
210        header: &'_ Header<'i>,
211        input: Input<'i>,
212    ) -> IResult<Input<'i>, Self, Self::Error> {
213        // Encoding shall be primitive (X.690: 10.2)
214        header.assert_primitive_input(&input).map_err(Err::Error)?;
215
216        fn is_visible(b: u8) -> bool {
217            (0x20..=0x7f).contains(&b)
218        }
219        if !input.iter_elements().all(is_visible) {
220            return Err(BerError::nom_err_input(
221                &input,
222                InnerError::StringInvalidCharset,
223            ));
224        }
225
226        let (rem, data) = input.take_split(input.len());
227
228        // X.690 section 11.7.1: The encoding shall terminate with a "Z"
229        if data.as_bytes2().last() != Some(&b'Z') {
230            return Err(BerError::nom_err_input(
231                &data,
232                InnerError::DerConstraintFailed(DerConstraint::MissingTimeZone),
233            ));
234        }
235        // The seconds element shall always be present (X.690: 11.7.2)
236        // XXX
237        // The decimal point element, if present, shall be the point option "." (X.690: 11.7.4)
238        if data.as_bytes2().contains(&b',') {
239            return Err(BerError::nom_err_input(
240                &data,
241                InnerError::DerConstraintFailed(DerConstraint::MissingSeconds),
242            ));
243        }
244
245        let time = GeneralizedTime::from_bytes(data.as_bytes2())
246            .map_err(|e| BerError::nom_err_input(&data, e.into()))?;
247        Ok((rem, time))
248    }
249}
250
251impl fmt::Display for GeneralizedTime {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        let dt = &self.0;
254        let fsec = match self.0.millisecond {
255            Some(v) => format!(".{}", v),
256            None => String::new(),
257        };
258        match dt.tz {
259            ASN1TimeZone::Undefined => write!(
260                f,
261                "{:04}-{:02}-{:02} {:02}:{:02}:{:02}{}",
262                dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, fsec
263            ),
264            ASN1TimeZone::Z => write!(
265                f,
266                "{:04}-{:02}-{:02} {:02}:{:02}:{:02}{}Z",
267                dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, fsec
268            ),
269            ASN1TimeZone::Offset(hh, mm) => {
270                let (s, hh) = if hh > 0 { ('+', hh) } else { ('-', -hh) };
271                write!(
272                    f,
273                    "{:04}-{:02}-{:02} {:02}:{:02}:{:02}{}{}{:02}{:02}",
274                    dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, fsec, s, hh, mm
275                )
276            }
277        }
278    }
279}
280
281impl CheckDerConstraints for GeneralizedTime {
282    fn check_constraints(any: &Any) -> Result<()> {
283        // X.690 section 11.7.1: The encoding shall terminate with a "Z"
284        if any.data.as_bytes().last() != Some(&b'Z') {
285            return Err(Error::DerConstraintFailed(DerConstraint::MissingTimeZone));
286        }
287        // X.690 section 11.7.2: The seconds element shall always be present.
288        // XXX
289        // X.690 section 11.7.4: The decimal point element, if present, shall be the point option "."
290        if any.data.as_bytes2().contains(&b',') {
291            return Err(Error::DerConstraintFailed(DerConstraint::MissingSeconds));
292        }
293        Ok(())
294    }
295}
296
297impl DerAutoDerive for GeneralizedTime {}
298
299impl Tagged for GeneralizedTime {
300    const TAG: Tag = Tag::GeneralizedTime;
301}
302
303#[cfg(feature = "std")]
304const _: () = {
305    use std::io::Write;
306
307    impl ToBer for GeneralizedTime {
308        type Encoder = Primitive<{ Tag::GeneralizedTime.0 }>;
309
310        fn ber_content_len(&self) -> Length {
311            // data:
312            // - 8 bytes for YYYYMMDD
313            // - 6 for hhmmss in DER (X.690 section 11.7.2)
314            // - (variable) the fractional part, without trailing zeros, with a point "."
315            // - 1 for the character Z in DER (X.690 section 11.7.1)
316            // data length: 15 + fractional part
317            let num_digits = match self.0.millisecond {
318                None => 0,
319                Some(v) => 1 + v.to_string().len(),
320            };
321
322            Length::Definite(15 + num_digits)
323        }
324
325        fn ber_write_content<W: Write>(&self, target: &mut W) -> SerializeResult<usize> {
326            let fractional = match self.0.millisecond {
327                None => "".to_string(),
328                Some(v) => format!(".{}", v),
329            };
330            let num_digits = fractional.len();
331            write!(
332                target,
333                "{:04}{:02}{:02}{:02}{:02}{:02}{}Z",
334                self.0.year,
335                self.0.month,
336                self.0.day,
337                self.0.hour,
338                self.0.minute,
339                self.0.second,
340                fractional,
341            )?;
342            // write_fmt returns (), see above for length value
343            Ok(15 + num_digits)
344        }
345
346        fn ber_tag_info(&self) -> (Class, bool, Tag) {
347            (Self::CLASS, false, Self::TAG)
348        }
349    }
350
351    impl_toder_from_tober!(TY GeneralizedTime);
352};
353
354#[cfg(test)]
355mod tests {
356    use hex_literal::hex;
357
358    use crate::{ASN1TimeZone, DerConstraint, DerParser, GeneralizedTime, InnerError};
359
360    #[test]
361    fn parse_der_generalizedtime() {
362        let input = &hex!("18 0F 32 30 30 32 31 32 31 33 31 34 32 39 32 33 5A FF");
363        let (rem, result) = GeneralizedTime::parse_der(input.into()).expect("parsing failed");
364        assert_eq!(rem.as_bytes2(), &[0xff]);
365        #[cfg(feature = "datetime")]
366        {
367            use time::macros::datetime;
368            let datetime = datetime! {2002-12-13 14:29:23 UTC};
369            assert_eq!(result.utc_datetime(), Ok(datetime));
370        }
371        let _ = result;
372        // local time with fractional seconds (should fail: no 'Z' at end)
373        let input = b"\x18\x1019851106210627.3";
374        let result = GeneralizedTime::parse_der(input.into()).expect_err("should not parse");
375        assert!(matches!(
376            result,
377            nom::Err::Error(e) if *e.inner() == InnerError::DerConstraintFailed(DerConstraint::MissingTimeZone)
378
379        ));
380        // coordinated universal time with fractional seconds
381        let input = b"\x18\x1119851106210627.3Z";
382        let (rem, result) = GeneralizedTime::parse_der(input.into()).expect("parsing failed");
383        assert!(rem.is_empty());
384        assert_eq!(result.0.millisecond, Some(300));
385        assert_eq!(result.0.tz, ASN1TimeZone::Z);
386        #[cfg(feature = "datetime")]
387        {
388            use time::macros::datetime;
389            let datetime = datetime! {1985-11-06 21:06:27.3 UTC};
390            assert_eq!(result.utc_datetime(), Ok(datetime));
391        }
392        #[cfg(feature = "std")]
393        let _ = result.to_string();
394        // local time with fractional seconds, and with local time 5 hours retarded in relation to coordinated universal time.
395        // (should fail: no 'Z' at end)
396        let input = b"\x18\x1519851106210627.3-0500";
397        let result = GeneralizedTime::parse_der(input.into()).expect_err("should not parse");
398        assert!(matches!(
399            result,
400            nom::Err::Error(e) if *e.inner() == InnerError::DerConstraintFailed(DerConstraint::MissingTimeZone)
401
402        ));
403    }
404
405    #[cfg(feature = "std")]
406    mod tests_std {
407        use hex_literal::hex;
408
409        use crate::{ASN1DateTime, ASN1TimeZone, GeneralizedTime, ToBer};
410
411        #[test]
412        fn tober_generalizedtime() {
413            // universal time, no millisecond
414            let datetime = ASN1DateTime::new(2013, 12, 2, 14, 29, 23, None, ASN1TimeZone::Z);
415            let time = GeneralizedTime::new(datetime);
416            let mut v: Vec<u8> = Vec::new();
417            time.ber_encode(&mut v).expect("serialization failed");
418            let expected = &[&hex!("18 0f") as &[u8], b"20131202142923Z"].concat();
419            assert_eq!(&v, expected);
420
421            // universal time with millisecond
422            let datetime = ASN1DateTime::new(1999, 12, 31, 23, 59, 59, Some(123), ASN1TimeZone::Z);
423            let time = GeneralizedTime::new(datetime);
424            let mut v: Vec<u8> = Vec::new();
425            time.ber_encode(&mut v).expect("serialization failed");
426            let expected = &[&hex!("18 13") as &[u8], b"19991231235959.123Z"].concat();
427            assert_eq!(&v, expected);
428        }
429    }
430}