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
157impl Calendar {
158 pub fn instant_at_minutes<W: structured_asn1::memory::Work>(
165 self,
166 minutes: i16,
167 execution: &W,
168 ) -> core::result::Result<Option<Instant>, structured_asn1::memory::MemoryError> {
169 execution.charge_work(128)?;
170 let Ok(year) = u32::try_from(self.year) else {
171 return Ok(None);
172 };
173 if year > 9999
174 || self.month == 0
175 || self.month > 12
176 || self.day == 0
177 || self.hour > 23
178 || self.minute > 59
179 || self.second > 60
180 || self.nanos >= 1_000_000_000
181 {
182 return Ok(None);
183 }
184 if u32::from(self.day) > days_in_month(year, u32::from(self.month)) {
185 return Ok(None);
186 }
187 let seconds = days_from_civil(self.year, u32::from(self.month), u32::from(self.day))
188 * 86_400
189 + i64::from(self.hour) * 3600
190 + i64::from(self.minute) * 60
191 + i64::from(self.second)
192 - i64::from(minutes) * 60;
193 Ok(if self.second == 60 {
194 Instant::leap_after(seconds - 1, self.nanos)
195 } else {
196 Instant::new(seconds, self.nanos)
197 })
198 }
199}
200
201pub const NO_WELL_DEFINED_EXPIRY: i64 = 253_402_300_799;
204
205impl Time {
206 pub fn read(parent: &Reader<'_>, element: &Element<'_>, field: &'static str) -> Result<Self> {
213 read_in(parent, element, field)
214 }
215
216 pub fn xsd10_label<W: structured_asn1::memory::Work>(
220 self,
221 execution: &W,
222 ) -> core::result::Result<Xsd10Label, structured_asn1::memory::MemoryError> {
223 let calendar = self.calendar_at_minutes(0, execution)?;
224 Ok(Xsd10Label {
225 year: if calendar.year <= 0 {
226 calendar.year - 1
227 } else {
228 calendar.year
229 },
230 month: calendar.month,
231 day: calendar.day,
232 hour: calendar.hour,
233 minute: calendar.minute,
234 second: calendar.second,
235 nanos: calendar.nanos,
236 })
237 }
238
239 pub fn calendar_at_minutes<W: structured_asn1::memory::Work>(
249 self,
250 minutes: i16,
251 execution: &W,
252 ) -> core::result::Result<Calendar, structured_asn1::memory::MemoryError> {
253 execution.charge_work(64)?;
254 let (year, month, day, hour, minute, second) =
257 civil_from_seconds(self.seconds() + i64::from(minutes) * 60);
258 Ok(Calendar {
259 year,
260 month: u8::try_from(month).expect("Gregorian month"),
261 day: u8::try_from(day).expect("Gregorian day"),
262 hour: u8::try_from(hour).expect("Gregorian hour"),
263 minute: u8::try_from(minute).expect("Gregorian minute"),
264 second: if self.is_leap_second() {
265 60
266 } else {
267 u8::try_from(second).expect("Gregorian second")
268 },
269 nanos: self.instant.nanos(),
270 })
271 }
272
273 #[must_use]
277 pub fn seconds(self) -> i64 {
278 self.instant.seconds()
279 }
280
281 #[must_use]
283 pub fn instant(self) -> Instant {
284 self.instant
285 }
286
287 #[must_use]
289 pub fn is_leap_second(self) -> bool {
290 self.instant.is_leap_second()
291 }
292
293 #[must_use]
295 pub fn form(self) -> Form {
296 self.form
297 }
298
299 #[must_use]
303 pub fn is_no_well_defined_expiry(self) -> bool {
304 self.seconds() == NO_WELL_DEFINED_EXPIRY && !self.is_leap_second()
305 }
306}
307
308impl PartialEq for Time {
309 fn eq(&self, other: &Self) -> bool {
310 self.instant == other.instant
311 }
312}
313
314impl Eq for Time {}
315
316impl PartialOrd for Time {
317 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
318 Some(self.cmp(other))
319 }
320}
321
322impl Ord for Time {
323 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
324 self.instant.cmp(&other.instant)
325 }
326}
327
328impl fmt::Display for Time {
329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 let (year, month, day, hour, minute, ordinary) = civil_from_seconds(self.seconds());
331 let second = if self.is_leap_second() { 60 } else { ordinary };
332 write!(
333 f,
334 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
335 )
336 }
337}
338
339pub(crate) fn read(element: &Element<'_>, field: &'static str) -> Result<Time> {
344 read_as(element, field, Fraction::Absent)
345}
346
347#[derive(Clone, Copy, PartialEq, Eq)]
349pub(crate) enum Fraction {
350 Absent,
354 Written,
359}
360
361pub(crate) fn read_as(
363 element: &Element<'_>,
364 field: &'static str,
365 fraction: Fraction,
366) -> Result<Time> {
367 let (reading, form) = read_label_as(element, field, fraction)?;
368 Ok(Time {
369 instant: reading.start(),
370 form,
371 })
372}
373
374fn read_label_as(
375 element: &Element<'_>,
376 field: &'static str,
377 fraction: Fraction,
378) -> Result<(crate::temporal::Reading, Form)> {
379 let fail = |code| Error::new(code, field, element.span().start);
380
381 let form = if element.tag() == Tag::primitive(universal::UTC_TIME) {
382 Form::Utc
383 } else if element.tag() == Tag::primitive(universal::GENERALIZED_TIME) {
384 Form::Generalized
385 } else {
386 return Err(fail(ErrorCode::Encoding(
387 structured_asn1::ErrorCode::UnexpectedTag,
388 )));
389 };
390
391 let contents = element.contents();
392 let (year, rest) = match form {
393 Form::Utc => {
394 let (digits, rest) =
395 split(contents, 2).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
396 let two = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
397 (if two >= 50 { 1900 + two } else { 2000 + two }, rest)
401 }
402 Form::Generalized => {
403 let (digits, rest) =
414 split(contents, 4).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
415 let four = number(digits).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
416 (four, rest)
417 }
418 };
419
420 let ([month, day, hour, minute, second], tail) =
421 fields_of(rest).ok_or_else(|| fail(ErrorCode::MalformedTime))?;
422
423 let closed = match tail.split_first() {
427 Some((b'.', digits)) if fraction == Fraction::Written && form == Form::Generalized => {
428 matches!(digits.split_last(), Some((b'Z', fraction)) if !fraction.is_empty()
430 && fraction.iter().all(u8::is_ascii_digit)
431 && fraction.last() != Some(&b'0'))
432 }
433 _ => tail == b"Z",
434 };
435 if !closed {
436 return Err(fail(ErrorCode::MalformedTime));
437 }
438
439 if form == Form::Generalized {
440 let reading = crate::temporal::generalized(element, field)?;
448 return Ok((reading, form));
449 }
450
451 if !(1..=12).contains(&month) || day < 1 || day > days_in_month(year, month) {
452 return Err(fail(ErrorCode::TimeOutOfRange));
453 }
454 if hour > 23 || minute > 59 || second > 59 {
457 return Err(fail(ErrorCode::TimeOutOfRange));
458 }
459
460 let days = days_from_civil(i64::from(year), month, day);
464 let seconds =
465 days * 86_400 + i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second);
466
467 let instant = Instant::new(seconds, 0).ok_or_else(|| fail(ErrorCode::TimeOutOfRange))?;
468 Ok((instant.into(), form))
469}
470
471pub(crate) fn ber_fits(contents: &[u8], generalized: bool) -> bool {
476 let year_width = if generalized { 4 } else { 2 };
477 let Some((year, rest)) = split(contents, year_width) else {
478 return false;
479 };
480 let Some(year) = number(year) else {
481 return false;
482 };
483 let (clock, zone) = match rest
484 .iter()
485 .position(|byte| matches!(byte, b'Z' | b'+' | b'-'))
486 {
487 Some(at) => rest.split_at(at),
488 None => (rest, &[][..]),
489 };
490 if !ber_zone(zone, generalized) {
491 return false;
492 }
493 let digits = match clock.iter().position(|byte| matches!(byte, b'.' | b',')) {
494 Some(at) if generalized => {
495 let (digits, fraction) = clock.split_at(at);
496 if fraction.len() < 2 || !fraction[1..].iter().all(u8::is_ascii_digit) {
497 return false;
498 }
499 digits
500 }
501 Some(_) => return false,
502 None => clock,
503 };
504 let Some(([month, day, hour], tail)) = fields_of(digits) else {
505 return false;
506 };
507 if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) || hour > 23 {
508 return false;
509 }
510 if tail.is_empty() {
514 return generalized;
515 }
516 let Some(([minute], tail)) = fields_of(tail) else {
517 return false;
518 };
519 if minute > 59 {
520 return false;
521 }
522 if tail.is_empty() {
523 return true;
524 }
525 let Some(([second], [])) = fields_of(tail) else {
526 return false;
527 };
528 second <= if generalized { 60 } else { 59 }
529}
530
531fn ber_zone(zone: &[u8], generalized: bool) -> bool {
532 if zone.is_empty() {
533 return generalized;
534 }
535 if zone == b"Z" {
536 return true;
537 }
538 let Some((b'+' | b'-', digits)) = zone.split_first() else {
539 return false;
540 };
541 let Some(([hour], tail)) = fields_of(digits) else {
542 return false;
543 };
544 if hour > 23 {
545 return false;
546 }
547 if tail.is_empty() {
548 return generalized;
549 }
550 matches!(fields_of(tail), Some(([minute], [])) if minute <= 59)
551}
552
553pub(super) fn split(contents: &[u8], count: usize) -> Option<(&[u8], &[u8])> {
555 let (digits, rest) = contents.split_at_checked(count)?;
556 digits
557 .iter()
558 .all(u8::is_ascii_digit)
559 .then_some((digits, rest))
560}
561
562pub(super) fn number(digits: &[u8]) -> Option<u32> {
564 digits.iter().try_fold(0u32, |value, digit| {
565 value
566 .checked_mul(10)
567 .and_then(|value| value.checked_add(u32::from(digit - b'0')))
568 })
569}
570
571fn in_cycle(digits: &str, negative: bool) -> u32 {
581 let counted = digits.as_bytes().iter().fold(0u32, |cycle, digit| {
582 (cycle * 10 + u32::from(digit - b'0')) % 400
583 });
584 if negative {
585 (400 - counted) % 400
586 } else {
587 counted
588 }
589}
590
591fn weeks_in_year(year: u32) -> u32 {
600 let opens = |year: u32| (year + year / 4 - year / 100 + year / 400) % 7;
601 let long = opens(year) == 4 || year.checked_sub(1).is_some_and(|before| opens(before) == 3);
605 if long { 53 } else { 52 }
606}
607
608fn is_leap(year: u32) -> bool {
609 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
610}
611
612pub(super) fn days_in_month(year: u32, month: u32) -> u32 {
613 match month {
614 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
615 4 | 6 | 9 | 11 => 30,
616 2 if is_leap(year) => 29,
617 2 => 28,
618 _ => 0,
619 }
620}
621
622pub(super) fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
629 let month = i64::from(month);
630 let year = if month <= 2 { year - 1 } else { year };
631 let era = if year >= 0 { year } else { year - 399 } / 400;
632 let year_of_era = year - era * 400;
633 let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(day) - 1;
634 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
635 era * 146_097 + day_of_era - 719_468
638}
639
640pub(super) fn civil_from_seconds(seconds: i64) -> (i64, i64, i64, i64, i64, i64) {
642 let days = seconds.div_euclid(86_400);
643 let within = seconds.rem_euclid(86_400);
644
645 let shifted = days + 719_468;
646 let era = if shifted >= 0 {
647 shifted
648 } else {
649 shifted - 146_096
650 } / 146_097;
651 let day_of_era = shifted - era * 146_097;
652 let year_of_era =
653 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
654 let year = year_of_era + era * 400;
655 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
656 let shifted_month = (5 * day_of_year + 2) / 153;
657 let day = day_of_year - (153 * shifted_month + 2) / 5 + 1;
658 let month = if shifted_month < 10 {
659 shifted_month + 3
660 } else {
661 shifted_month - 9
662 };
663
664 (
665 if month <= 2 { year + 1 } else { year },
666 month,
667 day,
668 within / 3_600,
669 within / 60 % 60,
670 within % 60,
671 )
672}
673
674#[derive(Clone, Copy, PartialEq, Eq)]
677pub(crate) enum Defined {
678 Date,
680 TimeOfDay,
682 DateTime,
684 Duration,
686 Any,
689}
690
691pub(crate) fn defined_fits(kind: Defined, contents: &[u8]) -> bool {
700 match kind {
701 Defined::Date => date_fits(contents),
702 Defined::TimeOfDay => time_of_day_fits(contents),
703 Defined::DateTime => contents
704 .split_at_checked(8)
705 .is_some_and(|(date, time)| date_fits(date) && time_of_day_fits(time)),
706 Defined::Duration => duration_fits(contents),
707 Defined::Any => core::str::from_utf8(contents).is_ok_and(any_fits),
708 }
709}
710
711fn any_fits(text: &str) -> bool {
719 let interval = match text.strip_prefix('R') {
722 Some(after) => {
723 let count = after.bytes().take_while(u8::is_ascii_digit).count();
724 let (repeats, tail) = after.split_at(count);
725 if repeats.starts_with('0') && repeats.len() > 1 {
726 return false;
727 }
728 let Some(rest) = tail.strip_prefix('/') else {
729 return false;
730 };
731 rest
732 }
733 None => text,
734 };
735 match interval.split_once('/') {
736 Some((start, end)) => match (part_fits(start), part_fits(end)) {
740 (Some(starts), Some(ends)) => !(starts == Part::Duration && ends == Part::Duration),
741 _ => false,
742 },
743 None => part_fits(interval).is_some(),
744 }
745}
746
747#[derive(Clone, Copy, PartialEq, Eq)]
749enum Part {
750 Point,
751 Duration,
752}
753
754fn part_fits(text: &str) -> Option<Part> {
756 if let Some(duration) = text.strip_prefix('P') {
757 return duration_fits(duration.as_bytes()).then_some(Part::Duration);
758 }
759 point_fits(text).then_some(Part::Point)
760}
761
762fn point_fits(text: &str) -> bool {
765 match text.split_once('T') {
766 Some((date, time)) => date_part_fits(date) && zoned_time_fits(time),
767 None => date_part_fits(text) || zoned_time_fits(text),
771 }
772}
773
774fn zoned_time_fits(text: &str) -> bool {
779 let stated = match text.strip_suffix('Z') {
780 Some(rest) => rest,
781 None => match text.rfind(['+', '-']) {
782 Some(at) if difference_fits(&text[at..]) => &text[..at],
783 _ => text,
784 },
785 };
786 time_part_fits(stated)
787}
788
789fn difference_fits(text: &str) -> bool {
792 let Some(rest) = text.strip_prefix(['+', '-']) else {
793 return false;
794 };
795 match rest.split_once(':') {
796 Some((hours, minutes)) => {
797 two_digits(hours).is_some_and(|hours| hours <= 23)
798 && two_digits(minutes).is_some_and(|minutes| minutes <= 59)
799 }
800 None => two_digits(rest).is_some_and(|hours| hours <= 23),
801 }
802}
803
804fn date_part_fits(text: &str) -> bool {
808 let signed = text.starts_with(['+', '-']);
809 let rest = if signed { &text[1..] } else { text };
810 let mut parts = rest.split('-');
811 let Some(year) = parts.next() else {
812 return false;
813 };
814 if year.len() < 4 || !year.bytes().all(|byte| byte.is_ascii_digit()) {
816 return false;
817 }
818 let Some(next) = parts.next() else {
819 return parts.next().is_none();
820 };
821 let counted = in_cycle(year, signed);
826 if let Some(week) = next.strip_prefix('W') {
827 let last = weeks_in_year(counted);
829 let weeks = two_digits(week).is_some_and(|week| (1..=last).contains(&week));
830 return weeks
831 && match parts.next() {
832 Some(day) => {
833 day.len() == 1
834 && day.bytes().all(|byte| (b'1'..=b'7').contains(&byte))
835 && parts.next().is_none()
836 }
837 None => true,
838 };
839 }
840 if next.len() == 3 {
841 let last = if is_leap(counted) { 366 } else { 365 };
844 return next.bytes().all(|byte| byte.is_ascii_digit())
845 && number(next.as_bytes()).is_some_and(|day| (1..=last).contains(&day))
846 && parts.next().is_none();
847 }
848 let Some(month) = two_digits(next).filter(|month| (1..=12).contains(month)) else {
849 return false;
850 };
851 match parts.next() {
852 Some(day) => {
853 let Some(day) = two_digits(day) else {
854 return false;
855 };
856 let last = days_in_month(counted, month);
859 day >= 1 && day <= last && parts.next().is_none()
860 }
861 None => true,
862 }
863}
864
865fn time_part_fits(text: &str) -> bool {
869 let mut parts = text.split(':');
870 let bounds = [23u32, 59, 60];
871 let mut carried = None;
872 let mut counted = 0usize;
873 let mut ending = false;
874 for (piece, most) in parts.by_ref().zip(bounds) {
875 let (digits, fraction) = match piece.split_once(['.', ',']) {
876 Some((digits, fraction)) => (digits, Some(fraction)),
877 None => (piece, None),
878 };
879 let Some(value) = two_digits(digits) else {
880 return false;
881 };
882 if let Some(fraction) = fraction {
885 if fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
886 return false;
887 }
888 carried = Some(counted);
889 }
890 counted += 1;
891 if carried.is_some_and(|at| at + 1 != counted) {
892 return false;
893 }
894 if ending && fraction.is_some() {
901 return false;
902 }
903 if counted == 1 && value == 24 {
904 ending = true;
905 if fraction.is_some() {
906 return false;
907 }
908 continue;
909 }
910 if value > most || (ending && value != 0) {
911 return false;
912 }
913 }
914 counted > 0 && parts.next().is_none()
915}
916
917fn two_digits(text: &str) -> Option<u32> {
920 (text.len() == 2 && text.bytes().all(|byte| byte.is_ascii_digit()))
921 .then(|| number(text.as_bytes()))?
922}
923
924fn date_fits(contents: &[u8]) -> bool {
926 let Some((year, rest)) = split(contents, 4) else {
927 return false;
928 };
929 let Some(([month, day], [])) = fields_of(rest) else {
930 return false;
931 };
932 let Some(year) = number(year) else {
933 return false;
934 };
935 (1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
936}
937
938fn time_of_day_fits(contents: &[u8]) -> bool {
940 let Some(([hour, minute, second], [])) = fields_of(contents) else {
941 return false;
942 };
943 let midnight = hour == 24 && minute == 0 && second == 0;
948 (hour <= 23 || midnight) && minute <= 59 && second <= 60
949}
950
951fn duration_fits(contents: &[u8]) -> bool {
955 let Ok(text) = core::str::from_utf8(contents) else {
956 return false;
957 };
958 if text.ends_with('W') {
961 return components_fit(text, b"W").is_some_and(|weeks| weeks.any);
962 }
963 let (date, time) = match text.split_once('T') {
964 Some((date, time)) => (date, Some(time)),
965 None => (text, None),
966 };
967 if time.is_some_and(str::is_empty) {
969 return false;
970 }
971 let Some(dated) = components_fit(date, b"YMD") else {
972 return false;
973 };
974 if dated.fractional && time.is_some() {
977 return false;
978 }
979 let Some(timed) = time.map_or(Some(Components::NONE), |time| components_fit(time, b"HMS"))
980 else {
981 return false;
982 };
983 dated.any || timed.any
986}
987
988#[derive(Clone, Copy)]
990struct Components {
991 any: bool,
994 fractional: bool,
996}
997
998impl Components {
999 const NONE: Self = Self {
1000 any: false,
1001 fractional: false,
1002 };
1003}
1004
1005fn components_fit(text: &str, order: &[u8]) -> Option<Components> {
1010 let mut rest = text.as_bytes();
1011 let mut left = order;
1012 let mut seen = Components::NONE;
1013 while !rest.is_empty() {
1014 let count = rest.iter().take_while(|byte| byte.is_ascii_digit()).count();
1018 if count == 0 || (rest[0] == b'0' && count > 1) {
1019 return None;
1020 }
1021 let after = &rest[count..];
1022 let (after, fractional) = match after.split_first() {
1023 Some((b'.' | b',', tail)) => {
1024 let digits = tail.iter().take_while(|byte| byte.is_ascii_digit()).count();
1025 if digits == 0 {
1026 return None;
1027 }
1028 (&tail[digits..], true)
1029 }
1030 _ => (after, false),
1031 };
1032 let (designator, tail) = after.split_first()?;
1033 let at = left.iter().position(|kind| kind == designator)?;
1034 left = &left[at + 1..];
1035 if seen.fractional {
1040 return None;
1041 }
1042 seen = Components {
1043 any: true,
1044 fractional,
1045 };
1046 rest = tail;
1047 }
1048 Some(seen)
1049}
1050
1051pub(super) fn fields_of<const N: usize>(contents: &[u8]) -> Option<([u32; N], &[u8])> {
1054 let mut values = [0u32; N];
1055 let mut rest = contents;
1056 for value in &mut values {
1057 let (digits, tail) = split(rest, 2)?;
1058 *value = number(digits)?;
1059 rest = tail;
1060 }
1061 Some((values, rest))
1062}
1063
1064#[cfg(test)]
1065mod tests;