proto/finger: rewrite user session idle time parser

This commit is contained in:
2026-05-03 21:37:11 +09:00
parent 61fef28133
commit ad2a22e8bf
+72 -46
View File
@@ -1,4 +1,4 @@
use std::{path::PathBuf, str::FromStr};
use std::path::PathBuf;
use anyhow::Context;
use chrono::{
@@ -14,15 +14,16 @@ use crate::proto::finger_protocol::{
fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
let time_parts: Vec<_> = time.split_ascii_whitespace().collect();
let time = &time_parts[..time_parts.len() - 1].join(" ");
let time_ = &time_parts[..time_parts.len() - 1].join(" ");
let timezone = time_parts[time_parts.len() - 1]
.trim_start_matches('(')
.trim_end_matches(')');
let tz = chrono_tz::Tz::from_str(timezone)
let tz: chrono_tz::Tz = timezone
.parse()
.context(format!("Failed to parse timezone in login time: {}", time))?;
let mut parts = time.split_whitespace();
let mut parts = time_.split_whitespace();
let weekday = match parts.next() {
Some("Mon") => Weekday::Mon,
@@ -32,7 +33,7 @@ fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
Some("Fri") => Weekday::Fri,
Some("Sat") => Weekday::Sat,
Some("Sun") => Weekday::Sun,
_ => anyhow::bail!("Invalid weekday in login time: {}", time),
_ => anyhow::bail!("Invalid weekday in login time: {}", time_),
};
let month = match parts.next() {
@@ -48,22 +49,22 @@ fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
Some("Oct") => 10,
Some("Nov") => 11,
Some("Dec") => 12,
_ => anyhow::bail!("Invalid month in login time: {}", time),
_ => anyhow::bail!("Invalid month in login time: {}", time_),
};
let day: u32 = parts
.next()
.and_then(|d| d.parse().ok())
.ok_or_else(|| anyhow::anyhow!("Invalid day in login time: {}", time))?;
.ok_or_else(|| anyhow::anyhow!("Invalid day in login time: {}", time_))?;
let time_part = parts
.next()
.ok_or_else(|| anyhow::anyhow!("Missing time in login time: {}", time))?;
.ok_or_else(|| anyhow::anyhow!("Missing time in login time: {}", time_))?;
let clock = NaiveTime::parse_from_str(time_part, "%H:%M").map_err(|e| {
anyhow::anyhow!(
"Failed to parse time component in login time: {}: {}",
time,
time_,
e
)
})?;
@@ -89,7 +90,7 @@ fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
if year.is_none() {
return Err(anyhow::anyhow!(
"Could not find a valid year for login time {} within {} years",
time,
time_,
MAX_YEARS_BACK
));
}
@@ -99,7 +100,7 @@ fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
.ok_or_else(|| {
anyhow::anyhow!(
"Failed to convert login time to timezone-aware datetime: {}",
time
time_
)
})
.map(|dt| dt.with_timezone(&Utc))
@@ -279,8 +280,8 @@ fn parse_user_sessions(
Ok(session) => {
sessions.push(session);
}
Err(_) => {
tracing::warn!("Failed to parse user session from line: {}", line);
Err(err) => {
tracing::warn!("Failed to parse user session from line: {}\n{}", line, err);
}
}
}
@@ -351,34 +352,38 @@ pub fn parse_user_session(line: &str) -> anyhow::Result<FingerResponseUserSessio
/// Parse the idle time from the text string generated by bsd-finger
fn parse_user_session_idle_time(str: &str) -> anyhow::Result<Duration> {
// Parse idle time from finger response format.
// This has four cases: " ", "MMMMM", "HH:MM", "DDd"
if str.trim().is_empty() {
Ok(Duration::zero())
} else if str.contains(':') {
let parts: Vec<&str> = str.split(':').collect();
if parts.len() != 2 {
return Err(anyhow::anyhow!("Invalid idle time format: {}", str));
let mut total_duration = Duration::zero();
let parts: Vec<&str> = str.split_whitespace().collect();
let mut i = 0;
while i < parts.len() {
let value_str = parts[i];
let unit_str = parts
.get(i + 1)
.ok_or_else(|| anyhow::anyhow!("Missing time unit in idle time string: {}", str))?;
let value: i64 = value_str
.parse()
.map_err(|e| anyhow::anyhow!("Failed to parse value from idle time {}: {}", str, e))?;
match *unit_str {
"day" | "days" => total_duration += Duration::days(value),
"hour" | "hours" => total_duration += Duration::hours(value),
"minute" | "minutes" => total_duration += Duration::minutes(value),
"second" | "seconds" => total_duration += Duration::seconds(value),
_ => {
return Err(anyhow::anyhow!(
"Unknown time unit '{}' in idle time string: {}",
unit_str,
str
));
}
}
let hours: i64 = parts[0]
.parse()
.map_err(|e| anyhow::anyhow!("Failed to parse hours from idle time {}: {}", str, e))?;
let minutes: i64 = parts[1].parse().map_err(|e| {
anyhow::anyhow!("Failed to parse minutes from idle time {}: {}", str, e)
})?;
Ok(Duration::hours(hours) + Duration::minutes(minutes))
} else if str.ends_with('d') {
let days_str = str.trim_end_matches('d');
let days: i64 = days_str
.parse()
.map_err(|e| anyhow::anyhow!("Failed to parse days from idle time {}: {}", str, e))?;
Ok(Duration::days(days))
} else {
let minutes: i64 = str.parse().map_err(|e| {
anyhow::anyhow!("Failed to parse minutes from idle time {}: {}", str, e)
})?;
Ok(Duration::minutes(minutes))
i += 2;
}
Ok(total_duration)
}
fn parse_forward_status(lines: &[&str], current_index: &mut usize) -> Option<String> {
@@ -549,13 +554,21 @@ mod tests {
#[test]
fn test_parse_user_session_idle_time() {
let cases = vec![(" ", 0), ("5", 5), ("1:30", 90), ("2d", 2880)];
let cases = vec![
("1 second", 1),
("3 minutes 2 seconds", 3 * 60 + 2),
(
"1 day 5 hours 30 minutes",
1 * 24 * 60 * 60 + 5 * 60 * 60 + 30 * 60,
),
("1 day 1 second", 1 * 24 * 60 * 60 + 1),
];
for (input, expected_minutes) in cases {
for (input, expected_seconds) in cases {
let duration = parse_user_session_idle_time(input).unwrap();
assert_eq!(
duration.num_minutes(),
expected_minutes,
duration.num_seconds(),
expected_seconds,
"Failed on input: {}",
input
);
@@ -605,9 +618,9 @@ mod tests {
assert!(!session_idle_off.messages_on);
let line_host_and_idle = indoc::indoc! {"
On since Mon Mar 1 10:00 (UTC) on pts/4 from host.example.com
2 hours idle
"}
On since Mon Mar 1 10:00 (UTC) on pts/4 from host.example.com
2 hours idle
"}
.trim();
let session_host_and_idle = parse_user_session(line_host_and_idle).unwrap();
assert_eq!(session_host_and_idle.tty, "pts/4");
@@ -619,6 +632,19 @@ mod tests {
60 * 2
);
assert!(session_host_and_idle.messages_on);
let line_cest_timezone = indoc::indoc! {"
On since Thu Apr 30 05:45 (CEST) on pts/4 from 10.0.0.192
6 hours 34 minutes idle
"}
.trim();
let session_cest = parse_user_session(line_cest_timezone).unwrap();
assert_eq!(session_cest.tty, "pts/4");
assert_eq!(session_cest.host, Some("10.0.0.192".to_string()));
assert_eq!(session_cest.login_time.weekday(), Weekday::Thu);
assert_eq!(session_cest.login_time.hour(), 5);
assert_eq!(session_cest.idle_time.unwrap().num_minutes(), 6 * 60 + 34);
assert!(session_cest.messages_on);
}
#[test]