From 92f77bfaff58107d85115ed80e02f921b82fb03c Mon Sep 17 00:00:00 2001 From: h7x4 Date: Thu, 23 Apr 2026 15:34:23 +0900 Subject: [PATCH] finger: implement basic fuzzy search mechanism --- Cargo.lock | 10 ++++++++ Cargo.toml | 1 + src/bin/finger.rs | 4 ++-- src/server/fingerd.rs | 50 ++++++++++++++++++++++++++++++++++++++- src/server/varlink_api.rs | 42 +++++++++++++++++++++++++------- 5 files changed, 96 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83a52d5..b74d3d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -845,6 +845,7 @@ dependencies = [ "toml", "tracing", "tracing-subscriber", + "users", "uucore", "zlink", ] @@ -1286,6 +1287,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", +] + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 7550de6..4f732c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ clap_complete = "4.6.2" itertools = "0.14.0" tokio-util = "0.7.18" caps = "0.5.6" +users = { version = "0.11.0", default-features = false } [features] default = ["systemd"] diff --git a/src/bin/finger.rs b/src/bin/finger.rs index a9fa2d1..57db795 100644 --- a/src/bin/finger.rs +++ b/src/bin/finger.rs @@ -123,7 +123,7 @@ pub struct Args { #[arg(long, value_enum, hide = true)] completions: Option, - users: Vec, + users: Option>, } #[tokio::main] @@ -154,7 +154,7 @@ async fn main() -> anyhow::Result<()> { println!("{}", serde_json::to_string_pretty(&reply).unwrap()); } else { for user in reply { - println!("{:#?}", user.unwrap()); + println!("{:#?}", user); } } diff --git a/src/server/fingerd.rs b/src/server/fingerd.rs index 2131e0e..06c15da 100644 --- a/src/server/fingerd.rs +++ b/src/server/fingerd.rs @@ -6,10 +6,58 @@ use std::{ use chrono::{DateTime, Duration, Timelike, Utc}; use nix::sys::stat::stat; +use users::all_users; use uucore::utmpx::Utmpx; use crate::proto::finger_protocol::{FingerResponseUserEntry, FingerResponseUserSession}; +/// Search for users whose username or full name contains the search string. +pub fn search_for_user( + search_string: &str, + dont_search_fullnames: bool, +) -> Vec> { + (unsafe { all_users() }) + .filter_map(|user| { + let user = match nix::unistd::User::from_uid(user.uid().into()) { + Ok(Some(user)) => user, + Ok(None) => return None, // User disappeared, skip + Err(e) => { + return Some(Err(anyhow::anyhow!( + "Failed to get user entry for UID {}: {}", + user.uid(), + e + ))); + } + }; + + let username = user.name; + let full_name = String::from_utf8_lossy( + &user + .gecos + .as_bytes() + .split(|&b| b == b',') + .next() + .unwrap_or(&[]), + ) + .to_string(); + + let matches_username = username.contains(search_string); + let matches_fullname = !dont_search_fullnames && full_name.contains(search_string); + if matches_username || matches_fullname { + match get_local_user(&username) { + Ok(Some(user_entry)) => Some(Ok(user_entry)), + Ok(None) => None, // User exists but has .nofinger, skip + Err(err) => Some(Err(err.into())), + } + } else { + None + } + }) + .collect() +} + +/// Helper function to read the content of a file if it exists and is readable, +/// returning None if the file does not exist or is not readable. fn read_file_content_if_exists(path: &Path) -> anyhow::Result> { let file_is_readable = path.exists() && path.is_file() @@ -35,7 +83,7 @@ fn read_file_content_if_exists(path: &Path) -> anyhow::Result> { /// Retrieve local user information for the given username. /// /// Returns None if the user does not exist. -pub fn get_local_user(username: &str) -> anyhow::Result> { +fn get_local_user(username: &str) -> anyhow::Result> { let username = username.to_string(); let user_entry = match nix::unistd::User::from_name(&username) { Ok(Some(user)) => user, diff --git a/src/server/varlink_api.rs b/src/server/varlink_api.rs index a51e78f..a55a076 100644 --- a/src/server/varlink_api.rs +++ b/src/server/varlink_api.rs @@ -61,7 +61,7 @@ pub enum VarlinkRwhodClientError { pub trait VarlinkFingerClientProxy { async fn finger( &mut self, - user_queries: Vec, + user_queries: Option>, ) -> zlink::Result>; } @@ -69,7 +69,7 @@ pub trait VarlinkFingerClientProxy { #[serde(tag = "method", content = "parameters")] pub enum VarlinkFingerClientRequest { #[serde(rename = "no.ntnu.pvv.roowho2.finger.Finger")] - Finger { user_queries: Vec }, + Finger { user_queries: Option> }, } #[derive(Debug, Serialize)] @@ -78,7 +78,7 @@ pub enum VarlinkFingerClientResponse { Finger(VarlinkFingerResponse), } -pub type VarlinkFingerResponse = Vec>; +pub type VarlinkFingerResponse = Vec; #[derive(Debug, Clone, PartialEq, ReplyError)] #[zlink(interface = "no.ntnu.pvv.roowho2.finger")] @@ -147,11 +147,37 @@ impl VarlinkRoowhoo2ClientServer { store.values().cloned().collect() } - async fn handle_finger_request(&self, user_queries: Vec) -> VarlinkFingerResponse { - user_queries - .into_iter() - .map(|username| fingerd::get_local_user(&username).unwrap()) - .collect() + async fn handle_finger_request( + &self, + user_queries: Option>, + ) -> VarlinkFingerResponse { + match user_queries { + // TODO: deduplicate results + Some(usernames) => usernames + .into_iter() + .flat_map::, _>(|username| { + fingerd::search_for_user(&username, false) + .into_iter() + .map(|res| (username.clone(), res)) + .collect() + }) + .filter_map(|(username, user)| match user { + Ok(user_info) => Some(user_info), + Err(err) => { + tracing::error!( + "Error retrieving local user information for '{}': {}", + username, + err + ); + None + } + }) + .collect(), + None => { + // TODO: fetch logged in users using utmp entries + todo!() + } + } } }