use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct NormalizedPath {
path: PathBuf,
case_key: Option<String>,
}
impl NormalizedPath {
pub fn new(path: impl AsRef<Path>) -> Self {
let path = normalize(path.as_ref());
let case_key = if cfg!(windows) || cfg!(target_os = "macos") {
path.to_str().map(|s| s.to_lowercase())
} else {
None
};
Self { path, case_key }
}
#[must_use]
pub fn as_path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn case_key(&self) -> Option<&str> {
self.case_key.as_deref()
}
}
#[must_use]
pub fn normalize(path: &Path) -> PathBuf {
use std::path::Component;
let mut components = Vec::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if let Some(Component::Normal(_)) = components.last() {
components.pop();
} else {
components.push(component);
}
}
_ => components.push(component),
}
}
components.iter().collect()
}
#[must_use]
pub fn normalize_msys_path(path: &str) -> String {
#[cfg(windows)]
{
let bytes = path.as_bytes();
if bytes.len() >= 2
&& bytes[0] == b'/'
&& bytes[1].is_ascii_alphabetic()
&& (bytes.len() == 2 || bytes[2] == b'/')
{
let drive = (bytes[1] as char).to_ascii_uppercase();
let rest = if bytes.len() > 2 { &path[2..] } else { "" };
return format!("{drive}:{rest}").replace('/', "\\");
}
path.to_string()
}
#[cfg(not(windows))]
{
path.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_removes_dot() {
let p = normalize(Path::new("a/./b/c"));
assert_eq!(p, PathBuf::from("a/b/c"));
}
#[test]
fn normalize_resolves_dotdot() {
let p = normalize(Path::new("a/b/../c"));
assert_eq!(p, PathBuf::from("a/c"));
}
#[test]
fn msys_path_drive_letter() {
let result = normalize_msys_path("/c/Users/foo/bar");
#[cfg(windows)]
assert_eq!(result, r"C:\Users\foo\bar");
#[cfg(not(windows))]
assert_eq!(result, "/c/Users/foo/bar");
}
#[test]
fn msys_path_uppercase_drive() {
let result = normalize_msys_path("/D/project/build");
#[cfg(windows)]
assert_eq!(result, r"D:\project\build");
#[cfg(not(windows))]
assert_eq!(result, "/D/project/build");
}
#[test]
fn msys_path_bare_drive() {
let result = normalize_msys_path("/c");
#[cfg(windows)]
assert_eq!(result, "C:");
#[cfg(not(windows))]
assert_eq!(result, "/c");
}
#[test]
fn native_windows_path_unchanged() {
let result = normalize_msys_path(r"C:\Users\foo\bar");
assert_eq!(result, r"C:\Users\foo\bar");
}
#[test]
fn relative_path_unchanged() {
let result = normalize_msys_path("relative/path");
assert_eq!(result, "relative/path");
}
#[test]
fn empty_path_unchanged() {
let result = normalize_msys_path("");
assert_eq!(result, "");
}
#[test]
fn unix_absolute_path_not_drive() {
let result = normalize_msys_path("/usr/bin/gcc");
assert_eq!(result, "/usr/bin/gcc");
}
}