pub fn normalize_path(path: &str) -> String {
#[cfg(windows)]
let path = {
let mut p = path.replace('\\', "/");
if p.starts_with("//?/") {
p = p[4..].to_string();
}
p
};
#[cfg(windows)]
let path = path.as_str();
if path.is_empty() {
return String::new();
}
let drive_prefix = if path.len() >= 2
&& path.as_bytes()[0].is_ascii_alphabetic()
&& path.as_bytes()[1] == b':'
{
Some(&path[..2])
} else {
None
};
let is_abs = path.starts_with('/') || drive_prefix.is_some();
let path_to_split = if let Some(d) = drive_prefix {
if path.len() > d.len() {
&path[d.len() + 1..]
} else {
""
}
} else {
path
};
let mut parts: Vec<&str> = Vec::new();
for component in path_to_split.split('/') {
match component {
"" | "." => {}
".." => {
if is_abs {
parts.pop();
} else if parts.last().is_none_or(|&p| p == "..") {
parts.push("..");
} else {
parts.pop();
}
}
c => parts.push(c),
}
}
if is_abs {
if let Some(d) = drive_prefix {
if parts.is_empty() {
format!("{d}/")
} else {
format!("{d}/{}", parts.join("/"))
}
} else {
format!("/{}", parts.join("/"))
}
} else if parts.is_empty() {
".".to_string()
} else {
parts.join("/")
}
}
pub fn is_absolute_path(path: &str) -> bool {
#[cfg(windows)]
let path = path.replace('\\', "/");
#[cfg(windows)]
let path = path.as_str();
path.starts_with('/')
|| path.starts_with("\\\\")
|| (path.len() >= 2
&& path.as_bytes()[0].is_ascii_alphabetic()
&& path.as_bytes()[1] == b':')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_simple() {
assert_eq!(normalize_path("a/b/c"), "a/b/c");
}
#[test]
fn normalize_dot() {
assert_eq!(normalize_path("a/./b"), "a/b");
}
#[test]
fn normalize_dotdot() {
assert_eq!(normalize_path("a/b/../c"), "a/c");
}
#[test]
fn normalize_trailing_slash() {
assert_eq!(normalize_path("a/b/"), "a/b");
}
#[test]
fn normalize_root() {
assert_eq!(normalize_path("/"), "/");
}
#[test]
fn normalize_absolute() {
assert_eq!(normalize_path("/a/b/../c"), "/a/c");
}
#[test]
fn normalize_empty() {
assert_eq!(normalize_path(""), "");
}
#[test]
fn normalize_only_dots() {
assert_eq!(normalize_path("./././."), ".");
}
#[cfg(windows)]
#[test]
fn normalize_windows_backslash() {
assert_eq!(normalize_path("a\\b\\c"), "a/b/c");
}
#[cfg(not(windows))]
#[test]
fn normalize_posix_preserves_backslash() {
assert_eq!(normalize_path("a\\b\\c"), "a\\b\\c");
}
#[cfg(windows)]
#[test]
fn normalize_windows_drive() {
assert_eq!(normalize_path("C:\\Users\\test"), "C:/Users/test");
}
#[cfg(windows)]
#[test]
fn normalize_windows_unc_prefix() {
assert_eq!(normalize_path("\\\\?\\C:\\foo\\bar"), "C:/foo/bar");
}
#[test]
fn normalize_dotdot_at_root() {
assert_eq!(normalize_path("/../a"), "/a");
}
#[test]
fn normalize_relative_dotdot_beyond() {
assert_eq!(normalize_path("a/../../b"), "../b");
}
#[test]
fn is_absolute_unix() {
assert!(is_absolute_path("/foo/bar"));
}
#[test]
fn is_absolute_windows() {
assert!(is_absolute_path("C:/Users"));
assert!(is_absolute_path("C:\\Users"));
assert!(is_absolute_path("C:"));
}
#[test]
fn is_not_absolute() {
assert!(!is_absolute_path("foo/bar"));
assert!(!is_absolute_path(""));
assert!(!is_absolute_path("./foo"));
}
#[test]
fn normalize_bare_drive_letter() {
assert_eq!(normalize_path("C:"), "C:/");
}
#[cfg(not(windows))]
#[test]
fn normalize_posix_backslash_in_filename() {
assert_eq!(normalize_path("dir/file\\name.txt"), "dir/file\\name.txt");
}
#[test]
fn is_absolute_windows_unc() {
assert!(is_absolute_path("//server/share/file.txt"));
}
#[test]
fn is_absolute_rejects_relative_with_dotdot() {
assert!(!is_absolute_path("../file.txt"));
assert!(!is_absolute_path("./file.txt"));
assert!(!is_absolute_path("assets/file.txt"));
assert!(!is_absolute_path("file.txt"));
}
#[test]
fn normalize_collapses_double_slash() {
assert_eq!(normalize_path("assets//textures"), "assets/textures");
}
#[cfg(windows)]
#[test]
fn normalize_windows_converts_nested_backslashes() {
assert_eq!(
normalize_path("assets\\textures\\wood"),
"assets/textures/wood"
);
}
#[cfg(not(windows))]
#[test]
fn normalize_posix_preserves_nested_backslashes() {
assert_eq!(
normalize_path("assets\\textures\\wood"),
"assets\\textures\\wood"
);
}
#[cfg(not(windows))]
#[test]
fn normalize_posix_preserves_backslashes_after_drive_prefix() {
assert_eq!(normalize_path("C:\\projects\\scene"), "C:/projects\\scene");
}
#[test]
fn normalize_preserves_forward_slashes() {
assert_eq!(normalize_path("dir/name"), "dir/name");
}
#[test]
fn is_absolute_unc_variants() {
assert!(is_absolute_path("//server/share"));
assert!(is_absolute_path("\\\\server\\share"));
}
}