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 anyhow::Context;
use chrono::{ use chrono::{
@@ -14,15 +14,16 @@ use crate::proto::finger_protocol::{
fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> { fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
let time_parts: Vec<_> = time.split_ascii_whitespace().collect(); 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] let timezone = time_parts[time_parts.len() - 1]
.trim_start_matches('(') .trim_start_matches('(')
.trim_end_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))?; .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() { let weekday = match parts.next() {
Some("Mon") => Weekday::Mon, Some("Mon") => Weekday::Mon,
@@ -32,7 +33,7 @@ fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
Some("Fri") => Weekday::Fri, Some("Fri") => Weekday::Fri,
Some("Sat") => Weekday::Sat, Some("Sat") => Weekday::Sat,
Some("Sun") => Weekday::Sun, 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() { let month = match parts.next() {
@@ -48,22 +49,22 @@ fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
Some("Oct") => 10, Some("Oct") => 10,
Some("Nov") => 11, Some("Nov") => 11,
Some("Dec") => 12, Some("Dec") => 12,
_ => anyhow::bail!("Invalid month in login time: {}", time), _ => anyhow::bail!("Invalid month in login time: {}", time_),
}; };
let day: u32 = parts let day: u32 = parts
.next() .next()
.and_then(|d| d.parse().ok()) .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 let time_part = parts
.next() .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| { let clock = NaiveTime::parse_from_str(time_part, "%H:%M").map_err(|e| {
anyhow::anyhow!( anyhow::anyhow!(
"Failed to parse time component in login time: {}: {}", "Failed to parse time component in login time: {}: {}",
time, time_,
e e
) )
})?; })?;
@@ -89,7 +90,7 @@ fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
if year.is_none() { if year.is_none() {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"Could not find a valid year for login time {} within {} years", "Could not find a valid year for login time {} within {} years",
time, time_,
MAX_YEARS_BACK MAX_YEARS_BACK
)); ));
} }
@@ -99,7 +100,7 @@ fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
.ok_or_else(|| { .ok_or_else(|| {
anyhow::anyhow!( anyhow::anyhow!(
"Failed to convert login time to timezone-aware datetime: {}", "Failed to convert login time to timezone-aware datetime: {}",
time time_
) )
}) })
.map(|dt| dt.with_timezone(&Utc)) .map(|dt| dt.with_timezone(&Utc))
@@ -279,8 +280,8 @@ fn parse_user_sessions(
Ok(session) => { Ok(session) => {
sessions.push(session); sessions.push(session);
} }
Err(_) => { Err(err) => {
tracing::warn!("Failed to parse user session from line: {}", line); 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 /// Parse the idle time from the text string generated by bsd-finger
fn parse_user_session_idle_time(str: &str) -> anyhow::Result<Duration> { fn parse_user_session_idle_time(str: &str) -> anyhow::Result<Duration> {
// Parse idle time from finger response format. let mut total_duration = Duration::zero();
// This has four cases: " ", "MMMMM", "HH:MM", "DDd"
if str.trim().is_empty() { let parts: Vec<&str> = str.split_whitespace().collect();
Ok(Duration::zero()) let mut i = 0;
} else if str.contains(':') { while i < parts.len() {
let parts: Vec<&str> = str.split(':').collect(); let value_str = parts[i];
if parts.len() != 2 { let unit_str = parts
return Err(anyhow::anyhow!("Invalid idle time format: {}", str)); .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() i += 2;
.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))
} }
Ok(total_duration)
} }
fn parse_forward_status(lines: &[&str], current_index: &mut usize) -> Option<String> { fn parse_forward_status(lines: &[&str], current_index: &mut usize) -> Option<String> {
@@ -549,13 +554,21 @@ mod tests {
#[test] #[test]
fn test_parse_user_session_idle_time() { 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(); let duration = parse_user_session_idle_time(input).unwrap();
assert_eq!( assert_eq!(
duration.num_minutes(), duration.num_seconds(),
expected_minutes, expected_seconds,
"Failed on input: {}", "Failed on input: {}",
input input
); );
@@ -605,9 +618,9 @@ mod tests {
assert!(!session_idle_off.messages_on); assert!(!session_idle_off.messages_on);
let line_host_and_idle = indoc::indoc! {" let line_host_and_idle = indoc::indoc! {"
On since Mon Mar 1 10:00 (UTC) on pts/4 from host.example.com On since Mon Mar 1 10:00 (UTC) on pts/4 from host.example.com
2 hours idle 2 hours idle
"} "}
.trim(); .trim();
let session_host_and_idle = parse_user_session(line_host_and_idle).unwrap(); let session_host_and_idle = parse_user_session(line_host_and_idle).unwrap();
assert_eq!(session_host_and_idle.tty, "pts/4"); assert_eq!(session_host_and_idle.tty, "pts/4");
@@ -619,6 +632,19 @@ mod tests {
60 * 2 60 * 2
); );
assert!(session_host_and_idle.messages_on); 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] #[test]