structured_pkix/
temporal.rs1const NANOS: i128 = 1_000_000_000;
9
10mod parse;
11mod precision;
12pub(crate) use parse::{admit_generalized, generalized};
13pub use parse::{read_generalized, read_generalized_exact};
14pub use precision::{CompareError, ExactReading};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct Reading {
20 start: Instant,
21 subnanosecond: bool,
22}
23
24impl Reading {
25 #[must_use]
27 pub const fn start(self) -> Instant {
28 self.start
29 }
30
31 #[must_use]
33 pub const fn has_subnanosecond_remainder(self) -> bool {
34 self.subnanosecond
35 }
36
37 pub fn upper_bound(self, table: &LeapTable<'_>) -> Result<Instant, Uncertainty> {
39 table.shift(self.start, i128::from(self.subnanosecond))
40 }
41}
42
43impl From<Instant> for Reading {
44 fn from(start: Instant) -> Self {
45 Self {
46 start,
47 subnanosecond: false,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
57pub struct Instant {
58 seconds: i64,
59 leap: bool,
60 nanos: u32,
61}
62
63impl Instant {
64 pub(crate) const fn whole(seconds: i64) -> Self {
66 Self {
67 seconds,
68 leap: false,
69 nanos: 0,
70 }
71 }
72
73 #[must_use]
75 pub const fn new(seconds: i64, nanos: u32) -> Option<Self> {
76 if nanos >= 1_000_000_000 {
77 return None;
78 }
79 Some(Self {
80 seconds,
81 leap: false,
82 nanos,
83 })
84 }
85
86 #[must_use]
89 pub const fn leap_after(seconds: i64, nanos: u32) -> Option<Self> {
90 if seconds == i64::MAX || seconds.rem_euclid(86_400) != 86_399 || nanos >= 1_000_000_000 {
91 return None;
92 }
93 Some(Self {
94 seconds,
95 leap: true,
96 nanos,
97 })
98 }
99
100 #[must_use]
103 pub const fn seconds(self) -> i64 {
104 self.seconds
105 }
106
107 #[must_use]
109 pub const fn is_leap_second(self) -> bool {
110 self.leap
111 }
112
113 #[must_use]
115 pub const fn nanos(self) -> u32 {
116 self.nanos
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct Transition {
123 at: i64,
124 correction: i32,
125}
126
127impl Transition {
128 #[must_use]
132 pub const fn new(at: i64, correction: i32) -> Self {
133 Self { at, correction }
134 }
135
136 #[must_use]
138 pub const fn at(self) -> i64 {
139 self.at
140 }
141
142 #[must_use]
144 pub const fn correction(self) -> i32 {
145 self.correction
146 }
147
148 fn linear(self) -> i128 {
149 i128::from(self.at) + i128::from(self.correction)
150 }
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum InvalidTable {
156 Coverage,
158 Order,
160 Correction,
162 Boundary,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum Uncertainty {
170 OutsideCoverage,
172 NotInUtc,
175 OutOfRange,
177}
178
179#[derive(Debug, Clone, Copy)]
185pub struct LeapTable<'a> {
186 start: i64,
187 end: i64,
188 transitions: &'a [Transition],
189}
190
191impl<'a> LeapTable<'a> {
192 pub fn new(start: i64, end: i64, transitions: &'a [Transition]) -> Result<Self, InvalidTable> {
195 if start >= end {
196 return Err(InvalidTable::Coverage);
197 }
198 let mut previous = start;
199 let mut correction = 0i32;
200 for entry in transitions {
201 if entry.at <= previous || entry.at > end {
202 return Err(InvalidTable::Order);
203 }
204 if (i64::from(entry.correction) - i64::from(correction)).abs() != 1 {
205 return Err(InvalidTable::Correction);
206 }
207 if entry.at.rem_euclid(86_400) != 0 || super::time::civil_from_seconds(entry.at).2 != 1
211 {
212 return Err(InvalidTable::Boundary);
213 }
214 previous = entry.at;
215 correction = entry.correction;
216 }
217 let table = Self {
218 start,
219 end,
220 transitions,
221 };
222 if table
223 .coordinate(Instant {
224 seconds: start,
225 leap: false,
226 nanos: 0,
227 })
228 .is_err()
229 {
230 return Err(InvalidTable::Coverage);
231 }
232 Ok(table)
233 }
234
235 #[must_use]
237 pub const fn transitions(&self) -> &'a [Transition] {
238 self.transitions
239 }
240
241 pub fn shift(&self, at: Instant, nanos: i128) -> Result<Instant, Uncertainty> {
249 let here = self.coordinate(at)?;
250 let moved = here.checked_add(nanos).ok_or(Uncertainty::OutOfRange)?;
251 let second = moved.div_euclid(NANOS);
252 let fraction =
253 u32::try_from(moved.rem_euclid(NANOS)).map_err(|_| Uncertainty::OutOfRange)?;
254 let count = self
255 .transitions
256 .partition_point(|entry| entry.linear() <= second);
257 let correction = self.correction_before(count);
258 let result = if let Some(next) = self.transitions.get(count)
261 && next.correction > correction
262 && second == next.linear() - 1
263 {
264 Instant {
265 seconds: next.at - 1,
266 leap: true,
267 nanos: fraction,
268 }
269 } else {
270 let seconds = i64::try_from(second - i128::from(correction))
271 .map_err(|_| Uncertainty::OutOfRange)?;
272 Instant {
273 seconds,
274 leap: false,
275 nanos: fraction,
276 }
277 };
278 self.coordinate(result)?;
279 Ok(result)
280 }
281
282 fn correction_before(&self, count: usize) -> i32 {
283 if count == 0 {
284 0
285 } else {
286 self.transitions[count - 1].correction
287 }
288 }
289
290 fn coordinate(&self, at: Instant) -> Result<i128, Uncertainty> {
291 if at.seconds < self.start || at.seconds >= self.end {
292 return Err(Uncertainty::OutsideCoverage);
293 }
294 let count = self
295 .transitions
296 .partition_point(|entry| entry.at <= at.seconds);
297 let correction = self.correction_before(count);
298 let next = self
299 .transitions
300 .get(count)
301 .filter(|entry| i128::from(entry.at) == i128::from(at.seconds) + 1);
302 if at.leap {
303 if next.is_none_or(|entry| entry.correction <= correction) {
304 return Err(Uncertainty::NotInUtc);
305 }
306 } else if next.is_some_and(|entry| entry.correction < correction) {
307 return Err(Uncertainty::NotInUtc);
308 }
309 Ok(
310 (i128::from(at.seconds) + i128::from(correction) + i128::from(at.leap)) * NANOS
311 + i128::from(at.nanos),
312 )
313 }
314}
315
316#[cfg(test)]
317mod tests;