use std::borrow::Borrow;
use std::fmt;
use std::fmt::Display;
use std::fmt::Formatter;
use std::ops::Deref;
#[cfg(windows)]
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
#[cfg(windows)]
use std::path::Prefix;
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub(crate) struct AbsolutePath(PathBuf);
impl AbsolutePath {
pub fn as_path(&self) -> &Path { &self.0 }
pub fn to_path_buf(&self) -> PathBuf { self.0.clone() }
pub fn display_path(&self) -> DisplayPath { DisplayPath::new(home_relative_path(&self.0)) }
pub fn resolve(raw: &str, base: &Path) -> Self {
let raw_path = Path::new(raw);
let resolved = if raw_path.is_absolute() {
PathBuf::from(raw)
} else {
base.join(raw)
};
Self::from(resolved.canonicalize().unwrap_or(resolved))
}
pub fn resolve_no_canonicalize(raw: &str, base: &Path) -> Self {
let raw_path = Path::new(raw);
if raw_path.is_absolute() {
Self::from(raw)
} else {
Self::from(base.join(raw))
}
}
}
impl PartialEq<Path> for AbsolutePath {
fn eq(&self, other: &Path) -> bool { self.0.as_path() == other }
}
impl PartialEq<AbsolutePath> for Path {
fn eq(&self, other: &AbsolutePath) -> bool { self == other.0.as_path() }
}
impl PartialEq<AbsolutePath> for PathBuf {
fn eq(&self, other: &AbsolutePath) -> bool { self.as_path() == other.0.as_path() }
}
impl PartialEq<PathBuf> for AbsolutePath {
fn eq(&self, other: &PathBuf) -> bool { self.0.as_path() == other.as_path() }
}
impl Deref for AbsolutePath {
type Target = Path;
fn deref(&self) -> &Path { &self.0 }
}
impl AsRef<Path> for AbsolutePath {
fn as_ref(&self) -> &Path { &self.0 }
}
impl Borrow<Path> for AbsolutePath {
fn borrow(&self) -> &Path { &self.0 }
}
impl Display for AbsolutePath {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.display().fmt(f) }
}
impl From<PathBuf> for AbsolutePath {
fn from(path: PathBuf) -> Self {
#[cfg(windows)]
let path = strip_verbatim_disk_prefix(path);
#[cfg(all(test, windows))]
let path = normalize_test_path(&path);
debug_assert!(
path.is_absolute(),
"AbsolutePath requires an absolute path: {}",
path.display()
);
Self(path)
}
}
impl From<String> for AbsolutePath {
fn from(path: String) -> Self { Self::from(PathBuf::from(path)) }
}
impl From<&str> for AbsolutePath {
fn from(path: &str) -> Self { Self::from(PathBuf::from(path)) }
}
impl From<&Path> for AbsolutePath {
fn from(path: &Path) -> Self { Self::from(path.to_path_buf()) }
}
#[cfg(windows)]
fn strip_verbatim_disk_prefix(path: PathBuf) -> PathBuf {
let mut components = path.components();
let Some(Component::Prefix(prefix)) = components.next() else {
return path;
};
let Prefix::VerbatimDisk(drive) = prefix.kind() else {
return path;
};
let mut conventional = PathBuf::from(format!("{}:\\", char::from(drive)));
conventional.extend(components.filter(|component| !matches!(component, Component::RootDir)));
conventional
}
#[cfg(test)]
pub(crate) fn normalize_test_path(path: &Path) -> PathBuf {
#[cfg(windows)]
if let Ok(rest) = path.strip_prefix("/") {
return Path::new("C:/").join(rest);
}
path.to_path_buf()
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct DisplayPath(String);
impl DisplayPath {
pub const fn new(s: String) -> Self { Self(s) }
pub fn as_str(&self) -> &str { &self.0 }
pub fn into_string(self) -> String { self.0 }
}
impl Display for DisplayPath {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.fmt(f) }
}
impl AsRef<str> for DisplayPath {
fn as_ref(&self) -> &str { self.as_str() }
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct RootDirectoryName(pub(super) String);
impl RootDirectoryName {
pub(crate) fn as_str(&self) -> &str { &self.0 }
pub(crate) fn into_string(self) -> String { self.0 }
}
impl Display for RootDirectoryName {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.fmt(f) }
}
impl AsRef<str> for RootDirectoryName {
fn as_ref(&self) -> &str { self.as_str() }
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct PackageName(pub(super) String);
impl PackageName {
pub(crate) fn as_str(&self) -> &str { &self.0 }
pub(crate) fn into_string(self) -> String { self.0 }
}
impl Display for PackageName {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.fmt(f) }
}
impl AsRef<str> for PackageName {
fn as_ref(&self) -> &str { self.as_str() }
}
pub(crate) fn home_relative_path(path: &Path) -> String {
if let Some(home) = dirs::home_dir()
&& let Ok(rel) = path.strip_prefix(&home)
{
return format!("~/{}", rel.display());
}
path.display().to_string()
}
pub(super) fn directory_leaf(path: &Path) -> String {
path.file_name()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or("")
.to_string()
}
#[cfg(all(test, windows))]
mod tests {
use super::*;
#[test]
fn verbatim_disk_prefix_is_stored_in_conventional_form() {
let canonicalized = AbsolutePath::from(PathBuf::from(r"\\?\C:\Users\dev\rust\bevy"));
assert_eq!(
canonicalized.as_path(),
Path::new(r"C:\Users\dev\rust\bevy")
);
assert!(
canonicalized.starts_with(Path::new(r"C:\")),
"a volume whose mount point reads `C:\\` must prefix-match the stored path"
);
}
#[test]
fn conventional_path_is_stored_unchanged() {
let path = AbsolutePath::from(PathBuf::from(r"C:\Users\dev\rust"));
assert_eq!(path.as_path(), Path::new(r"C:\Users\dev\rust"));
}
}