proto/rwhod: add forward-allowance based overflow correction
Build and test / check (push) Failing after 1m3s
Build and test / build (push) Successful in 1m44s
Build and test / test (push) Successful in 3m31s
Build and test / docs (push) Successful in 5m18s

This commit is contained in:
2026-07-19 23:10:59 +09:00
parent 3df4d638a9
commit 3297d145b4
2 changed files with 284 additions and 160 deletions
+103 -73
View File
@@ -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<Whod> 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<Whod> 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::<Result<Vec<WhodUserEntry>, String>>()?;
@@ -440,7 +400,7 @@ impl TryFrom<Whod> 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<Utc> {
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()
);
}
}