use std::path::{Path, PathBuf};
pub fn home_dir() -> Option<PathBuf> {
if let Some(override_home) = std::env::var_os("LEVIATH_HOME") {
return Some(PathBuf::from(override_home));
}
dirs::home_dir()
}
pub fn data_dir() -> Option<PathBuf> {
home_dir().map(|home| home.join(".leviath"))
}
pub fn tools_dir() -> Option<PathBuf> {
data_dir().map(|d| d.join("tools"))
}
pub fn providers_dir() -> Option<PathBuf> {
data_dir().map(|d| d.join("providers"))
}
pub fn agents_dir() -> Option<PathBuf> {
data_dir().map(|d| d.join("agents"))
}
pub fn is_safe_path_component(name: &str) -> bool {
!name.is_empty()
&& name != "."
&& name != ".."
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
}
pub fn resolves_within(path: &Path, root: &Path) -> bool {
let root = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
match canonicalize_existing_prefix(path) {
Some(real) => real.starts_with(&root),
None => false,
}
}
fn canonicalize_existing_prefix(path: &Path) -> Option<PathBuf> {
let mut probe = path.to_path_buf();
let mut tail: Vec<std::ffi::OsString> = Vec::new();
loop {
if let Ok(real) = std::fs::canonicalize(&probe) {
let mut full = real;
for component in tail.iter().rev() {
full.push(component);
}
return Some(full);
}
let (Some(name), Some(parent)) = (probe.file_name(), probe.parent()) else {
return None;
};
let (name, parent) = (name.to_os_string(), parent.to_path_buf());
tail.push(name);
probe = parent;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_data_path_follows_leviath_home_together() {
temp_env::with_var("LEVIATH_HOME", Some("/tmp/lev-paths-test"), || {
let root = PathBuf::from("/tmp/lev-paths-test");
assert_eq!(home_dir(), Some(root.clone()));
let data = root.join(".leviath");
assert_eq!(data_dir(), Some(data.clone()));
assert_eq!(tools_dir(), Some(data.join("tools")));
assert_eq!(providers_dir(), Some(data.join("providers")));
assert_eq!(agents_dir(), Some(data.join("agents")));
});
}
#[test]
fn without_the_override_paths_sit_under_the_real_home() {
temp_env::with_var_unset("LEVIATH_HOME", || {
let home = home_dir().expect("a home directory resolves");
let data = data_dir().expect("so does the data dir");
assert_eq!(data, home.join(".leviath"));
assert_eq!(tools_dir(), Some(data.join("tools")));
assert!(tools_dir().expect("set").ends_with(".leviath/tools"));
});
}
#[test]
fn safe_components_are_ordinary_names() {
for name in ["coder", "my-agent", "agent_2", "v1.2.3", ".hidden", "a"] {
assert!(is_safe_path_component(name), "{name}");
}
}
#[test]
fn traversal_and_separators_are_refused() {
for name in [
"",
".",
"..",
"../evil",
"../../../../tmp/x",
"/etc/passwd",
"a/b",
"a\\b",
"..%2fevil",
"a\0b",
"a b",
"a;rm -rf",
"C:evil",
] {
assert!(!is_safe_path_component(name), "{name:?} should be refused");
}
}
#[test]
fn plain_paths_inside_the_root_are_contained() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(root.join("file.txt"), b"x").unwrap();
assert!(resolves_within(&root.join("file.txt"), root));
assert!(resolves_within(&root.join("new.txt"), root));
assert!(resolves_within(&root.join("a/b/c.txt"), root));
}
#[test]
fn a_path_outside_the_root_is_not_contained() {
let dir = tempfile::tempdir().unwrap();
let other = tempfile::tempdir().unwrap();
assert!(!resolves_within(other.path(), dir.path()));
}
#[cfg(unix)]
#[test]
fn a_symlink_out_of_the_root_is_not_contained() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let link = root.join("link");
std::os::unix::fs::symlink("/", &link).unwrap();
let target = link.join("etc/passwd");
assert!(
target.starts_with(root),
"precondition: textually contained"
);
assert!(!resolves_within(&target, root));
}
#[cfg(unix)]
#[test]
fn a_symlink_within_the_root_is_contained() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir(root.join("real")).unwrap();
std::fs::write(root.join("real/file.txt"), b"x").unwrap();
std::os::unix::fs::symlink(root.join("real"), root.join("link")).unwrap();
assert!(resolves_within(&root.join("link/file.txt"), root));
}
#[cfg(unix)]
#[test]
fn a_new_file_under_an_escaping_symlink_is_not_contained() {
let dir = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let root = dir.path();
std::os::unix::fs::symlink(outside.path(), root.join("link")).unwrap();
assert!(!resolves_within(&root.join("link/brand-new.txt"), root));
}
#[test]
fn an_uncanonicalized_root_still_matches() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(root.join("f.txt"), b"x").unwrap();
assert!(resolves_within(&root.join("f.txt"), root));
}
#[test]
fn a_relative_path_with_no_existing_ancestor_is_refused() {
assert!(!resolves_within(
Path::new("nonexistent-relative"),
Path::new("/definitely/not/here")
));
}
}