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 pub(crate) const fn whole(seconds: i64) -> Self {
63 Self {
64 seconds,
65 leap: false,
66 nanos: 0,
67 }
68 }
69
70 #[must_use]
72 pub const fn new(seconds: i64, nanos: u32) -> Option<Self> {
73 if nanos >= 1_000_000_000 {
74 return None;
75 }
76 Some(Self {
77 seconds,
78 leap: false,
79 nanos,
80 })
81 }
82
83 #[must_use]
86 pub const fn leap_after(seconds: i64, nanos: u32) -> Option<Self> {
87 if seconds == i64::MAX || seconds.rem_euclid(86_400) != 86_399 || nanos >= 1_000_000_000 {
88 return None;
89 }
90 Some(Self {
91 seconds,
92 leap: true,
93 nanos,
94 })
95 }
96
97 #[must_use]
100 pub const fn seconds(self) -> i64 {
101 self.seconds
102 }
103
104 #[must_use]
106 pub const fn is_leap_second(self) -> bool {
107 self.leap
108 }
109
110 #[must_use]
112 pub const fn nanos(self) -> u32 {
113 self.nanos
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct Transition {
120 at: i64,
121 correction: i32,
122}
123
124impl Transition {
125 #[must_use]
129 pub const fn new(at: i64, correction: i32) -> Self {
130 Self { at, correction }
131 }
132
133 #[must_use]
135 pub const fn at(self) -> i64 {
136 self.at
137 }
138
139 #[must_use]
141 pub const fn correction(self) -> i32 {
142 self.correction
143 }
144
145 fn linear(self) -> i128 {
146 i128::from(self.at) + i128::from(self.correction)
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum InvalidTable {
153 Coverage,
155 Order,
157 Correction,
159 Boundary,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum Uncertainty {
167 OutsideCoverage,
169 NotInUtc,
172 OutOfRange,
174}
175
176#[derive(Debug, Clone, Copy)]
182pub struct LeapTable<'a> {
183 start: i64,
184 end: i64,
185 transitions: &'a [Transition],
186}
187
188impl<'a> LeapTable<'a> {
189 pub fn new(start: i64, end: i64, transitions: &'a [Transition]) -> Result<Self, InvalidTable> {
192 if start >= end {
193 return Err(InvalidTable::Coverage);
194 }
195 let mut previous = start;
196 let mut correction = 0i32;
197 for entry in transitions {
198 if entry.at <= previous || entry.at > end {
199 return Err(InvalidTable::Order);
200 }
201 if (i64::from(entry.correction) - i64::from(correction)).abs() != 1 {
202 return Err(InvalidTable::Correction);
203 }
204 if entry.at.rem_euclid(86_400) != 0 || super::time::civil_from_seconds(entry.at).2 != 1
208 {
209 return Err(InvalidTable::Boundary);
210 }
211 previous = entry.at;
212 correction = entry.correction;
213 }
214 let table = Self {
215 start,
216 end,
217 transitions,
218 };
219 if table
220 .coordinate(Instant {
221 seconds: start,
222 leap: false,
223 nanos: 0,
224 })
225 .is_err()
226 {
227 return Err(InvalidTable::Coverage);
228 }
229 Ok(table)
230 }
231
232 #[must_use]
234 pub const fn transitions(&self) -> &'a [Transition] {
235 self.transitions
236 }
237
238 pub fn shift(&self, at: Instant, nanos: i128) -> Result<Instant, Uncertainty> {
246 let here = self.coordinate(at)?;
247 let moved = here.checked_add(nanos).ok_or(Uncertainty::OutOfRange)?;
248 let second = moved.div_euclid(NANOS);
249 let fraction =
250 u32::try_from(moved.rem_euclid(NANOS)).map_err(|_| Uncertainty::OutOfRange)?;
251 let count = self
252 .transitions
253 .partition_point(|entry| entry.linear() <= second);
254 let correction = self.correction_before(count);
255 let result = if let Some(next) = self.transitions.get(count)
258 && next.correction > correction
259 && second == next.linear() - 1
260 {
261 Instant {
262 seconds: next.at - 1,
263 leap: true,
264 nanos: fraction,
265 }
266 } else {
267 let seconds = i64::try_from(second - i128::from(correction))
268 .map_err(|_| Uncertainty::OutOfRange)?;
269 Instant {
270 seconds,
271 leap: false,
272 nanos: fraction,
273 }
274 };
275 self.coordinate(result)?;
276 Ok(result)
277 }
278
279 fn correction_before(&self, count: usize) -> i32 {
280 if count == 0 {
281 0
282 } else {
283 self.transitions[count - 1].correction
284 }
285 }
286
287 fn coordinate(&self, at: Instant) -> Result<i128, Uncertainty> {
288 if at.seconds < self.start || at.seconds >= self.end {
289 return Err(Uncertainty::OutsideCoverage);
290 }
291 let count = self
292 .transitions
293 .partition_point(|entry| entry.at <= at.seconds);
294 let correction = self.correction_before(count);
295 let next = self
296 .transitions
297 .get(count)
298 .filter(|entry| i128::from(entry.at) == i128::from(at.seconds) + 1);
299 if at.leap {
300 if next.is_none_or(|entry| entry.correction <= correction) {
301 return Err(Uncertainty::NotInUtc);
302 }
303 } else if next.is_some_and(|entry| entry.correction < correction) {
304 return Err(Uncertainty::NotInUtc);
305 }
306 Ok(
307 (i128::from(at.seconds) + i128::from(correction) + i128::from(at.leap)) * NANOS
308 + i128::from(at.nanos),
309 )
310 }
311}
312
313#[cfg(test)]
314mod tests;