proto/finger: rewrite user session idle time parser
This commit is contained in:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user