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
19mod xsd10;
20pub use xsd10::Xsd10Label;
21
22/// UTC-qualified relation of a source label to the caller's lossless reading.
23/// This is a temporal fact, not a rule for application acceptance.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Relation {
26    /// Source label is strictly before the reading.
27    Before,
28    /// Source label equals the exact reading.
29    At,
30    /// Source label is strictly after the reading.
31    After,
32    /// Supplied UTC facts cannot establish an input label.
33    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    // Two labels, three binary searches each, before calling the table core.
45    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
65/// Profile calendar conversion with producing-reader admission and location.
66pub(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
75/// Bare DER `GeneralizedTime` retains fractional precision, unlike a
76/// certificate-validity [`Time`]. X.690 §11.7 permits canonical decimal fractions.
77/// <https://www.itu.int/rec/T-REC-X.690-202102-I/en>
78pub(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    // Fixed calendar conversion plus four byte passes includes validation,
96    // digit conversion and the shared GeneralizedTime semantic core.
97    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/// Which form a time was written in.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum Form {
120    /// Two digits of year, which the profile requires through 2049.
121    Utc,
122    /// Four digits of year, which the profile requires from 2050.
123    Generalized,
124}
125
126/// A moment, and how the certificate wrote it.
127///
128/// Two times are equal when they name the same moment. The form is what the
129/// certificate used to say it, not part of which moment it is.
130#[derive(Debug, Clone, Copy)]
131pub struct Time {
132    instant: Instant,
133    form: Form,
134}
135
136/// Gregorian calendar components of a labelled time, not an elapsed-time proof.
137/// The year uses astronomical numbering (zero denotes 1 BCE); callers of other
138/// calendar contracts must apply their own specified year-numbering convention.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct Calendar {
141    /// Signed astronomical Gregorian year.
142    pub year: i64,
143    /// Month, one through twelve.
144    pub month: u8,
145    /// Day within the month.
146    pub day: u8,
147    /// Hour, zero through twenty-three.
148    pub hour: u8,
149    /// Minute, zero through fifty-nine.
150    pub minute: u8,
151    /// Labelled second, including sixty for an inserted second.
152    pub second: u8,
153    /// Fractional nanoseconds retained by this `Time` value.
154    pub nanos: u32,
155}
156
157impl Calendar {
158    /// Validates a four-digit Gregorian calendar label and projects its signed
159    /// minute offset into UTC. This is a scalar bridge, not a wire parser or
160    /// proof that an indicated leap second occurred. It shares the certificate
161    /// time calendar kernel and permits offsets wider than XSD's fourteen hours.
162    /// Years outside 0000..9999 and invalid calendar components return None;
163    /// resource refusal remains a separate error before arithmetic.
164    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
201/// The moment `99991231235959Z` names, which the profile uses to say that a
202/// certificate has no well defined expiry rather than that it expires then.
203pub const NO_WELL_DEFINED_EXPIRY: i64 = 253_402_300_799;
204
205impl Time {
206    /// Reads an original fixed-width UTC or `GeneralizedTime` profile label.
207    ///
208    /// The enclosing schema selects the permitted tag. This uses the same
209    /// calendar kernel as certificate validity, with seconds and `Z`, without
210    /// fractions. The supplied reader must be the element's producing context;
211    /// its original and current-operation ceilings fund the conversion.
212    pub fn read(parent: &Reader<'_>, element: &Element<'_>, field: &'static str) -> Result<Self> {
213        read_in(parent, element, field)
214    }
215
216    /// Project the original UTC label into XSD1.0 year numbering.
217    /// This retains second sixty; [`Xsd10Label::at_utc`] performs the separate
218    /// XSD value normalization. It does not change this time's UTC identity.
219    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    /// Project the original label at a signed calendar offset.
240    /// Offset minutes need not describe a valid wire timezone; this also permits
241    /// inspecting the previous local date of a midnight label without a second
242    /// calendar implementation. No UTC correction history is inferred.
243    /// The same inverse Gregorian core serves [`Self`]'s text rendering.
244    ///
245    /// # Panics
246    /// Only if that private Gregorian core violates its bounded component
247    /// invariants; parsed time labels and any i16-minute offset preserve them.
248    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        // Parsed Time years are 0000..9999. The complete i16-minute offset
255        // changes this bounded label by less than 23 days, so no i64 overflow.
256        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    /// POSIX label, negative before the epoch. For an inserted second this
274    /// is the preceding ordinary second's label. Use [`Self::instant`] for
275    /// comparisons and [`crate::temporal::LeapTable`] for elapsed arithmetic.
276    #[must_use]
277    pub fn seconds(self) -> i64 {
278        self.instant.seconds()
279    }
280
281    /// The lossless UTC label; parsing does not authenticate its occurrence.
282    #[must_use]
283    pub fn instant(self) -> Instant {
284        self.instant
285    }
286
287    /// Whether the encoded time labels an inserted second.
288    #[must_use]
289    pub fn is_leap_second(self) -> bool {
290        self.instant.is_leap_second()
291    }
292
293    /// Which form the certificate wrote this in.
294    #[must_use]
295    pub fn form(self) -> Form {
296        self.form
297    }
298
299    /// Whether this is the moment the profile uses to mean that no expiry was
300    /// defined. What that means for a verdict is a policy question, and this
301    /// only reports that the certificate said it.
302    #[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
339/// Reads an element that a schema says is a time.
340///
341/// The tag chooses the form, so a field declared as a choice between the two is
342/// read by handing whichever element was found to this.
343pub(crate) fn read(element: &Element<'_>, field: &'static str) -> Result<Time> {
344    read_as(element, field, Fraction::Absent)
345}
346
347/// Whether a fractional-seconds element may close the value.
348#[derive(Clone, Copy, PartialEq, Eq)]
349pub(crate) enum Fraction {
350    /// The profile writes a certificate's validity dates without one
351    /// (RFC 5280 4.1.2.5.2), and this reader answers with one moment, which
352    /// a fraction would not change.
353    Absent,
354    /// The encoding writes one where there is one to write, with no trailing
355    /// zeros and none at all where it stands for zero (X.690 11.7.3). A
356    /// value preserved inside somebody else's payload is written under that
357    /// rule and not under the profile's narrower one.
358    Written,
359}
360
361/// Reads a time under whichever of the two rules the caller is asking about.
362pub(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            // The profile's pivot: a two-digit year is read into the window
398            // 1950 to 2049, so a certificate written in 1998 does not become
399            // one written in 2098.
400            (if two >= 50 { 1900 + two } else { 2000 + two }, rest)
401        }
402        Form::Generalized => {
403            // Any year, including one the profile tells issuers to write as a
404            // UTCTime instead. That rule binds the issuer: "CAs conforming to
405            // this profile MUST always encode certificate validity dates
406            // through the year 2049 as UTCTime". The sentence after it binds
407            // this reader the other way: "Conforming applications MUST be
408            // able to process validity dates that are encoded in either
409            // UTCTime or GeneralizedTime" (RFC 5280 4.1.2.5). A date written
410            // in the wrong form still names one moment, unambiguously, and
411            // refusing to read it would leave a person holding a document
412            // this product declined to explain over its issuer's spelling.
413            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    // A fraction closes the value where one is written at all: only the wider
424    // form has one, and only where the caller is reading under the encoding's
425    // rule rather than the profile's.
426    let closed = match tail.split_first() {
427        Some((b'.', digits)) if fraction == Fraction::Written && form == Form::Generalized => {
428            // At least one digit, no trailing zero, and the Z after them.
429            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        // X.680 (2021) §46.3 incorporates ISO 8601 time-of-day notation,
441        // including inserted-second labels. X.690 §11.7 restricts spelling,
442        // not UTC occurrence. After the spelling checks above, use the same
443        // calendar/label semantics as the temporal reader; a LeapTable alone
444        // can establish occurrence. UTCTime has its own 00..59 grammar below.
445        // https://www.itu.int/rec/T-REC-X.680-202102-I/en
446        // https://www.itu.int/rec/T-REC-X.690-202102-I/en
447        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    // X.680 (2021) §47.3(b)(2): "ss is seconds (00 to 59)" for UTCTime.
455    // https://www.itu.int/rec/T-REC-X.680-202102-I/en
456    if hour > 23 || minute > 59 || second > 59 {
457        return Err(fail(ErrorCode::TimeOutOfRange));
458    }
459
460    // Every field is bounded by the checks above and the year by its digit
461    // count, so the seconds count is at most the year 9999 in seconds and
462    // nothing here can overflow.
463    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
471/// Useful time syntax under BER, independently of a PKIX validity field.
472/// X.680 (2021) §§46.2–46.3, 47.3; X.690 §8.25. CER/DER §§11.7–11.8
473/// narrow this syntax and are deliberately applied only by the DER reader.
474/// <https://www.itu.int/rec/T-REC-X.680-202102-I/en>
475pub(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    // UTCTime gives only the low two year digits, not a century pivot. The
511    // cycle check allows February 29 for yy=00 (some possible centuries are
512    // leap years); selecting a century belongs to the interpreting schema.
513    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
553/// Splits off `count` bytes, which must all be digits.
554pub(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
562/// Reads a run of digits already known to be digits.
563pub(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
571/// Where a year falls in the four-hundred-year cycle the calendar repeats
572/// on, which is what every question about its length answers to.
573///
574/// The Gregorian calendar comes back to the same shape every four hundred
575/// years: the leap rule is stated in fours, hundreds and four hundreds, and
576/// four hundred years hold a whole number of weeks. So a year of any width
577/// is answered by its last four digits, which is what makes a year too wide
578/// for a counter no less readable than a short one. The sign turns the count
579/// the other way round the cycle.
580fn 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
591/// How many weeks a year is written in: fifty-two, and fifty-three where the
592/// year is the long one.
593///
594/// A year is long where it begins on a Thursday, and where it is a leap year
595/// beginning on a Wednesday (ISO 8601 4.1.4, which the week components of
596/// X.680 Table 7 are written in). Both are said by the day the first of
597/// January falls on, which is what the count below is: the days from the
598/// start of the calendar to that year, over seven.
599fn weeks_in_year(year: u32) -> u32 {
600    let opens = |year: u32| (year + year / 4 - year / 100 + year / 400) % 7;
601    // The second reading needs the year before, which the first year of the
602    // calendar does not have; it opens the calendar rather than continuing
603    // one, so only the first reading applies to it.
604    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
622/// Days from the epoch to a date, by the shift-the-year-to-March method: with
623/// March first, the four-year, hundred-year and four-hundred-year corrections
624/// all land at the end of the year and the leap day needs no special case.
625///
626/// Every value is bounded by the calendar checks above, so ordinary arithmetic
627/// cannot overflow here.
628pub(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    // 719468 is the number of days from the epoch back to the start of the era
636    // this counts from.
637    era * 146_097 + day_of_era - 719_468
638}
639
640/// The inverse, for rendering. Bounded the same way.
641pub(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/// Which of the types defined in X.680 38.4 a value claims to be, for the
675/// four whose notation is settled enough to hold it to.
676#[derive(Clone, Copy, PartialEq, Eq)]
677pub(crate) enum Defined {
678    /// `DATE`, a year, a month and a day.
679    Date,
680    /// `TIME-OF-DAY`, an hour, a minute and a second.
681    TimeOfDay,
682    /// `DATE-TIME`, the two of them.
683    DateTime,
684    /// `DURATION`, a length of time rather than a moment.
685    Duration,
686    /// `TIME` itself, whose notation is the whole of the interchange format
687    /// and is read here only as the characters it is written from.
688    Any,
689}
690
691/// Whether the contents of one of the newer time types are the notation that
692/// type writes.
693///
694/// The contents are the value notation in UTF-8, with the characters X.690
695/// 8.26 names removed: the hyphens of a date, the colons of a time, the T
696/// between them, the P before a duration. What is left is what this reads.
697/// A value that is not the notation is not a value of the type, and one
698/// preserved for somebody else must not claim a type it does not have.
699pub(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
711/// The unrestricted type, whose notation is the whole of what the property
712/// settings can choose: a point in time, a duration, an interval of the two,
713/// and any of those repeated (X.680 Table 7).
714///
715/// Written in the extended formats, which keep their separators, and X.690
716/// 8.26.1.2 takes only the quotation marks off, so what arrives here is the
717/// notation as the table writes it.
718fn any_fits(text: &str) -> bool {
719    // Recurrence opens the value: R, how many times if it is not unlimited,
720    // and then the interval it repeats.
721    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        // An interval is a start point and an end point, a start point and a
737        // duration, or a duration and an end point (Table 7, "Time
738        // interval"). Two durations name no interval: nothing says when.
739        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/// Which of the two an end of an interval, or a value standing alone, is.
748#[derive(Clone, Copy, PartialEq, Eq)]
749enum Part {
750    Point,
751    Duration,
752}
753
754/// Whether one end of a value is a point in time or a length of one.
755fn 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
762/// Whether text is a point in time: a date, a time of day, or both with the
763/// T between them, and the zone the value is read in if it carries one.
764fn point_fits(text: &str) -> bool {
765    match text.split_once('T') {
766        Some((date, time)) => date_part_fits(date) && zoned_time_fits(time),
767        // The hours component of a time standing alone is written without
768        // the T (Table 7, "Hours component" with Basic=Time), so what is
769        // left is one or the other.
770        None => date_part_fits(text) || zoned_time_fits(text),
771    }
772}
773
774/// Whether text is a time of day and, where it carries one, the zone it is
775/// read in: the UTC designator, or the difference from it (Table 7, the last
776/// two rows). The zone follows the time and nothing else, so it is looked
777/// for here rather than at the end of the whole point.
778fn 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
789/// Whether text is the difference from UTC: a sign, hours, and minutes where
790/// the difference is not a whole number of hours.
791fn 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
804/// Whether text is a date in the extended format: a year, and then a month
805/// and a day, a week and a day of it, or a day of the year (Table 7, the
806/// year, month, week and day components).
807fn 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    // Four digits, or more where the settings ask for a wider year.
815    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    // How far the days and the weeks of this year run is the year's to say,
822    // and a year of any width says it: the calendar repeats every four
823    // hundred years, so where in that cycle the year falls is the whole of
824    // the answer.
825    let counted = in_cycle(year, signed);
826    if let Some(week) = next.strip_prefix('W') {
827        // A week, and then which day of it, one digit through seven.
828        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        // The day of the year, which stands in for the month and the day,
842        // and runs to the length of that year.
843        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            // The calendar decides the last day of the month, and the year
857            // decides February.
858            let last = days_in_month(counted, month);
859            day >= 1 && day <= last && parts.next().is_none()
860        }
861        None => true,
862    }
863}
864
865/// Whether text is a time of day in the extended format: hours, and then
866/// minutes and seconds, the last component present carrying a fraction where
867/// the value is written to that accuracy (Table 7).
868fn 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        // A fraction says how accurately the value is written, so the
883        // component carrying one is the last (ISO 8601, 4.2.2.4).
884        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        // Midnight at the end of a day is the twenty-fourth hour, which is
895        // the one value past the bounds (Table 7, "Hours component"). It
896        // names one instant rather than an hour a value can sit inside, so
897        // what follows it is zero and nothing carries a fraction: a
898        // fraction of that hour, or of a component after it, is a moment
899        // past the end of the day.
900        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
917/// Two digits read as a number, which is how every component but the year
918/// and the day of the year is written.
919fn 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
924/// Eight digits naming a day of the calendar (`Date=YMD Year=Basic`).
925fn 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
938/// Six digits naming a time of day (`Time=HMS`).
939fn time_of_day_fits(contents: &[u8]) -> bool {
940    let Some(([hour, minute, second], [])) = fields_of(contents) else {
941        return false;
942    };
943    // Midnight is written at either end of the day, so the hour reaches
944    // twenty-four where the rest of the value is zero (X.680 38.4.2), and
945    // the second reaches sixty because the interchange format writes a leap
946    // second that way.
947    let midnight = hour == 24 && minute == 0 && second == 0;
948    (hour <= 23 || midnight) && minute <= 59 && second <= 60
949}
950
951/// A duration, as X.690 8.26.5 leaves it: the notation without its leading P,
952/// so a count and its designator for each component that is present, largest
953/// first, with the time components after a T.
954fn duration_fits(contents: &[u8]) -> bool {
955    let Ok(text) = core::str::from_utf8(contents) else {
956        return false;
957    };
958    // The week form stands alone: it is one of the three alternatives and
959    // does not mix with the others (X.680 38.3.6.1).
960    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    // A T with nothing after it names no component.
968    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    // A fraction ends the duration, so a date part that carries one may not
975    // be followed by a time part either (X.680 38.3.6.13).
976    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    // Something has to be there: a duration of no components is no length of
984    // time.
985    dated.any || timed.any
986}
987
988/// What a run of designations turned out to be.
989#[derive(Clone, Copy)]
990struct Components {
991    /// Whether there were any at all, which is a form the caller decides
992    /// about rather than a malformation on its own.
993    any: bool,
994    /// Whether the last one carried a fraction, which closes the duration.
995    fractional: bool,
996}
997
998impl Components {
999    const NONE: Self = Self {
1000        any: false,
1001        fractional: false,
1002    };
1003}
1004
1005/// Whether a run is designations in the order given, each designator used at
1006/// most once: a count, an optional fraction, and the letter naming the
1007/// component (X.680 38.3.6.2 to 38.3.6.13). `None` says the run is not
1008/// designations at all.
1009fn 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        // The count opens with a zero only where it is the single digit zero
1015        // (38.3.6.11), so the accuracy a value is written to is written one
1016        // way.
1017        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 a designation contains a fractional part, there shall be no
1036        // following designation" (38.3.6.13): the fraction says how
1037        // accurately the duration is written, and a component after it
1038        // would say something more accurate again.
1039        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
1051/// The two-digit fields at the front of a value, and whatever follows them
1052/// for the caller to hold to its own rule.
1053pub(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;