From ad2a22e8bf79a32dddbb1552273ec66eb24cd843 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Sun, 3 May 2026 21:37:11 +0900 Subject: [PATCH] proto/finger: rewrite user session idle time parser --- src/proto/finger_protocol/parser.rs | 118 +++++++++++++++++----------- 1 file changed, 72 insertions(+), 46 deletions(-) diff --git a/src/proto/finger_protocol/parser.rs b/src/proto/finger_protocol/parser.rs index 040efc5..1be539f 100644 --- a/src/proto/finger_protocol/parser.rs +++ b/src/proto/finger_protocol/parser.rs @@ -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> { 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> { 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> { 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> { 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> { .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 anyhow::Result { - // 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 { @@ -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]