Skip to main content

roowho2_lib/proto/rwhod_protocol/
time_codec.rs

1//! The original rwhod protocol uses 32-bit integers for timestamps,
2//!  which will cause overflow issues after 2038-01-19. To mitigate this,
3//!  we decode timestamps by looking at the time the packet was received,
4//!  comparing it to the current time or the time the packet was received,
5//!  and ensuring any overflowed timestamps are corrected accordingly.
6
7use chrono::{DateTime, Utc};
8
9const RWHOD_TIMESTAMP_CORRECTION_WINDOW: i64 = 0x40000000_i64;
10const RWHOD_TIMESTAMP_WRAP_INCREMENT: i64 = 0x1_0000_0000_i64;
11
12const RWHOD_TIMESTAMP_FORWARD_ALLOWANCE: i64 = RWHOD_TIMESTAMP_CORRECTION_WINDOW;
13
14/// Decodes a raw rwhod timestamp (i32) into a DateTime<Utc>,
15/// adding a correction value to count for any detected overflows.
16#[allow(dead_code)]
17pub fn decode_rwhod_timestamp(raw: i32, correction: i64) -> Result<DateTime<Utc>, String> {
18    DateTime::from_timestamp_secs(i64::from(raw) + correction).ok_or(format!(
19        "Invalid timestamp: {} with correction {}",
20        raw, correction
21    ))
22}
23
24/// Calculates the correction value for a received rwhod timestamp (i32),
25/// based on the current time (now) and the received timestamp (recvtime).
26///
27/// If the difference between the current time and the received timestamp is
28/// above a certain threshold (`RWHOD_TIMESTAMP_CORRECTION_WINDOW`),
29/// this function attempts to determine how many times the timestamp has
30/// wrapped around and returns the appropriate additional correction value
31/// to account for the overflow.
32///
33/// This function is roughly equivalent to the logic used in the original
34/// program to handle timestamp overflows, but please consider using
35/// `decode_rwhod_timestamp_near_time` for most use cases, as it is designed
36/// to be more robust.
37#[allow(dead_code)]
38pub fn rwhod_time_correction(now: DateTime<Utc>, recvtime: i32) -> i64 {
39    let delta = now.timestamp() - i64::from(recvtime);
40
41    if delta <= RWHOD_TIMESTAMP_CORRECTION_WINDOW {
42        return 0;
43    }
44
45    let wrap_count = (delta - RWHOD_TIMESTAMP_CORRECTION_WINDOW - 1)
46        .div_euclid(RWHOD_TIMESTAMP_WRAP_INCREMENT)
47        + 1;
48
49    wrap_count * RWHOD_TIMESTAMP_WRAP_INCREMENT
50}
51
52/// Decodes a raw rwhod timestamp (i32) into a DateTime<Utc> using a wrap-sized
53/// interval around the provided `time` reference.
54///
55/// You can think of this function as looking at the current timestamp and choosing
56/// the closest valid timestamp behind it for the given raw value, while also allowing
57/// it to be a certain amount of time in the future (forward allowance) to account for
58/// clock skew or other synchronization issues.
59pub fn decode_rwhod_timestamp_near_time_with_forward_allowance(
60    raw: i32,
61    time: DateTime<Utc>,
62    forward_allowance: i64,
63) -> Result<DateTime<Utc>, String> {
64    debug_assert!(forward_allowance >= 0);
65    debug_assert!(forward_allowance <= RWHOD_TIMESTAMP_WRAP_INCREMENT);
66
67    let raw = i64::from(raw);
68    let candidate = raw
69        + (time.timestamp() + forward_allowance - raw).div_euclid(RWHOD_TIMESTAMP_WRAP_INCREMENT)
70            * RWHOD_TIMESTAMP_WRAP_INCREMENT;
71
72    DateTime::from_timestamp_secs(candidate).ok_or(format!(
73        "Invalid timestamp: {} near {} with forward allowance {}",
74        raw, time, forward_allowance
75    ))
76}
77
78/// Decodes a raw rwhod timestamp (i32) into a DateTime<Utc> using the default
79/// wrap-sized interval around the reference time.
80pub fn decode_rwhod_timestamp_near_time(
81    raw: i32,
82    time: DateTime<Utc>,
83) -> Result<DateTime<Utc>, String> {
84    decode_rwhod_timestamp_near_time_with_forward_allowance(
85        raw,
86        time,
87        RWHOD_TIMESTAMP_FORWARD_ALLOWANCE,
88    )
89}
90
91pub struct RwhodTimestamps {
92    pub boottime: DateTime<Utc>,
93    pub sendtime: DateTime<Utc>,
94    pub recvtime: Option<DateTime<Utc>>,
95}
96
97pub fn decode_rwhod_timestamps(
98    wd_boottime: i32,
99    wd_sendtime: i32,
100    wd_recvtime: i32,
101    now: DateTime<Utc>,
102) -> Result<RwhodTimestamps, String> {
103    let recvtime = if wd_recvtime == 0 {
104        None
105    } else {
106        Some(decode_rwhod_timestamp_near_time(wd_recvtime, now)?)
107    };
108
109    let sendtime = decode_rwhod_timestamp_near_time(wd_sendtime, now)?;
110    let boottime = decode_rwhod_timestamp_near_time(wd_boottime, now)?;
111
112    Ok(RwhodTimestamps {
113        boottime,
114        sendtime,
115        recvtime,
116    })
117}
118
119/// Encodes a DateTime<Utc> into a raw rwhod timestamp (i32).
120pub fn encode_rwhod_timestamp(timestamp: DateTime<Utc>) -> i32 {
121    timestamp.timestamp() as i32
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_rwhod_time_correction_no_wrap() {
130        let now = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap();
131        let recvtime = i32::MAX;
132        let correction = rwhod_time_correction(now, recvtime);
133
134        assert_eq!(correction, 0);
135    }
136
137    #[test]
138    fn test_rwhod_time_correction_with_single_wrap() {
139        let now = DateTime::from_timestamp(i32::MAX as i64 + 2, 0).unwrap();
140        let recvtime = i32::MAX.wrapping_add(1);
141        let correction = rwhod_time_correction(now, recvtime);
142
143        assert_eq!(correction, RWHOD_TIMESTAMP_WRAP_INCREMENT * 1);
144    }
145
146    #[test]
147    fn test_rwhod_time_correction_with_multiple_wraps() {
148        let now = DateTime::from_timestamp(i32::MAX as i64 + RWHOD_TIMESTAMP_WRAP_INCREMENT + 1, 0)
149            .unwrap();
150        let recvtime = i32::MAX.wrapping_add(1);
151        let correction = rwhod_time_correction(now, recvtime);
152
153        assert_eq!(correction, RWHOD_TIMESTAMP_WRAP_INCREMENT * 2);
154    }
155
156    #[test]
157    fn test_rwhod_time_correction_with_negative_delta() {
158        let now = DateTime::from_timestamp(i32::MAX as i64 - 1, 0).unwrap();
159        let recvtime = i32::MAX;
160        let correction = rwhod_time_correction(now, recvtime);
161
162        assert_eq!(correction, 0);
163    }
164
165    #[test]
166    fn test_rwhod_time_correction_with_large_negative_delta() {
167        let now = DateTime::from_timestamp(i32::MIN as i64 - 1, 0).unwrap();
168        let recvtime = i32::MIN;
169        let correction = rwhod_time_correction(now, recvtime);
170
171        assert_eq!(correction, 0);
172    }
173
174    #[test]
175    fn test_decode_rwhod_timestamp_near_time_no_wrap() {
176        let now = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap();
177        let expected = now - chrono::Duration::days(1);
178        let raw = expected.timestamp() as i32;
179
180        let decoded = decode_rwhod_timestamp_near_time(raw, now).unwrap();
181        assert_eq!(decoded, expected);
182    }
183
184    #[test]
185    fn test_decode_rwhod_timestamp_near_time_with_wrap() {
186        let now = DateTime::from_timestamp(i32::MAX as i64 + RWHOD_TIMESTAMP_WRAP_INCREMENT + 1, 0)
187            .unwrap();
188        let expected = now - chrono::Duration::days(1);
189        let raw = expected.timestamp() as i32;
190
191        let decoded = decode_rwhod_timestamp_near_time(raw, now).unwrap();
192        assert_eq!(decoded, expected);
193    }
194
195    #[test]
196    fn test_decode_rwhod_timestamp_near_time_with_negative_delta() {
197        let time = DateTime::from_timestamp(-(RWHOD_TIMESTAMP_WRAP_INCREMENT / 2) - 10, 0).unwrap();
198        let raw = 0;
199
200        let decoded = decode_rwhod_timestamp_near_time_with_forward_allowance(
201            raw,
202            time,
203            RWHOD_TIMESTAMP_WRAP_INCREMENT / 2,
204        )
205        .unwrap();
206        assert_eq!(
207            decoded,
208            DateTime::from_timestamp(-RWHOD_TIMESTAMP_WRAP_INCREMENT, 0).unwrap()
209        );
210    }
211
212    #[test]
213    fn test_decode_rwhod_timestamp_near_time_uses_forward_allowance() {
214        let time = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap();
215        let expected = time + chrono::Duration::seconds(5);
216        let raw = expected.timestamp() as i32;
217
218        let decoded =
219            decode_rwhod_timestamp_near_time_with_forward_allowance(raw, time, 5).unwrap();
220        assert_eq!(decoded, expected);
221    }
222
223    #[test]
224    fn test_decode_rwhod_timestamp_near_time_small_forward_allowance_prefers_previous_wrap() {
225        let time = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap();
226        let future = time + chrono::Duration::seconds(RWHOD_TIMESTAMP_WRAP_INCREMENT / 4);
227        let raw = future.timestamp() as i32;
228
229        let decoded = decode_rwhod_timestamp_near_time_with_forward_allowance(
230            raw,
231            time,
232            RWHOD_TIMESTAMP_WRAP_INCREMENT / 10,
233        )
234        .unwrap();
235        assert_eq!(
236            decoded,
237            future - chrono::Duration::seconds(RWHOD_TIMESTAMP_WRAP_INCREMENT)
238        );
239    }
240
241    #[test]
242    fn test_encode_rwhod_timestamp_no_wrap() {
243        let timestamp = DateTime::from_timestamp(i32::MAX as i64, 0).unwrap();
244        let encoded = encode_rwhod_timestamp(timestamp);
245        assert_eq!(encoded, i32::MAX);
246    }
247
248    #[test]
249    fn test_encode_rwhod_timestamp_with_wrap() {
250        let timestamp = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap();
251        let encoded = encode_rwhod_timestamp(timestamp);
252        assert_eq!(encoded, i32::MIN);
253    }
254}