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

            
7
use chrono::{DateTime, Utc};
8

            
9
const RWHOD_TIMESTAMP_CORRECTION_WINDOW: i64 = 0x40000000_i64;
10
const RWHOD_TIMESTAMP_WRAP_INCREMENT: i64 = 0x1_0000_0000_i64;
11

            
12
const 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)]
17
pub 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)]
38
5
pub fn rwhod_time_correction(now: DateTime<Utc>, recvtime: i32) -> i64 {
39
5
    let delta = now.timestamp() - i64::from(recvtime);
40

            
41
5
    if delta <= RWHOD_TIMESTAMP_CORRECTION_WINDOW {
42
3
        return 0;
43
2
    }
44

            
45
2
    let wrap_count = (delta - RWHOD_TIMESTAMP_CORRECTION_WINDOW - 1)
46
2
        .div_euclid(RWHOD_TIMESTAMP_WRAP_INCREMENT)
47
2
        + 1;
48

            
49
2
    wrap_count * RWHOD_TIMESTAMP_WRAP_INCREMENT
50
5
}
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.
59
136
pub fn decode_rwhod_timestamp_near_time_with_forward_allowance(
60
136
    raw: i32,
61
136
    time: DateTime<Utc>,
62
136
    forward_allowance: i64,
63
136
) -> Result<DateTime<Utc>, String> {
64
136
    debug_assert!(forward_allowance >= 0);
65
136
    debug_assert!(forward_allowance <= RWHOD_TIMESTAMP_WRAP_INCREMENT);
66

            
67
136
    let raw = i64::from(raw);
68
136
    let candidate = raw
69
136
        + (time.timestamp() + forward_allowance - raw).div_euclid(RWHOD_TIMESTAMP_WRAP_INCREMENT)
70
136
            * RWHOD_TIMESTAMP_WRAP_INCREMENT;
71

            
72
136
    DateTime::from_timestamp_secs(candidate).ok_or(format!(
73
        "Invalid timestamp: {} near {} with forward allowance {}",
74
        raw, time, forward_allowance
75
    ))
76
136
}
77

            
78
/// Decodes a raw rwhod timestamp (i32) into a DateTime<Utc> using the default
79
/// wrap-sized interval around the reference time.
80
133
pub fn decode_rwhod_timestamp_near_time(
81
133
    raw: i32,
82
133
    time: DateTime<Utc>,
83
133
) -> Result<DateTime<Utc>, String> {
84
133
    decode_rwhod_timestamp_near_time_with_forward_allowance(
85
133
        raw,
86
133
        time,
87
        RWHOD_TIMESTAMP_FORWARD_ALLOWANCE,
88
    )
89
133
}
90

            
91
pub struct RwhodTimestamps {
92
    pub boottime: DateTime<Utc>,
93
    pub sendtime: DateTime<Utc>,
94
    pub recvtime: Option<DateTime<Utc>>,
95
}
96

            
97
7
pub fn decode_rwhod_timestamps(
98
7
    wd_boottime: i32,
99
7
    wd_sendtime: i32,
100
7
    wd_recvtime: i32,
101
7
    now: DateTime<Utc>,
102
7
) -> Result<RwhodTimestamps, String> {
103
7
    let recvtime = if wd_recvtime == 0 {
104
1
        None
105
    } else {
106
6
        Some(decode_rwhod_timestamp_near_time(wd_recvtime, now)?)
107
    };
108

            
109
7
    let sendtime = decode_rwhod_timestamp_near_time(wd_sendtime, now)?;
110
7
    let boottime = decode_rwhod_timestamp_near_time(wd_boottime, now)?;
111

            
112
7
    Ok(RwhodTimestamps {
113
7
        boottime,
114
7
        sendtime,
115
7
        recvtime,
116
7
    })
117
7
}
118

            
119
/// Encodes a DateTime<Utc> into a raw rwhod timestamp (i32).
120
74
pub fn encode_rwhod_timestamp(timestamp: DateTime<Utc>) -> i32 {
121
74
    timestamp.timestamp() as i32
122
74
}
123

            
124
#[cfg(test)]
125
mod tests {
126
    use super::*;
127

            
128
    #[test]
129
1
    fn test_rwhod_time_correction_no_wrap() {
130
1
        let now = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap();
131
1
        let recvtime = i32::MAX;
132
1
        let correction = rwhod_time_correction(now, recvtime);
133

            
134
1
        assert_eq!(correction, 0);
135
1
    }
136

            
137
    #[test]
138
1
    fn test_rwhod_time_correction_with_single_wrap() {
139
1
        let now = DateTime::from_timestamp(i32::MAX as i64 + 2, 0).unwrap();
140
1
        let recvtime = i32::MAX.wrapping_add(1);
141
1
        let correction = rwhod_time_correction(now, recvtime);
142

            
143
1
        assert_eq!(correction, RWHOD_TIMESTAMP_WRAP_INCREMENT * 1);
144
1
    }
145

            
146
    #[test]
147
1
    fn test_rwhod_time_correction_with_multiple_wraps() {
148
1
        let now = DateTime::from_timestamp(i32::MAX as i64 + RWHOD_TIMESTAMP_WRAP_INCREMENT + 1, 0)
149
1
            .unwrap();
150
1
        let recvtime = i32::MAX.wrapping_add(1);
151
1
        let correction = rwhod_time_correction(now, recvtime);
152

            
153
1
        assert_eq!(correction, RWHOD_TIMESTAMP_WRAP_INCREMENT * 2);
154
1
    }
155

            
156
    #[test]
157
1
    fn test_rwhod_time_correction_with_negative_delta() {
158
1
        let now = DateTime::from_timestamp(i32::MAX as i64 - 1, 0).unwrap();
159
1
        let recvtime = i32::MAX;
160
1
        let correction = rwhod_time_correction(now, recvtime);
161

            
162
1
        assert_eq!(correction, 0);
163
1
    }
164

            
165
    #[test]
166
1
    fn test_rwhod_time_correction_with_large_negative_delta() {
167
1
        let now = DateTime::from_timestamp(i32::MIN as i64 - 1, 0).unwrap();
168
1
        let recvtime = i32::MIN;
169
1
        let correction = rwhod_time_correction(now, recvtime);
170

            
171
1
        assert_eq!(correction, 0);
172
1
    }
173

            
174
    #[test]
175
1
    fn test_decode_rwhod_timestamp_near_time_no_wrap() {
176
1
        let now = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap();
177
1
        let expected = now - chrono::Duration::days(1);
178
1
        let raw = expected.timestamp() as i32;
179

            
180
1
        let decoded = decode_rwhod_timestamp_near_time(raw, now).unwrap();
181
1
        assert_eq!(decoded, expected);
182
1
    }
183

            
184
    #[test]
185
1
    fn test_decode_rwhod_timestamp_near_time_with_wrap() {
186
1
        let now = DateTime::from_timestamp(i32::MAX as i64 + RWHOD_TIMESTAMP_WRAP_INCREMENT + 1, 0)
187
1
            .unwrap();
188
1
        let expected = now - chrono::Duration::days(1);
189
1
        let raw = expected.timestamp() as i32;
190

            
191
1
        let decoded = decode_rwhod_timestamp_near_time(raw, now).unwrap();
192
1
        assert_eq!(decoded, expected);
193
1
    }
194

            
195
    #[test]
196
1
    fn test_decode_rwhod_timestamp_near_time_with_negative_delta() {
197
1
        let time = DateTime::from_timestamp(-(RWHOD_TIMESTAMP_WRAP_INCREMENT / 2) - 10, 0).unwrap();
198
1
        let raw = 0;
199

            
200
1
        let decoded = decode_rwhod_timestamp_near_time_with_forward_allowance(
201
1
            raw,
202
1
            time,
203
1
            RWHOD_TIMESTAMP_WRAP_INCREMENT / 2,
204
        )
205
1
        .unwrap();
206
1
        assert_eq!(
207
            decoded,
208
1
            DateTime::from_timestamp(-RWHOD_TIMESTAMP_WRAP_INCREMENT, 0).unwrap()
209
        );
210
1
    }
211

            
212
    #[test]
213
1
    fn test_decode_rwhod_timestamp_near_time_uses_forward_allowance() {
214
1
        let time = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap();
215
1
        let expected = time + chrono::Duration::seconds(5);
216
1
        let raw = expected.timestamp() as i32;
217

            
218
1
        let decoded =
219
1
            decode_rwhod_timestamp_near_time_with_forward_allowance(raw, time, 5).unwrap();
220
1
        assert_eq!(decoded, expected);
221
1
    }
222

            
223
    #[test]
224
1
    fn test_decode_rwhod_timestamp_near_time_small_forward_allowance_prefers_previous_wrap() {
225
1
        let time = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap();
226
1
        let future = time + chrono::Duration::seconds(RWHOD_TIMESTAMP_WRAP_INCREMENT / 4);
227
1
        let raw = future.timestamp() as i32;
228

            
229
1
        let decoded = decode_rwhod_timestamp_near_time_with_forward_allowance(
230
1
            raw,
231
1
            time,
232
1
            RWHOD_TIMESTAMP_WRAP_INCREMENT / 10,
233
        )
234
1
        .unwrap();
235
1
        assert_eq!(
236
            decoded,
237
1
            future - chrono::Duration::seconds(RWHOD_TIMESTAMP_WRAP_INCREMENT)
238
        );
239
1
    }
240

            
241
    #[test]
242
1
    fn test_encode_rwhod_timestamp_no_wrap() {
243
1
        let timestamp = DateTime::from_timestamp(i32::MAX as i64, 0).unwrap();
244
1
        let encoded = encode_rwhod_timestamp(timestamp);
245
1
        assert_eq!(encoded, i32::MAX);
246
1
    }
247

            
248
    #[test]
249
1
    fn test_encode_rwhod_timestamp_with_wrap() {
250
1
        let timestamp = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap();
251
1
        let encoded = encode_rwhod_timestamp(timestamp);
252
1
        assert_eq!(encoded, i32::MIN);
253
1
    }
254
}