1use crate::error::{Error, ErrorCode, Result};
15use crate::temporal::Instant;
16use core::fmt;
17use structured_asn1::{Element, Reader, Tag, universal};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Relation {
23 Before,
25 At,
27 After,
29 Uncertain(crate::temporal::Uncertainty),
31}
32
33pub(crate) fn relation_in(
34 label: Instant,
35 now: crate::temporal::Reading,
36 utc: &crate::temporal::LeapTable<'_>,
37 execution: &structured_asn1::memory::Memory,
38 origin: (&'static str, u64),
39) -> Result<Relation> {
40 let (field, at) = origin;
41 let searches = u64::from(usize::BITS - utc.transitions().len().leading_zeros() + 1);
43 execution
44 .charge_work(searches * 6 + 64)
45 .map_err(|reason| Error::resource(reason, field, at))?;
46 for compared in [label, now.start()] {
47 if let Err(reason) = utc.shift(compared, 0) {
48 return Ok(Relation::Uncertain(reason));
49 }
50 }
51 Ok(
52 if label < now.start() || (label == now.start() && now.has_subnanosecond_remainder()) {
53 Relation::Before
54 } else if label == now.start() {
55 Relation::At
56 } else {
57 Relation::After
58 },
59 )
60}
61
62pub(crate) fn read_in(
64 parent: &Reader<'_>,
65 element: &Element<'_>,
66 field: &'static str,
67) -> Result<Time> {
68 let work = u64::try_from(element.contents().len())
71 .ok()
72 .and_then(|length| length.checked_mul(4))
73 .and_then(|work| work.checked_add(64))
74 .ok_or_else(|| {
75 Error::resource(
76 structured_asn1::memory::MemoryError::WorkExceeded,
77 field,
78 element.span().start,
79 )
80 })?;
81 parent.charge(work).map_err(|error| {
82 Error::new(
83 ErrorCode::Encoding(error.code()),
84 field,
85 element.span().start,
86 )
87 })?;
88 read(element, field)
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum Form {
94 Utc,
96 Generalized,
98}
99
100#[derive(Debug, Clone, Copy)]
105pub struct Time {
106 instant: Instant,
107 form: Form,
108}
109
110pub const NO_WELL_DEFINED_EXPIRY: i64 = 253_402_300_799;
113
114impl Time {
115 #[must_use]
119 pub fn seconds(self) -> i64 {
120 self.instant.seconds()
121 }
122
123 #[must_use]
125 pub fn instant(self) -> Instant {
126 self.instant
127 }
128
129 #[must_use]
131 pub fn is_leap_second(self) -> bool {
132 self.instant.is_leap_second()
133 }
134
135 #[must_use]
137 pub fn form(self) -> Form {
138 self.form
139 }
140
141 #[must_use]
145 pub fn is_no_well_defined_expiry(self) -> bool {
146 self.seconds() == NO_WELL_DEFINED_EXPIRY && !self.is_leap_second()
147 }
148}
149
150impl PartialEq for Time {
151 fn eq(&self, other: &Self) -> bool {
152 self.instant == other.instant
153 }
154}
155
156impl Eq for Time {}
157
158impl PartialOrd for Time {
159 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
160 Some(self.cmp(other))
161 }
162}
163
164impl Ord for Time {
165 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
166 self.instant.cmp(&other.instant)
167 }
168}
169
170impl fmt::Display for Time {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 let (year, month, day, hour, minute, ordinary) = civil_from_seconds(self.seconds());
173 let second = if self.is_leap_second() { 60 } else { ordinary };
174 write!(
175 f,
176 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
177 )
178 }
179}
180
181pub(crate) fn read(element: &Element<'_>, field: &'static str) -> Result<Time> {
186 read_as(element, field, Fraction::Absent)
187}
188
189#[derive(Clone, Copy, PartialEq, Eq)]
191pub(crate) enum Fraction {
192 Absent,
196 Written,
201}
202
203pub(crate) fn read_as(
205 element: &Element<'_>,
206 field: &'static str,
207 fraction: Fraction,
208) -> Result<Time> {
209 let fail = |code| Error::new(code, field, element.span().start);
210
211 let form = if element.tag() == Tag::primitive(universal::UTC_TIME) {
212 Form::Utc
213 } else if element.tag() == Tag::primitive(universal::GENERALIZED_TIME) {
214 Form::Generalized
215 } else {
216 return Err(fail(ErrorCode::Encoding(
217 structured_asn1::ErrorCode::UnexpectedTag,
218 )));
219 };
220
221 let contents = element.contents();
222 let (year, rest) = match form {
223 Form::Utc => {
224 let (digits, rest) =
225 split(contents, 2).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
226 let two = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
227 (if two >= 50 { 1900 + two } else { 2000 + two }, rest)
231 }
232 Form::Generalized => {
233 let (digits, rest) =
244 split(contents, 4).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
245 let four = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
246 (four, rest)
247 }
248 };
249
250 let ([month, day, hour, minute, second], tail) =
251 fields_of(rest).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
252
253 let closed = match tail.split_first() {
257 Some((b'.', digits)) if fraction == Fraction::Written && form == Form::Generalized => {
258 matches!(digits.split_last(), Some((b'Z', fraction)) if !fraction.is_empty()
260 && fraction.iter().all(u8::is_ascii_digit)
261 && fraction.last() != Some(&b'0'))
262 }
263 _ => tail == b"Z",
264 };
265 if !closed {
266 return Err(fail(ErrorCode::MalformedTime));
267 }
268
269 if form == Form::Generalized {
270 let instant = crate::temporal::generalized(element, field)?.start();
278 return Ok(Time { instant, form });
279 }
280
281 if !(1..=12).contains(&month) || day < 1 || day > days_in_month(year, month) {
282 return Err(fail(ErrorCode::TimeOutOfRange));
283 }
284 if hour > 23 || minute > 59 || second > 59 {
287 return Err(fail(ErrorCode::TimeOutOfRange));
288 }
289
290 let days = days_from_civil(i64::from(year), month, day);
294 let seconds =
295 days * 86_400 + i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second);
296
297 let instant = Instant::new(seconds, 0).ok_or_else(|| fail(ErrorCode::TimeOutOfRange))?;
298 Ok(Time { instant, form })
299}
300
301pub(crate) fn ber_fits(contents: &[u8], generalized: bool) -> bool {
306 let year_width = if generalized { 4 } else { 2 };
307 let Some((year, rest)) = split(contents, year_width) else {
308 return false;
309 };
310 let Some(year) = number(year) else {
311 return false;
312 };
313 let (clock, zone) = match rest
314 .iter()
315 .position(|byte| matches!(byte, b'Z' | b'+' | b'-'))
316 {
317 Some(at) => rest.split_at(at),
318 None => (rest, &[][..]),
319 };
320 if !ber_zone(zone, generalized) {
321 return false;
322 }
323 let digits = match clock.iter().position(|byte| matches!(byte, b'.' | b',')) {
324 Some(at) if generalized => {
325 let (digits, fraction) = clock.split_at(at);
326 if fraction.len() < 2 || !fraction[1..].iter().all(u8::is_ascii_digit) {
327 return false;
328 }
329 digits
330 }
331 Some(_) => return false,
332 None => clock,
333 };
334 let Some(([month, day, hour], tail)) = fields_of(digits) else {
335 return false;
336 };
337 if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) || hour > 23 {
338 return false;
339 }
340 if tail.is_empty() {
344 return generalized;
345 }
346 let Some(([minute], tail)) = fields_of(tail) else {
347 return false;
348 };
349 if minute > 59 {
350 return false;
351 }
352 if tail.is_empty() {
353 return true;
354 }
355 let Some(([second], [])) = fields_of(tail) else {
356 return false;
357 };
358 second <= if generalized { 60 } else { 59 }
359}
360
361fn ber_zone(zone: &[u8], generalized: bool) -> bool {
362 if zone.is_empty() {
363 return generalized;
364 }
365 if zone == b"Z" {
366 return true;
367 }
368 let Some((b'+' | b'-', digits)) = zone.split_first() else {
369 return false;
370 };
371 let Some(([hour], tail)) = fields_of(digits) else {
372 return false;
373 };
374 if hour > 23 {
375 return false;
376 }
377 if tail.is_empty() {
378 return generalized;
379 }
380 matches!(fields_of(tail), Some(([minute], [])) if minute <= 59)
381}
382
383pub(super) fn split(contents: &[u8], count: usize) -> Option<(&[u8], &[u8])> {
385 let (digits, rest) = contents.split_at_checked(count)?;
386 digits
387 .iter()
388 .all(u8::is_ascii_digit)
389 .then_some((digits, rest))
390}
391
392pub(super) fn number(digits: &[u8]) -> Option<u32> {
394 digits.iter().try_fold(0u32, |value, digit| {
395 value
396 .checked_mul(10)
397 .and_then(|value| value.checked_add(u32::from(digit - b'0')))
398 })
399}
400
401fn in_cycle(digits: &str, negative: bool) -> u32 {
411 let counted = digits.as_bytes().iter().fold(0u32, |cycle, digit| {
412 (cycle * 10 + u32::from(digit - b'0')) % 400
413 });
414 if negative {
415 (400 - counted) % 400
416 } else {
417 counted
418 }
419}
420
421fn weeks_in_year(year: u32) -> u32 {
430 let opens = |year: u32| (year + year / 4 - year / 100 + year / 400) % 7;
431 let long = opens(year) == 4 || year.checked_sub(1).is_some_and(|before| opens(before) == 3);
435 if long { 53 } else { 52 }
436}
437
438fn is_leap(year: u32) -> bool {
439 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
440}
441
442pub(super) fn days_in_month(year: u32, month: u32) -> u32 {
443 match month {
444 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
445 4 | 6 | 9 | 11 => 30,
446 2 if is_leap(year) => 29,
447 2 => 28,
448 _ => 0,
449 }
450}
451
452pub(super) fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
459 let month = i64::from(month);
460 let year = if month <= 2 { year - 1 } else { year };
461 let era = if year >= 0 { year } else { year - 399 } / 400;
462 let year_of_era = year - era * 400;
463 let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(day) - 1;
464 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
465 era * 146_097 + day_of_era - 719_468
468}
469
470pub(super) fn civil_from_seconds(seconds: i64) -> (i64, i64, i64, i64, i64, i64) {
472 let days = seconds.div_euclid(86_400);
473 let within = seconds.rem_euclid(86_400);
474
475 let shifted = days + 719_468;
476 let era = if shifted >= 0 {
477 shifted
478 } else {
479 shifted - 146_096
480 } / 146_097;
481 let day_of_era = shifted - era * 146_097;
482 let year_of_era =
483 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
484 let year = year_of_era + era * 400;
485 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
486 let shifted_month = (5 * day_of_year + 2) / 153;
487 let day = day_of_year - (153 * shifted_month + 2) / 5 + 1;
488 let month = if shifted_month < 10 {
489 shifted_month + 3
490 } else {
491 shifted_month - 9
492 };
493
494 (
495 if month <= 2 { year + 1 } else { year },
496 month,
497 day,
498 within / 3_600,
499 within / 60 % 60,
500 within % 60,
501 )
502}
503
504#[derive(Clone, Copy, PartialEq, Eq)]
507pub(crate) enum Defined {
508 Date,
510 TimeOfDay,
512 DateTime,
514 Duration,
516 Any,
519}
520
521pub(crate) fn defined_fits(kind: Defined, contents: &[u8]) -> bool {
530 match kind {
531 Defined::Date => date_fits(contents),
532 Defined::TimeOfDay => time_of_day_fits(contents),
533 Defined::DateTime => contents
534 .split_at_checked(8)
535 .is_some_and(|(date, time)| date_fits(date) && time_of_day_fits(time)),
536 Defined::Duration => duration_fits(contents),
537 Defined::Any => core::str::from_utf8(contents).is_ok_and(any_fits),
538 }
539}
540
541fn any_fits(text: &str) -> bool {
549 let interval = match text.strip_prefix('R') {
552 Some(after) => {
553 let count = after.bytes().take_while(u8::is_ascii_digit).count();
554 let (repeats, tail) = after.split_at(count);
555 if repeats.starts_with('0') && repeats.len() > 1 {
556 return false;
557 }
558 let Some(rest) = tail.strip_prefix('/') else {
559 return false;
560 };
561 rest
562 }
563 None => text,
564 };
565 match interval.split_once('/') {
566 Some((start, end)) => match (part_fits(start), part_fits(end)) {
570 (Some(starts), Some(ends)) => !(starts == Part::Duration && ends == Part::Duration),
571 _ => false,
572 },
573 None => part_fits(interval).is_some(),
574 }
575}
576
577#[derive(Clone, Copy, PartialEq, Eq)]
579enum Part {
580 Point,
581 Duration,
582}
583
584fn part_fits(text: &str) -> Option<Part> {
586 if let Some(duration) = text.strip_prefix('P') {
587 return duration_fits(duration.as_bytes()).then_some(Part::Duration);
588 }
589 point_fits(text).then_some(Part::Point)
590}
591
592fn point_fits(text: &str) -> bool {
595 match text.split_once('T') {
596 Some((date, time)) => date_part_fits(date) && zoned_time_fits(time),
597 None => date_part_fits(text) || zoned_time_fits(text),
601 }
602}
603
604fn zoned_time_fits(text: &str) -> bool {
609 let stated = match text.strip_suffix('Z') {
610 Some(rest) => rest,
611 None => match text.rfind(['+', '-']) {
612 Some(at) if difference_fits(&text[at..]) => &text[..at],
613 _ => text,
614 },
615 };
616 time_part_fits(stated)
617}
618
619fn difference_fits(text: &str) -> bool {
622 let Some(rest) = text.strip_prefix(['+', '-']) else {
623 return false;
624 };
625 match rest.split_once(':') {
626 Some((hours, minutes)) => {
627 two_digits(hours).is_some_and(|hours| hours <= 23)
628 && two_digits(minutes).is_some_and(|minutes| minutes <= 59)
629 }
630 None => two_digits(rest).is_some_and(|hours| hours <= 23),
631 }
632}
633
634fn date_part_fits(text: &str) -> bool {
638 let signed = text.starts_with(['+', '-']);
639 let rest = if signed { &text[1..] } else { text };
640 let mut parts = rest.split('-');
641 let Some(year) = parts.next() else {
642 return false;
643 };
644 if year.len() < 4 || !year.bytes().all(|byte| byte.is_ascii_digit()) {
646 return false;
647 }
648 let Some(next) = parts.next() else {
649 return parts.next().is_none();
650 };
651 let counted = in_cycle(year, signed);
656 if let Some(week) = next.strip_prefix('W') {
657 let last = weeks_in_year(counted);
659 let weeks = two_digits(week).is_some_and(|week| (1..=last).contains(&week));
660 return weeks
661 && match parts.next() {
662 Some(day) => {
663 day.len() == 1
664 && day.bytes().all(|byte| (b'1'..=b'7').contains(&byte))
665 && parts.next().is_none()
666 }
667 None => true,
668 };
669 }
670 if next.len() == 3 {
671 let last = if is_leap(counted) { 366 } else { 365 };
674 return next.bytes().all(|byte| byte.is_ascii_digit())
675 && number(next.as_bytes()).is_some_and(|day| (1..=last).contains(&day))
676 && parts.next().is_none();
677 }
678 let Some(month) = two_digits(next).filter(|month| (1..=12).contains(month)) else {
679 return false;
680 };
681 match parts.next() {
682 Some(day) => {
683 let Some(day) = two_digits(day) else {
684 return false;
685 };
686 let last = days_in_month(counted, month);
689 day >= 1 && day <= last && parts.next().is_none()
690 }
691 None => true,
692 }
693}
694
695fn time_part_fits(text: &str) -> bool {
699 let mut parts = text.split(':');
700 let bounds = [23u32, 59, 60];
701 let mut carried = None;
702 let mut counted = 0usize;
703 let mut ending = false;
704 for (piece, most) in parts.by_ref().zip(bounds) {
705 let (digits, fraction) = match piece.split_once(['.', ',']) {
706 Some((digits, fraction)) => (digits, Some(fraction)),
707 None => (piece, None),
708 };
709 let Some(value) = two_digits(digits) else {
710 return false;
711 };
712 if let Some(fraction) = fraction {
715 if fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
716 return false;
717 }
718 carried = Some(counted);
719 }
720 counted += 1;
721 if carried.is_some_and(|at| at + 1 != counted) {
722 return false;
723 }
724 if ending && fraction.is_some() {
731 return false;
732 }
733 if counted == 1 && value == 24 {
734 ending = true;
735 if fraction.is_some() {
736 return false;
737 }
738 continue;
739 }
740 if value > most || (ending && value != 0) {
741 return false;
742 }
743 }
744 counted > 0 && parts.next().is_none()
745}
746
747fn two_digits(text: &str) -> Option<u32> {
750 (text.len() == 2 && text.bytes().all(|byte| byte.is_ascii_digit()))
751 .then(|| number(text.as_bytes()))?
752}
753
754fn date_fits(contents: &[u8]) -> bool {
756 let Some((year, rest)) = split(contents, 4) else {
757 return false;
758 };
759 let Some(([month, day], [])) = fields_of(rest) else {
760 return false;
761 };
762 let Some(year) = number(year) else {
763 return false;
764 };
765 (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
766}
767
768fn time_of_day_fits(contents: &[u8]) -> bool {
770 let Some(([hour, minute, second], [])) = fields_of(contents) else {
771 return false;
772 };
773 let midnight = hour == 24 && minute == 0 && second == 0;
778 (hour <= 23 || midnight) && minute <= 59 && second <= 60
779}
780
781fn duration_fits(contents: &[u8]) -> bool {
785 let Ok(text) = core::str::from_utf8(contents) else {
786 return false;
787 };
788 if text.ends_with('W') {
791 return components_fit(text, b"W").is_some_and(|weeks| weeks.any);
792 }
793 let (date, time) = match text.split_once('T') {
794 Some((date, time)) => (date, Some(time)),
795 None => (text, None),
796 };
797 if time.is_some_and(str::is_empty) {
799 return false;
800 }
801 let Some(dated) = components_fit(date, b"YMD") else {
802 return false;
803 };
804 if dated.fractional && time.is_some() {
807 return false;
808 }
809 let Some(timed) = time.map_or(Some(Components::NONE), |time| components_fit(time, b"HMS"))
810 else {
811 return false;
812 };
813 dated.any || timed.any
816}
817
818#[derive(Clone, Copy)]
820struct Components {
821 any: bool,
824 fractional: bool,
826}
827
828impl Components {
829 const NONE: Self = Self {
830 any: false,
831 fractional: false,
832 };
833}
834
835fn components_fit(text: &str, order: &[u8]) -> Option<Components> {
840 let mut rest = text.as_bytes();
841 let mut left = order;
842 let mut seen = Components::NONE;
843 while !rest.is_empty() {
844 let count = rest.iter().take_while(|byte| byte.is_ascii_digit()).count();
848 if count == 0 || (rest[0] == b'0' && count > 1) {
849 return None;
850 }
851 let after = &rest[count..];
852 let (after, fractional) = match after.split_first() {
853 Some((b'.' | b',', tail)) => {
854 let digits = tail.iter().take_while(|byte| byte.is_ascii_digit()).count();
855 if digits == 0 {
856 return None;
857 }
858 (&tail[digits..], true)
859 }
860 _ => (after, false),
861 };
862 let (designator, tail) = after.split_first()?;
863 let at = left.iter().position(|kind| kind == designator)?;
864 left = &left[at + 1..];
865 if seen.fractional {
870 return None;
871 }
872 seen = Components {
873 any: true,
874 fractional,
875 };
876 rest = tail;
877 }
878 Some(seen)
879}
880
881pub(super) fn fields_of<const N: usize>(contents: &[u8]) -> Option<([u32; N], &[u8])> {
884 let mut values = [0u32; N];
885 let mut rest = contents;
886 for value in &mut values {
887 let (digits, tail) = split(rest, 2)?;
888 *value = number(digits)?;
889 rest = tail;
890 }
891 Some((values, rest))
892}
893
894#[cfg(test)]
895mod tests;