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 Instant::new(secs, subsec_nanos).expect("ShortFormat produces same-sign components")
174 }
175}
176
177#[cfg(feature = "std")]
178impl From<protocol::TimestampFormat> for Instant {
179 fn from(t: protocol::TimestampFormat) -> Self {
185 timestamp_to_instant(t, &Instant::now())
186 }
187}
188
189impl From<Instant> for protocol::ShortFormat {
190 fn from(t: Instant) -> Self {
191 let sec = t.secs() + EPOCH_DELTA;
192 let frac = t.subsec_nanos() as f64 * NTP_SCALE / 1e9;
193 protocol::ShortFormat {
194 seconds: sec as u16,
195 fraction: frac as u16,
196 }
197 }
198}
199
200impl From<Instant> for protocol::TimestampFormat {
201 fn from(t: Instant) -> Self {
207 let sec = t.secs() + EPOCH_DELTA;
208 let frac = t.subsec_nanos() as f64 * NTP_SCALE / 1e9;
209 protocol::TimestampFormat {
210 seconds: sec as u32,
211 fraction: frac as u32,
212 }
213 }
214}
215
216impl From<protocol::DateFormat> for Instant {
217 fn from(d: protocol::DateFormat) -> Self {
221 let ntp_secs = d.era_number as i64 * ERA_SECONDS + d.era_offset as i64;
222 let secs = ntp_secs - EPOCH_DELTA;
223 let subsec_nanos = (d.fraction as f64 / NTP_SCALE_64 * 1e9) as i32;
224 Instant::new(secs, subsec_nanos).expect("DateFormat produces same-sign components")
225 }
226}
227
228impl From<Instant> for protocol::DateFormat {
229 fn from(t: Instant) -> Self {
233 let ntp_secs = t.secs() + EPOCH_DELTA;
234 let era_number = ntp_secs.div_euclid(ERA_SECONDS) as i32;
235 let era_offset = ntp_secs.rem_euclid(ERA_SECONDS) as u32;
236 let fraction = (t.subsec_nanos().unsigned_abs() as f64 / 1e9 * NTP_SCALE_64) as u64;
237 protocol::DateFormat {
238 era_number,
239 era_offset,
240 fraction,
241 }
242 }
243}
244
245#[cfg(all(test, feature = "std"))]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn era0_timestamp_to_instant() {
251 let ts = protocol::TimestampFormat {
253 seconds: 3_913_056_000,
254 fraction: 0,
255 };
256 let pivot = Instant::new(1_704_067_200, 0).unwrap();
257 let result = timestamp_to_instant(ts, &pivot);
258 assert_eq!(result.secs(), 1_704_067_200);
259 }
260
261 #[test]
262 fn era1_timestamp_with_era1_pivot() {
263 let ts = protocol::TimestampFormat {
266 seconds: 100_000_000,
267 fraction: 0,
268 };
269 let pivot = Instant::new(2_185_978_496, 0).unwrap();
270 let result = timestamp_to_instant(ts, &pivot);
271 assert_eq!(result.secs(), 2_185_978_496);
272 }
273
274 #[test]
275 fn era_boundary_pivot_before_ts_after() {
276 let pivot = Instant::new(2_082_758_400, 0).unwrap(); let ts = protocol::TimestampFormat {
279 seconds: 1000,
280 fraction: 0,
281 };
282 let result = timestamp_to_instant(ts, &pivot);
283 let expected = ERA_SECONDS + 1000 - EPOCH_DELTA;
284 assert_eq!(result.secs(), expected);
285 }
286
287 #[test]
288 fn era_boundary_pivot_after_ts_before() {
289 let pivot = Instant::new(2_087_942_400, 0).unwrap(); let ts = protocol::TimestampFormat {
292 seconds: u32::MAX,
293 fraction: 0,
294 };
295 let result = timestamp_to_instant(ts, &pivot);
296 let expected = u32::MAX as i64 - EPOCH_DELTA;
297 assert_eq!(result.secs(), expected);
298 }
299
300 #[test]
301 fn date_format_roundtrip_era0() {
302 let instant = Instant::new(1_704_067_200, 500_000_000).unwrap();
303 let date: protocol::DateFormat = instant.into();
304 assert_eq!(date.era_number, 0);
305 let back: Instant = date.into();
306 assert_eq!(back.secs(), instant.secs());
307 assert!((back.subsec_nanos() - instant.subsec_nanos()).abs() <= 1);
308 }
309
310 #[test]
311 fn date_format_roundtrip_era1() {
312 let instant = Instant::new(2_185_978_496, 0).unwrap(); let date: protocol::DateFormat = instant.into();
314 assert_eq!(date.era_number, 1);
315 let back: Instant = date.into();
316 assert_eq!(back.secs(), instant.secs());
317 }
318
319 #[test]
320 fn timestamp_format_roundtrip_with_pivot() {
321 let original = Instant::new(1_704_067_200, 0).unwrap();
322 let ts: protocol::TimestampFormat = original.into();
323 let restored = timestamp_to_instant(ts, &original);
324 assert_eq!(restored.secs(), original.secs());
325 }
326
327 #[test]
328 fn date_format_negative_era() {
329 let instant = Instant::new(-2_300_000_000, 0).unwrap();
331 let date: protocol::DateFormat = instant.into();
332 assert_eq!(date.era_number, -1);
333 let back: Instant = date.into();
334 assert_eq!(back.secs(), instant.secs());
335 }
336}