use crate::error::{Error, Result};
use std::path::{Path, PathBuf};
use tokio::fs;
const EXTENSIONS: &[&str] = &[
"json", "json5", "jsonc", "yaml", "yml", "toml", "ini", "xml",
];
fn has_supported_extension(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| EXTENSIONS.contains(&ext))
}
pub fn get_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(cwd) = std::env::current_dir() {
paths.push(cwd);
}
#[cfg(target_family = "unix")]
{
if let Some(config_home) = std::env::var_os("XDG_CONFIG_HOME") {
paths.push(PathBuf::from(config_home));
} else if let Some(home) = dirs::home_dir() {
paths.push(home.join(".config"));
}
if let Some(config_dirs) = std::env::var_os("XDG_CONFIG_DIRS") {
for dir in std::env::split_paths(&config_dirs) {
paths.push(dir);
}
}
if let Some(home) = dirs::home_dir() {
paths.push(home);
}
paths.push(PathBuf::from("/usr/local/etc"));
paths.push(PathBuf::from("/usr/etc"));
paths.push(PathBuf::from("/etc"));
}
#[cfg(target_family = "windows")]
{
if let Some(profile) = dirs::home_dir() {
paths.push(profile);
}
if let Some(appdata) = dirs::config_dir() {
paths.push(appdata);
}
if let Some(program_data) = std::env::var_os("ProgramData") {
paths.push(PathBuf::from(program_data));
}
if let Some(system_root) = std::env::var_os("SystemRoot") {
paths.push(PathBuf::from(system_root));
}
}
paths
}
pub async fn find_config_file(name: &str) -> Result<PathBuf> {
let search_paths = get_search_paths();
for base_path in &search_paths {
let exact_path = base_path.join(name);
if has_supported_extension(&exact_path) && fs::metadata(&exact_path).await.is_ok() {
return Ok(exact_path);
}
for ext in EXTENSIONS {
let file_path = base_path.join(format!("{}.{}", name, ext));
if fs::metadata(&file_path).await.is_ok() {
return Ok(file_path);
}
}
}
let path = Path::new(name);
let is_explicit_path = path.is_absolute() || name.starts_with("./") || name.starts_with("../");
if is_explicit_path && has_supported_extension(path) && fs::metadata(path).await.is_ok() {
return Ok(path.to_path_buf());
}
Err(Error::FileNotFound(name.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn test_get_search_paths() {
let paths = get_search_paths();
assert!(!paths.is_empty());
if let Ok(cwd) = std::env::current_dir() {
assert_eq!(paths[0], cwd);
}
}
#[cfg(target_family = "unix")]
#[test]
fn test_unix_paths_included() {
let paths = get_search_paths();
let path_strings: Vec<String> = paths.iter().map(|p| p.display().to_string()).collect();
assert!(path_strings.iter().any(|p| p.contains("/etc")));
}
#[cfg(target_family = "windows")]
#[test]
fn test_windows_paths_included() {
let paths = get_search_paths();
assert!(paths.iter().any(|p| {
let s = p.display().to_string();
s.contains("Users") || s.contains("AppData")
}));
}
#[test]
fn test_has_supported_extension() {
assert!(has_supported_extension(Path::new("config.toml")));
assert!(has_supported_extension(Path::new("config.json")));
assert!(has_supported_extension(Path::new("config.yaml")));
assert!(has_supported_extension(Path::new("config.yml")));
assert!(has_supported_extension(Path::new("/path/to/config.toml")));
assert!(!has_supported_extension(Path::new("config")));
assert!(!has_supported_extension(Path::new("config.txt")));
assert!(!has_supported_extension(Path::new("config.rs")));
}
#[tokio::test]
#[serial]
async fn test_find_exact_file_with_extension() {
use std::io::Write;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("myconfig.toml");
let mut file = std::fs::File::create(&file_path).unwrap();
writeln!(file, "key = \"value\"").unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let result = find_config_file("myconfig.toml").await;
assert!(result.is_ok());
let found_path = result.unwrap();
assert!(found_path.ends_with("myconfig.toml"));
std::env::set_current_dir(original_dir).unwrap();
}
#[tokio::test]
#[serial]
async fn test_find_file_appends_extension() {
use std::io::Write;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("myconfig.json");
let mut file = std::fs::File::create(&file_path).unwrap();
writeln!(file, r#"{{"key": "value"}}"#).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let result = find_config_file("myconfig").await;
assert!(result.is_ok());
let found_path = result.unwrap();
assert!(found_path.ends_with("myconfig.json"));
std::env::set_current_dir(original_dir).unwrap();
}
}