From 16b2bc5c2759e20ecb952374509f1e1f9d6c06e7 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Wed, 29 Apr 2026 08:27:31 +0900 Subject: [PATCH] proto/finger: parse timezones --- Cargo.lock | 35 +++++++++++++++++++++++ Cargo.toml | 1 + src/proto/finger_protocol.rs | 55 ++++++++++++++++++++++-------------- 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b165ddd..de95138 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + [[package]] name = "clap" version = "4.6.1" @@ -747,6 +757,24 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -831,6 +859,7 @@ dependencies = [ "bytes", "caps", "chrono", + "chrono-tz", "clap", "clap_complete", "futures-util", @@ -974,6 +1003,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 617ca82..9e15930 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ tokio-util = "0.7.18" caps = "0.5.6" users = { version = "0.11.0", default-features = false } tracing-journald = "0.3.2" +chrono-tz = "0.10.4" [features] default = ["systemd"] diff --git a/src/proto/finger_protocol.rs b/src/proto/finger_protocol.rs index 5ca0333..710d773 100644 --- a/src/proto/finger_protocol.rs +++ b/src/proto/finger_protocol.rs @@ -1,6 +1,9 @@ -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr}; -use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveTime, TimeDelta, Utc, Weekday}; +use anyhow::Context; +use chrono::{ + DateTime, Datelike, Duration, NaiveDate, NaiveTime, TimeDelta, TimeZone, Timelike, Utc, Weekday, +}; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -132,9 +135,13 @@ 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 _timezone = time_parts[time_parts.len() - 1]; + let timezone = time_parts[time_parts.len() - 1] + .trim_start_matches('(') + .trim_end_matches(')'); + + let tz = chrono_tz::Tz::from_str(timezone) + .context(format!("Failed to parse timezone in login time: {}", time))?; - let now = Utc::now(); let mut parts = time.split_whitespace(); let weekday = match parts.next() { @@ -181,12 +188,13 @@ fn parse_bsd_finger_time(time: &str) -> anyhow::Result> { ) })?; + let now = Utc::now(); const MAX_YEARS_BACK: i32 = 10; - + let mut year = None; for offset in 0..=MAX_YEARS_BACK { - let year = now.year() - offset; + let year_ = now.year() - offset; - let date = match NaiveDate::from_ymd_opt(year, month, day) { + let date = match NaiveDate::from_ymd_opt(year_, month, day) { Some(d) => d, None => continue, }; @@ -195,22 +203,26 @@ fn parse_bsd_finger_time(time: &str) -> anyhow::Result> { continue; } - let dt = date.and_time(clock); - - if dt <= now.naive_utc() { - // TODO: apply timezone if we are able to parse it. - // if not, try to get the local timezone offset. - // if not, assume UTC. - - return Ok(DateTime::::from_naive_utc_and_offset(dt, Utc)); - } + year = Some(year_); } - Err(anyhow::anyhow!( - "Could not infer year for login time {} within {} years", - time, - MAX_YEARS_BACK - )) + if year.is_none() { + return Err(anyhow::anyhow!( + "Could not find a valid year for login time {} within {} years", + time, + MAX_YEARS_BACK + )); + } + + tz.with_ymd_and_hms(year.unwrap(), month, day, clock.hour(), clock.minute(), 0) + .single() + .ok_or_else(|| { + anyhow::anyhow!( + "Failed to convert login time to timezone-aware datetime: {}", + time + ) + }) + .map(|dt| dt.with_timezone(&Utc)) } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -763,6 +775,7 @@ mod tests { "Tue Feb 28 23:59 (UTC)", "Wed Dec 31 00:00 (UTC)", "Wed Dec 31 00:00 (GMT)", + "Wed Dec 31 00:00 (Asia/Tokyo)", ]; for input in cases {