// ABOUTME: Current user detection and avatar loading for the lockscreen. // ABOUTME: Retrieves user info from the system (nix getuid, AccountsService, ~/.face). use nix::unistd::{getuid, User as NixUser}; use std::path::{Path, PathBuf}; const DEFAULT_ACCOUNTSSERVICE_DIR: &str = "/var/lib/AccountsService/icons"; const GRESOURCE_PREFIX: &str = "/dev/moonarch/moonlock"; #[derive(Debug, Clone)] pub struct User { pub username: String, pub display_name: String, pub home: PathBuf, pub uid: u32, } pub fn get_current_user() -> Option { let uid = getuid(); let nix_user = NixUser::from_uid(uid).ok()??; let gecos = match nix_user.gecos.to_str() { Ok(s) => s.to_string(), Err(_) => { log::warn!("GECOS field is not valid UTF-8, falling back to username"); String::new() } }; let display_name = if !gecos.is_empty() { let first = gecos.split(',').next().unwrap_or(""); if first.is_empty() { nix_user.name.clone() } else { first.to_string() } } else { nix_user.name.clone() }; Some(User { username: nix_user.name, display_name, home: nix_user.dir, uid: uid.as_raw() }) } pub fn get_avatar_path(home: &Path, username: &str) -> Option { get_avatar_path_with(home, username, Path::new(DEFAULT_ACCOUNTSSERVICE_DIR)) } pub fn get_avatar_path_with(home: &Path, username: &str, accountsservice_dir: &Path) -> Option { // ~/.face takes priority — single stat via symlink_metadata to avoid TOCTOU let face = home.join(".face"); if let Ok(meta) = face.symlink_metadata() { if meta.is_file() && !meta.file_type().is_symlink() { return Some(face); } } // AccountsService icon if accountsservice_dir.exists() { let icon = accountsservice_dir.join(username); if let Ok(meta) = icon.symlink_metadata() { if meta.is_file() && !meta.file_type().is_symlink() { return Some(icon); } } } None } pub fn get_default_avatar_path() -> String { format!("{GRESOURCE_PREFIX}/default-avatar.svg") } #[cfg(test)] mod tests { use super::*; use std::fs; #[test] fn current_user_exists() { let u = get_current_user(); assert!(u.is_some()); let u = u.unwrap(); assert!(!u.username.is_empty()); } #[test] fn face_file_priority() { let dir = tempfile::tempdir().unwrap(); let face = dir.path().join(".face"); fs::write(&face, "img").unwrap(); let icons = dir.path().join("icons"); fs::create_dir(&icons).unwrap(); let icon = icons.join("test"); fs::write(&icon, "img").unwrap(); assert_eq!(get_avatar_path_with(dir.path(), "test", &icons), Some(face)); } #[test] fn accountsservice_fallback() { let dir = tempfile::tempdir().unwrap(); let icons = dir.path().join("icons"); fs::create_dir(&icons).unwrap(); let icon = icons.join("test"); fs::write(&icon, "img").unwrap(); assert_eq!(get_avatar_path_with(dir.path(), "test", &icons), Some(icon)); } #[test] fn no_avatar() { let dir = tempfile::tempdir().unwrap(); assert!(get_avatar_path_with(dir.path(), "test", Path::new("/nonexistent")).is_none()); } #[test] fn rejects_symlink() { let dir = tempfile::tempdir().unwrap(); let real = dir.path().join("real"); fs::write(&real, "x").unwrap(); std::os::unix::fs::symlink(&real, dir.path().join(".face")).unwrap(); assert!(get_avatar_path_with(dir.path(), "test", Path::new("/nonexistent")).is_none()); } #[test] fn default_avatar_gresource() { let p = get_default_avatar_path(); assert!(p.contains("moonlock")); assert!(p.contains("default-avatar.svg")); } }