use std::borrow::Cow;
use std::ffi::OsStr;
use std::io::{Error as IoError, ErrorKind};
use std::path::{Path, PathBuf};
type Result<T, E = IoError> = core::result::Result<T, E>;
pub trait PathResolveExt {
fn resolve(&self) -> Cow<Path> {
self.try_resolve()
.expect("should resolve path in current directory")
}
fn try_resolve(&self) -> Result<Cow<Path>> {
let cwd = std::env::current_dir()?;
let resolved = self.try_resolve_in(&cwd)?;
Ok(resolved)
}
fn resolve_in<P: AsRef<Path>>(&self, base: P) -> Cow<Path> {
self.try_resolve_in(base).expect("should resolve path")
}
fn try_resolve_in<P: AsRef<Path>>(&self, base: P) -> Result<Cow<Path>>;
}
impl<T: AsRef<OsStr>> PathResolveExt for T {
fn try_resolve_in<P: AsRef<Path>>(&self, base: P) -> Result<Cow<Path>> {
try_resolve_path(base.as_ref(), Path::new(self))
}
}
fn try_resolve_path<'a>(base: &Path, to_resolve: &'a Path) -> Result<Cow<'a, Path>> {
if to_resolve.is_absolute() {
return Ok(Cow::Borrowed(to_resolve));
}
if to_resolve.starts_with(Path::new("~")) {
let resolved = resolve_tilde(to_resolve)?;
return Ok(resolved);
}
let absolute_base = if base.is_absolute() {
base.to_owned()
} else {
let base_resolved_tilde = resolve_tilde(base)?;
if base_resolved_tilde.is_relative() {
return Err(IoError::new(
ErrorKind::InvalidData,
"the base path must be able to resolve to an absolute path",
));
}
base_resolved_tilde.into_owned()
};
let base_directory = match std::fs::metadata(&absolute_base) {
Ok(meta) => {
if meta.is_file() {
match absolute_base.parent() {
Some(parent) => parent.to_path_buf(),
None => {
return Err(IoError::new(
ErrorKind::NotFound,
"the base path points to a file with no parent directory",
))
}
}
} else {
absolute_base
}
}
Err(_) => absolute_base,
};
let resolved = base_directory.join(to_resolve);
Ok(Cow::Owned(resolved))
}
fn resolve_tilde(path: &Path) -> Result<Cow<Path>> {
let home = home_dir().ok_or_else(|| IoError::new(ErrorKind::NotFound, "homedir not found"))?;
Ok(resolve_tilde_with_home(home, path))
}
fn resolve_tilde_with_home(home: PathBuf, path: &Path) -> Cow<Path> {
if !path.starts_with(Path::new("~")) {
return Cow::Borrowed(path);
}
let path_str = match path.to_str() {
Some(s) => s,
None => return Cow::Borrowed(path),
};
let stripped = &path_str[1..];
if stripped.is_empty() {
return Cow::Owned(home);
}
if stripped.starts_with('/') {
let stripped = stripped.trim_start_matches('/');
let resolved = home.join(stripped);
return Cow::Owned(resolved);
}
Cow::Borrowed(path)
}
#[allow(unused)]
#[cfg(not(test))]
fn home_dir() -> Option<PathBuf> {
dirs::home_dir()
}
#[allow(unused)]
#[cfg(test)]
fn home_dir() -> Option<PathBuf> {
Some(PathBuf::from("/home/test"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
#[test]
fn test_resolve_tilde() {
assert_eq!("~".resolve(), Path::new("/home/test"));
assert_eq!("~".to_string().resolve(), Path::new("/home/test"));
assert_eq!(Path::new("~").resolve(), Path::new("/home/test"));
assert_eq!(PathBuf::from("~").resolve(), Path::new("/home/test"));
assert_eq!(OsStr::new("~").resolve(), Path::new("/home/test"));
assert_eq!(OsString::from("~").resolve(), Path::new("/home/test"));
}
#[test]
fn test_resolve_tilde_slash() {
assert_eq!("~/".resolve(), Path::new("/home/test"));
}
#[test]
fn test_resolve_tilde_path() {
assert_eq!(
"~/.config/alacritty/alacritty.yml".resolve(),
Path::new("/home/test/.config/alacritty/alacritty.yml")
);
}
#[test]
fn test_resolve_tilde_multislash() {
assert_eq!("~/////".resolve(), Path::new("/home/test"));
}
#[test]
fn test_resolve_tilde_multislash_path() {
assert_eq!("~/////.config".resolve(), Path::new("/home/test/.config"));
}
#[test]
fn test_resolve_tilde_with_relative_segments() {
assert_eq!(
"~/.config/../.vim/".resolve(),
Path::new("/home/test/.config/../.vim/")
)
}
#[test]
fn test_resolve_path() {
assert_eq!(
"./config.yml".resolve_in("/home/user/.app"),
Path::new("/home/user/.app/config.yml")
);
}
#[test]
fn test_resolve_path_base_trailing_slash() {
assert_eq!(
"./config.yml".resolve_in("/home/user/.app/"),
Path::new("/home/user/.app/config.yml")
);
}
#[test]
fn test_resolve_path_with_tilde() {
assert_eq!(
"./config.yml".resolve_in("~/.app"),
Path::new("/home/test/.app/config.yml")
);
}
#[test]
fn test_resolve_absolute_path() {
assert_eq!(
"/etc/nixos/configuration.nix".resolve_in("/home/usr/.app"),
Path::new("/etc/nixos/configuration.nix")
);
}
#[test]
fn test_resolve_absolute_path2() {
assert_eq!(
"~/.config/alacritty/alacritty.yml".resolve_in("/tmp"),
Path::new("/home/test/.config/alacritty/alacritty.yml")
);
}
#[test]
fn test_resolve_relative_path() {
assert_eq!(
"../.app2/config.yml".resolve_in("/home/user/.app"),
Path::new("/home/user/.app/../.app2/config.yml")
);
}
#[test]
fn test_resolve_current_dir() {
assert_eq!(".".resolve_in("/home/user"), Path::new("/home/user"));
}
#[test]
fn test_resolve_cwd() {
std::env::set_current_dir("/tmp").unwrap();
assert_eq!("garbage.txt".resolve(), Path::new("/tmp/garbage.txt"));
}
#[test]
fn test_resolve_base_file() {
let base_path = "/tmp/path-resolve-test.txt";
std::fs::write(base_path, "Hello!").unwrap();
assert_eq!(
"./other-tmp-file.txt".resolve_in(base_path),
Path::new("/tmp/other-tmp-file.txt")
);
}
}