use anyhow::{Context, Result};
use glob::{MatchOptions, glob_with};
use std::path::{MAIN_SEPARATOR, PathBuf};
use std::sync::LazyLock;
use crate::os::{AbstractPlatform as _, Platform};
static HOME_PREFIX: LazyLock<String> = LazyLock::new(|| format!("~{MAIN_SEPARATOR}"));
fn expand_home_directory(path: &str) -> Result<PathBuf> {
Ok(match path {
"~" => homedir::my_home()?
.ok_or_else(|| anyhow::anyhow!("could not determine home directory"))?,
s if s.starts_with(&*HOME_PREFIX) => {
let Ok(Some(home)) = homedir::my_home() else {
anyhow::bail!("could not determine home directory")
};
home.join(&s[2..])
}
s if s.starts_with('~') => {
let mut parts = s[1..].splitn(2, MAIN_SEPARATOR);
let Some(username) = parts.next() else {
anyhow::bail!("could not extract username from path")
};
let pb = homedir::home(username)?
.ok_or_else(|| anyhow::anyhow!("could not determine other home directory"))?;
if let Some(path) = parts.next() {
pb.join(path)
} else {
pb
}
}
s => PathBuf::from(s),
})
}
pub fn find_include_files(arg: &str, is_user: bool) -> Result<Vec<String>> {
let mut path = if arg.starts_with('~') {
anyhow::ensure!(
is_user,
"include paths may not start with ~ in a system configuration file"
);
expand_home_directory(arg).with_context(|| format!("expanding include expression {arg}"))?
} else {
PathBuf::from(arg)
};
if !path.is_absolute() {
if is_user {
let Some(mut buf) = dirs::home_dir() else {
anyhow::bail!("could not determine home directory");
};
buf.push(".ssh");
buf.push(path);
path = buf;
} else {
let Some(mut buf) = Platform::system_ssh_dir_path() else {
anyhow::bail!("could not determine system ssh config directory");
};
buf.push(path);
path = buf;
}
}
let mut result = Vec::new();
let options = MatchOptions {
case_sensitive: true,
require_literal_leading_dot: true,
require_literal_separator: true,
};
for entry in (glob_with(path.to_string_lossy().as_ref(), options)?).flatten() {
if let Some(s) = entry.to_str() {
result.push(s.into());
}
}
Ok(result)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod test {
use super::{expand_home_directory, find_include_files};
use pretty_assertions::assert_eq;
macro_rules! xhd {
($s:expr) => {
*expand_home_directory($s).unwrap().as_os_str()
};
}
#[test]
fn home_dir() {
use super::MAIN_SEPARATOR;
let home_env = homedir::my_home()
.unwrap()
.unwrap_or("dummy-test-home".into());
assert_eq!(xhd!("~"), *home_env);
let expect = format!("{}{MAIN_SEPARATOR}file", home_env.display());
let lookup = format!("~{MAIN_SEPARATOR}file");
assert_eq!(xhd!(&lookup), *expect);
}
#[cfg_attr(target_os = "windows", ignore)] #[test]
fn home_dir_other_user() {
let user = std::env::var("USER").expect("this test requires a USER");
assert!(!user.is_empty());
let home = dirs::home_dir().expect("this test requires a home directory");
let path_part = format!("~{user}");
assert_eq!(xhd!(&path_part), home);
let s = "any/old~file/";
assert_eq!(xhd!("any/old~file/"), *s);
}
#[test]
fn include_paths() {
let _ = find_include_files("~", false).expect_err("~ in system should be disallowed");
let _ = dirs::home_dir().expect("this test requires a HOME");
let testpath = format!("~{}zzznonexistent", super::MAIN_SEPARATOR);
let d = find_include_files(&testpath, true).expect("home directory should have expanded");
assert!(d.is_empty());
let _ = find_include_files("~nonexistent-user-xyzy", true)
.expect_err("non existent user should have bailed");
}
#[test]
fn relative_paths() {
let d = find_include_files("nonexistent-really----", false).expect("");
assert_eq!(d, Vec::<String>::new());
let d = find_include_files("nonexistent-really----", true).expect("");
assert!(d.is_empty());
}
}