use std::path::PathBuf;
pub fn normalize_path<S: AsRef<str>>(input: S) -> PathBuf {
let input = input.as_ref();
let unified = input.replace('\\', "/");
let mut components = unified.split('/').filter(|p| !p.is_empty()).peekable();
let mut result = PathBuf::new();
if unified.starts_with('/') {
result.push(std::path::MAIN_SEPARATOR.to_string());
}
if let Some(first) = components.peek() {
if is_windows_drive(first) {
let drive = components.next().unwrap();
result.push(format!("{drive}{}", std::path::MAIN_SEPARATOR));
}
}
for part in components {
result.push(part);
}
result
}
fn is_windows_drive(s: &str) -> bool {
let bytes = s.as_bytes();
bytes.len() == 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
}
#[allow(dead_code)]
pub fn looks_absolute(input: &str) -> bool {
let unified = input.replace('\\', "/");
if unified.starts_with('/') {
return true;
}
let bytes = unified.as_bytes();
bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mixed_separators_become_uniform() {
let p = normalize_path("a/b\\c");
let s = p.to_string_lossy().replace('\\', "/");
assert_eq!(s, "a/b/c");
}
#[test]
fn backslash_only_path() {
let p = normalize_path("foo\\bar\\baz");
let s = p.to_string_lossy().replace('\\', "/");
assert_eq!(s, "foo/bar/baz");
}
#[test]
fn forward_slash_absolute_preserved() {
let p = normalize_path("/usr/local/bin");
let s = p.to_string_lossy();
assert!(p.is_absolute() || s.starts_with('/'));
}
#[test]
fn windows_drive_is_detected() {
assert!(looks_absolute("C:\\Users\\test"));
assert!(looks_absolute("C:/Users/test"));
}
#[test]
fn relative_path_is_not_absolute() {
assert!(!looks_absolute("foo/bar"));
assert!(!looks_absolute("foo\\bar"));
}
#[test]
fn trailing_and_duplicate_separators_collapse() {
let p = normalize_path("a//b///c/");
let s = p.to_string_lossy().replace('\\', "/");
assert_eq!(s, "a/b/c");
}
}