Skip to main content

structured_pkix/
time.rs

1//! The two ways a certificate writes a moment, read into one.
2//!
3//! Both preserve UTC labels, including an inserted second. The form is kept because it is
4//! part of what the certificate says: the profile requires the two-digit form
5//! through 2049 and the four-digit form afterwards, and a report that has to
6//! explain a certificate cannot explain what it did not keep.
7//!
8//! Only the fixed-width form with seconds and `Z` is accepted. The wider
9//! encodings the base standard allows, an offset from local time, minutes
10//! without seconds, a fractional part, all let one moment be written several
11//! ways, and here they would let two documents disagree about when a
12//! certificate expired while both claiming to be the same certificate.
13
14use crate::error::{Error, ErrorCode, Result};
15use crate::temporal::Instant;
16use core::fmt;
17use structured_asn1::{Element, Reader, Tag, universal};
18
19/// UTC-qualified relation of a source label to the caller's lossless reading.
20/// This is a temporal fact, not a rule for application acceptance.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Relation {
23    /// Source label is strictly before the reading.
24    Before,
25    /// Source label equals the exact reading.
26    At,
27    /// Source label is strictly after the reading.
28    After,
29    /// Supplied UTC facts cannot establish an input label.
30    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    // Two labels, three binary searches each, before calling the table core.
42    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
62/// Profile calendar conversion with producing-reader admission and location.
63pub(crate) fn read_in(
64    parent: &Reader<'_>,
65    element: &Element<'_>,
66    field: &'static str,
67) -> Result<Time> {
68    // Fixed calendar conversion plus four byte passes includes validation,
69    // digit conversion and the shared GeneralizedTime semantic core.
70    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/// Which form a time was written in.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum Form {
94    /// Two digits of year, which the profile requires through 2049.
95    Utc,
96    /// Four digits of year, which the profile requires from 2050.
97    Generalized,
98}
99
100/// A moment, and how the certificate wrote it.
101///
102/// Two times are equal when they name the same moment. The form is what the
103/// certificate used to say it, not part of which moment it is.
104#[derive(Debug, Clone, Copy)]
105pub struct Time {
106    instant: Instant,
107    form: Form,
108}
109
110/// The moment `99991231235959Z` names, which the profile uses to say that a
111/// certificate has no well defined expiry rather than that it expires then.
112pub const NO_WELL_DEFINED_EXPIRY: i64 = 253_402_300_799;
113
114impl Time {
115    /// POSIX label, negative before the epoch. For an inserted second this
116    /// is the preceding ordinary second's label. Use [`Self::instant`] for
117    /// comparisons and [`crate::temporal::LeapTable`] for elapsed arithmetic.
118    #[must_use]
119    pub fn seconds(self) -> i64 {
120        self.instant.seconds()
121    }
122
123    /// The lossless UTC label; parsing does not authenticate its occurrence.
124    #[must_use]
125    pub fn instant(self) -> Instant {
126        self.instant
127    }
128
129    /// Whether the encoded time labels an inserted second.
130    #[must_use]
131    pub fn is_leap_second(self) -> bool {
132        self.instant.is_leap_second()
133    }
134
135    /// Which form the certificate wrote this in.
136    #[must_use]
137    pub fn form(self) -> Form {
138        self.form
139    }
140
141    /// Whether this is the moment the profile uses to mean that no expiry was
142    /// defined. What that means for a verdict is a policy question, and this
143    /// only reports that the certificate said it.
144    #[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
181/// Reads an element that a schema says is a time.
182///
183/// The tag chooses the form, so a field declared as a choice between the two is
184/// read by handing whichever element was found to this.
185pub(crate) fn read(element: &Element<'_>, field: &'static str) -> Result<Time> {
186    read_as(element, field, Fraction::Absent)
187}
188
189/// Whether a fractional-seconds element may close the value.
190#[derive(Clone, Copy, PartialEq, Eq)]
191pub(crate) enum Fraction {
192    /// The profile writes a certificate's validity dates without one
193    /// (RFC 5280 4.1.2.5.2), and this reader answers with one moment, which
194    /// a fraction would not change.
195    Absent,
196    /// The encoding writes one where there is one to write, with no trailing
197    /// zeros and none at all where it stands for zero (X.690 11.7.3). A
198    /// value preserved inside somebody else's payload is written under that
199    /// rule and not under the profile's narrower one.
200    Written,
201}
202
203/// Reads a time under whichever of the two rules the caller is asking about.
204pub(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            // The profile's pivot: a two-digit year is read into the window
228            // 1950 to 2049, so a certificate written in 1998 does not become
229            // one written in 2098.
230            (if two >= 50 { 1900 + two } else { 2000 + two }, rest)
231        }
232        Form::Generalized => {
233            // Any year, including one the profile tells issuers to write as a
234            // UTCTime instead. That rule binds the issuer: "CAs conforming to
235            // this profile MUST always encode certificate validity dates
236            // through the year 2049 as UTCTime". The sentence after it binds
237            // this reader the other way: "Conforming applications MUST be
238            // able to process validity dates that are encoded in either
239            // UTCTime or GeneralizedTime" (RFC 5280 4.1.2.5). A date written
240            // in the wrong form still names one moment, unambiguously, and
241            // refusing to read it would leave a person holding a document
242            // this product declined to explain over its issuer's spelling.
243            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    // A fraction closes the value where one is written at all: only the wider
254    // form has one, and only where the caller is reading under the encoding's
255    // rule rather than the profile's.
256    let closed = match tail.split_first() {
257        Some((b'.', digits)) if fraction == Fraction::Written && form == Form::Generalized => {
258            // At least one digit, no trailing zero, and the Z after them.
259            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        // X.680 (2021) §46.3 incorporates ISO 8601 time-of-day notation,
271        // including inserted-second labels. X.690 §11.7 restricts spelling,
272        // not UTC occurrence. After the spelling checks above, use the same
273        // calendar/label semantics as the temporal reader; a LeapTable alone
274        // can establish occurrence. UTCTime has its own 00..59 grammar below.
275        // https://www.itu.int/rec/T-REC-X.680-202102-I/en
276        // https://www.itu.int/rec/T-REC-X.690-202102-I/en
277        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    // X.680 (2021) §47.3(b)(2): "ss is seconds (00 to 59)" for UTCTime.
285    // https://www.itu.int/rec/T-REC-X.680-202102-I/en
286    if hour > 23 || minute > 59 || second > 59 {
287        return Err(fail(ErrorCode::TimeOutOfRange));
288    }
289
290    // Every field is bounded by the checks above and the year by its digit
291    // count, so the seconds count is at most the year 9999 in seconds and
292    // nothing here can overflow.
293    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
301/// Useful time syntax under BER, independently of a PKIX validity field.
302/// X.680 (2021) §§46.2–46.3, 47.3; X.690 §8.25. CER/DER §§11.7–11.8
303/// narrow this syntax and are deliberately applied only by the DER reader.
304/// <https://www.itu.int/rec/T-REC-X.680-202102-I/en>
305pub(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    // UTCTime gives only the low two year digits, not a century pivot. The
341    // cycle check allows February 29 for yy=00 (some possible centuries are
342    // leap years); selecting a century belongs to the interpreting schema.
343    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
383/// Splits off `count` bytes, which must all be digits.
384pub(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
392/// Reads a run of digits already known to be digits.
393pub(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
401/// Where a year falls in the four-hundred-year cycle the calendar repeats
402/// on, which is what every question about its length answers to.
403///
404/// The Gregorian calendar comes back to the same shape every four hundred
405/// years: the leap rule is stated in fours, hundreds and four hundreds, and
406/// four hundred years hold a whole number of weeks. So a year of any width
407/// is answered by its last four digits, which is what makes a year too wide
408/// for a counter no less readable than a short one. The sign turns the count
409/// the other way round the cycle.
410fn 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
421/// How many weeks a year is written in: fifty-two, and fifty-three where the
422/// year is the long one.
423///
424/// A year is long where it begins on a Thursday, and where it is a leap year
425/// beginning on a Wednesday (ISO 8601 4.1.4, which the week components of
426/// X.680 Table 7 are written in). Both are said by the day the first of
427/// January falls on, which is what the count below is: the days from the
428/// start of the calendar to that year, over seven.
429fn weeks_in_year(year: u32) -> u32 {
430    let opens = |year: u32| (year + year / 4 - year / 100 + year / 400) % 7;
431    // The second reading needs the year before, which the first year of the
432    // calendar does not have; it opens the calendar rather than continuing
433    // one, so only the first reading applies to it.
434    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
452/// Days from the epoch to a date, by the shift-the-year-to-March method: with
453/// March first, the four-year, hundred-year and four-hundred-year corrections
454/// all land at the end of the year and the leap day needs no special case.
455///
456/// Every value is bounded by the calendar checks above, so ordinary arithmetic
457/// cannot overflow here.
458pub(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    // 719468 is the number of days from the epoch back to the start of the era
466    // this counts from.
467    era * 146_097 + day_of_era - 719_468
468}
469
470/// The inverse, for rendering. Bounded the same way.
471pub(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/// Which of the types defined in X.680 38.4 a value claims to be, for the
505/// four whose notation is settled enough to hold it to.
506#[derive(Clone, Copy, PartialEq, Eq)]
507pub(crate) enum Defined {
508    /// `DATE`, a year, a month and a day.
509    Date,
510    /// `TIME-OF-DAY`, an hour, a minute and a second.
511    TimeOfDay,
512    /// `DATE-TIME`, the two of them.
513    DateTime,
514    /// `DURATION`, a length of time rather than a moment.
515    Duration,
516    /// `TIME` itself, whose notation is the whole of the interchange format
517    /// and is read here only as the characters it is written from.
518    Any,
519}
520
521/// Whether the contents of one of the newer time types are the notation that
522/// type writes.
523///
524/// The contents are the value notation in UTF-8, with the characters X.690
525/// 8.26 names removed: the hyphens of a date, the colons of a time, the T
526/// between them, the P before a duration. What is left is what this reads.
527/// A value that is not the notation is not a value of the type, and one
528/// preserved for somebody else must not claim a type it does not have.
529pub(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
541/// The unrestricted type, whose notation is the whole of what the property
542/// settings can choose: a point in time, a duration, an interval of the two,
543/// and any of those repeated (X.680 Table 7).
544///
545/// Written in the extended formats, which keep their separators, and X.690
546/// 8.26.1.2 takes only the quotation marks off, so what arrives here is the
547/// notation as the table writes it.
548fn any_fits(text: &str) -> bool {
549    // Recurrence opens the value: R, how many times if it is not unlimited,
550    // and then the interval it repeats.
551    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        // An interval is a start point and an end point, a start point and a
567        // duration, or a duration and an end point (Table 7, "Time
568        // interval"). Two durations name no interval: nothing says when.
569        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/// Which of the two an end of an interval, or a value standing alone, is.
578#[derive(Clone, Copy, PartialEq, Eq)]
579enum Part {
580    Point,
581    Duration,
582}
583
584/// Whether one end of a value is a point in time or a length of one.
585fn 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
592/// Whether text is a point in time: a date, a time of day, or both with the
593/// T between them, and the zone the value is read in if it carries one.
594fn point_fits(text: &str) -> bool {
595    match text.split_once('T') {
596        Some((date, time)) => date_part_fits(date) && zoned_time_fits(time),
597        // The hours component of a time standing alone is written without
598        // the T (Table 7, "Hours component" with Basic=Time), so what is
599        // left is one or the other.
600        None => date_part_fits(text) || zoned_time_fits(text),
601    }
602}
603
604/// Whether text is a time of day and, where it carries one, the zone it is
605/// read in: the UTC designator, or the difference from it (Table 7, the last
606/// two rows). The zone follows the time and nothing else, so it is looked
607/// for here rather than at the end of the whole point.
608fn 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
619/// Whether text is the difference from UTC: a sign, hours, and minutes where
620/// the difference is not a whole number of hours.
621fn 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
634/// Whether text is a date in the extended format: a year, and then a month
635/// and a day, a week and a day of it, or a day of the year (Table 7, the
636/// year, month, week and day components).
637fn 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    // Four digits, or more where the settings ask for a wider year.
645    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    // How far the days and the weeks of this year run is the year's to say,
652    // and a year of any width says it: the calendar repeats every four
653    // hundred years, so where in that cycle the year falls is the whole of
654    // the answer.
655    let counted = in_cycle(year, signed);
656    if let Some(week) = next.strip_prefix('W') {
657        // A week, and then which day of it, one digit through seven.
658        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        // The day of the year, which stands in for the month and the day,
672        // and runs to the length of that year.
673        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            // The calendar decides the last day of the month, and the year
687            // decides February.
688            let last = days_in_month(counted, month);
689            day >= 1 && day <= last && parts.next().is_none()
690        }
691        None => true,
692    }
693}
694
695/// Whether text is a time of day in the extended format: hours, and then
696/// minutes and seconds, the last component present carrying a fraction where
697/// the value is written to that accuracy (Table 7).
698fn 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        // A fraction says how accurately the value is written, so the
713        // component carrying one is the last (ISO 8601, 4.2.2.4).
714        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        // Midnight at the end of a day is the twenty-fourth hour, which is
725        // the one value past the bounds (Table 7, "Hours component"). It
726        // names one instant rather than an hour a value can sit inside, so
727        // what follows it is zero and nothing carries a fraction: a
728        // fraction of that hour, or of a component after it, is a moment
729        // past the end of the day.
730        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
747/// Two digits read as a number, which is how every component but the year
748/// and the day of the year is written.
749fn 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
754/// Eight digits naming a day of the calendar (`Date=YMD Year=Basic`).
755fn 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
768/// Six digits naming a time of day (`Time=HMS`).
769fn time_of_day_fits(contents: &[u8]) -> bool {
770    let Some(([hour, minute, second], [])) = fields_of(contents) else {
771        return false;
772    };
773    // Midnight is written at either end of the day, so the hour reaches
774    // twenty-four where the rest of the value is zero (X.680 38.4.2), and
775    // the second reaches sixty because the interchange format writes a leap
776    // second that way.
777    let midnight = hour == 24 && minute == 0 && second == 0;
778    (hour <= 23 || midnight) && minute <= 59 && second <= 60
779}
780
781/// A duration, as X.690 8.26.5 leaves it: the notation without its leading P,
782/// so a count and its designator for each component that is present, largest
783/// first, with the time components after a T.
784fn duration_fits(contents: &[u8]) -> bool {
785    let Ok(text) = core::str::from_utf8(contents) else {
786        return false;
787    };
788    // The week form stands alone: it is one of the three alternatives and
789    // does not mix with the others (X.680 38.3.6.1).
790    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    // A T with nothing after it names no component.
798    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    // A fraction ends the duration, so a date part that carries one may not
805    // be followed by a time part either (X.680 38.3.6.13).
806    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    // Something has to be there: a duration of no components is no length of
814    // time.
815    dated.any || timed.any
816}
817
818/// What a run of designations turned out to be.
819#[derive(Clone, Copy)]
820struct Components {
821    /// Whether there were any at all, which is a form the caller decides
822    /// about rather than a malformation on its own.
823    any: bool,
824    /// Whether the last one carried a fraction, which closes the duration.
825    fractional: bool,
826}
827
828impl Components {
829    const NONE: Self = Self {
830        any: false,
831        fractional: false,
832    };
833}
834
835/// Whether a run is designations in the order given, each designator used at
836/// most once: a count, an optional fraction, and the letter naming the
837/// component (X.680 38.3.6.2 to 38.3.6.13). `None` says the run is not
838/// designations at all.
839fn 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        // The count opens with a zero only where it is the single digit zero
845        // (38.3.6.11), so the accuracy a value is written to is written one
846        // way.
847        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 a designation contains a fractional part, there shall be no
866        // following designation" (38.3.6.13): the fraction says how
867        // accurately the duration is written, and a component after it
868        // would say something more accurate again.
869        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
881/// The two-digit fields at the front of a value, and whatever follows them
882/// for the caller to hold to its own rule.
883pub(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;