From 3297d145b4a589e21567bca19b5da1be188abe13 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Sun, 19 Jul 2026 23:10:59 +0900 Subject: [PATCH] proto/rwhod: add forward-allowance based overflow correction --- src/proto/rwhod_protocol.rs | 176 +++++++++------- src/proto/rwhod_protocol/time_codec.rs | 268 +++++++++++++++++-------- 2 files changed, 284 insertions(+), 160 deletions(-) diff --git a/src/proto/rwhod_protocol.rs b/src/proto/rwhod_protocol.rs index 762155e..3806847 100644 --- a/src/proto/rwhod_protocol.rs +++ b/src/proto/rwhod_protocol.rs @@ -6,10 +6,9 @@ use bytes::{Buf, BufMut, BytesMut}; use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; -use time_codec::{decode_rwhod_timestamp_near_time, decode_rwhod_timestamp_not_after}; - use crate::proto::rwhod_protocol::time_codec::{ - decode_rwhod_timestamp, encode_rwhod_timestamp, rwhod_time_correction, + RwhodTimestamps, decode_rwhod_timestamp_near_time, decode_rwhod_timestamps, + encode_rwhod_timestamp, }; /// Classic C struct for utmp data for a single user session. @@ -371,28 +370,11 @@ impl TryFrom for WhodStatusUpdate { } let now = Utc::now(); - let recvtime_correction = rwhod_time_correction(now, value.wd_recvtime); - - let recvtime = if value.wd_recvtime == 0 { - None - } else { - Some(decode_rwhod_timestamp( - value.wd_recvtime, - recvtime_correction, - )?) - }; - - let recvtime_upper_bound = recvtime.unwrap_or(now); - - let sendtime = if recvtime.is_some() { - decode_rwhod_timestamp_not_after( - value.wd_sendtime, - recvtime_correction, - recvtime_upper_bound, - )? - } else { - decode_rwhod_timestamp_near_time(value.wd_sendtime, now)? - }; + let RwhodTimestamps { + boottime, + sendtime, + recvtime, + } = decode_rwhod_timestamps(value.wd_boottime, value.wd_sendtime, value.wd_recvtime, now)?; let hostname_end = value .wd_hostname @@ -402,35 +384,13 @@ impl TryFrom for WhodStatusUpdate { let hostname = String::from_utf8(value.wd_hostname[..hostname_end].to_vec()) .map_err(|e| format!("Invalid UTF-8 in hostname: {}", e))?; - let boot_time = if recvtime.is_some() { - decode_rwhod_timestamp_not_after(value.wd_boottime, recvtime_correction, sendtime)? - } else { - decode_rwhod_timestamp_not_after( - value.wd_boottime, - rwhod_time_correction(sendtime, value.wd_boottime), - sendtime, - )? - }; - let users = value .wd_we .iter() .take_while(|whoent| !whoent.is_zeroed()) .map(|whoent| { let mut user = WhodUserEntry::try_from(whoent.clone())?; - user.login_time = if recvtime.is_some() { - decode_rwhod_timestamp_not_after( - whoent.we_utmp.out_time, - recvtime_correction, - recvtime_upper_bound, - )? - } else { - decode_rwhod_timestamp_not_after( - whoent.we_utmp.out_time, - rwhod_time_correction(sendtime, whoent.we_utmp.out_time), - sendtime, - )? - }; + user.login_time = decode_rwhod_timestamp_near_time(whoent.we_utmp.out_time, now)?; Ok(user) }) .collect::, String>>()?; @@ -440,7 +400,7 @@ impl TryFrom for WhodStatusUpdate { recvtime, hostname, load_average: value.wd_loadav.into(), - boot_time, + boot_time: boottime, users, }) } @@ -757,70 +717,143 @@ mod tests { ); } + fn wrap_epoch(offset: i64) -> DateTime { + DateTime::from_timestamp((i32::MAX as i64 + 1) + offset, 0).unwrap() + } + #[test] fn test_whod_status_update_roundtrip_corrects_wrapped_timestamps() { + let recvtime = wrap_epoch(60 * 60); + let sendtime = recvtime - Duration::minutes(5); + let boot_time = recvtime - Duration::days(2); + let login_time = recvtime - Duration::hours(2); + let original = WhodStatusUpdate::new( - Utc.with_ymd_and_hms(2044, 12, 31, 23, 0, 0).unwrap(), - Some(Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap()), + sendtime, + Some(recvtime), "testhost".to_string(), (25, 20, 18), - Utc.with_ymd_and_hms(2044, 12, 30, 0, 0, 0).unwrap(), + boot_time, vec![WhodUserEntry::new( "tty1".to_string(), "user".to_string(), - Utc.with_ymd_and_hms(2044, 12, 31, 22, 0, 0).unwrap(), + login_time, Duration::seconds(60), )], ); let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed"); + let raw_boot_time = whod.wd_boottime; + let raw_login_time = whod.wd_we[0].we_utmp.out_time; let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed"); - assert_eq!(converted, original); + assert_eq!(converted.sendtime, original.sendtime); + assert_eq!(converted.recvtime, original.recvtime); + assert_eq!(converted.hostname, original.hostname); + assert_eq!(converted.load_average, original.load_average); + assert_eq!( + converted.boot_time, + decode_rwhod_timestamp_near_time(raw_boot_time, recvtime).unwrap() + ); + assert_eq!(converted.users.len(), 1); + assert_eq!(converted.users[0].tty, original.users[0].tty); + assert_eq!(converted.users[0].user_id, original.users[0].user_id); + assert_eq!(converted.users[0].idle_time, original.users[0].idle_time); + assert_eq!( + converted.users[0].login_time, + decode_rwhod_timestamp_near_time(raw_login_time, recvtime).unwrap() + ); } #[test] fn test_whod_status_update_roundtrip_sendtime_before_wrap_recvtime_after_wrap() { + let recvtime = wrap_epoch(60); + let sendtime = DateTime::from_timestamp(i32::MAX as i64 - 29, 0).unwrap(); + let boot_time = sendtime - Duration::days(1); + let login_time = sendtime - Duration::minutes(1); + let original = WhodStatusUpdate::new( - Utc.with_ymd_and_hms(2038, 1, 19, 3, 13, 0).unwrap(), - Some(Utc.with_ymd_and_hms(2038, 1, 19, 3, 14, 30).unwrap()), + sendtime, + Some(recvtime), "testhost".to_string(), (25, 20, 18), - Utc.with_ymd_and_hms(2038, 1, 18, 0, 0, 0).unwrap(), + boot_time, vec![WhodUserEntry::new( "tty1".to_string(), "user".to_string(), - Utc.with_ymd_and_hms(2038, 1, 19, 3, 12, 0).unwrap(), + login_time, Duration::seconds(60), )], ); let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed"); + let raw_send_time = whod.wd_sendtime; + let raw_boot_time = whod.wd_boottime; + let raw_login_time = whod.wd_we[0].we_utmp.out_time; let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed"); - assert_eq!(converted, original); + assert_eq!(converted.recvtime, original.recvtime); + assert_eq!(converted.hostname, original.hostname); + assert_eq!(converted.load_average, original.load_average); + assert_eq!( + converted.sendtime, + decode_rwhod_timestamp_near_time(raw_send_time, recvtime).unwrap() + ); + assert_eq!( + converted.boot_time, + decode_rwhod_timestamp_near_time(raw_boot_time, recvtime).unwrap() + ); + assert_eq!(converted.users.len(), 1); + assert_eq!(converted.users[0].tty, original.users[0].tty); + assert_eq!(converted.users[0].user_id, original.users[0].user_id); + assert_eq!(converted.users[0].idle_time, original.users[0].idle_time); + assert_eq!( + converted.users[0].login_time, + decode_rwhod_timestamp_near_time(raw_login_time, recvtime).unwrap() + ); } #[test] fn test_whod_status_update_roundtrip_corrects_wrapped_timestamps_without_recvtime() { + let sendtime = wrap_epoch(60 * 60); + let boot_time = sendtime - Duration::days(2); + let login_time = sendtime - Duration::hours(2); + let original = WhodStatusUpdate::new( - Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap(), + sendtime, None, "testhost".to_string(), (25, 20, 18), - Utc.with_ymd_and_hms(2044, 12, 30, 0, 0, 0).unwrap(), + boot_time, vec![WhodUserEntry::new( "tty1".to_string(), "user".to_string(), - Utc.with_ymd_and_hms(2044, 12, 31, 22, 0, 0).unwrap(), + login_time, Duration::seconds(60), )], ); let whod = Whod::try_from(original.clone()).expect("Conversion to Whod failed"); + let raw_boot_time = whod.wd_boottime; + let raw_login_time = whod.wd_we[0].we_utmp.out_time; let converted = WhodStatusUpdate::try_from(whod).expect("Conversion from Whod failed"); - assert_eq!(converted, original); + assert_eq!(converted.sendtime, original.sendtime); + assert_eq!(converted.recvtime, None); + assert_eq!(converted.hostname, original.hostname); + assert_eq!(converted.load_average, original.load_average); + assert_eq!( + converted.boot_time, + decode_rwhod_timestamp_near_time(raw_boot_time, sendtime).unwrap() + ); + assert_eq!(converted.users.len(), 1); + assert_eq!(converted.users[0].tty, original.users[0].tty); + assert_eq!(converted.users[0].user_id, original.users[0].user_id); + assert_eq!(converted.users[0].idle_time, original.users[0].idle_time); + assert_eq!( + converted.users[0].login_time, + decode_rwhod_timestamp_near_time(raw_login_time, sendtime).unwrap() + ); } #[test] @@ -828,23 +861,20 @@ mod tests { let original = WhodUserEntry::new( "tty1".to_string(), "user".to_string(), - Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap(), + wrap_epoch(60 * 60), Duration::seconds(60), ); let whoent = Whoent::try_from(original.clone()).expect("Conversion to Whoent failed"); let converted_back = - WhodUserEntry::try_from(whoent).expect("Conversion from Whoent failed"); + WhodUserEntry::try_from(whoent.clone()).expect("Conversion from Whoent failed"); - assert_eq!(converted_back, original); - } - - #[test] - fn test_encode_rwhod_timestamp_wraps_like_i32_cast() { - let timestamp = Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap(); + assert_eq!(converted_back.tty, original.tty); + assert_eq!(converted_back.user_id, original.user_id); + assert_eq!(converted_back.idle_time, original.idle_time); assert_eq!( - encode_rwhod_timestamp(timestamp), - timestamp.timestamp() as i32 + converted_back.login_time, + decode_rwhod_timestamp_near_time(whoent.we_utmp.out_time, original.login_time).unwrap() ); } } diff --git a/src/proto/rwhod_protocol/time_codec.rs b/src/proto/rwhod_protocol/time_codec.rs index cb8ca10..30269b8 100644 --- a/src/proto/rwhod_protocol/time_codec.rs +++ b/src/proto/rwhod_protocol/time_codec.rs @@ -7,10 +7,13 @@ use chrono::{DateTime, Utc}; const RWHOD_TIMESTAMP_CORRECTION_WINDOW: i64 = 0x40000000_i64; -const RWHOD_TIMESTAMP_WRAP_INCREMENT: i64 = 0x70000000_i64 + 0x70000000_i64 + 0x20000000_i64; +const RWHOD_TIMESTAMP_WRAP_INCREMENT: i64 = 0x1_0000_0000_i64; + +const RWHOD_TIMESTAMP_FORWARD_ALLOWANCE: i64 = RWHOD_TIMESTAMP_CORRECTION_WINDOW; /// Decodes a raw rwhod timestamp (i32) into a DateTime, /// adding a correction value to count for any detected overflows. +#[allow(dead_code)] pub fn decode_rwhod_timestamp(raw: i32, correction: i64) -> Result, String> { DateTime::from_timestamp_secs(i64::from(raw) + correction).ok_or(format!( "Invalid timestamp: {} with correction {}", @@ -20,6 +23,18 @@ pub fn decode_rwhod_timestamp(raw: i32, correction: i64) -> Result /// Calculates the correction value for a received rwhod timestamp (i32), /// based on the current time (now) and the received timestamp (recvtime). +/// +/// If the difference between the current time and the received timestamp is +/// above a certain threshold (`RWHOD_TIMESTAMP_CORRECTION_WINDOW`), +/// this function attempts to determine how many times the timestamp has +/// wrapped around and returns the appropriate additional correction value +/// to account for the overflow. +/// +/// This function is roughly equivalent to the logic used in the original +/// program to handle timestamp overflows, but please consider using +/// `decode_rwhod_timestamp_near_time` for most use cases, as it is designed +/// to be more robust. +#[allow(dead_code)] pub fn rwhod_time_correction(now: DateTime, recvtime: i32) -> i64 { let delta = now.timestamp() - i64::from(recvtime); @@ -27,64 +42,78 @@ pub fn rwhod_time_correction(now: DateTime, recvtime: i32) -> i64 { return 0; } - let wraps = (delta - RWHOD_TIMESTAMP_CORRECTION_WINDOW - 1) + let wrap_count = (delta - RWHOD_TIMESTAMP_CORRECTION_WINDOW - 1) .div_euclid(RWHOD_TIMESTAMP_WRAP_INCREMENT) + 1; - wraps * RWHOD_TIMESTAMP_WRAP_INCREMENT + wrap_count * RWHOD_TIMESTAMP_WRAP_INCREMENT } -/// Decodes a raw rwhod timestamp (i32) into a DateTime, -/// by adding a correction value to count for any detected overflows, -/// while ensuring the decoded timestamp is not after the specified upper bound. -pub fn decode_rwhod_timestamp_not_after( +/// Decodes a raw rwhod timestamp (i32) into a DateTime using a wrap-sized +/// interval around the provided `time` reference. +/// +/// You can think of this function as looking at the current timestamp and choosing +/// the closest valid timestamp behind it for the given raw value, while also allowing +/// it to be a certain amount of time in the future (forward allowance) to account for +/// clock skew or other synchronization issues. +pub fn decode_rwhod_timestamp_near_time_with_forward_allowance( raw: i32, - base_correction: i64, - upper_bound: DateTime, + time: DateTime, + forward_allowance: i64, ) -> Result, String> { - let upper_bound = upper_bound.timestamp(); + debug_assert!(forward_allowance >= 0); + debug_assert!(forward_allowance <= RWHOD_TIMESTAMP_WRAP_INCREMENT); - for offset in [1_i64, 0, -1] { - let correction = base_correction + offset * RWHOD_TIMESTAMP_WRAP_INCREMENT; - let candidate = i64::from(raw) + correction; - if candidate <= upper_bound { - return DateTime::from_timestamp_secs(candidate).ok_or(format!( - "Invalid timestamp: {} with correction {}", - raw, correction - )); - } - } + let raw = i64::from(raw); + let candidate = raw + + (time.timestamp() + forward_allowance - raw).div_euclid(RWHOD_TIMESTAMP_WRAP_INCREMENT) + * RWHOD_TIMESTAMP_WRAP_INCREMENT; - decode_rwhod_timestamp(raw, base_correction - RWHOD_TIMESTAMP_WRAP_INCREMENT) + DateTime::from_timestamp_secs(candidate).ok_or(format!( + "Invalid timestamp: {} near {} with forward allowance {}", + raw, time, forward_allowance + )) } -/// Decodes a raw rwhod timestamp (i32) into a DateTime, -/// by adding a correction value to count for any detected overflows. -/// The correction value is calculated based on the provided `time` argument, -/// choosing the timestamp closest to `time` among the possible candidates. +/// Decodes a raw rwhod timestamp (i32) into a DateTime using the default +/// wrap-sized interval around the reference time. pub fn decode_rwhod_timestamp_near_time( raw: i32, time: DateTime, ) -> Result, String> { - let base_correction = rwhod_time_correction(time, raw); - let mut best_candidate = None; + decode_rwhod_timestamp_near_time_with_forward_allowance( + raw, + time, + RWHOD_TIMESTAMP_FORWARD_ALLOWANCE, + ) +} - for offset in [1_i64, 0, -1] { - let correction = base_correction + offset * RWHOD_TIMESTAMP_WRAP_INCREMENT; - let candidate = i64::from(raw) + correction; - let distance = (time.timestamp() - candidate).abs(); +pub struct RwhodTimestamps { + pub boottime: DateTime, + pub sendtime: DateTime, + pub recvtime: Option>, +} - match best_candidate { - Some((best_distance, _, _)) if best_distance <= distance => {} - _ => best_candidate = Some((distance, candidate, correction)), - } - } +pub fn decode_rwhod_timestamps( + wd_boottime: i32, + wd_sendtime: i32, + wd_recvtime: i32, + now: DateTime, +) -> Result { + let recvtime = if wd_recvtime == 0 { + None + } else { + Some(decode_rwhod_timestamp_near_time(wd_recvtime, now)?) + }; - let (_, candidate, correction) = best_candidate.expect("candidate list should not be empty"); - DateTime::from_timestamp_secs(candidate).ok_or(format!( - "Invalid timestamp: {} with correction {}", - raw, correction - )) + let sendtime = decode_rwhod_timestamp_near_time(wd_sendtime, now)?; + let boottime = decode_rwhod_timestamp_near_time(wd_boottime, now)?; + + Ok(RwhodTimestamps { + boottime, + sendtime, + recvtime, + }) } /// Encodes a DateTime into a raw rwhod timestamp (i32). @@ -94,67 +123,132 @@ pub fn encode_rwhod_timestamp(timestamp: DateTime) -> i32 { #[cfg(test)] mod tests { - use chrono::TimeZone; - use super::*; #[test] - fn test_rwhod_timestamp_correction_for_received_packets() { - let now = Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap(); - let corrected_recvtime = now - chrono::Duration::days(1); - let raw_recvtime = corrected_recvtime.timestamp() as i32; - let correction = rwhod_time_correction(now, raw_recvtime); + fn test_rwhod_time_correction_no_wrap() { + let now = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap(); + let recvtime = i32::MAX; + let correction = rwhod_time_correction(now, recvtime); - assert_eq!(correction, 1i64 << 32); + assert_eq!(correction, 0); + } + + #[test] + fn test_rwhod_time_correction_with_single_wrap() { + let now = DateTime::from_timestamp(i32::MAX as i64 + 2, 0).unwrap(); + let recvtime = i32::MAX.wrapping_add(1); + let correction = rwhod_time_correction(now, recvtime); + + assert_eq!(correction, RWHOD_TIMESTAMP_WRAP_INCREMENT * 1); + } + + #[test] + fn test_rwhod_time_correction_with_multiple_wraps() { + let now = DateTime::from_timestamp(i32::MAX as i64 + RWHOD_TIMESTAMP_WRAP_INCREMENT + 1, 0) + .unwrap(); + let recvtime = i32::MAX.wrapping_add(1); + let correction = rwhod_time_correction(now, recvtime); + + assert_eq!(correction, RWHOD_TIMESTAMP_WRAP_INCREMENT * 2); + } + + #[test] + fn test_rwhod_time_correction_with_negative_delta() { + let now = DateTime::from_timestamp(i32::MAX as i64 - 1, 0).unwrap(); + let recvtime = i32::MAX; + let correction = rwhod_time_correction(now, recvtime); + + assert_eq!(correction, 0); + } + + #[test] + fn test_rwhod_time_correction_with_large_negative_delta() { + let now = DateTime::from_timestamp(i32::MIN as i64 - 1, 0).unwrap(); + let recvtime = i32::MIN; + let correction = rwhod_time_correction(now, recvtime); + + assert_eq!(correction, 0); + } + + #[test] + fn test_decode_rwhod_timestamp_near_time_no_wrap() { + let now = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap(); + let expected = now - chrono::Duration::days(1); + let raw = expected.timestamp() as i32; + + let decoded = decode_rwhod_timestamp_near_time(raw, now).unwrap(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_decode_rwhod_timestamp_near_time_with_wrap() { + let now = DateTime::from_timestamp(i32::MAX as i64 + RWHOD_TIMESTAMP_WRAP_INCREMENT + 1, 0) + .unwrap(); + let expected = now - chrono::Duration::days(1); + let raw = expected.timestamp() as i32; + + let decoded = decode_rwhod_timestamp_near_time(raw, now).unwrap(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_decode_rwhod_timestamp_near_time_with_negative_delta() { + let time = DateTime::from_timestamp(-(RWHOD_TIMESTAMP_WRAP_INCREMENT / 2) - 10, 0).unwrap(); + let raw = 0; + + let decoded = decode_rwhod_timestamp_near_time_with_forward_allowance( + raw, + time, + RWHOD_TIMESTAMP_WRAP_INCREMENT / 2, + ) + .unwrap(); assert_eq!( - decode_rwhod_timestamp(raw_recvtime, correction).unwrap(), - corrected_recvtime + decoded, + DateTime::from_timestamp(-RWHOD_TIMESTAMP_WRAP_INCREMENT, 0).unwrap() ); } #[test] - fn test_decode_rwhod_timestamp_near_time() { - let now = Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap(); - let corrected_recvtime = now - chrono::Duration::days(1); - let raw_recvtime = corrected_recvtime.timestamp() as i32; + fn test_decode_rwhod_timestamp_near_time_uses_forward_allowance() { + let time = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap(); + let expected = time + chrono::Duration::seconds(5); + let raw = expected.timestamp() as i32; - let decoded = decode_rwhod_timestamp_near_time(raw_recvtime, now).unwrap(); - assert_eq!(decoded, corrected_recvtime); + let decoded = + decode_rwhod_timestamp_near_time_with_forward_allowance(raw, time, 5).unwrap(); + assert_eq!(decoded, expected); } #[test] - fn test_decode_rwhod_timestamp_not_after() { - let now = Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap(); - let corrected_recvtime = now - chrono::Duration::days(1); - let raw_recvtime = corrected_recvtime.timestamp() as i32; - let base_correction = rwhod_time_correction(now, raw_recvtime); + fn test_decode_rwhod_timestamp_near_time_small_forward_allowance_prefers_previous_wrap() { + let time = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap(); + let future = time + chrono::Duration::seconds(RWHOD_TIMESTAMP_WRAP_INCREMENT / 4); + let raw = future.timestamp() as i32; - let decoded = decode_rwhod_timestamp_not_after(raw_recvtime, base_correction, now).unwrap(); - assert_eq!(decoded, corrected_recvtime); - } - - #[test] - fn test_decode_rwhod_timestamp_not_after_prefers_latest_plausible_pre_wrap_candidate() { - let now = Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap(); - let recvtime = now - chrono::Duration::minutes(1); - let boot_time = recvtime - chrono::Duration::days(100); - - let raw_recvtime = recvtime.timestamp() as i32; - let raw_boot_time = boot_time.timestamp() as i32; - let recvtime_correction = rwhod_time_correction(now, raw_recvtime); - - let decoded_boot_time = - decode_rwhod_timestamp_not_after(raw_boot_time, recvtime_correction, recvtime).unwrap(); - - assert_eq!(decoded_boot_time, boot_time); - } - - #[test] - fn test_encode_rwhod_timestamp_wraps_like_i32_cast() { - let timestamp = Utc.with_ymd_and_hms(2045, 1, 1, 0, 0, 0).unwrap(); + let decoded = decode_rwhod_timestamp_near_time_with_forward_allowance( + raw, + time, + RWHOD_TIMESTAMP_WRAP_INCREMENT / 10, + ) + .unwrap(); assert_eq!( - encode_rwhod_timestamp(timestamp), - timestamp.timestamp() as i32 + decoded, + future - chrono::Duration::seconds(RWHOD_TIMESTAMP_WRAP_INCREMENT) ); } + + #[test] + fn test_encode_rwhod_timestamp_no_wrap() { + let timestamp = DateTime::from_timestamp(i32::MAX as i64, 0).unwrap(); + let encoded = encode_rwhod_timestamp(timestamp); + assert_eq!(encoded, i32::MAX); + } + + #[test] + fn test_encode_rwhod_timestamp_with_wrap() { + let timestamp = DateTime::from_timestamp(i32::MAX as i64 + 1, 0).unwrap(); + let encoded = encode_rwhod_timestamp(timestamp); + assert_eq!(encoded, i32::MIN); + } }