use std::borrow::Cow;
use std::path::{Path, PathBuf};
use cu::Context as _;
pub trait PathExtension {
fn file_name_str(&self) -> cu::Result<&str>;
fn ensure_exists(&self) -> cu::Result<()>;
fn simplified(&self) -> &Path;
fn normalize(&self) -> cu::Result<PathBuf>;
fn normalize_exists(&self) -> cu::Result<PathBuf> {
let x = self.normalize()?;
x.ensure_exists()?;
Ok(x)
}
fn normalize_executable(&self) -> cu::Result<PathBuf>;
#[inline(always)]
fn parent_abs(&self) -> cu::Result<PathBuf> {
self.parent_abs_times(1)
}
fn parent_abs_times(&self, x: usize) -> cu::Result<PathBuf>;
fn try_to_rel(&self) -> Cow<'_, Path> {
self.try_to_rel_from(".")
}
fn try_to_rel_from(&self, path: impl AsRef<Path>) -> Cow<'_, Path>;
#[cfg(feature = "process")]
fn command(&self) -> cu::CommandBuilder;
}
impl PathExtension for Path {
fn file_name_str(&self) -> cu::Result<&str> {
let file_name = self
.file_name()
.with_context(|| format!("cannot get file name for path: '{}'", self.display()))?;
let Some(file_name) = file_name.to_str() else {
crate::bail!("file name is not utf-8: '{}'", self.display());
};
Ok(file_name)
}
fn simplified(&self) -> &Path {
if self.as_os_str().as_encoded_bytes().starts_with(b"\\\\") {
dunce::simplified(self)
} else {
self
}
}
fn ensure_exists(&self) -> cu::Result<()> {
if !self.exists() {
crate::bail!("path '{}' does not exist.", self.display());
}
Ok(())
}
fn normalize(&self) -> cu::Result<PathBuf> {
if let Ok(x) = dunce::canonicalize(self) {
return Ok(x);
};
if self.is_absolute() {
return fallback_normalize_absolute(self);
}
let Ok(mut base) = dunce::canonicalize(".") else {
crate::bail!(
"failed to normalize current directory when normalizing relative path: '{}'",
self.display()
);
};
base.push(self);
fallback_normalize_absolute(&base)
}
fn normalize_executable(&self) -> crate::Result<PathBuf> {
let absolute_self = if self.is_absolute() {
fallback_normalize_absolute(self)?
} else {
let Ok(mut base) = dunce::canonicalize(".") else {
crate::bail!(
"failed to normalize current directory when normalizing relative path: '{}'",
self.display()
);
};
base.push(self);
fallback_normalize_absolute(&base)?
};
if !absolute_self.exists() {
crate::bail!(
"failed to normalize executable path '{}': does not exist",
absolute_self.display()
);
}
if absolute_self.is_dir() {
crate::bail!(
"failed to normalize executable path '{}': is a directory",
absolute_self.display()
)
}
Ok(absolute_self)
}
fn parent_abs_times(&self, x: usize) -> crate::Result<PathBuf> {
let mut out = self.normalize()?;
for _ in 0..x {
if !out.pop() {
crate::bail!("trying to get parent of root");
}
}
Ok(out)
}
#[inline(always)]
fn try_to_rel_from(&self, path: impl AsRef<Path>) -> Cow<'_, Path> {
try_to_rel_from(self, path.as_ref())
}
#[cfg(feature = "process")]
fn command(&self) -> crate::CommandBuilder {
crate::CommandBuilder::new(self)
}
}
fn fallback_normalize_absolute(path: &Path) -> crate::Result<PathBuf> {
let mut prefix = None;
let mut components = vec![];
for c in path.components() {
match c {
std::path::Component::Prefix(_prefix) => {
prefix = Some(_prefix);
}
std::path::Component::RootDir => {
components.clear();
}
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
if components.pop().is_none() {
crate::bail!(
"trying to get parent of root when normalizing: {}",
path.display()
);
}
}
std::path::Component::Normal(os_str) => components.push(os_str),
}
}
let mut out = match prefix {
None => PathBuf::from("/"),
Some(prefix) => {
let mut out = prefix.as_os_str().to_ascii_uppercase();
out.push("\\"); out.into()
}
};
out.extend(components);
if out.as_os_str().as_encoded_bytes().starts_with(b"\\\\") {
Ok(dunce::simplified(&out).to_path_buf())
} else {
Ok(out)
}
}
fn try_to_rel_from<'a>(self_: &'a Path, path: &Path) -> Cow<'a, Path> {
let res = match (self_.is_absolute(), path.is_absolute()) {
(true, true) => pathdiff::diff_paths(self_, path),
(true, false) => {
let Ok(base) = path.normalize() else {
return Cow::Borrowed(self_);
};
pathdiff::diff_paths(self_, base.as_path())
}
(false, true) => {
let Ok(self_) = self_.normalize() else {
return Cow::Borrowed(self_);
};
pathdiff::diff_paths(self_.as_path(), path)
}
(false, false) => {
let Ok(self_abs) = self_.normalize() else {
return Cow::Borrowed(self_);
};
let Ok(base) = path.normalize() else {
return Cow::Borrowed(self_);
};
pathdiff::diff_paths(self_abs.as_path(), base.as_path())
}
};
match res {
None => Cow::Borrowed(self_),
Some(x) => Cow::Owned(x),
}
}
impl PathExtension for PathBuf {
fn file_name_str(&self) -> crate::Result<&str> {
self.as_path().file_name_str()
}
fn simplified(&self) -> &Path {
self.as_path().simplified()
}
fn normalize(&self) -> crate::Result<PathBuf> {
self.as_path().normalize()
}
fn normalize_executable(&self) -> crate::Result<PathBuf> {
self.as_path().normalize_executable()
}
fn ensure_exists(&self) -> crate::Result<()> {
self.as_path().ensure_exists()
}
fn parent_abs_times(&self, x: usize) -> crate::Result<PathBuf> {
self.as_path().parent_abs_times(x)
}
fn try_to_rel_from(&self, path: impl AsRef<Path>) -> Cow<'_, Path> {
self.as_path().try_to_rel_from(path)
}
#[cfg(feature = "process")]
fn command(&self) -> crate::CommandBuilder {
self.as_path().command()
}
}