All checks were successful
Update PKGBUILD version / update-pkgver (push) Successful in 2s
- Replace canonicalize() with symlink_metadata + is_file + !is_symlink for avatar lookup (prevents symlink traversal to arbitrary files) - Fix blur padding offset from (0,0) to (-pad,-pad) to prevent edge darkening - Add MAX_BLUR_DIMENSION (1920px) downscale before GPU blur - Validate blur per config source (invalid user value preserves system default) - Wallpaper: use symlink_metadata + is_file + !is_symlink in resolve_background_path
164 lines
5.3 KiB
Rust
164 lines
5.3 KiB
Rust
// ABOUTME: Current user detection and avatar loading for the power menu.
|
|
// ABOUTME: Retrieves user info from the system (pwd, AccountsService, ~/.face).
|
|
|
|
use nix::unistd::{getuid, User as NixUser};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
const DEFAULT_ACCOUNTSSERVICE_DIR: &str = "/var/lib/AccountsService/icons";
|
|
|
|
/// Represents the current user for the power menu.
|
|
#[derive(Debug, Clone)]
|
|
pub struct User {
|
|
pub username: String,
|
|
pub display_name: String,
|
|
pub home: PathBuf,
|
|
pub uid: u32,
|
|
}
|
|
|
|
/// Get the currently logged-in user's info from the system.
|
|
pub fn get_current_user() -> Option<User> {
|
|
let uid = getuid();
|
|
let nix_user = NixUser::from_uid(uid).ok()??;
|
|
|
|
let gecos = nix_user.gecos.to_str().unwrap_or("").to_string();
|
|
// GECOS field may contain comma-separated values; first field is the full name
|
|
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(),
|
|
})
|
|
}
|
|
|
|
/// Find the user's avatar image, checking ~/.face then AccountsService.
|
|
pub fn get_avatar_path(home: &Path, username: Option<&str>) -> Option<PathBuf> {
|
|
get_avatar_path_with(home, username, Path::new(DEFAULT_ACCOUNTSSERVICE_DIR))
|
|
}
|
|
|
|
/// Find avatar with configurable AccountsService dir (for testing).
|
|
/// Rejects symlinks to prevent path traversal.
|
|
pub fn get_avatar_path_with(
|
|
home: &Path,
|
|
username: Option<&str>,
|
|
accountsservice_dir: &Path,
|
|
) -> Option<PathBuf> {
|
|
// ~/.face takes priority
|
|
let face = home.join(".face");
|
|
if let Ok(meta) = face.symlink_metadata() {
|
|
if meta.file_type().is_symlink() {
|
|
log::warn!("Rejecting symlink avatar: {}", face.display());
|
|
} else if meta.is_file() {
|
|
log::debug!("Avatar: using ~/.face ({})", face.display());
|
|
return Some(face);
|
|
}
|
|
}
|
|
|
|
// AccountsService icon fallback
|
|
if let Some(name) = username {
|
|
if accountsservice_dir.exists() {
|
|
let icon = accountsservice_dir.join(name);
|
|
if let Ok(meta) = icon.symlink_metadata() {
|
|
if meta.file_type().is_symlink() {
|
|
log::warn!("Rejecting symlink avatar: {}", icon.display());
|
|
} else if meta.is_file() {
|
|
log::debug!("Avatar: using AccountsService icon ({})", icon.display());
|
|
return Some(icon);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Return the GResource path to the default avatar SVG.
|
|
pub fn get_default_avatar_path() -> String {
|
|
let prefix = crate::GRESOURCE_PREFIX;
|
|
format!("{prefix}/default-avatar.svg")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::fs;
|
|
|
|
#[test]
|
|
fn get_current_user_returns_some() {
|
|
let user = get_current_user();
|
|
assert!(user.is_some());
|
|
let user = user.unwrap();
|
|
assert!(!user.username.is_empty());
|
|
assert!(!user.display_name.is_empty());
|
|
assert!(user.home.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn returns_face_file_if_exists() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let face = dir.path().join(".face");
|
|
fs::write(&face, "fake image").unwrap();
|
|
let path = get_avatar_path_with(dir.path(), None, Path::new("/nonexistent"));
|
|
assert_eq!(path, Some(face));
|
|
}
|
|
|
|
#[test]
|
|
fn returns_accountsservice_icon_if_exists() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let icons_dir = dir.path().join("icons");
|
|
fs::create_dir(&icons_dir).unwrap();
|
|
let icon = icons_dir.join("testuser");
|
|
fs::write(&icon, "fake image").unwrap();
|
|
let path = get_avatar_path_with(dir.path(), Some("testuser"), &icons_dir);
|
|
assert_eq!(path, Some(icon));
|
|
}
|
|
|
|
#[test]
|
|
fn face_file_takes_priority_over_accountsservice() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let face = dir.path().join(".face");
|
|
fs::write(&face, "fake image").unwrap();
|
|
let icons_dir = dir.path().join("icons");
|
|
fs::create_dir(&icons_dir).unwrap();
|
|
let icon = icons_dir.join("testuser");
|
|
fs::write(&icon, "fake image").unwrap();
|
|
let path = get_avatar_path_with(dir.path(), Some("testuser"), &icons_dir);
|
|
assert_eq!(path, Some(face));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_symlink_avatar() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let target = dir.path().join("secret");
|
|
fs::write(&target, "secret content").unwrap();
|
|
let face = dir.path().join(".face");
|
|
std::os::unix::fs::symlink(&target, &face).unwrap();
|
|
let path = get_avatar_path_with(dir.path(), None, Path::new("/nonexistent"));
|
|
assert!(path.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn returns_none_when_no_avatar() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = get_avatar_path_with(dir.path(), None, Path::new("/nonexistent"));
|
|
assert!(path.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn default_avatar_path_is_gresource() {
|
|
let path = get_default_avatar_path();
|
|
assert!(path.contains("default-avatar.svg"));
|
|
assert!(path.starts_with("/dev/moonarch/moonset"));
|
|
}
|
|
}
|