use camino::{Utf8Path, Utf8PathBuf};
pub fn needs_normalize(path: &Utf8Path) -> bool {
let path_str = path.as_str();
let mut chars = path_str.chars().peekable();
if path_str.starts_with(r"\\?\") {
return true;
}
while let Some(c) = chars.next() {
match c {
'\\' => return true,
'/' => match chars.peek() {
Some('/') => return true,
Some('.') => {
let mut lookahead = chars.clone();
lookahead.next(); match lookahead.peek() {
Some('/') | None => return true,
_ => {}
}
}
_ => {}
},
_ => {}
}
}
false
}
pub fn into_normalized(path: Utf8PathBuf) -> Utf8PathBuf {
let path_str = path.as_str();
let needs_normalization = needs_normalize(&path);
if !needs_normalization {
return path;
}
let mut result = String::with_capacity(path_str.len());
let mut chars = path_str.chars().peekable();
let mut last_was_slash = false;
if path_str.starts_with(r"\\?\") {
for _ in 0..4 {
chars.next(); }
}
while let Some(c) = chars.next() {
match c {
'\\' | '/' => {
if !last_was_slash {
result.push('/');
last_was_slash = true;
}
}
'.' => {
if last_was_slash {
match chars.peek() {
Some(&'/') | Some(&'\\') => {
if !result.is_empty() {
chars.next(); continue;
}
}
Some(&'.') => {
result.push('.');
last_was_slash = false;
}
_ => {
result.push('.');
last_was_slash = false;
}
}
} else {
result.push('.');
last_was_slash = false;
}
}
_ => {
result.push(c);
last_was_slash = false;
}
}
}
if (path_str.ends_with('/') || path_str.ends_with('\\')) && !result.ends_with('/') {
result.push('/');
}
Utf8PathBuf::from(result)
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use super::*;
#[test]
fn test_normalizer_into_normalize_backslashes() -> Result<()> {
let paths = [
(r"C:\Users\name\file.txt", "C:/Users/name/file.txt"),
(r"path\to\file.txt", "path/to/file.txt"),
(r"mixed/path\style", "mixed/path/style"),
];
for (input, expected) in paths {
let path = Utf8PathBuf::from(input);
let normalized = into_normalized(path);
assert_eq!(
normalized.as_str(),
expected,
"Failed to normalize backslashes in '{input}'"
);
}
Ok(())
}
#[test]
fn test_normalizer_into_normalize_multiple_slashes() -> Result<()> {
let paths = [
("//path//to///file.txt", "/path/to/file.txt"),
("path////file.txt", "path/file.txt"),
(r"\\server\\share\\file.txt", "/server/share/file.txt"),
];
for (input, expected) in paths {
let path = Utf8PathBuf::from(input);
let normalized = into_normalized(path);
assert_eq!(
normalized.as_str(),
expected,
"Failed to collapse multiple slashes in '{input}'"
);
}
Ok(())
}
#[test]
fn test_normalizer_into_normalize_single_dots() -> Result<()> {
let paths = [
("path/./file.txt", "path/file.txt"),
("./path/./to/./file.txt", "./path/to/file.txt"),
("path/to/./././file.txt", "path/to/file.txt"),
];
for (input, expected) in paths {
let path = Utf8PathBuf::from(input);
let normalized = into_normalized(path);
assert_eq!(
normalized.as_str(),
expected,
"Failed to handle single dots correctly in '{input}'"
);
}
Ok(())
}
#[test]
fn test_normalizer_into_normalize_preserve_parent_dirs() -> Result<()> {
let paths = [
("path/../file.txt", "path/../file.txt"),
("../path/file.txt", "../path/file.txt"),
("path/../../file.txt", "path/../../file.txt"),
];
for (input, expected) in paths {
let path = Utf8PathBuf::from(input);
let normalized = into_normalized(path);
assert_eq!(
normalized.as_str(),
expected,
"Should preserve parent directory references in '{input}'"
);
}
Ok(())
}
#[test]
fn test_normalizer_into_normalize_windows_prefix() -> Result<()> {
let paths = [
(r"\\?\C:\Users\name\file.txt", "C:/Users/name/file.txt"),
(r"\\?\UNC\server\share", "UNC/server/share"),
];
for (input, expected) in paths {
let path = Utf8PathBuf::from(input);
let normalized = into_normalized(path);
assert_eq!(
normalized.as_str(),
expected,
"Failed to remove Windows prefix in '{input}'"
);
}
Ok(())
}
#[test]
fn test_normalizer_into_normalize_no_change_needed() -> Result<()> {
let paths = ["path/to/file.txt", "/absolute/path/file.txt", "../parent/dir", "file.txt"];
for input in paths {
let path = Utf8PathBuf::from(input);
let path_clone = path.clone();
let normalized = into_normalized(path);
assert_eq!(
normalized, path_clone,
"Path should not change when normalization not needed"
);
}
Ok(())
}
#[test]
fn test_normalizer_into_normalize_trailing_slash() -> Result<()> {
let paths = [
("path/to/dir/", "path/to/dir/"),
(r"path\to\dir\", "path/to/dir/"),
("path//to///dir///", "path/to/dir/"),
];
for (input, expected) in paths {
let path = Utf8PathBuf::from(input);
let normalized = into_normalized(path);
assert_eq!(
normalized.as_str(),
expected,
"Should preserve trailing slash in '{input}'"
);
}
Ok(())
}
#[test]
fn test_normalizer_into_normalize_complex_paths() -> Result<()> {
let paths = [
(
r"C:\Users\.\name\..\admin\//docs\file.txt",
"C:/Users/name/../admin/docs/file.txt",
),
(
r"\\?\C:\Program Files\\.\multiple//slashes",
"C:/Program Files/multiple/slashes",
),
("./current/dir/./file.txt", "./current/dir/file.txt"),
];
for (input, expected) in paths {
let path = Utf8PathBuf::from(input);
let normalized = into_normalized(path);
assert_eq!(
normalized.as_str(),
expected,
"Failed to normalize complex path '{input}'"
);
}
Ok(())
}
}