use crate::EnvSource;
use std::borrow::Cow;
use std::path::{Path, PathBuf};
fn home_dir_with_env<E: EnvSource + ?Sized>(env: &E) -> Option<PathBuf> {
if let Some(home) = env.var("HOME").filter(|h| !h.is_empty()) {
return Some(PathBuf::from(home));
}
env.var("USERPROFILE")
.filter(|h| !h.is_empty())
.map(PathBuf::from)
}
pub fn display_under_root(root: &Path, path: &Path) -> String {
let relative = match path.strip_prefix(root) {
Ok(relative) => relative.to_path_buf(),
Err(_) => std::fs::canonicalize(root)
.ok()
.and_then(|real_root| path.strip_prefix(real_root).ok().map(Path::to_path_buf))
.unwrap_or_else(|| path.to_path_buf()),
};
let cleaned: PathBuf = relative
.components()
.filter(|c| !matches!(c, std::path::Component::CurDir))
.collect();
let rendered = slash_display(&cleaned);
if rendered.is_empty() {
".".to_string()
} else {
rendered
}
}
pub fn slash_display(path: &Path) -> String {
let rendered = path.display().to_string();
if cfg!(windows) {
rendered.replace('\\', "/")
} else {
rendered
}
}
pub fn probe_dir() -> PathBuf {
std::env::temp_dir()
}
pub fn expand_tilde(path: &str) -> Cow<'_, str> {
expand_tilde_with_env(path, &crate::ProcessEnvSource)
}
pub fn expand_tilde_with_env<'p, E: EnvSource + ?Sized>(path: &'p str, env: &E) -> Cow<'p, str> {
if let Some(rest) = path.strip_prefix('~')
&& (rest.is_empty() || rest.starts_with('/'))
&& let Some(home) = home_dir_with_env(env)
{
let rest_trimmed = rest.strip_prefix('/').unwrap_or(rest);
let resolved = if rest_trimmed.is_empty() {
home
} else {
home.join(rest_trimmed)
};
return Cow::Owned(resolved.to_string_lossy().into_owned());
}
Cow::Borrowed(path)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MapEnvSource;
#[cfg(unix)]
#[test]
fn display_under_root_strips_a_symlinked_root() {
let tmp = tempfile::tempdir().unwrap();
let real = tmp.path().join("real");
std::fs::create_dir_all(real.join("crates/app")).unwrap();
let link = tmp.path().join("link");
std::os::unix::fs::symlink(&real, &link).unwrap();
let resolved = std::fs::canonicalize(&link)
.unwrap()
.join("crates/app/Cargo.toml");
assert_eq!(
display_under_root(&link, &resolved),
PathBuf::from("crates/app/Cargo.toml").display().to_string()
);
}
#[test]
fn display_under_root_strips_the_root_and_the_dot() {
let root = Path::new("/repo");
assert_eq!(
display_under_root(root, &root.join("crates/app").join("Cargo.toml")),
PathBuf::from("crates/app/Cargo.toml").display().to_string()
);
assert_eq!(
display_under_root(root, &root.join(".").join("Cargo.toml")),
"Cargo.toml"
);
assert_eq!(display_under_root(root, &root.join(".")), ".");
assert_eq!(
display_under_root(root, Path::new("/elsewhere/Cargo.toml")),
PathBuf::from("/elsewhere/Cargo.toml").display().to_string()
);
}
#[test]
fn expands_leading_tilde_slash() {
let env = MapEnvSource::new().with("HOME", "/home/tester");
let expected = PathBuf::from("/home/tester")
.join("x")
.to_string_lossy()
.into_owned();
assert_eq!(expand_tilde_with_env("~/x", &env), expected);
}
#[test]
fn expands_bare_tilde() {
let env = MapEnvSource::new().with("HOME", "/home/tester");
assert_eq!(expand_tilde_with_env("~", &env), "/home/tester");
}
#[test]
fn passes_through_non_tilde_path() {
let env = MapEnvSource::new().with("HOME", "/home/tester");
assert_eq!(
expand_tilde_with_env("/etc/anodizer.yaml", &env),
"/etc/anodizer.yaml"
);
assert_eq!(
expand_tilde_with_env("./safe~backup.yaml", &env),
"./safe~backup.yaml"
);
}
#[test]
fn user_home_form_not_expanded() {
let env = MapEnvSource::new().with("HOME", "/home/tester");
assert_eq!(expand_tilde_with_env("~bob/foo", &env), "~bob/foo");
assert_eq!(expand_tilde_with_env("~bob", &env), "~bob");
}
#[test]
fn falls_back_to_userprofile() {
let env = MapEnvSource::new().with("USERPROFILE", "/Users/winuser");
let got = expand_tilde_with_env("~/docs", &env).into_owned();
let expected = PathBuf::from("/Users/winuser")
.join("docs")
.to_string_lossy()
.into_owned();
assert_eq!(got, expected);
}
}