1use crate::error::{Error, ErrorCode, Result};
15use crate::temporal::Instant;
16use core::fmt;
17use structured_asn1::{Element, Reader, Tag, universal};
18
19mod xsd10;
20pub use xsd10::Xsd10Label;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Relation {
26 Before,
28 At,
30 After,
32 Uncertain(crate::temporal::Uncertainty),
34}
35
36pub(crate) fn relation_in<W: structured_asn1::memory::Work>(
37 label: Instant,
38 now: crate::temporal::Reading,
39 utc: &crate::temporal::LeapTable<'_>,
40 execution: &W,
41 origin: (&'static str, u64),
42) -> Result<Relation> {
43 let (field, at) = origin;
44 let searches = u64::from(usize::BITS - utc.transitions().len().leading_zeros() + 1);
46 execution
47 .charge_work(searches * 6 + 64)
48 .map_err(|reason| Error::resource(reason, field, at))?;
49 for compared in [label, now.start()] {
50 if let Err(reason) = utc.shift(compared, 0) {
51 return Ok(Relation::Uncertain(reason));
52 }
53 }
54 Ok(
55 if label < now.start() || (label == now.start() && now.has_subnanosecond_remainder()) {
56 Relation::Before
57 } else if label == now.start() {
58 Relation::At
59 } else {
60 Relation::After
61 },
62 )
63}
64
65pub(crate) fn read_in(
67 parent: &Reader<'_>,
68 element: &Element<'_>,
69 field: &'static str,
70) -> Result<Time> {
71 admit_in(parent, element, field)?;
72 read(element, field)
73}
74
75pub(crate) fn read_generalized_in(
79 parent: &Reader<'_>,
80 element: &Element<'_>,
81 field: &'static str,
82) -> Result<crate::temporal::Reading> {
83 if element.tag() != Tag::primitive(universal::GENERALIZED_TIME) {
84 return Err(Error::new(
85 ErrorCode::Encoding(structured_asn1::ErrorCode::UnexpectedTag),
86 field,
87 element.span().start,
88 ));
89 }
90 admit_in(parent, element, field)?;
91 read_label_as(element, field, Fraction::Written).map(|(reading, _)| reading)
92}
93
94fn admit_in(parent: &Reader<'_>, element: &Element<'_>, field: &'static str) -> Result<()> {
95 let work = u64::try_from(element.contents().len())
98 .ok()
99 .and_then(|length| length.checked_mul(4))
100 .and_then(|work| work.checked_add(64))
101 .ok_or_else(|| {
102 Error::resource(
103 structured_asn1::memory::MemoryError::WorkExceeded,
104 field,
105 element.span().start,
106 )
107 })?;
108 parent.charge(work).map_err(|error| {
109 Error::new(
110 ErrorCode::Encoding(error.code()),
111 field,
112 element.span().start,
113 )
114 })
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum Form {
120 Utc,
122 Generalized,
124}
125
126#[derive(Debug, Clone, Copy)]
131pub struct Time {
132 instant: Instant,
133 form: Form,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct Calendar {
141 pub year: i64,
143 pub month: u8,
145 pub day: u8,
147 pub hour: u8,
149 pub minute: u8,
151 pub second: u8,
153 pub nanos: u32,
155}
156
157pub const NO_WELL_DEFINED_EXPIRY: i64 = 253_402_300_799;
160
161impl Time {
162 pub fn xsd10_label<W: structured_asn1::memory::Work>(
166 self,
167 execution: &W,
168 ) -> core::result::Result<Xsd10Label, structured_asn1::memory::MemoryError> {
169 let calendar = self.calendar_at_minutes(0, execution)?;
170 Ok(Xsd10Label {
171 year: if calendar.year <= 0 {
172 calendar.year - 1
173 } else {
174 calendar.year
175 },
176 month: calendar.month,
177 day: calendar.day,
178 hour: calendar.hour,
179 minute: calendar.minute,
180 second: calendar.second,
181 nanos: calendar.nanos,
182 })
183 }
184
185 pub fn calendar_at_minutes<W: structured_asn1::memory::Work>(
195 self,
196 minutes: i16,
197 execution: &W,
198 ) -> core::result::Result<Calendar, structured_asn1::memory::MemoryError> {
199 execution.charge_work(64)?;
200 let (year, month, day, hour, minute, second) =
203 civil_from_seconds(self.seconds() + i64::from(minutes) * 60);
204 Ok(Calendar {
205 year,
206 month: u8::try_from(month).expect("Gregorian month"),
207 day: u8::try_from(day).expect("Gregorian day"),
208 hour: u8::try_from(hour).expect("Gregorian hour"),
209 minute: u8::try_from(minute).expect("Gregorian minute"),
210 second: if self.is_leap_second() {
211 60
212 } else {
213 u8::try_from(second).expect("Gregorian second")
214 },
215 nanos: self.instant.nanos(),
216 })
217 }
218
219 #[must_use]
223 pub fn seconds(self) -> i64 {
224 self.instant.seconds()
225 }
226
227 #[must_use]
229 pub fn instant(self) -> Instant {
230 self.instant
231 }
232
233 #[must_use]
235 pub fn is_leap_second(self) -> bool {
236 self.instant.is_leap_second()
237 }
238
239 #[must_use]
241 pub fn form(self) -> Form {
242 self.form
243 }
244
245 #[must_use]
249 pub fn is_no_well_defined_expiry(self) -> bool {
250 self.seconds() == NO_WELL_DEFINED_EXPIRY && !self.is_leap_second()
251 }
252}
253
254impl PartialEq for Time {
255 fn eq(&self, other: &Self) -> bool {
256 self.instant == other.instant
257 }
258}
259
260impl Eq for Time {}
261
262impl PartialOrd for Time {
263 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
264 Some(self.cmp(other))
265 }
266}
267
268impl Ord for Time {
269 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
270 self.instant.cmp(&other.instant)
271 }
272}
273
274impl fmt::Display for Time {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 let (year, month, day, hour, minute, ordinary) = civil_from_seconds(self.seconds());
277 let second = if self.is_leap_second() { 60 } else { ordinary };
278 write!(
279 f,
280 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
281 )
282 }
283}
284
285pub(crate) fn read(element: &Element<'_>, field: &'static str) -> Result<Time> {
290 read_as(element, field, Fraction::Absent)
291}
292
293#[derive(Clone, Copy, PartialEq, Eq)]
295pub(crate) enum Fraction {
296 Absent,
300 Written,
305}
306
307pub(crate) fn read_as(
309 element: &Element<'_>,
310 field: &'static str,
311 fraction: Fraction,
312) -> Result<Time> {
313 let (reading, form) = read_label_as(element, field, fraction)?;
314 Ok(Time {
315 instant: reading.start(),
316 form,
317 })
318}
319
320fn read_label_as(
321 element: &Element<'_>,
322 field: &'static str,
323 fraction: Fraction,
324) -> Result<(crate::temporal::Reading, Form)> {
325 let fail = |code| Error::new(code, field, element.span().start);
326
327 let form = if element.tag() == Tag::primitive(universal::UTC_TIME) {
328 Form::Utc
329 } else if element.tag() == Tag::primitive(universal::GENERALIZED_TIME) {
330 Form::Generalized
331 } else {
332 return Err(fail(ErrorCode::Encoding(
333 structured_asn1::ErrorCode::UnexpectedTag,
334 )));
335 };
336
337 let contents = element.contents();
338 let (year, rest) = match form {
339 Form::Utc => {
340 let (digits, rest) =
341 split(contents, 2).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
342 let two = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
343 (if two >= 50 { 1900 + two } else { 2000 + two }, rest)
347 }
348 Form::Generalized => {
349 let (digits, rest) =
360 split(contents, 4).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
361 let four = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
362 (four, rest)
363 }
364 };
365
366 let ([month, day, hour, minute, second], tail) =
367 fields_of(rest).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
368
369 let closed = match tail.split_first() {
373 Some((b'.', digits)) if fraction == Fraction::Written && form == Form::Generalized => {
374 matches!(digits.split_last(), Some((b'Z', fraction)) if !fraction.is_empty()
376 && fraction.iter().all(u8::is_ascii_digit)
377 && fraction.last() != Some(&b'0'))
378 }
379 _ => tail == b"Z",
380 };
381 if !closed {
382 return Err(fail(ErrorCode::MalformedTime));
383 }
384
385 if form == Form::Generalized {
386 let reading = crate::temporal::generalized(element, field)?;
394 return Ok((reading, form));
395 }
396
397 if !(1..=12).contains(&month) || day < 1 || day > days_in_month(year, month) {
398 return Err(fail(ErrorCode::TimeOutOfRange));
399 }
400 if hour > 23 || minute > 59 || second > 59 {
403 return Err(fail(ErrorCode::TimeOutOfRange));
404 }
405
406 let days = days_from_civil(i64::from(year), month, day);
410 let seconds =
411 days * 86_400 + i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second);
412
413 let instant = Instant::new(seconds, 0).ok_or_else(|| fail(ErrorCode::TimeOutOfRange))?;
414 Ok((instant.into(), form))
415}
416
417pub(crate) fn ber_fits(contents: &[u8], generalized: bool) -> bool {
422 let year_width = if generalized { 4 } else { 2 };
423 let Some((year, rest)) = split(contents, year_width) else {
424 return false;
425 };
426 let Some(year) = number(year) else {
427 return false;
428 };
429 let (clock, zone) = match rest
430 .iter()
431 .position(|byte| matches!(byte, b'Z' | b'+' | b'-'))
432 {
433 Some(at) => rest.split_at(at),
434 None => (rest, &[][..]),
435 };
436 if !ber_zone(zone, generalized) {
437 return false;
438 }
439 let digits = match clock.iter().position(|byte| matches!(byte, b'.' | b',')) {
440 Some(at) if generalized => {
441 let (digits, fraction) = clock.split_at(at);
442 if fraction.len() < 2 || !fraction[1..].iter().all(u8::is_ascii_digit) {
443 return false;
444 }
445 digits
446 }
447 Some(_) => return false,
448 None => clock,
449 };
450 let Some(([month, day, hour], tail)) = fields_of(digits) else {
451 return false;
452 };
453 if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) || hour > 23 {
454 return false;
455 }
456 if tail.is_empty() {
460 return generalized;
461 }
462 let Some(([minute], tail)) = fields_of(tail) else {
463 return false;
464 };
465 if minute > 59 {
466 return false;
467 }
468 if tail.is_empty() {
469 return true;
470 }
471 let Some(([second], [])) = fields_of(tail) else {
472 return false;
473 };
474 second <= if generalized { 60 } else { 59 }
475}
476
477fn ber_zone(zone: &[u8], generalized: bool) -> bool {
478 if zone.is_empty() {
479 return generalized;
480 }
481 if zone == b"Z" {
482 return true;
483 }
484 let Some((b'+' | b'-', digits)) = zone.split_first() else {
485 return false;
486 };
487 let Some(([hour], tail)) = fields_of(digits) else {
488 return false;
489 };
490 if hour > 23 {
491 return false;
492 }
493 if tail.is_empty() {
494 return generalized;
495 }
496 matches!(fields_of(tail), Some(([minute], [])) if minute <= 59)
497}
498
499pub(super) fn split(contents: &[u8], count: usize) -> Option<(&[u8], &[u8])> {
501 let (digits, rest) = contents.split_at_checked(count)?;
502 digits
503 .iter()
504 .all(u8::is_ascii_digit)
505 .then_some((digits, rest))
506}
507
508pub(super) fn number(digits: &[u8]) -> Option<u32> {
510 digits.iter().try_fold(0u32, |value, digit| {
511 value
512 .checked_mul(10)
513 .and_then(|value| value.checked_add(u32::from(digit - b'0')))
514 })
515}
516
517fn in_cycle(digits: &str, negative: bool) -> u32 {
527 let counted = digits.as_bytes().iter().fold(0u32, |cycle, digit| {
528 (cycle * 10 + u32::from(digit - b'0')) % 400
529 });
530 if negative {
531 (400 - counted) % 400
532 } else {
533 counted
534 }
535}
536
537fn weeks_in_year(year: u32) -> u32 {
546 let opens = |year: u32| (year + year / 4 - year / 100 + year / 400) % 7;
547 let long = opens(year) == 4 || year.checked_sub(1).is_some_and(|before| opens(before) == 3);
551 if long { 53 } else { 52 }
552}
553
554fn is_leap(year: u32) -> bool {
555 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
556}
557
558pub(super) fn days_in_month(year: u32, month: u32) -> u32 {
559 match month {
560 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
561 4 | 6 | 9 | 11 => 30,
562 2 if is_leap(year) => 29,
563 2 => 28,
564 _ => 0,
565 }
566}
567
568pub(super) fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
575 let month = i64::from(month);
576 let year = if month <= 2 { year - 1 } else { year };
577 let era = if year >= 0 { year } else { year - 399 } / 400;
578 let year_of_era = year - era * 400;
579 let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(day) - 1;
580 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
581 era * 146_097 + day_of_era - 719_468
584}
585
586pub(super) fn civil_from_seconds(seconds: i64) -> (i64, i64, i64, i64, i64, i64) {
588 let days = seconds.div_euclid(86_400);
589 let within = seconds.rem_euclid(86_400);
590
591 let shifted = days + 719_468;
592 let era = if shifted >= 0 {
593 shifted
594 } else {
595 shifted - 146_096
596 } / 146_097;
597 let day_of_era = shifted - era * 146_097;
598 let year_of_era =
599 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
600 let year = year_of_era + era * 400;
601 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
602 let shifted_month = (5 * day_of_year + 2) / 153;
603 let day = day_of_year - (153 * shifted_month + 2) / 5 + 1;
604 let month = if shifted_month < 10 {
605 shifted_month + 3
606 } else {
607 shifted_month - 9
608 };
609
610 (
611 if month <= 2 { year + 1 } else { year },
612 month,
613 day,
614 within / 3_600,
615 within / 60 % 60,
616 within % 60,
617 )
618}
619
620#[derive(Clone, Copy, PartialEq, Eq)]
623pub(crate) enum Defined {
624 Date,
626 TimeOfDay,
628 DateTime,
630 Duration,
632 Any,
635}
636
637pub(crate) fn defined_fits(kind: Defined, contents: &[u8]) -> bool {
646 match kind {
647 Defined::Date => date_fits(contents),
648 Defined::TimeOfDay => time_of_day_fits(contents),
649 Defined::DateTime => contents
650 .split_at_checked(8)
651 .is_some_and(|(date, time)| date_fits(date) && time_of_day_fits(time)),
652 Defined::Duration => duration_fits(contents),
653 Defined::Any => core::str::from_utf8(contents).is_ok_and(any_fits),
654 }
655}
656
657fn any_fits(text: &str) -> bool {
665 let interval = match text.strip_prefix('R') {
668 Some(after) => {
669 let count = after.bytes().take_while(u8::is_ascii_digit).count();
670 let (repeats, tail) = after.split_at(count);
671 if repeats.starts_with('0') && repeats.len() > 1 {
672 return false;
673 }
674 let Some(rest) = tail.strip_prefix('/') else {
675 return false;
676 };
677 rest
678 }
679 None => text,
680 };
681 match interval.split_once('/') {
682 Some((start, end)) => match (part_fits(start), part_fits(end)) {
686 (Some(starts), Some(ends)) => !(starts == Part::Duration && ends == Part::Duration),
687 _ => false,
688 },
689 None => part_fits(interval).is_some(),
690 }
691}
692
693#[derive(Clone, Copy, PartialEq, Eq)]
695enum Part {
696 Point,
697 Duration,
698}
699
700fn part_fits(text: &str) -> Option<Part> {
702 if let Some(duration) = text.strip_prefix('P') {
703 return duration_fits(duration.as_bytes()).then_some(Part::Duration);
704 }
705 point_fits(text).then_some(Part::Point)
706}
707
708fn point_fits(text: &str) -> bool {
711 match text.split_once('T') {
712 Some((date, time)) => date_part_fits(date) && zoned_time_fits(time),
713 None => date_part_fits(text) || zoned_time_fits(text),
717 }
718}
719
720fn zoned_time_fits(text: &str) -> bool {
725 let stated = match text.strip_suffix('Z') {
726 Some(rest) => rest,
727 None => match text.rfind(['+', '-']) {
728 Some(at) if difference_fits(&text[at..]) => &text[..at],
729 _ => text,
730 },
731 };
732 time_part_fits(stated)
733}
734
735fn difference_fits(text: &str) -> bool {
738 let Some(rest) = text.strip_prefix(['+', '-']) else {
739 return false;
740 };
741 match rest.split_once(':') {
742 Some((hours, minutes)) => {
743 two_digits(hours).is_some_and(|hours| hours <= 23)
744 && two_digits(minutes).is_some_and(|minutes| minutes <= 59)
745 }
746 None => two_digits(rest).is_some_and(|hours| hours <= 23),
747 }
748}
749
750fn date_part_fits(text: &str) -> bool {
754 let signed = text.starts_with(['+', '-']);
755 let rest = if signed { &text[1..] } else { text };
756 let mut parts = rest.split('-');
757 let Some(year) = parts.next() else {
758 return false;
759 };
760 if year.len() < 4 || !year.bytes().all(|byte| byte.is_ascii_digit()) {
762 return false;
763 }
764 let Some(next) = parts.next() else {
765 return parts.next().is_none();
766 };
767 let counted = in_cycle(year, signed);
772 if let Some(week) = next.strip_prefix('W') {
773 let last = weeks_in_year(counted);
775 let weeks = two_digits(week).is_some_and(|week| (1..=last).contains(&week));
776 return weeks
777 && match parts.next() {
778 Some(day) => {
779 day.len() == 1
780 && day.bytes().all(|byte| (b'1'..=b'7').contains(&byte))
781 && parts.next().is_none()
782 }
783 None => true,
784 };
785 }
786 if next.len() == 3 {
787 let last = if is_leap(counted) { 366 } else { 365 };
790 return next.bytes().all(|byte| byte.is_ascii_digit())
791 && number(next.as_bytes()).is_some_and(|day| (1..=last).contains(&day))
792 && parts.next().is_none();
793 }
794 let Some(month) = two_digits(next).filter(|month| (1..=12).contains(month)) else {
795 return false;
796 };
797 match parts.next() {
798 Some(day) => {
799 let Some(day) = two_digits(day) else {
800 return false;
801 };
802 let last = days_in_month(counted, month);
805 day >= 1 && day <= last && parts.next().is_none()
806 }
807 None => true,
808 }
809}
810
811fn time_part_fits(text: &str) -> bool {
815 let mut parts = text.split(':');
816 let bounds = [23u32, 59, 60];
817 let mut carried = None;
818 let mut counted = 0usize;
819 let mut ending = false;
820 for (piece, most) in parts.by_ref().zip(bounds) {
821 let (digits, fraction) = match piece.split_once(['.', ',']) {
822 Some((digits, fraction)) => (digits, Some(fraction)),
823 None => (piece, None),
824 };
825 let Some(value) = two_digits(digits) else {
826 return false;
827 };
828 if let Some(fraction) = fraction {
831 if fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
832 return false;
833 }
834 carried = Some(counted);
835 }
836 counted += 1;
837 if carried.is_some_and(|at| at + 1 != counted) {
838 return false;
839 }
840 if ending && fraction.is_some() {
847 return false;
848 }
849 if counted == 1 && value == 24 {
850 ending = true;
851 if fraction.is_some() {
852 return false;
853 }
854 continue;
855 }
856 if value > most || (ending && value != 0) {
857 return false;
858 }
859 }
860 counted > 0 && parts.next().is_none()
861}
862
863fn two_digits(text: &str) -> Option<u32> {
866 (text.len() == 2 && text.bytes().all(|byte| byte.is_ascii_digit()))
867 .then(|| number(text.as_bytes()))?
868}
869
870fn date_fits(contents: &[u8]) -> bool {
872 let Some((year, rest)) = split(contents, 4) else {
873 return false;
874 };
875 let Some(([month, day], [])) = fields_of(rest) else {
876 return false;
877 };
878 let Some(year) = number(year) else {
879 return false;
880 };
881 (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
882}
883
884fn time_of_day_fits(contents: &[u8]) -> bool {
886 let Some(([hour, minute, second], [])) = fields_of(contents) else {
887 return false;
888 };
889 let midnight = hour == 24 && minute == 0 && second == 0;
894 (hour <= 23 || midnight) && minute <= 59 && second <= 60
895}
896
897fn duration_fits(contents: &[u8]) -> bool {
901 let Ok(text) = core::str::from_utf8(contents) else {
902 return false;
903 };
904 if text.ends_with('W') {
907 return components_fit(text, b"W").is_some_and(|weeks| weeks.any);
908 }
909 let (date, time) = match text.split_once('T') {
910 Some((date, time)) => (date, Some(time)),
911 None => (text, None),
912 };
913 if time.is_some_and(str::is_empty) {
915 return false;
916 }
917 let Some(dated) = components_fit(date, b"YMD") else {
918 return false;
919 };
920 if dated.fractional && time.is_some() {
923 return false;
924 }
925 let Some(timed) = time.map_or(Some(Components::NONE), |time| components_fit(time, b"HMS"))
926 else {
927 return false;
928 };
929 dated.any || timed.any
932}
933
934#[derive(Clone, Copy)]
936struct Components {
937 any: bool,
940 fractional: bool,
942}
943
944impl Components {
945 const NONE: Self = Self {
946 any: false,
947 fractional: false,
948 };
949}
950
951fn components_fit(text: &str, order: &[u8]) -> Option<Components> {
956 let mut rest = text.as_bytes();
957 let mut left = order;
958 let mut seen = Components::NONE;
959 while !rest.is_empty() {
960 let count = rest.iter().take_while(|byte| byte.is_ascii_digit()).count();
964 if count == 0 || (rest[0] == b'0' && count > 1) {
965 return None;
966 }
967 let after = &rest[count..];
968 let (after, fractional) = match after.split_first() {
969 Some((b'.' | b',', tail)) => {
970 let digits = tail.iter().take_while(|byte| byte.is_ascii_digit()).count();
971 if digits == 0 {
972 return None;
973 }
974 (&tail[digits..], true)
975 }
976 _ => (after, false),
977 };
978 let (designator, tail) = after.split_first()?;
979 let at = left.iter().position(|kind| kind == designator)?;
980 left = &left[at + 1..];
981 if seen.fractional {
986 return None;
987 }
988 seen = Components {
989 any: true,
990 fractional,
991 };
992 rest = tail;
993 }
994 Some(seen)
995}
996
997pub(super) fn fields_of<const N: usize>(contents: &[u8]) -> Option<([u32; N], &[u8])> {
1000 let mut values = [0u32; N];
1001 let mut rest = contents;
1002 for value in &mut values {
1003 let (digits, tail) = split(rest, 2)?;
1004 *value = number(digits)?;
1005 rest = tail;
1006 }
1007 Some((values, rest))
1008}
1009
1010#[cfg(test)]
1011mod tests;