1use crate::error::{Error, ErrorCode, Result};
15use crate::temporal::Instant;
16use core::fmt;
17use structured_asn1::{Element, Tag, universal};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Form {
22 Utc,
24 Generalized,
26}
27
28#[derive(Debug, Clone, Copy)]
33pub struct Time {
34 instant: Instant,
35 form: Form,
36}
37
38pub const NO_WELL_DEFINED_EXPIRY: i64 = 253_402_300_799;
41
42impl Time {
43 #[must_use]
47 pub fn seconds(self) -> i64 {
48 self.instant.seconds()
49 }
50
51 #[must_use]
53 pub fn instant(self) -> Instant {
54 self.instant
55 }
56
57 #[must_use]
59 pub fn is_leap_second(self) -> bool {
60 self.instant.is_leap_second()
61 }
62
63 #[must_use]
65 pub fn form(self) -> Form {
66 self.form
67 }
68
69 #[must_use]
73 pub fn is_no_well_defined_expiry(self) -> bool {
74 self.seconds() == NO_WELL_DEFINED_EXPIRY && !self.is_leap_second()
75 }
76}
77
78impl PartialEq for Time {
79 fn eq(&self, other: &Self) -> bool {
80 self.instant == other.instant
81 }
82}
83
84impl Eq for Time {}
85
86impl PartialOrd for Time {
87 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
88 Some(self.cmp(other))
89 }
90}
91
92impl Ord for Time {
93 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
94 self.instant.cmp(&other.instant)
95 }
96}
97
98impl fmt::Display for Time {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 let (year, month, day, hour, minute, ordinary) = civil_from_seconds(self.seconds());
101 let second = if self.is_leap_second() { 60 } else { ordinary };
102 write!(
103 f,
104 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
105 )
106 }
107}
108
109pub(crate) fn read(element: &Element<'_>, field: &'static str) -> Result<Time> {
114 read_as(element, field, Fraction::Absent)
115}
116
117#[derive(Clone, Copy, PartialEq, Eq)]
119pub(crate) enum Fraction {
120 Absent,
124 Written,
129}
130
131pub(crate) fn read_as(
133 element: &Element<'_>,
134 field: &'static str,
135 fraction: Fraction,
136) -> Result<Time> {
137 let fail = |code| Error::new(code, field, element.span().start);
138
139 let form = if element.tag() == Tag::primitive(universal::UTC_TIME) {
140 Form::Utc
141 } else if element.tag() == Tag::primitive(universal::GENERALIZED_TIME) {
142 Form::Generalized
143 } else {
144 return Err(fail(ErrorCode::Encoding(
145 structured_asn1::ErrorCode::UnexpectedTag,
146 )));
147 };
148
149 let contents = element.contents();
150 let (year, rest) = match form {
151 Form::Utc => {
152 let (digits, rest) =
153 split(contents, 2).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
154 let two = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
155 (if two >= 50 { 1900 + two } else { 2000 + two }, rest)
159 }
160 Form::Generalized => {
161 let (digits, rest) =
172 split(contents, 4).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
173 let four = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
174 (four, rest)
175 }
176 };
177
178 let ([month, day, hour, minute, second], tail) =
179 fields_of(rest).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
180
181 let closed = match tail.split_first() {
185 Some((b'.', digits)) if fraction == Fraction::Written && form == Form::Generalized => {
186 matches!(digits.split_last(), Some((b'Z', fraction)) if !fraction.is_empty()
188 && fraction.iter().all(u8::is_ascii_digit)
189 && fraction.last() != Some(&b'0'))
190 }
191 _ => tail == b"Z",
192 };
193 if !closed {
194 return Err(fail(ErrorCode::MalformedTime));
195 }
196
197 if form == Form::Generalized {
198 let instant = crate::temporal::generalized(element, field)?.start();
206 return Ok(Time { instant, form });
207 }
208
209 if !(1..=12).contains(&month) || day < 1 || day > days_in_month(year, month) {
210 return Err(fail(ErrorCode::TimeOutOfRange));
211 }
212 if hour > 23 || minute > 59 || second > 59 {
215 return Err(fail(ErrorCode::TimeOutOfRange));
216 }
217
218 let days = days_from_civil(i64::from(year), month, day);
222 let seconds =
223 days * 86_400 + i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second);
224
225 let instant = Instant::new(seconds, 0).ok_or_else(|| fail(ErrorCode::TimeOutOfRange))?;
226 Ok(Time { instant, form })
227}
228
229pub(crate) fn ber_fits(contents: &[u8], generalized: bool) -> bool {
234 let year_width = if generalized { 4 } else { 2 };
235 let Some((year, rest)) = split(contents, year_width) else {
236 return false;
237 };
238 let Some(year) = number(year) else {
239 return false;
240 };
241 let (clock, zone) = match rest
242 .iter()
243 .position(|byte| matches!(byte, b'Z' | b'+' | b'-'))
244 {
245 Some(at) => rest.split_at(at),
246 None => (rest, &[][..]),
247 };
248 if !ber_zone(zone, generalized) {
249 return false;
250 }
251 let digits = match clock.iter().position(|byte| matches!(byte, b'.' | b',')) {
252 Some(at) if generalized => {
253 let (digits, fraction) = clock.split_at(at);
254 if fraction.len() < 2 || !fraction[1..].iter().all(u8::is_ascii_digit) {
255 return false;
256 }
257 digits
258 }
259 Some(_) => return false,
260 None => clock,
261 };
262 let Some(([month, day, hour], tail)) = fields_of(digits) else {
263 return false;
264 };
265 if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) || hour > 23 {
266 return false;
267 }
268 if tail.is_empty() {
272 return generalized;
273 }
274 let Some(([minute], tail)) = fields_of(tail) else {
275 return false;
276 };
277 if minute > 59 {
278 return false;
279 }
280 if tail.is_empty() {
281 return true;
282 }
283 let Some(([second], [])) = fields_of(tail) else {
284 return false;
285 };
286 second <= if generalized { 60 } else { 59 }
287}
288
289fn ber_zone(zone: &[u8], generalized: bool) -> bool {
290 if zone.is_empty() {
291 return generalized;
292 }
293 if zone == b"Z" {
294 return true;
295 }
296 let Some((b'+' | b'-', digits)) = zone.split_first() else {
297 return false;
298 };
299 let Some(([hour], tail)) = fields_of(digits) else {
300 return false;
301 };
302 if hour > 23 {
303 return false;
304 }
305 if tail.is_empty() {
306 return generalized;
307 }
308 matches!(fields_of(tail), Some(([minute], [])) if minute <= 59)
309}
310
311pub(super) fn split(contents: &[u8], count: usize) -> Option<(&[u8], &[u8])> {
313 let (digits, rest) = contents.split_at_checked(count)?;
314 digits
315 .iter()
316 .all(u8::is_ascii_digit)
317 .then_some((digits, rest))
318}
319
320pub(super) fn number(digits: &[u8]) -> Option<u32> {
322 digits.iter().try_fold(0u32, |value, digit| {
323 value
324 .checked_mul(10)
325 .and_then(|value| value.checked_add(u32::from(digit - b'0')))
326 })
327}
328
329fn in_cycle(digits: &str, negative: bool) -> u32 {
339 let counted = digits.as_bytes().iter().fold(0u32, |cycle, digit| {
340 (cycle * 10 + u32::from(digit - b'0')) % 400
341 });
342 if negative {
343 (400 - counted) % 400
344 } else {
345 counted
346 }
347}
348
349fn weeks_in_year(year: u32) -> u32 {
358 let opens = |year: u32| (year + year / 4 - year / 100 + year / 400) % 7;
359 let long = opens(year) == 4 || year.checked_sub(1).is_some_and(|before| opens(before) == 3);
363 if long { 53 } else { 52 }
364}
365
366fn is_leap(year: u32) -> bool {
367 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
368}
369
370pub(super) fn days_in_month(year: u32, month: u32) -> u32 {
371 match month {
372 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
373 4 | 6 | 9 | 11 => 30,
374 2 if is_leap(year) => 29,
375 2 => 28,
376 _ => 0,
377 }
378}
379
380pub(super) fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
387 let month = i64::from(month);
388 let year = if month <= 2 { year - 1 } else { year };
389 let era = if year >= 0 { year } else { year - 399 } / 400;
390 let year_of_era = year - era * 400;
391 let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(day) - 1;
392 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
393 era * 146_097 + day_of_era - 719_468
396}
397
398pub(super) fn civil_from_seconds(seconds: i64) -> (i64, i64, i64, i64, i64, i64) {
400 let days = seconds.div_euclid(86_400);
401 let within = seconds.rem_euclid(86_400);
402
403 let shifted = days + 719_468;
404 let era = if shifted >= 0 {
405 shifted
406 } else {
407 shifted - 146_096
408 } / 146_097;
409 let day_of_era = shifted - era * 146_097;
410 let year_of_era =
411 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
412 let year = year_of_era + era * 400;
413 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
414 let shifted_month = (5 * day_of_year + 2) / 153;
415 let day = day_of_year - (153 * shifted_month + 2) / 5 + 1;
416 let month = if shifted_month < 10 {
417 shifted_month + 3
418 } else {
419 shifted_month - 9
420 };
421
422 (
423 if month <= 2 { year + 1 } else { year },
424 month,
425 day,
426 within / 3_600,
427 within / 60 % 60,
428 within % 60,
429 )
430}
431
432#[derive(Clone, Copy, PartialEq, Eq)]
435pub(crate) enum Defined {
436 Date,
438 TimeOfDay,
440 DateTime,
442 Duration,
444 Any,
447}
448
449pub(crate) fn defined_fits(kind: Defined, contents: &[u8]) -> bool {
458 match kind {
459 Defined::Date => date_fits(contents),
460 Defined::TimeOfDay => time_of_day_fits(contents),
461 Defined::DateTime => contents
462 .split_at_checked(8)
463 .is_some_and(|(date, time)| date_fits(date) && time_of_day_fits(time)),
464 Defined::Duration => duration_fits(contents),
465 Defined::Any => core::str::from_utf8(contents).is_ok_and(any_fits),
466 }
467}
468
469fn any_fits(text: &str) -> bool {
477 let interval = match text.strip_prefix('R') {
480 Some(after) => {
481 let count = after.bytes().take_while(u8::is_ascii_digit).count();
482 let (repeats, tail) = after.split_at(count);
483 if repeats.starts_with('0') && repeats.len() > 1 {
484 return false;
485 }
486 let Some(rest) = tail.strip_prefix('/') else {
487 return false;
488 };
489 rest
490 }
491 None => text,
492 };
493 match interval.split_once('/') {
494 Some((start, end)) => match (part_fits(start), part_fits(end)) {
498 (Some(starts), Some(ends)) => !(starts == Part::Duration && ends == Part::Duration),
499 _ => false,
500 },
501 None => part_fits(interval).is_some(),
502 }
503}
504
505#[derive(Clone, Copy, PartialEq, Eq)]
507enum Part {
508 Point,
509 Duration,
510}
511
512fn part_fits(text: &str) -> Option<Part> {
514 if let Some(duration) = text.strip_prefix('P') {
515 return duration_fits(duration.as_bytes()).then_some(Part::Duration);
516 }
517 point_fits(text).then_some(Part::Point)
518}
519
520fn point_fits(text: &str) -> bool {
523 match text.split_once('T') {
524 Some((date, time)) => date_part_fits(date) && zoned_time_fits(time),
525 None => date_part_fits(text) || zoned_time_fits(text),
529 }
530}
531
532fn zoned_time_fits(text: &str) -> bool {
537 let stated = match text.strip_suffix('Z') {
538 Some(rest) => rest,
539 None => match text.rfind(['+', '-']) {
540 Some(at) if difference_fits(&text[at..]) => &text[..at],
541 _ => text,
542 },
543 };
544 time_part_fits(stated)
545}
546
547fn difference_fits(text: &str) -> bool {
550 let Some(rest) = text.strip_prefix(['+', '-']) else {
551 return false;
552 };
553 match rest.split_once(':') {
554 Some((hours, minutes)) => {
555 two_digits(hours).is_some_and(|hours| hours <= 23)
556 && two_digits(minutes).is_some_and(|minutes| minutes <= 59)
557 }
558 None => two_digits(rest).is_some_and(|hours| hours <= 23),
559 }
560}
561
562fn date_part_fits(text: &str) -> bool {
566 let signed = text.starts_with(['+', '-']);
567 let rest = if signed { &text[1..] } else { text };
568 let mut parts = rest.split('-');
569 let Some(year) = parts.next() else {
570 return false;
571 };
572 if year.len() < 4 || !year.bytes().all(|byte| byte.is_ascii_digit()) {
574 return false;
575 }
576 let Some(next) = parts.next() else {
577 return parts.next().is_none();
578 };
579 let counted = in_cycle(year, signed);
584 if let Some(week) = next.strip_prefix('W') {
585 let last = weeks_in_year(counted);
587 let weeks = two_digits(week).is_some_and(|week| (1..=last).contains(&week));
588 return weeks
589 && match parts.next() {
590 Some(day) => {
591 day.len() == 1
592 && day.bytes().all(|byte| (b'1'..=b'7').contains(&byte))
593 && parts.next().is_none()
594 }
595 None => true,
596 };
597 }
598 if next.len() == 3 {
599 let last = if is_leap(counted) { 366 } else { 365 };
602 return next.bytes().all(|byte| byte.is_ascii_digit())
603 && number(next.as_bytes()).is_some_and(|day| (1..=last).contains(&day))
604 && parts.next().is_none();
605 }
606 let Some(month) = two_digits(next).filter(|month| (1..=12).contains(month)) else {
607 return false;
608 };
609 match parts.next() {
610 Some(day) => {
611 let Some(day) = two_digits(day) else {
612 return false;
613 };
614 let last = days_in_month(counted, month);
617 day >= 1 && day <= last && parts.next().is_none()
618 }
619 None => true,
620 }
621}
622
623fn time_part_fits(text: &str) -> bool {
627 let mut parts = text.split(':');
628 let bounds = [23u32, 59, 60];
629 let mut carried = None;
630 let mut counted = 0usize;
631 let mut ending = false;
632 for (piece, most) in parts.by_ref().zip(bounds) {
633 let (digits, fraction) = match piece.split_once(['.', ',']) {
634 Some((digits, fraction)) => (digits, Some(fraction)),
635 None => (piece, None),
636 };
637 let Some(value) = two_digits(digits) else {
638 return false;
639 };
640 if let Some(fraction) = fraction {
643 if fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
644 return false;
645 }
646 carried = Some(counted);
647 }
648 counted += 1;
649 if carried.is_some_and(|at| at + 1 != counted) {
650 return false;
651 }
652 if ending && fraction.is_some() {
659 return false;
660 }
661 if counted == 1 && value == 24 {
662 ending = true;
663 if fraction.is_some() {
664 return false;
665 }
666 continue;
667 }
668 if value > most || (ending && value != 0) {
669 return false;
670 }
671 }
672 counted > 0 && parts.next().is_none()
673}
674
675fn two_digits(text: &str) -> Option<u32> {
678 (text.len() == 2 && text.bytes().all(|byte| byte.is_ascii_digit()))
679 .then(|| number(text.as_bytes()))?
680}
681
682fn date_fits(contents: &[u8]) -> bool {
684 let Some((year, rest)) = split(contents, 4) else {
685 return false;
686 };
687 let Some(([month, day], [])) = fields_of(rest) else {
688 return false;
689 };
690 let Some(year) = number(year) else {
691 return false;
692 };
693 (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
694}
695
696fn time_of_day_fits(contents: &[u8]) -> bool {
698 let Some(([hour, minute, second], [])) = fields_of(contents) else {
699 return false;
700 };
701 let midnight = hour == 24 && minute == 0 && second == 0;
706 (hour <= 23 || midnight) && minute <= 59 && second <= 60
707}
708
709fn duration_fits(contents: &[u8]) -> bool {
713 let Ok(text) = core::str::from_utf8(contents) else {
714 return false;
715 };
716 if text.ends_with('W') {
719 return components_fit(text, b"W").is_some_and(|weeks| weeks.any);
720 }
721 let (date, time) = match text.split_once('T') {
722 Some((date, time)) => (date, Some(time)),
723 None => (text, None),
724 };
725 if time.is_some_and(str::is_empty) {
727 return false;
728 }
729 let Some(dated) = components_fit(date, b"YMD") else {
730 return false;
731 };
732 if dated.fractional && time.is_some() {
735 return false;
736 }
737 let Some(timed) = time.map_or(Some(Components::NONE), |time| components_fit(time, b"HMS"))
738 else {
739 return false;
740 };
741 dated.any || timed.any
744}
745
746#[derive(Clone, Copy)]
748struct Components {
749 any: bool,
752 fractional: bool,
754}
755
756impl Components {
757 const NONE: Self = Self {
758 any: false,
759 fractional: false,
760 };
761}
762
763fn components_fit(text: &str, order: &[u8]) -> Option<Components> {
768 let mut rest = text.as_bytes();
769 let mut left = order;
770 let mut seen = Components::NONE;
771 while !rest.is_empty() {
772 let count = rest.iter().take_while(|byte| byte.is_ascii_digit()).count();
776 if count == 0 || (rest[0] == b'0' && count > 1) {
777 return None;
778 }
779 let after = &rest[count..];
780 let (after, fractional) = match after.split_first() {
781 Some((b'.' | b',', tail)) => {
782 let digits = tail.iter().take_while(|byte| byte.is_ascii_digit()).count();
783 if digits == 0 {
784 return None;
785 }
786 (&tail[digits..], true)
787 }
788 _ => (after, false),
789 };
790 let (designator, tail) = after.split_first()?;
791 let at = left.iter().position(|kind| kind == designator)?;
792 left = &left[at + 1..];
793 if seen.fractional {
798 return None;
799 }
800 seen = Components {
801 any: true,
802 fractional,
803 };
804 rest = tail;
805 }
806 Some(seen)
807}
808
809pub(super) fn fields_of<const N: usize>(contents: &[u8]) -> Option<([u32; N], &[u8])> {
812 let mut values = [0u32; N];
813 let mut rest = contents;
814 for value in &mut values {
815 let (digits, tail) = split(rest, 2)?;
816 *value = number(digits)?;
817 rest = tail;
818 }
819 Some((values, rest))
820}
821
822#[cfg(test)]
823mod tests;