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 let work = u64::try_from(element.contents().len())
74 .ok()
75 .and_then(|length| length.checked_mul(4))
76 .and_then(|work| work.checked_add(64))
77 .ok_or_else(|| {
78 Error::resource(
79 structured_asn1::memory::MemoryError::WorkExceeded,
80 field,
81 element.span().start,
82 )
83 })?;
84 parent.charge(work).map_err(|error| {
85 Error::new(
86 ErrorCode::Encoding(error.code()),
87 field,
88 element.span().start,
89 )
90 })?;
91 read(element, field)
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum Form {
97 Utc,
99 Generalized,
101}
102
103#[derive(Debug, Clone, Copy)]
108pub struct Time {
109 instant: Instant,
110 form: Form,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct Calendar {
118 pub year: i64,
120 pub month: u8,
122 pub day: u8,
124 pub hour: u8,
126 pub minute: u8,
128 pub second: u8,
130 pub nanos: u32,
132}
133
134pub const NO_WELL_DEFINED_EXPIRY: i64 = 253_402_300_799;
137
138impl Time {
139 pub fn xsd10_label<W: structured_asn1::memory::Work>(
143 self,
144 execution: &W,
145 ) -> core::result::Result<Xsd10Label, structured_asn1::memory::MemoryError> {
146 let calendar = self.calendar_at_minutes(0, execution)?;
147 Ok(Xsd10Label {
148 year: if calendar.year <= 0 {
149 calendar.year - 1
150 } else {
151 calendar.year
152 },
153 month: calendar.month,
154 day: calendar.day,
155 hour: calendar.hour,
156 minute: calendar.minute,
157 second: calendar.second,
158 nanos: calendar.nanos,
159 })
160 }
161
162 pub fn calendar_at_minutes<W: structured_asn1::memory::Work>(
172 self,
173 minutes: i16,
174 execution: &W,
175 ) -> core::result::Result<Calendar, structured_asn1::memory::MemoryError> {
176 execution.charge_work(64)?;
177 let (year, month, day, hour, minute, second) =
180 civil_from_seconds(self.seconds() + i64::from(minutes) * 60);
181 Ok(Calendar {
182 year,
183 month: u8::try_from(month).expect("Gregorian month"),
184 day: u8::try_from(day).expect("Gregorian day"),
185 hour: u8::try_from(hour).expect("Gregorian hour"),
186 minute: u8::try_from(minute).expect("Gregorian minute"),
187 second: if self.is_leap_second() {
188 60
189 } else {
190 u8::try_from(second).expect("Gregorian second")
191 },
192 nanos: self.instant.nanos(),
193 })
194 }
195
196 #[must_use]
200 pub fn seconds(self) -> i64 {
201 self.instant.seconds()
202 }
203
204 #[must_use]
206 pub fn instant(self) -> Instant {
207 self.instant
208 }
209
210 #[must_use]
212 pub fn is_leap_second(self) -> bool {
213 self.instant.is_leap_second()
214 }
215
216 #[must_use]
218 pub fn form(self) -> Form {
219 self.form
220 }
221
222 #[must_use]
226 pub fn is_no_well_defined_expiry(self) -> bool {
227 self.seconds() == NO_WELL_DEFINED_EXPIRY && !self.is_leap_second()
228 }
229}
230
231impl PartialEq for Time {
232 fn eq(&self, other: &Self) -> bool {
233 self.instant == other.instant
234 }
235}
236
237impl Eq for Time {}
238
239impl PartialOrd for Time {
240 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
241 Some(self.cmp(other))
242 }
243}
244
245impl Ord for Time {
246 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
247 self.instant.cmp(&other.instant)
248 }
249}
250
251impl fmt::Display for Time {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 let (year, month, day, hour, minute, ordinary) = civil_from_seconds(self.seconds());
254 let second = if self.is_leap_second() { 60 } else { ordinary };
255 write!(
256 f,
257 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
258 )
259 }
260}
261
262pub(crate) fn read(element: &Element<'_>, field: &'static str) -> Result<Time> {
267 read_as(element, field, Fraction::Absent)
268}
269
270#[derive(Clone, Copy, PartialEq, Eq)]
272pub(crate) enum Fraction {
273 Absent,
277 Written,
282}
283
284pub(crate) fn read_as(
286 element: &Element<'_>,
287 field: &'static str,
288 fraction: Fraction,
289) -> Result<Time> {
290 let fail = |code| Error::new(code, field, element.span().start);
291
292 let form = if element.tag() == Tag::primitive(universal::UTC_TIME) {
293 Form::Utc
294 } else if element.tag() == Tag::primitive(universal::GENERALIZED_TIME) {
295 Form::Generalized
296 } else {
297 return Err(fail(ErrorCode::Encoding(
298 structured_asn1::ErrorCode::UnexpectedTag,
299 )));
300 };
301
302 let contents = element.contents();
303 let (year, rest) = match form {
304 Form::Utc => {
305 let (digits, rest) =
306 split(contents, 2).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
307 let two = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
308 (if two >= 50 { 1900 + two } else { 2000 + two }, rest)
312 }
313 Form::Generalized => {
314 let (digits, rest) =
325 split(contents, 4).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
326 let four = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
327 (four, rest)
328 }
329 };
330
331 let ([month, day, hour, minute, second], tail) =
332 fields_of(rest).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
333
334 let closed = match tail.split_first() {
338 Some((b'.', digits)) if fraction == Fraction::Written && form == Form::Generalized => {
339 matches!(digits.split_last(), Some((b'Z', fraction)) if !fraction.is_empty()
341 && fraction.iter().all(u8::is_ascii_digit)
342 && fraction.last() != Some(&b'0'))
343 }
344 _ => tail == b"Z",
345 };
346 if !closed {
347 return Err(fail(ErrorCode::MalformedTime));
348 }
349
350 if form == Form::Generalized {
351 let instant = crate::temporal::generalized(element, field)?.start();
359 return Ok(Time { instant, form });
360 }
361
362 if !(1..=12).contains(&month) || day < 1 || day > days_in_month(year, month) {
363 return Err(fail(ErrorCode::TimeOutOfRange));
364 }
365 if hour > 23 || minute > 59 || second > 59 {
368 return Err(fail(ErrorCode::TimeOutOfRange));
369 }
370
371 let days = days_from_civil(i64::from(year), month, day);
375 let seconds =
376 days * 86_400 + i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second);
377
378 let instant = Instant::new(seconds, 0).ok_or_else(|| fail(ErrorCode::TimeOutOfRange))?;
379 Ok(Time { instant, form })
380}
381
382pub(crate) fn ber_fits(contents: &[u8], generalized: bool) -> bool {
387 let year_width = if generalized { 4 } else { 2 };
388 let Some((year, rest)) = split(contents, year_width) else {
389 return false;
390 };
391 let Some(year) = number(year) else {
392 return false;
393 };
394 let (clock, zone) = match rest
395 .iter()
396 .position(|byte| matches!(byte, b'Z' | b'+' | b'-'))
397 {
398 Some(at) => rest.split_at(at),
399 None => (rest, &[][..]),
400 };
401 if !ber_zone(zone, generalized) {
402 return false;
403 }
404 let digits = match clock.iter().position(|byte| matches!(byte, b'.' | b',')) {
405 Some(at) if generalized => {
406 let (digits, fraction) = clock.split_at(at);
407 if fraction.len() < 2 || !fraction[1..].iter().all(u8::is_ascii_digit) {
408 return false;
409 }
410 digits
411 }
412 Some(_) => return false,
413 None => clock,
414 };
415 let Some(([month, day, hour], tail)) = fields_of(digits) else {
416 return false;
417 };
418 if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) || hour > 23 {
419 return false;
420 }
421 if tail.is_empty() {
425 return generalized;
426 }
427 let Some(([minute], tail)) = fields_of(tail) else {
428 return false;
429 };
430 if minute > 59 {
431 return false;
432 }
433 if tail.is_empty() {
434 return true;
435 }
436 let Some(([second], [])) = fields_of(tail) else {
437 return false;
438 };
439 second <= if generalized { 60 } else { 59 }
440}
441
442fn ber_zone(zone: &[u8], generalized: bool) -> bool {
443 if zone.is_empty() {
444 return generalized;
445 }
446 if zone == b"Z" {
447 return true;
448 }
449 let Some((b'+' | b'-', digits)) = zone.split_first() else {
450 return false;
451 };
452 let Some(([hour], tail)) = fields_of(digits) else {
453 return false;
454 };
455 if hour > 23 {
456 return false;
457 }
458 if tail.is_empty() {
459 return generalized;
460 }
461 matches!(fields_of(tail), Some(([minute], [])) if minute <= 59)
462}
463
464pub(super) fn split(contents: &[u8], count: usize) -> Option<(&[u8], &[u8])> {
466 let (digits, rest) = contents.split_at_checked(count)?;
467 digits
468 .iter()
469 .all(u8::is_ascii_digit)
470 .then_some((digits, rest))
471}
472
473pub(super) fn number(digits: &[u8]) -> Option<u32> {
475 digits.iter().try_fold(0u32, |value, digit| {
476 value
477 .checked_mul(10)
478 .and_then(|value| value.checked_add(u32::from(digit - b'0')))
479 })
480}
481
482fn in_cycle(digits: &str, negative: bool) -> u32 {
492 let counted = digits.as_bytes().iter().fold(0u32, |cycle, digit| {
493 (cycle * 10 + u32::from(digit - b'0')) % 400
494 });
495 if negative {
496 (400 - counted) % 400
497 } else {
498 counted
499 }
500}
501
502fn weeks_in_year(year: u32) -> u32 {
511 let opens = |year: u32| (year + year / 4 - year / 100 + year / 400) % 7;
512 let long = opens(year) == 4 || year.checked_sub(1).is_some_and(|before| opens(before) == 3);
516 if long { 53 } else { 52 }
517}
518
519fn is_leap(year: u32) -> bool {
520 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
521}
522
523pub(super) fn days_in_month(year: u32, month: u32) -> u32 {
524 match month {
525 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
526 4 | 6 | 9 | 11 => 30,
527 2 if is_leap(year) => 29,
528 2 => 28,
529 _ => 0,
530 }
531}
532
533pub(super) fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
540 let month = i64::from(month);
541 let year = if month <= 2 { year - 1 } else { year };
542 let era = if year >= 0 { year } else { year - 399 } / 400;
543 let year_of_era = year - era * 400;
544 let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(day) - 1;
545 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
546 era * 146_097 + day_of_era - 719_468
549}
550
551pub(super) fn civil_from_seconds(seconds: i64) -> (i64, i64, i64, i64, i64, i64) {
553 let days = seconds.div_euclid(86_400);
554 let within = seconds.rem_euclid(86_400);
555
556 let shifted = days + 719_468;
557 let era = if shifted >= 0 {
558 shifted
559 } else {
560 shifted - 146_096
561 } / 146_097;
562 let day_of_era = shifted - era * 146_097;
563 let year_of_era =
564 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
565 let year = year_of_era + era * 400;
566 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
567 let shifted_month = (5 * day_of_year + 2) / 153;
568 let day = day_of_year - (153 * shifted_month + 2) / 5 + 1;
569 let month = if shifted_month < 10 {
570 shifted_month + 3
571 } else {
572 shifted_month - 9
573 };
574
575 (
576 if month <= 2 { year + 1 } else { year },
577 month,
578 day,
579 within / 3_600,
580 within / 60 % 60,
581 within % 60,
582 )
583}
584
585#[derive(Clone, Copy, PartialEq, Eq)]
588pub(crate) enum Defined {
589 Date,
591 TimeOfDay,
593 DateTime,
595 Duration,
597 Any,
600}
601
602pub(crate) fn defined_fits(kind: Defined, contents: &[u8]) -> bool {
611 match kind {
612 Defined::Date => date_fits(contents),
613 Defined::TimeOfDay => time_of_day_fits(contents),
614 Defined::DateTime => contents
615 .split_at_checked(8)
616 .is_some_and(|(date, time)| date_fits(date) && time_of_day_fits(time)),
617 Defined::Duration => duration_fits(contents),
618 Defined::Any => core::str::from_utf8(contents).is_ok_and(any_fits),
619 }
620}
621
622fn any_fits(text: &str) -> bool {
630 let interval = match text.strip_prefix('R') {
633 Some(after) => {
634 let count = after.bytes().take_while(u8::is_ascii_digit).count();
635 let (repeats, tail) = after.split_at(count);
636 if repeats.starts_with('0') && repeats.len() > 1 {
637 return false;
638 }
639 let Some(rest) = tail.strip_prefix('/') else {
640 return false;
641 };
642 rest
643 }
644 None => text,
645 };
646 match interval.split_once('/') {
647 Some((start, end)) => match (part_fits(start), part_fits(end)) {
651 (Some(starts), Some(ends)) => !(starts == Part::Duration && ends == Part::Duration),
652 _ => false,
653 },
654 None => part_fits(interval).is_some(),
655 }
656}
657
658#[derive(Clone, Copy, PartialEq, Eq)]
660enum Part {
661 Point,
662 Duration,
663}
664
665fn part_fits(text: &str) -> Option<Part> {
667 if let Some(duration) = text.strip_prefix('P') {
668 return duration_fits(duration.as_bytes()).then_some(Part::Duration);
669 }
670 point_fits(text).then_some(Part::Point)
671}
672
673fn point_fits(text: &str) -> bool {
676 match text.split_once('T') {
677 Some((date, time)) => date_part_fits(date) && zoned_time_fits(time),
678 None => date_part_fits(text) || zoned_time_fits(text),
682 }
683}
684
685fn zoned_time_fits(text: &str) -> bool {
690 let stated = match text.strip_suffix('Z') {
691 Some(rest) => rest,
692 None => match text.rfind(['+', '-']) {
693 Some(at) if difference_fits(&text[at..]) => &text[..at],
694 _ => text,
695 },
696 };
697 time_part_fits(stated)
698}
699
700fn difference_fits(text: &str) -> bool {
703 let Some(rest) = text.strip_prefix(['+', '-']) else {
704 return false;
705 };
706 match rest.split_once(':') {
707 Some((hours, minutes)) => {
708 two_digits(hours).is_some_and(|hours| hours <= 23)
709 && two_digits(minutes).is_some_and(|minutes| minutes <= 59)
710 }
711 None => two_digits(rest).is_some_and(|hours| hours <= 23),
712 }
713}
714
715fn date_part_fits(text: &str) -> bool {
719 let signed = text.starts_with(['+', '-']);
720 let rest = if signed { &text[1..] } else { text };
721 let mut parts = rest.split('-');
722 let Some(year) = parts.next() else {
723 return false;
724 };
725 if year.len() < 4 || !year.bytes().all(|byte| byte.is_ascii_digit()) {
727 return false;
728 }
729 let Some(next) = parts.next() else {
730 return parts.next().is_none();
731 };
732 let counted = in_cycle(year, signed);
737 if let Some(week) = next.strip_prefix('W') {
738 let last = weeks_in_year(counted);
740 let weeks = two_digits(week).is_some_and(|week| (1..=last).contains(&week));
741 return weeks
742 && match parts.next() {
743 Some(day) => {
744 day.len() == 1
745 && day.bytes().all(|byte| (b'1'..=b'7').contains(&byte))
746 && parts.next().is_none()
747 }
748 None => true,
749 };
750 }
751 if next.len() == 3 {
752 let last = if is_leap(counted) { 366 } else { 365 };
755 return next.bytes().all(|byte| byte.is_ascii_digit())
756 && number(next.as_bytes()).is_some_and(|day| (1..=last).contains(&day))
757 && parts.next().is_none();
758 }
759 let Some(month) = two_digits(next).filter(|month| (1..=12).contains(month)) else {
760 return false;
761 };
762 match parts.next() {
763 Some(day) => {
764 let Some(day) = two_digits(day) else {
765 return false;
766 };
767 let last = days_in_month(counted, month);
770 day >= 1 && day <= last && parts.next().is_none()
771 }
772 None => true,
773 }
774}
775
776fn time_part_fits(text: &str) -> bool {
780 let mut parts = text.split(':');
781 let bounds = [23u32, 59, 60];
782 let mut carried = None;
783 let mut counted = 0usize;
784 let mut ending = false;
785 for (piece, most) in parts.by_ref().zip(bounds) {
786 let (digits, fraction) = match piece.split_once(['.', ',']) {
787 Some((digits, fraction)) => (digits, Some(fraction)),
788 None => (piece, None),
789 };
790 let Some(value) = two_digits(digits) else {
791 return false;
792 };
793 if let Some(fraction) = fraction {
796 if fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
797 return false;
798 }
799 carried = Some(counted);
800 }
801 counted += 1;
802 if carried.is_some_and(|at| at + 1 != counted) {
803 return false;
804 }
805 if ending && fraction.is_some() {
812 return false;
813 }
814 if counted == 1 && value == 24 {
815 ending = true;
816 if fraction.is_some() {
817 return false;
818 }
819 continue;
820 }
821 if value > most || (ending && value != 0) {
822 return false;
823 }
824 }
825 counted > 0 && parts.next().is_none()
826}
827
828fn two_digits(text: &str) -> Option<u32> {
831 (text.len() == 2 && text.bytes().all(|byte| byte.is_ascii_digit()))
832 .then(|| number(text.as_bytes()))?
833}
834
835fn date_fits(contents: &[u8]) -> bool {
837 let Some((year, rest)) = split(contents, 4) else {
838 return false;
839 };
840 let Some(([month, day], [])) = fields_of(rest) else {
841 return false;
842 };
843 let Some(year) = number(year) else {
844 return false;
845 };
846 (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
847}
848
849fn time_of_day_fits(contents: &[u8]) -> bool {
851 let Some(([hour, minute, second], [])) = fields_of(contents) else {
852 return false;
853 };
854 let midnight = hour == 24 && minute == 0 && second == 0;
859 (hour <= 23 || midnight) && minute <= 59 && second <= 60
860}
861
862fn duration_fits(contents: &[u8]) -> bool {
866 let Ok(text) = core::str::from_utf8(contents) else {
867 return false;
868 };
869 if text.ends_with('W') {
872 return components_fit(text, b"W").is_some_and(|weeks| weeks.any);
873 }
874 let (date, time) = match text.split_once('T') {
875 Some((date, time)) => (date, Some(time)),
876 None => (text, None),
877 };
878 if time.is_some_and(str::is_empty) {
880 return false;
881 }
882 let Some(dated) = components_fit(date, b"YMD") else {
883 return false;
884 };
885 if dated.fractional && time.is_some() {
888 return false;
889 }
890 let Some(timed) = time.map_or(Some(Components::NONE), |time| components_fit(time, b"HMS"))
891 else {
892 return false;
893 };
894 dated.any || timed.any
897}
898
899#[derive(Clone, Copy)]
901struct Components {
902 any: bool,
905 fractional: bool,
907}
908
909impl Components {
910 const NONE: Self = Self {
911 any: false,
912 fractional: false,
913 };
914}
915
916fn components_fit(text: &str, order: &[u8]) -> Option<Components> {
921 let mut rest = text.as_bytes();
922 let mut left = order;
923 let mut seen = Components::NONE;
924 while !rest.is_empty() {
925 let count = rest.iter().take_while(|byte| byte.is_ascii_digit()).count();
929 if count == 0 || (rest[0] == b'0' && count > 1) {
930 return None;
931 }
932 let after = &rest[count..];
933 let (after, fractional) = match after.split_first() {
934 Some((b'.' | b',', tail)) => {
935 let digits = tail.iter().take_while(|byte| byte.is_ascii_digit()).count();
936 if digits == 0 {
937 return None;
938 }
939 (&tail[digits..], true)
940 }
941 _ => (after, false),
942 };
943 let (designator, tail) = after.split_first()?;
944 let at = left.iter().position(|kind| kind == designator)?;
945 left = &left[at + 1..];
946 if seen.fractional {
951 return None;
952 }
953 seen = Components {
954 any: true,
955 fractional,
956 };
957 rest = tail;
958 }
959 Some(seen)
960}
961
962pub(super) fn fields_of<const N: usize>(contents: &[u8]) -> Option<([u32; N], &[u8])> {
965 let mut values = [0u32; N];
966 let mut rest = contents;
967 for value in &mut values {
968 let (digits, tail) = split(rest, 2)?;
969 *value = number(digits)?;
970 rest = tail;
971 }
972 Some((values, rest))
973}
974
975#[cfg(test)]
976mod tests;