1use {
8 bcder::{
9 decode::{Constructed, DecodeError, Primitive, SliceSource, Source},
10 encode::{PrimitiveContent, Values},
11 Mode, Tag,
12 },
13 chrono::{Datelike, TimeZone, Timelike},
14 std::{
15 fmt::{Display, Formatter},
16 io::Write,
17 ops::{Add, Deref},
18 str::FromStr,
19 },
20};
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum GeneralizedTimeAllowedTimezone {
29 Any,
31
32 Z,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum Time {
38 UtcTime(UtcTime),
39 GeneralTime(GeneralizedTime),
40}
41
42impl Time {
43 pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, DecodeError<S::Error>> {
44 cons.take_primitive(|tag, prim| match tag {
45 Tag::UTC_TIME => Ok(Self::UtcTime(UtcTime::from_primitive(prim)?)),
46 Tag::GENERALIZED_TIME => Ok(Self::GeneralTime(
47 GeneralizedTime::from_primitive_no_fractional_or_timezone_offsets(prim)?,
48 )),
49 _ => Err(prim.content_err(format!("expected UTCTime or GeneralizedTime; got {}", tag))),
50 })
51 }
52
53 pub fn take_opt_from<S: Source>(
54 cons: &mut Constructed<S>,
55 ) -> Result<Option<Self>, DecodeError<S::Error>> {
56 cons.take_opt_primitive(|tag, prim| match tag {
57 Tag::UTC_TIME => Ok(Self::UtcTime(UtcTime::from_primitive(prim)?)),
58 Tag::GENERALIZED_TIME => Ok(Self::GeneralTime(
59 GeneralizedTime::from_primitive_no_fractional_or_timezone_offsets(prim)?,
60 )),
61 _ => Err(prim.content_err(format!("expected UTCTime or GeneralizedTime; got {}", tag))),
62 })
63 }
64
65 pub fn encode_ref(&self) -> impl Values + '_ {
66 match self {
67 Self::UtcTime(utc) => (Some(utc.encode()), None),
68 Self::GeneralTime(gt) => (None, Some(gt.encode())),
69 }
70 }
71}
72
73impl From<chrono::DateTime<chrono::Utc>> for Time {
74 fn from(t: chrono::DateTime<chrono::Utc>) -> Self {
75 Self::UtcTime(UtcTime(t))
76 }
77}
78
79impl From<Time> for chrono::DateTime<chrono::Utc> {
80 fn from(value: Time) -> Self {
81 match value {
82 Time::UtcTime(v) => v.into(),
83 Time::GeneralTime(v) => v.into(),
84 }
85 }
86}
87
88#[derive(Clone, Debug, Eq, PartialEq)]
89enum Zone {
90 Utc,
91 Offset(chrono::FixedOffset),
92}
93
94impl Display for Zone {
95 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
96 match self {
97 Self::Utc => f.write_str("Z"),
98 Self::Offset(offset) => f.write_str(format!("{}", offset).replace(':', "").as_str()),
99 }
100 }
101}
102
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub struct GeneralizedTime {
105 time: chrono::NaiveDateTime,
106 fractional_seconds: bool,
107 timezone: Zone,
108}
109
110impl GeneralizedTime {
111 pub fn take_from_no_fractional_z<S: Source>(
113 cons: &mut Constructed<S>,
114 ) -> Result<Self, DecodeError<S::Error>> {
115 cons.take_primitive_if(Tag::GENERALIZED_TIME, |prim| {
116 Self::from_primitive_no_fractional_or_timezone_offsets(prim)
117 })
118 }
119
120 pub fn take_from_allow_fractional_z<S: Source>(
122 cons: &mut Constructed<S>,
123 ) -> Result<Self, DecodeError<S::Error>> {
124 cons.take_primitive_if(Tag::GENERALIZED_TIME, |prim| {
125 let data = prim.take_all()?;
126
127 Self::parse(
128 SliceSource::new(data.as_ref()),
129 true,
130 GeneralizedTimeAllowedTimezone::Z,
131 )
132 .map_err(|e| e.convert())
133 })
134 }
135
136 pub fn from_primitive_no_fractional_or_timezone_offsets<S: Source>(
138 prim: &mut Primitive<S>,
139 ) -> Result<Self, DecodeError<S::Error>> {
140 let data = prim.take_all()?;
141
142 Self::parse(
143 SliceSource::new(data.as_ref()),
144 false,
145 GeneralizedTimeAllowedTimezone::Z,
146 )
147 .map_err(|e| e.convert())
148 }
149
150 pub fn parse(
152 source: SliceSource,
153 allow_fractional_seconds: bool,
154 tz: GeneralizedTimeAllowedTimezone,
155 ) -> Result<Self, DecodeError<<SliceSource as Source>::Error>> {
156 let data = source.slice();
157
158 if !allow_fractional_seconds
160 && matches!(tz, GeneralizedTimeAllowedTimezone::Z)
161 && data.len() != "YYYYMMDDHHMMSSZ".len()
162 {
163 return Err(source.content_err(format!(
164 "{} is not of format YYYYMMDDHHMMSSZ",
165 String::from_utf8_lossy(data)
166 )));
167 }
168
169 if data.len() < 15 {
170 return Err(source.content_err("GeneralizedTime value too short"));
171 }
172
173 let year = i32::from_str(
174 std::str::from_utf8(&data[0..4]).map_err(|s| source.content_err(s.to_string()))?,
175 )
176 .map_err(|s| source.content_err(s.to_string()))?;
177 let month = u32::from_str(
178 std::str::from_utf8(&data[4..6]).map_err(|s| source.content_err(s.to_string()))?,
179 )
180 .map_err(|s| source.content_err(s.to_string()))?;
181 let day = u32::from_str(
182 std::str::from_utf8(&data[6..8]).map_err(|s| source.content_err(s.to_string()))?,
183 )
184 .map_err(|s| source.content_err(s.to_string()))?;
185 let hour = u32::from_str(
186 std::str::from_utf8(&data[8..10]).map_err(|s| source.content_err(s.to_string()))?,
187 )
188 .map_err(|s| source.content_err(s.to_string()))?;
189 let minute = u32::from_str(
190 std::str::from_utf8(&data[10..12]).map_err(|s| source.content_err(s.to_string()))?,
191 )
192 .map_err(|s| source.content_err(s.to_string()))?;
193 let second = u32::from_str(
194 std::str::from_utf8(&data[12..14]).map_err(|s| source.content_err(s.to_string()))?,
195 )
196 .map_err(|s| source.content_err(s.to_string()))?;
197
198 let remaining = &data[14..];
199
200 let (nano, remaining) = match allow_fractional_seconds {
201 true => {
202 if remaining.starts_with(b".") {
203 if let Some((nondigit_offset, _)) = remaining
204 .iter()
205 .enumerate()
206 .skip(1)
207 .find(|(_, c)| !c.is_ascii_digit())
208 {
209 let digits_count = nondigit_offset - 1;
210
211 let (digits, remaining) = remaining.split_at(nondigit_offset);
212
213 let mut digits = std::str::from_utf8(&digits[1..])
214 .map_err(|s| source.content_err(s.to_string()))?
215 .to_string();
216 digits.extend(std::iter::repeat('0').take(9 - digits_count));
217
218 (
219 u32::from_str(&digits)
220 .map_err(|s| source.content_err(s.to_string()))?,
221 remaining,
222 )
223 } else {
224 return Err(source.content_err(
225 "failed to locate timezone identifier after fractional seconds",
226 ));
227 }
228 } else {
229 (0, remaining)
230 }
231 }
232 false => (0, remaining),
233 };
234
235 let timezone = match tz {
236 GeneralizedTimeAllowedTimezone::Z => {
237 if remaining != b"Z" {
238 return Err(source.content_err(format!(
239 "timezone identifier {} not `Z`",
240 String::from_utf8_lossy(remaining)
241 )));
242 }
243
244 Zone::Utc
245 }
246 GeneralizedTimeAllowedTimezone::Any => {
247 if remaining == b"Z" {
248 Zone::Utc
249 } else {
250 if remaining.len() != 5 {
251 return Err(source.content_err(format!(
252 "`{}` is not a 5 character timezone identifier",
253 String::from_utf8_lossy(remaining)
254 )));
255 }
256
257 let east = match remaining[0] {
258 b'+' => true,
259 b'-' => false,
260 _ => {
261 return Err(source.content_err(format!(
262 "`{}` does not begin with a +- offset",
263 String::from_utf8_lossy(remaining)
264 )))
265 }
266 };
267
268 let offset_hours = u32::from_str(
269 std::str::from_utf8(&remaining[1..3])
270 .map_err(|s| source.content_err(s.to_string()))?,
271 )
272 .map_err(|s| source.content_err(s.to_string()))?;
273 let offset_minutes = u32::from_str(
274 std::str::from_utf8(&remaining[3..])
275 .map_err(|s| source.content_err(s.to_string()))?,
276 )
277 .map_err(|s| source.content_err(s.to_string()))?;
278
279 let offset_seconds = (offset_hours * 3600 + offset_minutes * 60) as i32;
280
281 Zone::Offset(if east {
282 chrono::FixedOffset::east_opt(offset_seconds)
283 .ok_or_else(|| source.content_err("bad timezone time"))?
284 } else {
285 chrono::FixedOffset::west_opt(offset_seconds)
286 .ok_or_else(|| source.content_err("bad timezone offset"))?
287 })
288 }
289 }
290 };
291
292 if let chrono::LocalResult::Single(dt) =
293 chrono::Utc.with_ymd_and_hms(year, month, day, hour, minute, second)
294 {
295 if let Some(dt) = dt.with_nanosecond(nano) {
296 Ok(Self {
297 time: dt.naive_utc(),
298 fractional_seconds: allow_fractional_seconds,
299 timezone,
300 })
301 } else {
302 Err(source.content_err("invalid time value"))
303 }
304 } else {
305 Err(source.content_err("invalid datetime value"))
306 }
307 }
308}
309
310impl Display for GeneralizedTime {
311 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
312 let str = format!(
313 "{}{}",
314 self.time.format(if self.fractional_seconds {
315 "%Y%m%d%H%M%S%.f"
316 } else {
317 "%Y%m%d%H%M%S"
318 }),
319 self.timezone
320 );
321 write!(f, "{}", str)
322 }
323}
324
325impl From<GeneralizedTime> for chrono::DateTime<chrono::Utc> {
326 fn from(gt: GeneralizedTime) -> Self {
327 match gt.timezone {
328 Zone::Utc => {
329 chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(gt.time, chrono::Utc)
330 }
331 Zone::Offset(offset) => chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
332 gt.time.add(offset),
333 chrono::Utc,
334 ),
335 }
336 }
337}
338
339impl From<chrono::DateTime<chrono::Utc>> for GeneralizedTime {
340 fn from(utc: chrono::DateTime<chrono::Utc>) -> Self {
341 Self {
342 time: utc.naive_utc(),
343 fractional_seconds: utc.timestamp_subsec_micros() > 0,
344 timezone: Zone::Utc,
345 }
346 }
347}
348
349impl PrimitiveContent for GeneralizedTime {
350 const TAG: Tag = Tag::GENERALIZED_TIME;
351
352 fn encoded_len(&self, _: Mode) -> usize {
353 self.to_string().len()
354 }
355
356 fn write_encoded<W: Write>(&self, _: Mode, target: &mut W) -> Result<(), std::io::Error> {
357 target.write_all(self.to_string().as_bytes())
358 }
359}
360
361#[derive(Clone, Debug, Eq, PartialEq)]
362pub struct UtcTime(chrono::DateTime<chrono::Utc>);
363
364impl From<chrono::DateTime<chrono::Utc>> for UtcTime {
365 fn from(value: chrono::DateTime<chrono::Utc>) -> Self {
366 Self(value)
367 }
368}
369
370impl From<UtcTime> for chrono::DateTime<chrono::Utc> {
371 fn from(value: UtcTime) -> Self {
372 *value.deref()
373 }
374}
375
376impl UtcTime {
377 pub fn now() -> Self {
379 Self(chrono::Utc::now())
380 }
381
382 pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, DecodeError<S::Error>> {
383 cons.take_primitive_if(Tag::UTC_TIME, |prim| Self::from_primitive(prim))
384 }
385
386 pub fn from_primitive<S: Source>(
387 prim: &mut Primitive<S>,
388 ) -> Result<Self, DecodeError<S::Error>> {
389 let data = prim.take_all()?;
390
391 if data.len() != "YYMMDDHHMMSSZ".len() {
392 return Err(prim.content_err("UTCTime not of expected length"));
393 }
394
395 let year = i32::from_str(
396 std::str::from_utf8(&data[0..2]).map_err(|s| prim.content_err(s.to_string()))?,
397 )
398 .map_err(|s| prim.content_err(s.to_string()))?;
399
400 let year = if year >= 50 { year + 1900 } else { year + 2000 };
401
402 let month = u32::from_str(
403 std::str::from_utf8(&data[2..4]).map_err(|s| prim.content_err(s.to_string()))?,
404 )
405 .map_err(|s| prim.content_err(s.to_string()))?;
406 let day = u32::from_str(
407 std::str::from_utf8(&data[4..6]).map_err(|s| prim.content_err(s.to_string()))?,
408 )
409 .map_err(|s| prim.content_err(s.to_string()))?;
410 let hour = u32::from_str(
411 std::str::from_utf8(&data[6..8]).map_err(|s| prim.content_err(s.to_string()))?,
412 )
413 .map_err(|s| prim.content_err(s.to_string()))?;
414 let minute = u32::from_str(
415 std::str::from_utf8(&data[8..10]).map_err(|s| prim.content_err(s.to_string()))?,
416 )
417 .map_err(|s| prim.content_err(s.to_string()))?;
418 let second = u32::from_str(
419 std::str::from_utf8(&data[10..12]).map_err(|s| prim.content_err(s.to_string()))?,
420 )
421 .map_err(|s| prim.content_err(s.to_string()))?;
422
423 if data[12] != b'Z' {
424 return Err(prim.content_err("UTCTime must end with `Z`"));
425 }
426
427 if let chrono::LocalResult::Single(dt) =
428 chrono::Utc.with_ymd_and_hms(year, month, day, hour, minute, second)
429 {
430 Ok(Self(dt))
431 } else {
432 Err(prim.content_err("invalid year month day hour minute second value"))
433 }
434 }
435}
436
437impl Display for UtcTime {
438 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
439 let str = format!(
440 "{:02}{:02}{:02}{:02}{:02}{:02}Z",
441 self.0.year() % 100,
442 self.0.month(),
443 self.0.day(),
444 self.0.hour(),
445 self.0.minute(),
446 self.0.second()
447 );
448 write!(f, "{}", str)
449 }
450}
451
452impl Deref for UtcTime {
453 type Target = chrono::DateTime<chrono::Utc>;
454
455 fn deref(&self) -> &Self::Target {
456 &self.0
457 }
458}
459
460impl PrimitiveContent for UtcTime {
461 const TAG: Tag = Tag::UTC_TIME;
462
463 fn encoded_len(&self, _: Mode) -> usize {
464 self.to_string().len()
465 }
466
467 fn write_encoded<W: Write>(&self, _: Mode, target: &mut W) -> Result<(), std::io::Error> {
468 target.write_all(self.to_string().as_bytes())
469 }
470}
471
472#[cfg(test)]
473mod test {
474 use {super::*, bcder::decode::ContentError};
475
476 #[test]
477 fn generalized_time() -> Result<(), ContentError> {
478 let gt = GeneralizedTime {
479 time: chrono::DateTime::from_timestamp(1643510772, 0)
480 .unwrap()
481 .naive_utc(),
482 fractional_seconds: false,
483 timezone: Zone::Utc,
484 };
485 assert_eq!(gt.to_string(), "20220130024612Z");
486
487 let gt = GeneralizedTime {
488 time: chrono::DateTime::from_timestamp(1643510772, 0)
489 .unwrap()
490 .naive_utc(),
491 fractional_seconds: false,
492 timezone: Zone::Offset(chrono::FixedOffset::east_opt(3600).unwrap()),
493 };
494 assert_eq!(gt.to_string(), "20220130024612+0100");
495
496 let gt = GeneralizedTime {
497 time: chrono::DateTime::from_timestamp(1643510772, 0)
498 .unwrap()
499 .naive_utc(),
500 fractional_seconds: false,
501 timezone: Zone::Offset(chrono::FixedOffset::west_opt(7200).unwrap()),
502 };
503 assert_eq!(gt.to_string(), "20220130024612-0200");
504
505 let gt = GeneralizedTime::parse(
506 SliceSource::new(b"20220129133742Z"),
507 true,
508 GeneralizedTimeAllowedTimezone::Z,
509 )?;
510 assert_eq!(gt.time.year(), 2022);
511 assert_eq!(gt.time.month(), 1);
512 assert_eq!(gt.time.day(), 29);
513 assert_eq!(gt.time.hour(), 13);
514 assert_eq!(gt.time.minute(), 37);
515 assert_eq!(gt.time.second(), 42);
516 assert_eq!(gt.time.nanosecond(), 0);
517 assert_eq!(format!("{}", gt.timezone), "Z");
518
519 assert_eq!(gt.to_string(), "20220129133742Z");
520
521 let gt = GeneralizedTime::parse(
522 SliceSource::new(b"20220129133742.333Z"),
523 true,
524 GeneralizedTimeAllowedTimezone::Z,
525 )?;
526 assert_eq!(gt.to_string(), "20220129133742.333Z");
527
528 let gt = GeneralizedTime::parse(
529 SliceSource::new(b"20220129133742-0800"),
530 false,
531 GeneralizedTimeAllowedTimezone::Any,
532 )?;
533 assert_eq!(format!("{}", gt.timezone), "-0800");
534
535 let gt = GeneralizedTime::parse(
536 SliceSource::new(b"20220129133742+1000"),
537 false,
538 GeneralizedTimeAllowedTimezone::Any,
539 )?;
540 assert_eq!(format!("{}", gt.timezone), "+1000");
541
542 let gt = GeneralizedTime::parse(
543 SliceSource::new(b"20220129133742.333-0800"),
544 true,
545 GeneralizedTimeAllowedTimezone::Any,
546 )?;
547 assert_eq!(gt.to_string(), "20220129133742.333-0800");
548
549 Ok(())
550 }
551
552 #[test]
553 fn generalized_time_invalid() {
554 for allow_fractional_seconds in [false, true] {
555 for allowed_timezone in [
556 GeneralizedTimeAllowedTimezone::Any,
557 GeneralizedTimeAllowedTimezone::Z,
558 ] {
559 assert!(GeneralizedTime::parse(
560 SliceSource::new(b""),
561 allow_fractional_seconds,
562 allowed_timezone
563 )
564 .is_err());
565 assert!(GeneralizedTime::parse(
566 SliceSource::new(b"abcd"),
567 allow_fractional_seconds,
568 allowed_timezone
569 )
570 .is_err());
571 assert!(GeneralizedTime::parse(
572 SliceSource::new(b"2022"),
573 allow_fractional_seconds,
574 allowed_timezone
575 )
576 .is_err());
577 assert!(GeneralizedTime::parse(
578 SliceSource::new(b"202201"),
579 allow_fractional_seconds,
580 allowed_timezone
581 )
582 .is_err());
583 assert!(GeneralizedTime::parse(
584 SliceSource::new(b"20220130"),
585 allow_fractional_seconds,
586 allowed_timezone
587 )
588 .is_err());
589 assert!(GeneralizedTime::parse(
590 SliceSource::new(b"2022013012"),
591 allow_fractional_seconds,
592 allowed_timezone
593 )
594 .is_err());
595 assert!(GeneralizedTime::parse(
596 SliceSource::new(b"202201301230"),
597 allow_fractional_seconds,
598 allowed_timezone
599 )
600 .is_err());
601 assert!(GeneralizedTime::parse(
602 SliceSource::new(b"20220130123015"),
603 allow_fractional_seconds,
604 allowed_timezone
605 )
606 .is_err());
607 assert!(GeneralizedTime::parse(
608 SliceSource::new(b"20220130123015."),
609 allow_fractional_seconds,
610 allowed_timezone
611 )
612 .is_err());
613 assert!(GeneralizedTime::parse(
614 SliceSource::new(b"20220130123015.a"),
615 allow_fractional_seconds,
616 allowed_timezone
617 )
618 .is_err());
619 assert!(GeneralizedTime::parse(
620 SliceSource::new(b"20220130123015.0a"),
621 allow_fractional_seconds,
622 allowed_timezone
623 )
624 .is_err());
625 assert!(GeneralizedTime::parse(
626 SliceSource::new(b"20220130123015a"),
627 allow_fractional_seconds,
628 allowed_timezone
629 )
630 .is_err());
631 assert!(GeneralizedTime::parse(
632 SliceSource::new(b"20220130123015-"),
633 allow_fractional_seconds,
634 allowed_timezone
635 )
636 .is_err());
637 assert!(GeneralizedTime::parse(
638 SliceSource::new(b"20220130123015+"),
639 allow_fractional_seconds,
640 allowed_timezone
641 )
642 .is_err());
643 assert!(GeneralizedTime::parse(
644 SliceSource::new(b"20220130123015+01"),
645 allow_fractional_seconds,
646 allowed_timezone
647 )
648 .is_err());
649 assert!(GeneralizedTime::parse(
650 SliceSource::new(b"20220130123015+01000"),
651 allow_fractional_seconds,
652 allowed_timezone
653 )
654 .is_err());
655 assert!(GeneralizedTime::parse(
656 SliceSource::new(b"20220130123015+0100a"),
657 allow_fractional_seconds,
658 allowed_timezone
659 )
660 .is_err());
661 assert!(GeneralizedTime::parse(
662 SliceSource::new(b"20220130123015-01000"),
663 allow_fractional_seconds,
664 allowed_timezone
665 )
666 .is_err());
667 assert!(GeneralizedTime::parse(
668 SliceSource::new(b"20220130123015-0100a"),
669 allow_fractional_seconds,
670 allowed_timezone
671 )
672 .is_err());
673 }
674 }
675 }
676}