All checks were successful
Update PKGBUILD version / update-pkgver (push) Successful in 2s
- Add zeroize dependency, wrap password in Zeroizing<String> from entry extraction through to login_worker (prevents heap-resident plaintext) - Add MAX_BLUR_DIMENSION (1920px) downscale before GPU blur to reduce 4K workload - Wallpaper: use symlink_metadata + is_symlink rejection in greeter.rs and config.rs - Avatar: add is_file() check, swap lookup order to ~/.face first (consistent with moonlock/moonset) - greetd errors: show generic fallback in UI, log raw PAM details at debug level only - fprintd: validate device path prefix before creating D-Bus proxy - Locale: cache detected locale via OnceLock (avoid repeated env/file reads)
334 lines
12 KiB
Rust
334 lines
12 KiB
Rust
// ABOUTME: Configuration loading for the greeter.
|
|
// ABOUTME: Reads moongreet.toml for wallpaper and GTK theme settings with fallback hierarchy.
|
|
|
|
use serde::Deserialize;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
const MOONARCH_WALLPAPER: &str = "/usr/share/moonarch/wallpaper.jpg";
|
|
|
|
/// Default config search path: system-wide config.
|
|
fn default_config_paths() -> Vec<PathBuf> {
|
|
vec![PathBuf::from("/etc/moongreet/moongreet.toml")]
|
|
}
|
|
|
|
/// Raw TOML structure for deserialization.
|
|
#[derive(Debug, Clone, Default, Deserialize)]
|
|
struct TomlConfig {
|
|
appearance: Option<Appearance>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Deserialize)]
|
|
struct Appearance {
|
|
background: Option<String>,
|
|
#[serde(rename = "background-blur")]
|
|
background_blur: Option<f32>,
|
|
#[serde(rename = "gtk-theme")]
|
|
gtk_theme: Option<String>,
|
|
#[serde(rename = "fingerprint-enabled")]
|
|
fingerprint_enabled: Option<bool>,
|
|
}
|
|
|
|
/// Greeter configuration.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Config {
|
|
pub background_path: Option<String>,
|
|
pub background_blur: Option<f32>,
|
|
pub gtk_theme: Option<String>,
|
|
pub fingerprint_enabled: bool,
|
|
}
|
|
|
|
impl Default for Config {
|
|
fn default() -> Self {
|
|
Config {
|
|
background_path: None,
|
|
background_blur: None,
|
|
gtk_theme: None,
|
|
fingerprint_enabled: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load config from TOML files. Later paths override earlier ones.
|
|
pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
|
let default_paths = default_config_paths();
|
|
let paths = config_paths.unwrap_or(&default_paths);
|
|
|
|
let mut merged = Config::default();
|
|
for path in paths {
|
|
match fs::read_to_string(path) {
|
|
Ok(content) => {
|
|
match toml::from_str::<TomlConfig>(&content) {
|
|
Ok(parsed) => {
|
|
log::debug!("Config loaded: {}", path.display());
|
|
if let Some(appearance) = parsed.appearance {
|
|
if let Some(bg) = appearance.background {
|
|
// Resolve relative paths against config file directory
|
|
let bg_path = PathBuf::from(&bg);
|
|
if bg_path.is_absolute() {
|
|
merged.background_path = Some(bg);
|
|
} else if let Some(parent) = path.parent() {
|
|
merged.background_path =
|
|
Some(parent.join(&bg).to_string_lossy().to_string());
|
|
}
|
|
}
|
|
if let Some(blur) = appearance.background_blur {
|
|
if blur.is_finite() {
|
|
merged.background_blur = Some(blur.clamp(0.0, 200.0));
|
|
} else {
|
|
log::warn!("Ignoring non-finite background-blur value");
|
|
}
|
|
}
|
|
if appearance.gtk_theme.is_some() {
|
|
merged.gtk_theme = appearance.gtk_theme;
|
|
}
|
|
if let Some(fp) = appearance.fingerprint_enabled {
|
|
merged.fingerprint_enabled = fp;
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
log::warn!("Config parse error in {}: {e}", path.display());
|
|
}
|
|
}
|
|
}
|
|
Err(_) => {
|
|
log::debug!("Config not found: {}", path.display());
|
|
}
|
|
}
|
|
}
|
|
|
|
log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}, fingerprint={}", merged.background_path, merged.background_blur, merged.gtk_theme, merged.fingerprint_enabled);
|
|
merged
|
|
}
|
|
|
|
/// Resolve the wallpaper path using the fallback hierarchy.
|
|
///
|
|
/// Priority: config background_path > Moonarch system default > None (GTK background color).
|
|
pub fn resolve_background_path(config: &Config) -> Option<PathBuf> {
|
|
resolve_background_path_with(config, Path::new(MOONARCH_WALLPAPER))
|
|
}
|
|
|
|
/// Resolve with configurable moonarch wallpaper path (for testing).
|
|
pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> Option<PathBuf> {
|
|
// User-configured path — reject symlinks to prevent path traversal
|
|
if let Some(ref bg) = config.background_path {
|
|
let path = PathBuf::from(bg);
|
|
if let Ok(meta) = path.symlink_metadata() {
|
|
if meta.is_file() && !meta.file_type().is_symlink() {
|
|
log::debug!("Wallpaper: using config path {}", path.display());
|
|
return Some(path);
|
|
}
|
|
}
|
|
log::debug!("Wallpaper: config path {} not usable, trying fallbacks", path.display());
|
|
}
|
|
|
|
// Moonarch ecosystem default
|
|
if moonarch_wallpaper.is_file() {
|
|
log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display());
|
|
return Some(moonarch_wallpaper.to_path_buf());
|
|
}
|
|
|
|
log::debug!("Wallpaper: no wallpaper found, using GTK background color");
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn default_config_has_none_fields() {
|
|
let config = Config::default();
|
|
assert!(config.background_path.is_none());
|
|
assert!(config.background_blur.is_none());
|
|
assert!(config.gtk_theme.is_none());
|
|
assert!(config.fingerprint_enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_returns_default_when_no_files_exist() {
|
|
let paths = vec![PathBuf::from("/nonexistent/moongreet.toml")];
|
|
let config = load_config(Some(&paths));
|
|
assert!(config.background_path.is_none());
|
|
assert!(config.gtk_theme.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_reads_appearance_section() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let conf = dir.path().join("moongreet.toml");
|
|
fs::write(
|
|
&conf,
|
|
"[appearance]\nbackground = \"/custom/wallpaper.jpg\"\nbackground-blur = 20.0\ngtk-theme = \"catppuccin\"\n",
|
|
)
|
|
.unwrap();
|
|
let paths = vec![conf];
|
|
let config = load_config(Some(&paths));
|
|
assert_eq!(
|
|
config.background_path.as_deref(),
|
|
Some("/custom/wallpaper.jpg")
|
|
);
|
|
assert_eq!(config.background_blur, Some(20.0));
|
|
assert_eq!(config.gtk_theme.as_deref(), Some("catppuccin"));
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_blur_optional() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let conf = dir.path().join("moongreet.toml");
|
|
fs::write(&conf, "[appearance]\nbackground = \"/bg.jpg\"\n").unwrap();
|
|
let paths = vec![conf];
|
|
let config = load_config(Some(&paths));
|
|
assert!(config.background_blur.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_resolves_relative_background() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let conf = dir.path().join("moongreet.toml");
|
|
fs::write(&conf, "[appearance]\nbackground = \"bg.jpg\"\n").unwrap();
|
|
let paths = vec![conf];
|
|
let config = load_config(Some(&paths));
|
|
let expected = dir.path().join("bg.jpg").to_string_lossy().to_string();
|
|
assert_eq!(config.background_path.as_deref(), Some(expected.as_str()));
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_later_paths_override_earlier() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let conf1 = dir.path().join("first.toml");
|
|
let conf2 = dir.path().join("second.toml");
|
|
fs::write(
|
|
&conf1,
|
|
"[appearance]\nbackground = \"/first.jpg\"\ngtk-theme = \"first\"\n",
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
&conf2,
|
|
"[appearance]\nbackground = \"/second.jpg\"\ngtk-theme = \"second\"\n",
|
|
)
|
|
.unwrap();
|
|
let paths = vec![conf1, conf2];
|
|
let config = load_config(Some(&paths));
|
|
assert_eq!(config.background_path.as_deref(), Some("/second.jpg"));
|
|
assert_eq!(config.gtk_theme.as_deref(), Some("second"));
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_skips_missing_files() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let conf = dir.path().join("exists.toml");
|
|
fs::write(
|
|
&conf,
|
|
"[appearance]\nbackground = \"/exists.jpg\"\n",
|
|
)
|
|
.unwrap();
|
|
let paths = vec![PathBuf::from("/nonexistent.toml"), conf];
|
|
let config = load_config(Some(&paths));
|
|
assert_eq!(config.background_path.as_deref(), Some("/exists.jpg"));
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_uses_config_path_when_file_exists() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let wallpaper = dir.path().join("custom.jpg");
|
|
fs::write(&wallpaper, "fake").unwrap();
|
|
let config = Config {
|
|
background_path: Some(wallpaper.to_str().unwrap().to_string()),
|
|
..Config::default()
|
|
};
|
|
assert_eq!(
|
|
resolve_background_path_with(&config, Path::new("/nonexistent")),
|
|
Some(wallpaper)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_ignores_config_path_when_file_missing() {
|
|
let config = Config {
|
|
background_path: Some("/nonexistent/wallpaper.jpg".to_string()),
|
|
..Config::default()
|
|
};
|
|
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_uses_moonarch_wallpaper_as_second_fallback() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let moonarch_wp = dir.path().join("wallpaper.jpg");
|
|
fs::write(&moonarch_wp, "fake").unwrap();
|
|
let config = Config::default();
|
|
assert_eq!(
|
|
resolve_background_path_with(&config, &moonarch_wp),
|
|
Some(moonarch_wp)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_returns_none_when_no_wallpaper_found() {
|
|
let config = Config::default();
|
|
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_fingerprint_enabled_default_true() {
|
|
let paths = vec![PathBuf::from("/nonexistent/moongreet.toml")];
|
|
let config = load_config(Some(&paths));
|
|
assert!(config.fingerprint_enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_fingerprint_disabled() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let conf = dir.path().join("moongreet.toml");
|
|
fs::write(&conf, "[appearance]\nfingerprint-enabled = false\n").unwrap();
|
|
let paths = vec![conf];
|
|
let config = load_config(Some(&paths));
|
|
assert!(!config.fingerprint_enabled);
|
|
}
|
|
|
|
// -- Blur validation tests --
|
|
|
|
#[test]
|
|
fn load_config_blur_clamped_to_max() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let conf = dir.path().join("moongreet.toml");
|
|
fs::write(&conf, "[appearance]\nbackground-blur = 999.0\n").unwrap();
|
|
let config = load_config(Some(&[conf]));
|
|
assert_eq!(config.background_blur, Some(200.0));
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_blur_negative_clamped_to_zero() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let conf = dir.path().join("moongreet.toml");
|
|
fs::write(&conf, "[appearance]\nbackground-blur = -5.0\n").unwrap();
|
|
let config = load_config(Some(&[conf]));
|
|
assert_eq!(config.background_blur, Some(0.0));
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_blur_nan_rejected() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let conf = dir.path().join("moongreet.toml");
|
|
// TOML doesn't support NaN literals, but the parser may return NaN for nan
|
|
fs::write(&conf, "[appearance]\nbackground-blur = nan\n").unwrap();
|
|
let config = load_config(Some(&[conf]));
|
|
// nan is not valid TOML float, so the whole config parse fails → no blur
|
|
assert!(config.background_blur.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_blur_inf_rejected() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let conf = dir.path().join("moongreet.toml");
|
|
fs::write(&conf, "[appearance]\nbackground-blur = inf\n").unwrap();
|
|
let config = load_config(Some(&[conf]));
|
|
// inf is valid TOML → parsed as f32::INFINITY → rejected by is_finite() guard
|
|
assert!(config.background_blur.is_none());
|
|
}
|
|
}
|