rusty_time_core/
refclock.rs1use core::fmt;
11
12#[derive(Clone, Copy, Debug, PartialEq)]
22pub struct RefclockSample {
23 pub local_s: f64,
25 pub offset_s: f64,
27 pub precision_log2: i8,
29 pub leap: LeapWarning,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum LeapWarning {
34 None,
35 AddSecond,
36 DeleteSecond,
37 NotSynchronized,
39}
40
41impl LeapWarning {
42 pub fn from_wire(value: i32) -> LeapWarning {
44 match value {
45 0 => LeapWarning::None,
46 1 => LeapWarning::AddSecond,
47 2 => LeapWarning::DeleteSecond,
48 _ => LeapWarning::NotSynchronized,
49 }
50 }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum RefclockError {
55 NotSynchronized,
57 OffsetTooLarge,
59 Implausible,
61 Stale,
63}
64
65impl fmt::Display for RefclockError {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 let s = match self {
68 RefclockError::NotSynchronized => "reference clock reports it is unsynchronized",
69 RefclockError::OffsetTooLarge => "reference offset is implausibly large",
70 RefclockError::Implausible => "reference timestamps are not plausible times",
71 RefclockError::Stale => "reference sample is not newer than the last one",
72 };
73 f.write_str(s)
74 }
75}
76
77pub const MAX_REFCLOCK_OFFSET_S: f64 = 16.0;
84
85const MIN_PLAUSIBLE_UNIX_S: f64 = 1_577_836_800.0;
89
90impl RefclockSample {
91 pub fn offset_s(&self) -> f64 {
93 self.offset_s
94 }
95
96 pub fn reference_s(&self) -> f64 {
102 self.local_s + self.offset_s
103 }
104
105 pub fn validate(&self, last_local_s: Option<f64>) -> Result<(), RefclockError> {
108 if self.leap == LeapWarning::NotSynchronized {
109 return Err(RefclockError::NotSynchronized);
110 }
111 if !self.local_s.is_finite()
112 || !self.offset_s.is_finite()
113 || self.local_s < MIN_PLAUSIBLE_UNIX_S
114 || self.reference_s() < MIN_PLAUSIBLE_UNIX_S
115 {
116 return Err(RefclockError::Implausible);
117 }
118 if let Some(previous) = last_local_s
119 && self.local_s <= previous
120 {
121 return Err(RefclockError::Stale);
125 }
126 if self.offset_s().abs() > MAX_REFCLOCK_OFFSET_S {
127 return Err(RefclockError::OffsetTooLarge);
128 }
129 Ok(())
130 }
131
132 pub fn dispersion_s(&self) -> f64 {
135 2f64.powi(self.precision_log2 as i32)
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn sample(local: f64, reference: f64) -> RefclockSample {
144 RefclockSample {
145 local_s: local,
146 offset_s: reference - local,
147 precision_log2: -20,
148 leap: LeapWarning::None,
149 }
150 }
151
152 fn with_offset(local: f64, offset: f64) -> RefclockSample {
154 RefclockSample {
155 local_s: local,
156 offset_s: offset,
157 precision_log2: -20,
158 leap: LeapWarning::None,
159 }
160 }
161
162 #[test]
163 fn offset_is_reference_minus_local() {
164 let s = sample(1_756_224_000.0, 1_756_224_000.5);
165 assert!((s.offset_s() - 0.5).abs() < 1e-12);
166 assert!(s.validate(None).is_ok());
167 }
168
169 #[test]
170 fn an_unsynchronized_reference_is_refused() {
171 let mut s = sample(1_756_224_000.0, 1_756_224_000.0);
172 s.leap = LeapWarning::NotSynchronized;
173 assert_eq!(s.validate(None), Err(RefclockError::NotSynchronized));
174 }
175
176 #[test]
177 fn a_stale_segment_cannot_keep_confirming_a_frozen_time() {
178 let s = sample(1_756_224_000.0, 1_756_224_000.0);
181 assert!(s.validate(Some(1_756_223_999.0)).is_ok());
182 assert_eq!(
183 s.validate(Some(1_756_224_000.0)),
184 Err(RefclockError::Stale),
185 "the same timestamp twice is not two samples"
186 );
187 assert_eq!(s.validate(Some(1_756_224_001.0)), Err(RefclockError::Stale));
188 }
189
190 #[test]
191 fn a_zeroed_segment_reads_as_implausible_not_as_1970() {
192 let s = sample(0.0, 0.0);
195 assert_eq!(s.validate(None), Err(RefclockError::Implausible));
196 let half = sample(1_756_224_000.0, 0.0);
198 assert_eq!(half.validate(None), Err(RefclockError::Implausible));
199 }
200
201 #[test]
202 fn a_wildly_stale_reading_is_refused_however_well_formed() {
203 let s = sample(1_756_224_000.0, 1_756_224_000.0 - 3600.0);
205 assert_eq!(s.validate(None), Err(RefclockError::OffsetTooLarge));
206 let ok = sample(1_756_224_000.0, 1_756_224_000.0 + 15.0);
208 assert!(ok.validate(None).is_ok());
209 }
210
211 #[test]
212 fn an_offset_survives_a_round_trip_exactly() {
213 for offset in [1e-9, -1e-9, 1.5e-3, -1.5e-3, 123e-6] {
217 let s = with_offset(1_756_224_000.0, offset);
218 assert_eq!(
219 s.offset_s(),
220 offset,
221 "offset {offset} did not survive storage exactly"
222 );
223 }
224 }
225
226 #[test]
227 fn nan_and_infinity_are_not_times() {
228 for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
229 assert_eq!(
230 sample(bad, 1_756_224_000.0).validate(None),
231 Err(RefclockError::Implausible)
232 );
233 assert_eq!(
234 with_offset(1_756_224_000.0, bad).validate(None),
235 Err(RefclockError::Implausible)
236 );
237 }
238 }
239
240 #[test]
241 fn leap_decoding_matches_the_wire_values() {
242 assert_eq!(LeapWarning::from_wire(0), LeapWarning::None);
243 assert_eq!(LeapWarning::from_wire(1), LeapWarning::AddSecond);
244 assert_eq!(LeapWarning::from_wire(2), LeapWarning::DeleteSecond);
245 for unknown in [3, 4, -1, 99] {
247 assert_eq!(
248 LeapWarning::from_wire(unknown),
249 LeapWarning::NotSynchronized
250 );
251 }
252 }
253
254 #[test]
255 fn dispersion_follows_the_precision_claim() {
256 let mut s = sample(1_756_224_000.0, 1_756_224_000.0);
257 s.precision_log2 = -20; assert!((s.dispersion_s() - 9.5367e-7).abs() < 1e-9);
259 s.precision_log2 = 0; assert!((s.dispersion_s() - 1.0).abs() < 1e-12);
261 }
262}