structured_pkix/
temporal.rs1const NANOS: i128 = 1_000_000_000;
8
9mod parse;
10pub use parse::read_generalized;
11pub(crate) use parse::{admit_generalized, generalized};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct Reading {
17 start: Instant,
18 subnanosecond: bool,
19}
20
21impl Reading {
22 #[must_use]
24 pub const fn start(self) -> Instant {
25 self.start
26 }
27
28 #[must_use]
30 pub const fn has_subnanosecond_remainder(self) -> bool {
31 self.subnanosecond
32 }
33
34 pub fn upper_bound(self, table: &LeapTable<'_>) -> Result<Instant, Uncertainty> {
36 table.shift(self.start, i128::from(self.subnanosecond))
37 }
38}
39
40impl From<Instant> for Reading {
41 fn from(start: Instant) -> Self {
42 Self {
43 start,
44 subnanosecond: false,
45 }
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
54pub struct Instant {
55 seconds: i64,
56 leap: bool,
57 nanos: u32,
58}
59
60impl Instant {
61 #[must_use]
63 pub const fn new(seconds: i64, nanos: u32) -> Option<Self> {
64 if nanos >= 1_000_000_000 {
65 return None;
66 }
67 Some(Self {
68 seconds,
69 leap: false,
70 nanos,
71 })
72 }
73
74 #[must_use]
77 pub const fn leap_after(seconds: i64, nanos: u32) -> Option<Self> {
78 if seconds == i64::MAX || seconds.rem_euclid(86_400) != 86_399 || nanos >= 1_000_000_000 {
79 return None;
80 }
81 Some(Self {
82 seconds,
83 leap: true,
84 nanos,
85 })
86 }
87
88 #[must_use]
91 pub const fn seconds(self) -> i64 {
92 self.seconds
93 }
94
95 #[must_use]
97 pub const fn is_leap_second(self) -> bool {
98 self.leap
99 }
100
101 #[must_use]
103 pub const fn nanos(self) -> u32 {
104 self.nanos
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct Transition {
111 at: i64,
112 correction: i32,
113}
114
115impl Transition {
116 #[must_use]
120 pub const fn new(at: i64, correction: i32) -> Self {
121 Self { at, correction }
122 }
123
124 #[must_use]
126 pub const fn at(self) -> i64 {
127 self.at
128 }
129
130 #[must_use]
132 pub const fn correction(self) -> i32 {
133 self.correction
134 }
135
136 fn linear(self) -> i128 {
137 i128::from(self.at) + i128::from(self.correction)
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum InvalidTable {
144 Coverage,
146 Order,
148 Correction,
150 Boundary,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum Uncertainty {
158 OutsideCoverage,
160 NotInUtc,
163 OutOfRange,
165}
166
167#[derive(Debug, Clone, Copy)]
173pub struct LeapTable<'a> {
174 start: i64,
175 end: i64,
176 transitions: &'a [Transition],
177}
178
179impl<'a> LeapTable<'a> {
180 pub fn new(start: i64, end: i64, transitions: &'a [Transition]) -> Result<Self, InvalidTable> {
183 if start >= end {
184 return Err(InvalidTable::Coverage);
185 }
186 let mut previous = start;
187 let mut correction = 0i32;
188 for entry in transitions {
189 if entry.at <= previous || entry.at > end {
190 return Err(InvalidTable::Order);
191 }
192 if (i64::from(entry.correction) - i64::from(correction)).abs() != 1 {
193 return Err(InvalidTable::Correction);
194 }
195 if entry.at.rem_euclid(86_400) != 0 || super::time::civil_from_seconds(entry.at).2 != 1
199 {
200 return Err(InvalidTable::Boundary);
201 }
202 previous = entry.at;
203 correction = entry.correction;
204 }
205 let table = Self {
206 start,
207 end,
208 transitions,
209 };
210 if table
211 .coordinate(Instant {
212 seconds: start,
213 leap: false,
214 nanos: 0,
215 })
216 .is_err()
217 {
218 return Err(InvalidTable::Coverage);
219 }
220 Ok(table)
221 }
222
223 #[must_use]
225 pub const fn transitions(&self) -> &'a [Transition] {
226 self.transitions
227 }
228
229 pub fn shift(&self, at: Instant, nanos: i128) -> Result<Instant, Uncertainty> {
237 let here = self.coordinate(at)?;
238 let moved = here.checked_add(nanos).ok_or(Uncertainty::OutOfRange)?;
239 let second = moved.div_euclid(NANOS);
240 let fraction =
241 u32::try_from(moved.rem_euclid(NANOS)).map_err(|_| Uncertainty::OutOfRange)?;
242 let count = self
243 .transitions
244 .partition_point(|entry| entry.linear() <= second);
245 let correction = self.correction_before(count);
246 let result = if let Some(next) = self.transitions.get(count)
249 && next.correction > correction
250 && second == next.linear() - 1
251 {
252 Instant {
253 seconds: next.at - 1,
254 leap: true,
255 nanos: fraction,
256 }
257 } else {
258 let seconds = i64::try_from(second - i128::from(correction))
259 .map_err(|_| Uncertainty::OutOfRange)?;
260 Instant {
261 seconds,
262 leap: false,
263 nanos: fraction,
264 }
265 };
266 self.coordinate(result)?;
267 Ok(result)
268 }
269
270 fn correction_before(&self, count: usize) -> i32 {
271 if count == 0 {
272 0
273 } else {
274 self.transitions[count - 1].correction
275 }
276 }
277
278 fn coordinate(&self, at: Instant) -> Result<i128, Uncertainty> {
279 if at.seconds < self.start || at.seconds >= self.end {
280 return Err(Uncertainty::OutsideCoverage);
281 }
282 let count = self
283 .transitions
284 .partition_point(|entry| entry.at <= at.seconds);
285 let correction = self.correction_before(count);
286 let next = self
287 .transitions
288 .get(count)
289 .filter(|entry| i128::from(entry.at) == i128::from(at.seconds) + 1);
290 if at.leap {
291 if next.is_none_or(|entry| entry.correction <= correction) {
292 return Err(Uncertainty::NotInUtc);
293 }
294 } else if next.is_some_and(|entry| entry.correction < correction) {
295 return Err(Uncertainty::NotInUtc);
296 }
297 Ok(
298 (i128::from(at.seconds) + i128::from(correction) + i128::from(at.leap)) * NANOS
299 + i128::from(at.nanos),
300 )
301 }
302}
303
304#[cfg(test)]
305mod tests;