zed/crates/path/src/abs_path.rs
Lukas Wirth f181a2f47b
Split out RelPath into a separate crate (#61029)
This is necessary to remove some `util` dependencies from crates, as
well as better sharing for our projects. This also includes the WIP
AbsPath abstraction as well as some bug fixes from internal tooling.


Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-15 08:33:25 +00:00

286 lines
7.7 KiB
Rust

use std::{
borrow::{Borrow, Cow},
fmt, io,
ops::Deref,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use anyhow::Context;
use crate::{PathStyle, rel_path::RelPath};
// An absolute path on the user's local filesystem.
// Requires paths to be valid utf-8
#[derive(PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
#[repr(transparent)]
pub struct AbsPath(Path);
impl AbsPath {
pub fn new(path: &Path) -> anyhow::Result<&Self> {
if !path.is_absolute() {
return Err(anyhow::anyhow!("Path is not absolute: {:?}", path));
}
if path.to_str().is_none() {
return Err(anyhow::anyhow!("Path is not valid utf-8: {:?}", path));
}
Ok(Self::new_unchecked(path))
}
fn new_unchecked(path: &Path) -> &Self {
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
unsafe { &*(path as *const Path as *const Self) }
}
pub fn to_abs_path_buf(&self) -> AbsPathBuf {
AbsPathBuf(self.0.to_owned())
}
pub fn join(&self, name: impl AsRef<str>) -> AbsPathBuf {
AbsPathBuf(self.0.join(name.as_ref()))
}
pub fn join_rel_path(&self, relative_path: &RelPath) -> AbsPathBuf {
AbsPathBuf(self.0.join(relative_path.as_std_path()))
}
pub fn parent(&self) -> Option<&AbsPath> {
let parent = self.0.parent()?;
Some(AbsPath::new_unchecked(parent))
}
pub fn starts_with(&self, other: &AbsPath) -> bool {
self.0.starts_with(&other.0)
}
pub fn ends_with(&self, other: &RelPath) -> bool {
self.0.ends_with(other.as_std_path())
}
pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
if self == ancestor {
return false;
}
self.starts_with(ancestor)
}
pub fn file_name(&self) -> Option<&str> {
self.0.file_name()?.to_str()
}
pub fn display(&self) -> impl fmt::Display + '_ {
self.0.display()
}
pub fn as_std_path(&self) -> &Path {
&self.0
}
pub fn as_str(&self) -> &str {
self.0
.to_str()
.expect("valid UTF-8 enforced in constructor")
}
pub fn ancestors(&self) -> impl Iterator<Item = &AbsPath> {
self.0.ancestors().map(|p| AbsPath::new_unchecked(p))
}
pub fn strip_prefix<'a>(&'a self, prefix: &AbsPath) -> Option<Cow<'a, RelPath>> {
let prefix = self.0.strip_prefix(&prefix.0).ok()?;
RelPath::new(prefix, PathStyle::local()).ok()
}
}
impl ToOwned for AbsPath {
type Owned = AbsPathBuf;
fn to_owned(&self) -> Self::Owned {
self.to_abs_path_buf()
}
}
impl AsRef<Path> for AbsPath {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl AsRef<AbsPath> for AbsPath {
fn as_ref(&self) -> &AbsPath {
self
}
}
impl From<&AbsPath> for Arc<AbsPath> {
fn from(path: &AbsPath) -> Self {
let arc: Arc<Path> = Arc::from(&path.0);
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
unsafe { Arc::from_raw(Arc::into_raw(arc) as *const AbsPath) }
}
}
impl From<&AbsPath> for Rc<AbsPath> {
fn from(path: &AbsPath) -> Self {
let arc: Rc<Path> = Rc::from(&path.0);
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
unsafe { Rc::from_raw(Rc::into_raw(arc) as *const AbsPath) }
}
}
// An absolute path on the user's local filesystem.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct AbsPathBuf(PathBuf);
impl AbsPathBuf {
pub fn new(path: impl Into<PathBuf>) -> anyhow::Result<Self> {
let path = path.into();
if let Err(e) = AbsPath::new(&path) {
return Err(e);
}
Ok(Self(path))
}
pub fn home_dir() -> anyhow::Result<Self> {
let home = std::env::home_dir().context("no home dir available")?;
Self::new(home)
}
/// Resolves `path` to its canonical on-disk spelling: symlinks and `..`
/// are resolved, relative input is anchored on the current directory, and
/// on case-insensitive filesystems each component takes the casing stored
/// on disk. Unlike [`std::fs::canonicalize`], the result never uses
/// Windows extended-length (`\\?\`) syntax, which chokes tools the path
/// is later handed to (e.g. `git`).
///
/// Paths act as identity in several places (lock keys, watch-target
/// comparisons, persisted repository records), so canonicalize a path
/// where it enters the system whenever it may have been spelled by a user
/// or an external tool.
pub fn canonicalize(path: impl AsRef<Path>) -> io::Result<Self> {
let canonical = dunce::canonicalize(path.as_ref())?;
Self::new(canonical).map_err(io::Error::other)
}
pub fn push(&mut self, name: &str) {
self.0.push(name);
}
#[cfg(any(test, feature = "test-support"))]
pub fn new_test(path: &'static str) -> Self {
if cfg!(windows) {
Self::new(format!("C:{path}")).unwrap()
} else {
Self::new(path).unwrap()
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn abs_path(path: &str) -> AbsPathBuf {
if cfg!(windows) {
AbsPathBuf::new(format!("C:{path}")).unwrap()
} else {
AbsPathBuf::new(path).unwrap()
}
}
impl Deref for AbsPathBuf {
type Target = AbsPath;
fn deref(&self) -> &Self::Target {
AbsPath::new_unchecked(&self.0)
}
}
impl Borrow<AbsPath> for AbsPathBuf {
fn borrow(&self) -> &AbsPath {
self
}
}
impl AsRef<Path> for AbsPathBuf {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl AsRef<AbsPath> for AbsPathBuf {
fn as_ref(&self) -> &AbsPath {
self
}
}
impl From<AbsPathBuf> for PathBuf {
fn from(path: AbsPathBuf) -> PathBuf {
path.0
}
}
impl fmt::Display for AbsPathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}
impl PartialEq<AbsPath> for AbsPathBuf {
fn eq(&self, other: &AbsPath) -> bool {
**self == *other
}
}
impl PartialEq<AbsPathBuf> for AbsPath {
fn eq(&self, other: &AbsPathBuf) -> bool {
*self == **other
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canonicalize_restores_on_disk_spelling() {
let temp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(temp.path()).unwrap();
let dir = root.join("CamelCase");
std::fs::create_dir(&dir).unwrap();
let canonical = AbsPathBuf::canonicalize(&dir).unwrap();
assert_eq!(canonical.as_std_path(), dir);
assert!(
!canonical.as_str().starts_with(r"\\?\"),
"canonical paths must not use Windows extended-length syntax: {canonical}"
);
// A differently-cased spelling addresses the same directory only on a
// case-insensitive filesystem; when it does, canonicalization must
// restore the stored casing.
let lowercased = root.join("camelcase");
if std::fs::metadata(&lowercased).is_ok() {
assert_eq!(
AbsPathBuf::canonicalize(&lowercased).unwrap().as_std_path(),
dir,
"canonicalization should restore the on-disk casing"
);
}
}
#[test]
fn test_new_test_normalizes_rooted_paths() {
if cfg!(windows) {
assert_eq!(AbsPathBuf::new_test("/").as_str(), "C:/");
assert_eq!(
AbsPathBuf::new_test("/test/project").as_str(),
"C:/test/project"
);
} else {
assert_eq!(AbsPathBuf::new_test("/").as_str(), "/");
assert_eq!(
AbsPathBuf::new_test("/test/project").as_str(),
"/test/project"
);
}
}
}