// ABOUTME: Configuration loading for the lockscreen. // ABOUTME: Reads moonlock.toml for wallpaper and fingerprint settings with fallback hierarchy. use serde::Deserialize; use std::fs; use std::path::{Path, PathBuf}; const MOONARCH_WALLPAPER: &str = "/usr/share/moonarch/wallpaper.jpg"; fn default_config_paths() -> Vec { let mut paths = vec![PathBuf::from("/etc/moonlock/moonlock.toml")]; if let Some(home) = std::env::var_os("HOME") { paths.push(PathBuf::from(home).join(".config/moonlock/moonlock.toml")); } paths } /// Raw deserialization struct — fingerprint_enabled is optional so that /// an empty user config does not override the system config's value. #[derive(Debug, Clone, Default, Deserialize)] struct RawConfig { pub background_path: Option, pub background_blur: Option, pub fingerprint_enabled: Option, } /// Resolved configuration with concrete values. #[derive(Debug, Clone)] pub struct Config { pub background_path: Option, pub background_blur: Option, pub fingerprint_enabled: bool, } impl Default for Config { fn default() -> Self { Config { background_path: None, background_blur: None, fingerprint_enabled: true, } } } 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 { if let Ok(content) = fs::read_to_string(path) { match toml::from_str::(&content) { Ok(parsed) => { if parsed.background_path.is_some() { merged.background_path = parsed.background_path; } if let Some(blur) = parsed.background_blur { merged.background_blur = Some(blur.clamp(0.0, 200.0)); } if let Some(fp) = parsed.fingerprint_enabled { merged.fingerprint_enabled = fp; } } Err(e) => { log::warn!("Failed to parse {}: {e}", path.display()); } } } } merged } pub fn resolve_background_path(config: &Config) -> Option { resolve_background_path_with(config, Path::new(MOONARCH_WALLPAPER)) } pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> Option { 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() { return Some(path); } } } if moonarch_wallpaper.is_file() { return Some(moonarch_wallpaper.to_path_buf()); } None } #[cfg(test)] mod tests { use super::*; #[test] fn default_config() { let c = Config::default(); assert!(c.background_path.is_none()); assert!(c.background_blur.is_none()); assert!(c.fingerprint_enabled); } #[test] fn load_default_fingerprint_true() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("moonlock.toml"); fs::write(&conf, "").unwrap(); let c = load_config(Some(&[conf])); assert!(c.fingerprint_enabled); } #[test] fn load_background() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("moonlock.toml"); fs::write(&conf, "background_path = \"/custom/bg.jpg\"\nbackground_blur = 15.0\nfingerprint_enabled = false\n").unwrap(); let c = load_config(Some(&[conf])); assert_eq!(c.background_path.as_deref(), Some("/custom/bg.jpg")); assert_eq!(c.background_blur, Some(15.0)); assert!(!c.fingerprint_enabled); } #[test] fn load_blur_optional() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("moonlock.toml"); fs::write(&conf, "background_path = \"/bg.jpg\"\n").unwrap(); let c = load_config(Some(&[conf])); assert!(c.background_blur.is_none()); } #[test] fn resolve_config_path() { let dir = tempfile::tempdir().unwrap(); let wp = dir.path().join("bg.jpg"); fs::write(&wp, "fake").unwrap(); let c = Config { background_path: Some(wp.to_str().unwrap().to_string()), ..Config::default() }; assert_eq!(resolve_background_path_with(&c, Path::new("/nonexistent")), Some(wp)); } #[test] fn empty_user_config_preserves_system_fingerprint() { let dir = tempfile::tempdir().unwrap(); let sys_conf = dir.path().join("system.toml"); let usr_conf = dir.path().join("user.toml"); fs::write(&sys_conf, "fingerprint_enabled = false\n").unwrap(); fs::write(&usr_conf, "").unwrap(); let c = load_config(Some(&[sys_conf, usr_conf])); assert!(!c.fingerprint_enabled); } #[test] fn resolve_no_wallpaper_returns_none() { let c = Config::default(); let r = resolve_background_path_with(&c, Path::new("/nonexistent")); assert!(r.is_none()); } #[test] fn toml_parse_error_returns_default() { let dir = tempfile::tempdir().unwrap(); let conf = dir.path().join("moonlock.toml"); fs::write(&conf, "this is not valid toml {{{{").unwrap(); let c = load_config(Some(&[conf])); assert!(c.fingerprint_enabled); assert!(c.background_path.is_none()); } #[cfg(unix)] #[test] fn symlink_rejected_for_background() { let dir = tempfile::tempdir().unwrap(); let real = dir.path().join("bg.jpg"); let link = dir.path().join("link.jpg"); fs::write(&real, "fake").unwrap(); std::os::unix::fs::symlink(&real, &link).unwrap(); let c = Config { background_path: Some(link.to_str().unwrap().to_string()), ..Config::default() }; // Symlink should be rejected — falls through to None let r = resolve_background_path_with(&c, Path::new("/nonexistent")); assert!(r.is_none()); } }