322 lines · 10.6 KB · Rust
RawDownload
//! Relative path types for deltadb.//!//! Provides [`RelPath`] and [`RelPathBuf`] — path types that are guaranteed to be//! relative, normalized, and valid unicode. Internally stored in POSIX (`/`-delimited)//! format regardless of host platform.//!//! Adapted from Zed's `util::rel_path` module. 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()?;         // Match behavior of std::path::Path, which is case-insensitive for drive letters (e.g., "C:" == "c:")        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('\\')))} /// Normalizes a path by resolving `.` and `..` components without/// requiring the path to exist on disk (unlike `canonicalize`).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}