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 read(parent: &Reader<'_>, element: &Element<'_>, field: &'static str) -> Result<Self> {
169 read_in(parent, element, field)
170 }
171
172 pub fn xsd10_label<W: structured_asn1::memory::Work>(
176 self,
177 execution: &W,
178 ) -> core::result::Result<Xsd10Label, structured_asn1::memory::MemoryError> {
179 let calendar = self.calendar_at_minutes(0, execution)?;
180 Ok(Xsd10Label {
181 year: if calendar.year <= 0 {
182 calendar.year - 1
183 } else {
184 calendar.year
185 },
186 month: calendar.month,
187 day: calendar.day,
188 hour: calendar.hour,
189 minute: calendar.minute,
190 second: calendar.second,
191 nanos: calendar.nanos,
192 })
193 }
194
195 pub fn calendar_at_minutes<W: structured_asn1::memory::Work>(
205 self,
206 minutes: i16,
207 execution: &W,
208 ) -> core::result::Result<Calendar, structured_asn1::memory::MemoryError> {
209 execution.charge_work(64)?;
210 let (year, month, day, hour, minute, second) =
213 civil_from_seconds(self.seconds() + i64::from(minutes) * 60);
214 Ok(Calendar {
215 year,
216 month: u8::try_from(month).expect("Gregorian month"),
217 day: u8::try_from(day).expect("Gregorian day"),
218 hour: u8::try_from(hour).expect("Gregorian hour"),
219 minute: u8::try_from(minute).expect("Gregorian minute"),
220 second: if self.is_leap_second() {
221 60
222 } else {
223 u8::try_from(second).expect("Gregorian second")
224 },
225 nanos: self.instant.nanos(),
226 })
227 }
228
229 #[must_use]
233 pub fn seconds(self) -> i64 {
234 self.instant.seconds()
235 }
236
237 #[must_use]
239 pub fn instant(self) -> Instant {
240 self.instant
241 }
242
243 #[must_use]
245 pub fn is_leap_second(self) -> bool {
246 self.instant.is_leap_second()
247 }
248
249 #[must_use]
251 pub fn form(self) -> Form {
252 self.form
253 }
254
255 #[must_use]
259 pub fn is_no_well_defined_expiry(self) -> bool {
260 self.seconds() == NO_WELL_DEFINED_EXPIRY && !self.is_leap_second()
261 }
262}
263
264impl PartialEq for Time {
265 fn eq(&self, other: &Self) -> bool {
266 self.instant == other.instant
267 }
268}
269
270impl Eq for Time {}
271
272impl PartialOrd for Time {
273 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
274 Some(self.cmp(other))
275 }
276}
277
278impl Ord for Time {
279 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
280 self.instant.cmp(&other.instant)
281 }
282}
283
284impl fmt::Display for Time {
285 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286 let (year, month, day, hour, minute, ordinary) = civil_from_seconds(self.seconds());
287 let second = if self.is_leap_second() { 60 } else { ordinary };
288 write!(
289 f,
290 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
291 )
292 }
293}
294
295pub(crate) fn read(element: &Element<'_>, field: &'static str) -> Result<Time> {
300 read_as(element, field, Fraction::Absent)
301}
302
303#[derive(Clone, Copy, PartialEq, Eq)]
305pub(crate) enum Fraction {
306 Absent,
310 Written,
315}
316
317pub(crate) fn read_as(
319 element: &Element<'_>,
320 field: &'static str,
321 fraction: Fraction,
322) -> Result<Time> {
323 let (reading, form) = read_label_as(element, field, fraction)?;
324 Ok(Time {
325 instant: reading.start(),
326 form,
327 })
328}
329
330fn read_label_as(
331 element: &Element<'_>,
332 field: &'static str,
333 fraction: Fraction,
334) -> Result<(crate::temporal::Reading, Form)> {
335 let fail = |code| Error::new(code, field, element.span().start);
336
337 let form = if element.tag() == Tag::primitive(universal::UTC_TIME) {
338 Form::Utc
339 } else if element.tag() == Tag::primitive(universal::GENERALIZED_TIME) {
340 Form::Generalized
341 } else {
342 return Err(fail(ErrorCode::Encoding(
343 structured_asn1::ErrorCode::UnexpectedTag,
344 )));
345 };
346
347 let contents = element.contents();
348 let (year, rest) = match form {
349 Form::Utc => {
350 let (digits, rest) =
351 split(contents, 2).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
352 let two = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
353 (if two >= 50 { 1900 + two } else { 2000 + two }, rest)
357 }
358 Form::Generalized => {
359 let (digits, rest) =
370 split(contents, 4).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
371 let four = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
372 (four, rest)
373 }
374 };
375
376 let ([month, day, hour, minute, second], tail) =
377 fields_of(rest).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
378
379 let closed = match tail.split_first() {
383 Some((b'.', digits)) if fraction == Fraction::Written && form == Form::Generalized => {
384 matches!(digits.split_last(), Some((b'Z', fraction)) if !fraction.is_empty()
386 && fraction.iter().all(u8::is_ascii_digit)
387 && fraction.last() != Some(&b'0'))
388 }
389 _ => tail == b"Z",
390 };
391 if !closed {
392 return Err(fail(ErrorCode::MalformedTime));
393 }
394
395 if form == Form::Generalized {
396 let reading = crate::temporal::generalized(element, field)?;
404 return Ok((reading, form));
405 }
406
407 if !(1..=12).contains(&month) || day < 1 || day > days_in_month(year, month) {
408 return Err(fail(ErrorCode::TimeOutOfRange));
409 }
410 if hour > 23 || minute > 59 || second > 59 {
413 return Err(fail(ErrorCode::TimeOutOfRange));
414 }
415
416 let days = days_from_civil(i64::from(year), month, day);
420 let seconds =
421 days * 86_400 + i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second);
422
423 let instant = Instant::new(seconds, 0).ok_or_else(|| fail(ErrorCode::TimeOutOfRange))?;
424 Ok((instant.into(), form))
425}
426
427pub(crate) fn ber_fits(contents: &[u8], generalized: bool) -> bool {
432 let year_width = if generalized { 4 } else { 2 };
433 let Some((year, rest)) = split(contents, year_width) else {
434 return false;
435 };
436 let Some(year) = number(year) else {
437 return false;
438 };
439 let (clock, zone) = match rest
440 .iter()
441 .position(|byte| matches!(byte, b'Z' | b'+' | b'-'))
442 {
443 Some(at) => rest.split_at(at),
444 None => (rest, &[][..]),
445 };
446 if !ber_zone(zone, generalized) {
447 return false;
448 }
449 let digits = match clock.iter().position(|byte| matches!(byte, b'.' | b',')) {
450 Some(at) if generalized => {
451 let (digits, fraction) = clock.split_at(at);
452 if fraction.len() < 2 || !fraction[1..].iter().all(u8::is_ascii_digit) {
453 return false;
454 }
455 digits
456 }
457 Some(_) => return false,
458 None => clock,
459 };
460 let Some(([month, day, hour], tail)) = fields_of(digits) else {
461 return false;
462 };
463 if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) || hour > 23 {
464 return false;
465 }
466 if tail.is_empty() {
470 return generalized;
471 }
472 let Some(([minute], tail)) = fields_of(tail) else {
473 return false;
474 };
475 if minute > 59 {
476 return false;
477 }
478 if tail.is_empty() {
479 return true;
480 }
481 let Some(([second], [])) = fields_of(tail) else {
482 return false;
483 };
484 second <= if generalized { 60 } else { 59 }
485}
486
487fn ber_zone(zone: &[u8], generalized: bool) -> bool {
488 if zone.is_empty() {
489 return generalized;
490 }
491 if zone == b"Z" {
492 return true;
493 }
494 let Some((b'+' | b'-', digits)) = zone.split_first() else {
495 return false;
496 };
497 let Some(([hour], tail)) = fields_of(digits) else {
498 return false;
499 };
500 if hour > 23 {
501 return false;
502 }
503 if tail.is_empty() {
504 return generalized;
505 }
506 matches!(fields_of(tail), Some(([minute], [])) if minute <= 59)
507}
508
509pub(super) fn split(contents: &[u8], count: usize) -> Option<(&[u8], &[u8])> {
511 let (digits, rest) = contents.split_at_checked(count)?;
512 digits
513 .iter()
514 .all(u8::is_ascii_digit)
515 .then_some((digits, rest))
516}
517
518pub(super) fn number(digits: &[u8]) -> Option<u32> {
520 digits.iter().try_fold(0u32, |value, digit| {
521 value
522 .checked_mul(10)
523 .and_then(|value| value.checked_add(u32::from(digit - b'0')))
524 })
525}
526
527fn in_cycle(digits: &str, negative: bool) -> u32 {
537 let counted = digits.as_bytes().iter().fold(0u32, |cycle, digit| {
538 (cycle * 10 + u32::from(digit - b'0')) % 400
539 });
540 if negative {
541 (400 - counted) % 400
542 } else {
543 counted
544 }
545}
546
547fn weeks_in_year(year: u32) -> u32 {
556 let opens = |year: u32| (year + year / 4 - year / 100 + year / 400) % 7;
557 let long = opens(year) == 4 || year.checked_sub(1).is_some_and(|before| opens(before) == 3);
561 if long { 53 } else { 52 }
562}
563
564fn is_leap(year: u32) -> bool {
565 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
566}
567
568pub(super) fn days_in_month(year: u32, month: u32) -> u32 {
569 match month {
570 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
571 4 | 6 | 9 | 11 => 30,
572 2 if is_leap(year) => 29,
573 2 => 28,
574 _ => 0,
575 }
576}
577
578pub(super) fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
585 let month = i64::from(month);
586 let year = if month <= 2 { year - 1 } else { year };
587 let era = if year >= 0 { year } else { year - 399 } / 400;
588 let year_of_era = year - era * 400;
589 let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(day) - 1;
590 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
591 era * 146_097 + day_of_era - 719_468
594}
595
596pub(super) fn civil_from_seconds(seconds: i64) -> (i64, i64, i64, i64, i64, i64) {
598 let days = seconds.div_euclid(86_400);
599 let within = seconds.rem_euclid(86_400);
600
601 let shifted = days + 719_468;
602 let era = if shifted >= 0 {
603 shifted
604 } else {
605 shifted - 146_096
606 } / 146_097;
607 let day_of_era = shifted - era * 146_097;
608 let year_of_era =
609 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
610 let year = year_of_era + era * 400;
611 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
612 let shifted_month = (5 * day_of_year + 2) / 153;
613 let day = day_of_year - (153 * shifted_month + 2) / 5 + 1;
614 let month = if shifted_month < 10 {
615 shifted_month + 3
616 } else {
617 shifted_month - 9
618 };
619
620 (
621 if month <= 2 { year + 1 } else { year },
622 month,
623 day,
624 within / 3_600,
625 within / 60 % 60,
626 within % 60,
627 )
628}
629
630#[derive(Clone, Copy, PartialEq, Eq)]
633pub(crate) enum Defined {
634 Date,
636 TimeOfDay,
638 DateTime,
640 Duration,
642 Any,
645}
646
647pub(crate) fn defined_fits(kind: Defined, contents: &[u8]) -> bool {
656 match kind {
657 Defined::Date => date_fits(contents),
658 Defined::TimeOfDay => time_of_day_fits(contents),
659 Defined::DateTime => contents
660 .split_at_checked(8)
661 .is_some_and(|(date, time)| date_fits(date) && time_of_day_fits(time)),
662 Defined::Duration => duration_fits(contents),
663 Defined::Any => core::str::from_utf8(contents).is_ok_and(any_fits),
664 }
665}
666
667fn any_fits(text: &str) -> bool {
675 let interval = match text.strip_prefix('R') {
678 Some(after) => {
679 let count = after.bytes().take_while(u8::is_ascii_digit).count();
680 let (repeats, tail) = after.split_at(count);
681 if repeats.starts_with('0') && repeats.len() > 1 {
682 return false;
683 }
684 let Some(rest) = tail.strip_prefix('/') else {
685 return false;
686 };
687 rest
688 }
689 None => text,
690 };
691 match interval.split_once('/') {
692 Some((start, end)) => match (part_fits(start), part_fits(end)) {
696 (Some(starts), Some(ends)) => !(starts == Part::Duration && ends == Part::Duration),
697 _ => false,
698 },
699 None => part_fits(interval).is_some(),
700 }
701}
702
703#[derive(Clone, Copy, PartialEq, Eq)]
705enum Part {
706 Point,
707 Duration,
708}
709
710fn part_fits(text: &str) -> Option<Part> {
712 if let Some(duration) = text.strip_prefix('P') {
713 return duration_fits(duration.as_bytes()).then_some(Part::Duration);
714 }
715 point_fits(text).then_some(Part::Point)
716}
717
718fn point_fits(text: &str) -> bool {
721 match text.split_once('T') {
722 Some((date, time)) => date_part_fits(date) && zoned_time_fits(time),
723 None => date_part_fits(text) || zoned_time_fits(text),
727 }
728}
729
730fn zoned_time_fits(text: &str) -> bool {
735 let stated = match text.strip_suffix('Z') {
736 Some(rest) => rest,
737 None => match text.rfind(['+', '-']) {
738 Some(at) if difference_fits(&text[at..]) => &text[..at],
739 _ => text,
740 },
741 };
742 time_part_fits(stated)
743}
744
745fn difference_fits(text: &str) -> bool {
748 let Some(rest) = text.strip_prefix(['+', '-']) else {
749 return false;
750 };
751 match rest.split_once(':') {
752 Some((hours, minutes)) => {
753 two_digits(hours).is_some_and(|hours| hours <= 23)
754 && two_digits(minutes).is_some_and(|minutes| minutes <= 59)
755 }
756 None => two_digits(rest).is_some_and(|hours| hours <= 23),
757 }
758}
759
760fn date_part_fits(text: &str) -> bool {
764 let signed = text.starts_with(['+', '-']);
765 let rest = if signed { &text[1..] } else { text };
766 let mut parts = rest.split('-');
767 let Some(year) = parts.next() else {
768 return false;
769 };
770 if year.len() < 4 || !year.bytes().all(|byte| byte.is_ascii_digit()) {
772 return false;
773 }
774 let Some(next) = parts.next() else {
775 return parts.next().is_none();
776 };
777 let counted = in_cycle(year, signed);
782 if let Some(week) = next.strip_prefix('W') {
783 let last = weeks_in_year(counted);
785 let weeks = two_digits(week).is_some_and(|week| (1..=last).contains(&week));
786 return weeks
787 && match parts.next() {
788 Some(day) => {
789 day.len() == 1
790 && day.bytes().all(|byte| (b'1'..=b'7').contains(&byte))
791 && parts.next().is_none()
792 }
793 None => true,
794 };
795 }
796 if next.len() == 3 {
797 let last = if is_leap(counted) { 366 } else { 365 };
800 return next.bytes().all(|byte| byte.is_ascii_digit())
801 && number(next.as_bytes()).is_some_and(|day| (1..=last).contains(&day))
802 && parts.next().is_none();
803 }
804 let Some(month) = two_digits(next).filter(|month| (1..=12).contains(month)) else {
805 return false;
806 };
807 match parts.next() {
808 Some(day) => {
809 let Some(day) = two_digits(day) else {
810 return false;
811 };
812 let last = days_in_month(counted, month);
815 day >= 1 && day <= last && parts.next().is_none()
816 }
817 None => true,
818 }
819}
820
821fn time_part_fits(text: &str) -> bool {
825 let mut parts = text.split(':');
826 let bounds = [23u32, 59, 60];
827 let mut carried = None;
828 let mut counted = 0usize;
829 let mut ending = false;
830 for (piece, most) in parts.by_ref().zip(bounds) {
831 let (digits, fraction) = match piece.split_once(['.', ',']) {
832 Some((digits, fraction)) => (digits, Some(fraction)),
833 None => (piece, None),
834 };
835 let Some(value) = two_digits(digits) else {
836 return false;
837 };
838 if let Some(fraction) = fraction {
841 if fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
842 return false;
843 }
844 carried = Some(counted);
845 }
846 counted += 1;
847 if carried.is_some_and(|at| at + 1 != counted) {
848 return false;
849 }
850 if ending && fraction.is_some() {
857 return false;
858 }
859 if counted == 1 && value == 24 {
860 ending = true;
861 if fraction.is_some() {
862 return false;
863 }
864 continue;
865 }
866 if value > most || (ending && value != 0) {
867 return false;
868 }
869 }
870 counted > 0 && parts.next().is_none()
871}
872
873fn two_digits(text: &str) -> Option<u32> {
876 (text.len() == 2 && text.bytes().all(|byte| byte.is_ascii_digit()))
877 .then(|| number(text.as_bytes()))?
878}
879
880fn date_fits(contents: &[u8]) -> bool {
882 let Some((year, rest)) = split(contents, 4) else {
883 return false;
884 };
885 let Some(([month, day], [])) = fields_of(rest) else {
886 return false;
887 };
888 let Some(year) = number(year) else {
889 return false;
890 };
891 (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
892}
893
894fn time_of_day_fits(contents: &[u8]) -> bool {
896 let Some(([hour, minute, second], [])) = fields_of(contents) else {
897 return false;
898 };
899 let midnight = hour == 24 && minute == 0 && second == 0;
904 (hour <= 23 || midnight) && minute <= 59 && second <= 60
905}
906
907fn duration_fits(contents: &[u8]) -> bool {
911 let Ok(text) = core::str::from_utf8(contents) else {
912 return false;
913 };
914 if text.ends_with('W') {
917 return components_fit(text, b"W").is_some_and(|weeks| weeks.any);
918 }
919 let (date, time) = match text.split_once('T') {
920 Some((date, time)) => (date, Some(time)),
921 None => (text, None),
922 };
923 if time.is_some_and(str::is_empty) {
925 return false;
926 }
927 let Some(dated) = components_fit(date, b"YMD") else {
928 return false;
929 };
930 if dated.fractional && time.is_some() {
933 return false;
934 }
935 let Some(timed) = time.map_or(Some(Components::NONE), |time| components_fit(time, b"HMS"))
936 else {
937 return false;
938 };
939 dated.any || timed.any
942}
943
944#[derive(Clone, Copy)]
946struct Components {
947 any: bool,
950 fractional: bool,
952}
953
954impl Components {
955 const NONE: Self = Self {
956 any: false,
957 fractional: false,
958 };
959}
960
961fn components_fit(text: &str, order: &[u8]) -> Option<Components> {
966 let mut rest = text.as_bytes();
967 let mut left = order;
968 let mut seen = Components::NONE;
969 while !rest.is_empty() {
970 let count = rest.iter().take_while(|byte| byte.is_ascii_digit()).count();
974 if count == 0 || (rest[0] == b'0' && count > 1) {
975 return None;
976 }
977 let after = &rest[count..];
978 let (after, fractional) = match after.split_first() {
979 Some((b'.' | b',', tail)) => {
980 let digits = tail.iter().take_while(|byte| byte.is_ascii_digit()).count();
981 if digits == 0 {
982 return None;
983 }
984 (&tail[digits..], true)
985 }
986 _ => (after, false),
987 };
988 let (designator, tail) = after.split_first()?;
989 let at = left.iter().position(|kind| kind == designator)?;
990 left = &left[at + 1..];
991 if seen.fractional {
996 return None;
997 }
998 seen = Components {
999 any: true,
1000 fractional,
1001 };
1002 rest = tail;
1003 }
1004 Some(seen)
1005}
1006
1007pub(super) fn fields_of<const N: usize>(contents: &[u8]) -> Option<([u32; N], &[u8])> {
1010 let mut values = [0u32; N];
1011 let mut rest = contents;
1012 for value in &mut values {
1013 let (digits, tail) = split(rest, 2)?;
1014 *value = number(digits)?;
1015 rest = tail;
1016 }
1017 Some((values, rest))
1018}
1019
1020#[cfg(test)]
1021mod tests;