use crate::types::{AppConfig, AppError, Result};
use std::fs;
use std::path::{Path, PathBuf};
const DEFAULT_CONFIG_PATH: &str = "/etc/rust-network-manager/config.yaml";
const PKG_DEFAULT_CONFIG_PATH_FALLBACK: &str = "pkg-files/config/default.yaml";
fn get_pkg_default_config_path() -> PathBuf {
if cfg!(debug_assertions) {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(PKG_DEFAULT_CONFIG_PATH_FALLBACK)
} else {
PathBuf::from(PKG_DEFAULT_CONFIG_PATH_FALLBACK) }
}
pub fn load_config(config_path_opt: Option<&Path>) -> Result<AppConfig> {
let config_path = config_path_opt
.map(|p| p.to_path_buf())
.unwrap_or_else(|| Path::new(DEFAULT_CONFIG_PATH).to_path_buf());
tracing::info!("Attempting to load configuration from: {:?}", config_path);
let config_str = match fs::read_to_string(&config_path) {
Ok(s) => s,
Err(e) => {
let pkg_default_path = get_pkg_default_config_path();
tracing::warn!(
"Failed to read config file {:?}: {}. Trying package default at {:?}.",
config_path,
e,
pkg_default_path
);
fs::read_to_string(&pkg_default_path).map_err(|e_fallback| {
AppError::Config(format!(
"Failed to read both {:?} and {:?}: {} (fallback error: {})",
config_path,
pkg_default_path,
e,
e_fallback
))
})?
}
};
serde_yaml::from_str(&config_str)
.map_err(|e| AppError::Config(format!("Failed to parse YAML: {}", e)))
}
pub fn validate_config(config: &AppConfig) -> Result<()> {
if config.interfaces.is_empty() {
return Err(AppError::Config(
"Configuration must define at least one interface.".to_string(),
));
}
Ok(())
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::types::InterfaceConfig;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_load_valid_config() {
let yaml = r#"
interfaces:
- name: eth0
dhcp: true
nftables_zone: wan
- name: eth1
address: 192.168.1.1/24
nftables_zone: lan
socket_path: /tmp/test.sock
"#;
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "{}", yaml).unwrap();
let config = load_config(Some(file.path())).unwrap();
assert_eq!(config.interfaces.len(), 2);
assert_eq!(config.interfaces[0].name, "eth0");
assert_eq!(config.interfaces[0].dhcp, Some(true));
assert_eq!(config.interfaces[0].nftables_zone, Some("wan".to_string()));
assert_eq!(config.interfaces[1].name, "eth1");
assert_eq!(config.interfaces[1].address, Some("192.168.1.1/24".to_string()));
assert_eq!(config.interfaces[1].nftables_zone, Some("lan".to_string()));
assert_eq!(config.socket_path, Some("/tmp/test.sock".to_string()));
}
#[test]
fn test_load_fallback_config() {
let non_existent_path = Path::new("/tmp/non_existent_config_for_test.yaml");
let _ = std::fs::remove_file(&non_existent_path);
let fallback_path = get_pkg_default_config_path();
let fallback_dir = fallback_path.parent().unwrap();
std::fs::create_dir_all(fallback_dir).unwrap();
let fallback_yaml = r#"
interfaces:
- name: "fallback0"
dhcp: true
socket_path: "/tmp/fallback.sock"
"#;
std::fs::write(&fallback_path, fallback_yaml).unwrap();
let config = load_config(Some(&non_existent_path)).unwrap();
assert_eq!(config.interfaces.len(), 1);
assert_eq!(config.interfaces[0].name, "fallback0");
assert_eq!(config.socket_path, Some("/tmp/fallback.sock".to_string()));
let _ = std::fs::remove_file(&fallback_path);
let _ = std::fs::remove_dir(fallback_dir); }
#[test]
fn test_load_invalid_yaml() {
let yaml = "interfaces:\n - name: eth0\n invalid_indent: true";
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "{}", yaml).unwrap();
let result = load_config(Some(file.path()));
assert!(result.is_err());
if let Err(AppError::Config(msg)) = result {
assert!(msg.contains("Failed to parse YAML"));
} else {
panic!("Expected Config error");
}
}
#[test]
fn test_validate_empty_interfaces() {
let config = AppConfig {
interfaces: vec![],
socket_path: None,
nftables_rules_path: None,
};
let result = validate_config(&config);
assert!(result.is_err());
if let Err(AppError::Config(msg)) = result {
assert!(msg.contains("at least one interface"));
} else {
panic!("Expected Config error");
}
}
}