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}