Skip to main content

structured_pkix/time/
xsd10.rs

1//! XSD1.0 calendar value arithmetic, distinct from UTC elapsed-time arithmetic.
2
3use structured_asn1::memory::{MemoryError, Work};
4
5/// Bounded calendar components in XML Schema Datatypes 1.0 year numbering.
6/// Year -1 denotes 1 BCE; zero is not a valid label. This is a value-space
7/// bridge for already decoded fields, not an XML lexical parser. Arbitrary
8/// precision lexical years/fractions remain with the caller's schema reader.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Xsd10Label {
11    /// Signed XSD1.0 year, excluding zero.
12    pub year: i64,
13    /// Month, one through twelve.
14    pub month: u8,
15    /// Day within the XSD1.0 month.
16    pub day: u8,
17    /// Original hour, including end-of-day twenty-four.
18    pub hour: u8,
19    /// Minute, zero through fifty-nine.
20    pub minute: u8,
21    /// Second, including sixty before value normalization.
22    pub second: u8,
23    /// Exact representable fractional nanoseconds.
24    pub nanos: u32,
25}
26
27impl Xsd10Label {
28    /// Normalize a label with an explicit timezone to the XSD1.0 UTC value.
29    /// Returns None for invalid fields or a result outside this bounded year
30    /// representation. Work refusal remains an error, never a malformed label.
31    ///
32    /// XSD1.0 ยง3.2.7.3 and Appendix E normalize second sixty by carrying to the
33    /// next minute even at timezone zero. Appendix E's month length uses the
34    /// literal signed year, not XSD1.1/astronomical year numbering. No leap table,
35    /// occurrence proof or implicit timezone is inferred by this operation.
36    /// <https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#dateTime-order>
37    /// <https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#adding-durations-to-dateTimes>
38    ///
39    /// # Panics
40    /// Only if the private bounded calendar component invariants are violated.
41    pub fn at_utc<W: Work>(
42        mut self,
43        timezone_minutes: i16,
44        execution: &W,
45    ) -> Result<Option<Self>, MemoryError> {
46        // Fixed fields and at most one day boundary for the permitted fourteen
47        // hour offset, including hour24/second60. No attacker-driven loop.
48        execution.charge_work(128)?;
49        if self.year == 0
50            || !(1..=12).contains(&self.month)
51            || self.day == 0
52            || self.day > self.month_days()
53            || self.hour > 24
54            || self.minute > 59
55            || self.second > 60
56            || self.nanos >= 1_000_000_000
57            || !(-840..=840).contains(&timezone_minutes)
58        {
59            return Ok(None);
60        }
61        if self.hour == 24 && (self.minute != 0 || self.second != 0 || self.nanos != 0) {
62            return Ok(None);
63        }
64        let seconds =
65            i32::from(self.hour) * 3600 + i32::from(self.minute) * 60 + i32::from(self.second)
66                - i32::from(timezone_minutes) * 60;
67        let carry = seconds.div_euclid(86_400);
68        let seconds = seconds.rem_euclid(86_400);
69        self.hour = u8::try_from(seconds / 3600).expect("bounded hour");
70        self.minute = u8::try_from(seconds / 60 % 60).expect("bounded minute");
71        self.second = u8::try_from(seconds % 60).expect("bounded second");
72        if carry < 0 {
73            if self.day > 1 {
74                self.day -= 1;
75            } else {
76                if self.month > 1 {
77                    self.month -= 1;
78                } else {
79                    self.month = 12;
80                    let Some(year) = self.year.checked_sub(1) else {
81                        return Ok(None);
82                    };
83                    self.year = if year == 0 { -1 } else { year };
84                }
85                self.day = self.month_days();
86            }
87        } else if carry > 0 {
88            if self.day < self.month_days() {
89                self.day += 1;
90            } else {
91                self.day = 1;
92                if self.month < 12 {
93                    self.month += 1;
94                } else {
95                    self.month = 1;
96                    let Some(year) = self.year.checked_add(1) else {
97                        return Ok(None);
98                    };
99                    self.year = if year == 0 { 1 } else { year };
100                }
101            }
102        }
103        Ok(Some(self))
104    }
105
106    fn month_days(self) -> u8 {
107        // The existing Gregorian month kernel needs only the 400-year residue.
108        // This preserves Appendix E's signed-year modulus without narrowing a
109        // whole negative/wide year or introducing another month-length table.
110        let year = u32::try_from(self.year.rem_euclid(400)).expect("year residue");
111        u8::try_from(super::days_in_month(year, u32::from(self.month))).expect("month length")
112    }
113}