use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use crate::rel_path::RelPath;
pub mod abs_path;
pub mod rel_path;
pub trait PathExt {
fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf>;
}
impl<T: AsRef<Path> + ?Sized> PathExt for T {
fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf> {
Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PathStyle {
Unix,
Windows,
}
impl PathStyle {
#[cfg(target_os = "windows")]
pub const fn local() -> Self {
PathStyle::Windows
}
#[cfg(not(target_os = "windows"))]
pub const fn local() -> Self {
PathStyle::Unix
}
#[inline]
pub fn primary_separator(&self) -> &'static str {
match self {
PathStyle::Unix => "/",
PathStyle::Windows => "\\",
}
}
pub fn separators(&self) -> &'static [&'static str] {
match self {
PathStyle::Unix => &["/"],
PathStyle::Windows => &["\\", "/"],
}
}
pub fn separators_ch(&self) -> &'static [char] {
match self {
PathStyle::Unix => &['/'],
PathStyle::Windows => &['\\', '/'],
}
}
pub fn is_absolute(&self, path_like: &str) -> bool {
path_like.starts_with('/')
|| *self == PathStyle::Windows
&& (path_like.starts_with('\\')
|| path_like
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
&& path_like[1..]
.strip_prefix(':')
.is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
}
pub fn is_windows(&self) -> bool {
*self == PathStyle::Windows
}
pub fn is_posix(&self) -> bool {
*self == PathStyle::Unix
}
pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
let right = right.as_ref().to_str()?;
if is_absolute(right, self) {
return None;
}
let left = left.as_ref().to_str()?;
if left.is_empty() {
Some(right.into())
} else {
Some(format!(
"{left}{}{right}",
if left.ends_with(self.primary_separator()) {
""
} else {
self.primary_separator()
}
))
}
}
pub fn join_path(
self,
left: impl AsRef<Path>,
right: impl AsRef<Path>,
) -> anyhow::Result<PathBuf> {
let left = left
.as_ref()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
let right = right.as_ref();
let right_string = right
.to_str()
.ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
let joined = self
.join(left, right_string)
.ok_or_else(|| anyhow::anyhow!("Path must be relative: {right:?}"))?;
Ok(PathBuf::from(self.normalize(&joined)))
}
pub fn normalize(self, path_like: &str) -> String {
match self {
PathStyle::Windows => {
let drive_and_remainder = path_like.split_once(':').filter(|(drive, _)| {
let mut characters = drive.chars();
characters
.next()
.is_some_and(|character| character.is_ascii_alphabetic())
&& characters.next().is_none()
});
let unc_remainder = path_like
.strip_prefix("\\\\")
.or_else(|| path_like.strip_prefix("//"));
let (prefix, remainder) = if let Some((drive, remainder)) = drive_and_remainder {
if let Some(remainder) = remainder
.strip_prefix('\\')
.or_else(|| remainder.strip_prefix('/'))
{
(format!("{drive}:\\"), remainder)
} else {
(format!("{drive}:"), remainder)
}
} else if let Some(remainder) = unc_remainder {
let (server, remainder) = match remainder.split_once(['\\', '/']) {
Some(parts) => parts,
None => return path_like.to_string(),
};
let (share, remainder) = match remainder.split_once(['\\', '/']) {
Some(parts) => parts,
None => return format!("\\\\{server}\\{remainder}"),
};
(format!("\\\\{server}\\{share}\\"), remainder)
} else if let Some(remainder) = path_like
.strip_prefix('\\')
.or_else(|| path_like.strip_prefix('/'))
{
("\\".to_string(), remainder)
} else {
(String::new(), path_like)
};
let mut components: Vec<&str> = Vec::new();
for component in remainder.split(['\\', '/']) {
match component {
"" | "." => {}
".." => {
if components.last().is_some_and(|c| *c != "..") {
components.pop();
} else if prefix.is_empty() {
components.push(component);
}
}
component => components.push(component),
}
}
let normalized = components.join("\\");
if prefix.is_empty() {
normalized
} else {
format!("{prefix}{normalized}")
}
}
PathStyle::Unix => {
let is_absolute = path_like.starts_with('/');
let remainder = if is_absolute {
path_like.trim_start_matches('/')
} else {
path_like
};
let mut components = Vec::new();
for component in remainder.split(self.separators_ch()) {
match component {
"" | "." => {}
".." => {
if components
.last()
.is_some_and(|component| *component != "..")
{
components.pop();
} else if !is_absolute {
components.push(component);
}
}
component => components.push(component),
}
}
let normalized = components.join(self.primary_separator());
if is_absolute && normalized.is_empty() {
"/".to_string()
} else if is_absolute {
format!("/{normalized}")
} else {
normalized
}
}
}
}
pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
let Some(pos) = path_like.rfind(self.primary_separator()) else {
return (None, path_like);
};
let filename_start = pos + self.primary_separator().len();
(
Some(&path_like[..filename_start]),
&path_like[filename_start..],
)
}
pub fn strip_prefix<'a>(
&self,
child: &'a Path,
parent: &'a Path,
) -> Option<std::borrow::Cow<'a, RelPath>> {
let parent = parent.to_str()?;
if parent.is_empty() {
return RelPath::new(child, *self).ok();
}
let parent = self
.separators()
.iter()
.find_map(|sep| parent.strip_suffix(sep))
.unwrap_or(parent);
let child = child.to_str()?;
let stripped = if self.is_windows()
&& child.as_bytes().get(1) == Some(&b':')
&& parent.as_bytes().get(1) == Some(&b':')
&& child.as_bytes()[0].eq_ignore_ascii_case(&parent.as_bytes()[0])
{
child[2..].strip_prefix(&parent[2..])?
} else {
child.strip_prefix(parent)?
};
if let Some(relative) = self
.separators()
.iter()
.find_map(|sep| stripped.strip_prefix(sep))
{
RelPath::new(relative.as_ref(), *self).ok()
} else if stripped.is_empty() {
Some(Cow::Borrowed(RelPath::empty()))
} else {
None
}
}
}
fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
path_like.starts_with('/')
|| path_style == PathStyle::Windows
&& (path_like.starts_with('\\')
|| path_like
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
&& path_like[1..]
.strip_prefix(':')
.is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
}
pub fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}