1use crate::protocol;
2#[cfg(feature = "std")]
3use std::time;
4
5pub const EPOCH_DELTA: i64 = 2_208_988_800;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct InvalidInstantError {
14 pub secs: i64,
16 pub subsec_nanos: i32,
18}
19
20impl core::fmt::Display for InvalidInstantError {
21 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22 write!(
23 f,
24 "invalid instant: secs ({}) and subsec_nanos ({}) have mixed signs",
25 self.secs, self.subsec_nanos
26 )
27 }
28}
29
30#[cfg(feature = "std")]
31impl std::error::Error for InvalidInstantError {}
32
33pub const ERA_SECONDS: i64 = 4_294_967_296; const NTP_SCALE: f64 = u32::MAX as f64;
41
42const NTP_SCALE_64: f64 = u64::MAX as f64;
44
45#[derive(Copy, Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
69pub struct Instant {
70 secs: i64,
71 subsec_nanos: i32,
72}
73
74impl Instant {
75 pub fn new(secs: i64, subsec_nanos: i32) -> Result<Instant, InvalidInstantError> {
81 if (secs > 0 && subsec_nanos < 0) || (secs < 0 && subsec_nanos > 0) {
82 return Err(InvalidInstantError { secs, subsec_nanos });
83 }
84 Ok(Instant { secs, subsec_nanos })
85 }
86
87 #[cfg(feature = "std")]
96 pub fn now() -> Self {
97 match time::SystemTime::now().duration_since(time::UNIX_EPOCH) {
98 Ok(duration) => {
99 let secs = duration.as_secs() as i64;
100 let subsec_nanos = duration.subsec_nanos() as i32;
101 Instant::new(secs, subsec_nanos)
102 .expect("system clock produces same-sign components")
103 }
104 Err(sys_time_err) => {
105 let duration_pre_unix_epoch = sys_time_err.duration();
106 let secs = -(duration_pre_unix_epoch.as_secs() as i64);
107 let subsec_nanos = -(duration_pre_unix_epoch.subsec_nanos() as i32);
108 Instant::new(secs, subsec_nanos)
109 .expect("system clock produces same-sign components")
110 }
111 }
112 }
113
114 pub fn secs(&self) -> i64 {
116 self.secs
117 }
118
119 pub fn subsec_nanos(&self) -> i32 {
121 self.subsec_nanos
122 }
123}
124
125fn era_aware_ntp_seconds(raw_seconds: u32, pivot: &Instant) -> i64 {
132 let pivot_ntp = pivot.secs + EPOCH_DELTA;
133 let raw = raw_seconds as i64;
134
135 let pivot_era = pivot_ntp.div_euclid(ERA_SECONDS);
137 let candidate = pivot_era * ERA_SECONDS + raw;
138
139 let diff = candidate - pivot_ntp;
142 if diff > ERA_SECONDS / 2 {
143 candidate - ERA_SECONDS
144 } else if diff < -(ERA_SECONDS / 2) {
145 candidate + ERA_SECONDS
146 } else {
147 candidate
148 }
149}
150
151pub fn timestamp_to_instant(ts: protocol::TimestampFormat, pivot: &Instant) -> Instant {
161 let ntp_secs = era_aware_ntp_seconds(ts.seconds, pivot);
162 let secs = ntp_secs - EPOCH_DELTA;
163 let subsec_nanos = (ts.fraction as f64 / NTP_SCALE * 1e9) as i32;
164 Instant::new(secs, subsec_nanos).expect("era-aware conversion produces same-sign components")
165}
166
167impl From<protocol::ShortFormat> for Instant {
170 fn from(t: protocol::ShortFormat) -> Self {
171 let secs = t.seconds as i64 - EPOCH_DELTA;
172 let subsec_nanos = (t.fraction as f64 / NTP_SCALE * 1e9) as i32;
173 let (secs, subsec_nanos) = if secs < 0 && subsec_nanos > 0 {
177 (secs + 1, subsec_nanos - 1_000_000_000)
178 } else {
179 (secs, subsec_nanos)
180 };
181 Instant { secs, subsec_nanos }
182 }
183}
184
185#[cfg(feature = "std")]
186impl From<protocol::TimestampFormat> for Instant {
187 fn from(t: protocol::TimestampFormat) -> Self {
193 timestamp_to_instant(t, &Instant::now())
194 }
195}
196
197impl From<Instant> for protocol::ShortFormat {
198 fn from(t: Instant) -> Self {
199 let sec = t.secs() + EPOCH_DELTA;
200 let frac = t.subsec_nanos() as f64 * NTP_SCALE / 1e9;
201 protocol::ShortFormat {
202 seconds: sec as u16,
203 fraction: frac as u16,
204 }
205 }
206}
207
208impl From<Instant> for protocol::TimestampFormat {
209 fn from(t: Instant) -> Self {
215 let sec = t.secs() + EPOCH_DELTA;
216 let frac = t.subsec_nanos() as f64 * NTP_SCALE / 1e9;
217 protocol::TimestampFormat {
218 seconds: sec as u32,
219 fraction: frac as u32,
220 }
221 }
222}
223
224impl From<protocol::DateFormat> for Instant {
225 fn from(d: protocol::DateFormat) -> Self {
229 let ntp_secs = d.era_number as i64 * ERA_SECONDS + d.era_offset as i64;
230 let secs = ntp_secs - EPOCH_DELTA;
231 let subsec_nanos = (d.fraction as f64 / NTP_SCALE_64 * 1e9) as i32;
232 Instant::new(secs, subsec_nanos).expect("DateFormat produces same-sign components")
233 }
234}
235
236impl From<Instant> for protocol::DateFormat {
237 fn from(t: Instant) -> Self {
241 let ntp_secs = t.secs() + EPOCH_DELTA;
242 let era_number = ntp_secs.div_euclid(ERA_SECONDS) as i32;
243 let era_offset = ntp_secs.rem_euclid(ERA_SECONDS) as u32;
244 let fraction = (t.subsec_nanos().unsigned_abs() as f64 / 1e9 * NTP_SCALE_64) as u64;
245 protocol::DateFormat {
246 era_number,
247 era_offset,
248 fraction,
249 }
250 }
251}
252
253#[cfg(all(test, feature = "std"))]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn era0_timestamp_to_instant() {
259 let ts = protocol::TimestampFormat {
261 seconds: 3_913_056_000,
262 fraction: 0,
263 };
264 let pivot = Instant::new(1_704_067_200, 0).unwrap();
265 let result = timestamp_to_instant(ts, &pivot);
266 assert_eq!(result.secs(), 1_704_067_200);
267 }
268
269 #[test]
270 fn era1_timestamp_with_era1_pivot() {
271 let ts = protocol::TimestampFormat {
274 seconds: 100_000_000,
275 fraction: 0,
276 };
277 let pivot = Instant::new(2_185_978_496, 0).unwrap();
278 let result = timestamp_to_instant(ts, &pivot);
279 assert_eq!(result.secs(), 2_185_978_496);
280 }
281
282 #[test]
283 fn era_boundary_pivot_before_ts_after() {
284 let pivot = Instant::new(2_082_758_400, 0).unwrap(); let ts = protocol::TimestampFormat {
287 seconds: 1000,
288 fraction: 0,
289 };
290 let result = timestamp_to_instant(ts, &pivot);
291 let expected = ERA_SECONDS + 1000 - EPOCH_DELTA;
292 assert_eq!(result.secs(), expected);
293 }
294
295 #[test]
296 fn era_boundary_pivot_after_ts_before() {
297 let pivot = Instant::new(2_087_942_400, 0).unwrap(); let ts = protocol::TimestampFormat {
300 seconds: u32::MAX,
301 fraction: 0,
302 };
303 let result = timestamp_to_instant(ts, &pivot);
304 let expected = u32::MAX as i64 - EPOCH_DELTA;
305 assert_eq!(result.secs(), expected);
306 }
307
308 #[test]
309 fn date_format_roundtrip_era0() {
310 let instant = Instant::new(1_704_067_200, 500_000_000).unwrap();
311 let date: protocol::DateFormat = instant.into();
312 assert_eq!(date.era_number, 0);
313 let back: Instant = date.into();
314 assert_eq!(back.secs(), instant.secs());
315 assert!((back.subsec_nanos() - instant.subsec_nanos()).abs() <= 1);
316 }
317
318 #[test]
319 fn date_format_roundtrip_era1() {
320 let instant = Instant::new(2_185_978_496, 0).unwrap(); let date: protocol::DateFormat = instant.into();
322 assert_eq!(date.era_number, 1);
323 let back: Instant = date.into();
324 assert_eq!(back.secs(), instant.secs());
325 }
326
327 #[test]
328 fn timestamp_format_roundtrip_with_pivot() {
329 let original = Instant::new(1_704_067_200, 0).unwrap();
330 let ts: protocol::TimestampFormat = original.into();
331 let restored = timestamp_to_instant(ts, &original);
332 assert_eq!(restored.secs(), original.secs());
333 }
334
335 #[test]
336 fn date_format_negative_era() {
337 let instant = Instant::new(-2_300_000_000, 0).unwrap();
339 let date: protocol::DateFormat = instant.into();
340 assert_eq!(date.era_number, -1);
341 let back: Instant = date.into();
342 assert_eq!(back.secs(), instant.secs());
343 }
344}