moonset/src/config.rs
nevaforget 5a6900e85a fix: address audit findings — polling, symlinks, validation, wallpaper removal (v0.7.0)
Three parallel audits (quality, performance, security) identified issues
across the codebase. This commit addresses all remaining findings:

- Replace busy-loop polling in run_command with child.wait() + timeout thread
- Canonicalize ~/.face and AccountsService avatar paths to prevent symlink abuse
- Add detect_locale_with() DI function for testable locale detection
- Move config I/O from activate() to main() to avoid blocking GTK main loop
- Validate background_blur range (0–200), reject invalid values with warning
- Remove embedded wallpaper from GResource — moonarch provides it via filesystem
  (binary size ~3.2MB → ~1.3MB)
2026-03-28 23:09:29 +01:00

275 lines
9.7 KiB
Rust

// ABOUTME: Configuration loading for the session power menu.
// ABOUTME: Reads moonset.toml for wallpaper 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 paths: system-wide, then user-specific.
fn default_config_paths() -> Vec<PathBuf> {
let mut paths = vec![PathBuf::from("/etc/moonset/moonset.toml")];
if let Some(config_dir) = dirs::config_dir() {
paths.push(config_dir.join("moonset").join("moonset.toml"));
}
paths
}
/// Power menu configuration.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Config {
pub background_path: Option<String>,
pub background_blur: Option<f32>,
}
/// 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 {
if let Ok(content) = fs::read_to_string(path) {
match toml::from_str::<Config>(&content) {
Ok(parsed) => {
log::debug!("Config loaded: {}", path.display());
if parsed.background_path.is_some() {
merged.background_path = parsed.background_path;
}
if parsed.background_blur.is_some() {
merged.background_blur = parsed.background_blur;
}
}
Err(e) => {
log::warn!("Failed to parse {}: {e}", path.display());
}
}
}
}
// Validate blur range
if let Some(blur) = merged.background_blur {
if !blur.is_finite() || blur < 0.0 || blur > 200.0 {
log::warn!("Invalid background_blur value {blur}, ignoring");
merged.background_blur = None;
}
}
merged
}
/// Resolve the wallpaper path using the fallback hierarchy.
///
/// Priority: config background_path > Moonarch system default.
/// Returns None if no wallpaper is available (CSS background shows through).
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
if let Some(ref bg) = config.background_path {
let path = PathBuf::from(bg);
if path.is_file() {
log::debug!("Wallpaper source: config ({})", path.display());
return Some(path);
}
}
// Moonarch ecosystem default
if moonarch_wallpaper.is_file() {
log::debug!("Wallpaper source: moonarch default ({})", moonarch_wallpaper.display());
return Some(moonarch_wallpaper.to_path_buf());
}
log::debug!("No wallpaper found, using CSS background");
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_has_none_background() {
let config = Config::default();
assert!(config.background_path.is_none());
assert!(config.background_blur.is_none());
}
#[test]
fn load_config_returns_default_when_no_files_exist() {
let paths = vec![PathBuf::from("/nonexistent/moonset.toml")];
let config = load_config(Some(&paths));
assert!(config.background_path.is_none());
}
#[test]
fn load_config_reads_background_path() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moonset.toml");
fs::write(&conf, "background_path = \"/custom/wallpaper.jpg\"\n").unwrap();
let paths = vec![conf];
let config = load_config(Some(&paths));
assert_eq!(config.background_path.as_deref(), Some("/custom/wallpaper.jpg"));
}
#[test]
fn load_config_reads_background_blur() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moonset.toml");
fs::write(&conf, "background_blur = 20.0\n").unwrap();
let paths = vec![conf];
let config = load_config(Some(&paths));
assert_eq!(config.background_blur, Some(20.0));
}
#[test]
fn load_config_blur_override() {
let dir = tempfile::tempdir().unwrap();
let conf1 = dir.path().join("first.toml");
let conf2 = dir.path().join("second.toml");
fs::write(&conf1, "background_blur = 10.0\n").unwrap();
fs::write(&conf2, "background_blur = 25.0\n").unwrap();
let paths = vec![conf1, conf2];
let config = load_config(Some(&paths));
assert_eq!(config.background_blur, Some(25.0));
}
#[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, "background_path = \"/first.jpg\"\n").unwrap();
fs::write(&conf2, "background_path = \"/second.jpg\"\n").unwrap();
let paths = vec![conf1, conf2];
let config = load_config(Some(&paths));
assert_eq!(config.background_path.as_deref(), Some("/second.jpg"));
}
#[test]
fn load_config_skips_missing_files() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("exists.toml");
fs::write(&conf, "background_path = \"/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_eq!(result, 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_available() {
let config = Config::default();
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
assert_eq!(result, None);
}
#[test]
fn load_config_ignores_invalid_toml_syntax() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("bad.toml");
fs::write(&conf, "this is not valid [[[ toml").unwrap();
let paths = vec![conf];
let config = load_config(Some(&paths));
assert!(config.background_path.is_none());
assert!(config.background_blur.is_none());
}
#[test]
fn load_config_ignores_wrong_field_types() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("wrong_type.toml");
fs::write(&conf, "background_blur = \"not_a_number\"\n").unwrap();
let paths = vec![conf];
let config = load_config(Some(&paths));
assert!(config.background_blur.is_none());
}
#[test]
fn load_config_rejects_negative_blur() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("negative.toml");
fs::write(&conf, "background_blur = -5.0\n").unwrap();
let paths = vec![conf];
let config = load_config(Some(&paths));
assert_eq!(config.background_blur, None);
}
#[test]
fn load_config_rejects_excessive_blur() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("huge.toml");
fs::write(&conf, "background_blur = 999.0\n").unwrap();
let paths = vec![conf];
let config = load_config(Some(&paths));
assert_eq!(config.background_blur, None);
}
#[test]
fn load_config_accepts_valid_blur_range() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("valid.toml");
fs::write(&conf, "background_blur = 50.0\n").unwrap();
let paths = vec![conf];
let config = load_config(Some(&paths));
assert_eq!(config.background_blur, Some(50.0));
}
#[test]
fn load_config_accepts_zero_blur() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("zero.toml");
fs::write(&conf, "background_blur = 0.0\n").unwrap();
let paths = vec![conf];
let config = load_config(Some(&paths));
assert_eq!(config.background_blur, Some(0.0));
}
#[test]
fn load_config_accepts_max_blur() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("max.toml");
fs::write(&conf, "background_blur = 200.0\n").unwrap();
let paths = vec![conf];
let config = load_config(Some(&paths));
assert_eq!(config.background_blur, Some(200.0));
}
}