#![forbid(unsafe_code)]
use std::path::{Path, PathBuf};
pub trait Clean {
fn clean(&self) -> PathBuf;
}
impl Clean for PathBuf {
fn clean(&self) -> PathBuf {
clean(self)
}
}
impl Clean for Path {
fn clean(&self) -> PathBuf {
clean(self)
}
}
pub fn clean<P: AsRef<Path>>(path: P) -> PathBuf {
let path = path.as_ref();
clean_internal(path)
}
fn clean_internal(path: &Path) -> PathBuf {
use std::path::Component;
let mut components = path.components().peekable();
let mut cleaned = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
let mut dotdots = 0;
let mut component_count = 0;
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
cleaned.push(component.as_os_str());
component_count += 1;
}
Component::CurDir => {}
Component::ParentDir if component_count == 1 && cleaned.is_absolute() => {}
Component::ParentDir if component_count == dotdots => {
cleaned.push("..");
dotdots += 1;
component_count += 1;
}
Component::ParentDir => {
cleaned.pop();
component_count -= 1;
}
Component::Normal(c) => {
cleaned.push(c);
component_count += 1;
}
}
}
if component_count == 0 {
cleaned.push(".");
}
cleaned
}
#[cfg(test)]
mod tests {
use super::{clean, Clean};
use std::path::PathBuf;
#[test]
fn test_empty_path_is_current_dir() {
assert_eq!(clean(""), PathBuf::from("."));
}
#[test]
fn test_clean_paths_dont_change() {
let tests = vec![(".", "."), ("..", ".."), ("/", "/")];
for test in tests {
assert_eq!(
clean(test.0),
PathBuf::from(test.1),
"clean({}) == {}",
test.0,
test.1
);
}
}
#[test]
fn test_replace_multiple_slashes() {
let tests = vec![
("/", "/"),
("//", "/"),
("///", "/"),
(".//", "."),
("//..", "/"),
("..//", ".."),
("/..//", "/"),
("/.//./", "/"),
("././/./", "."),
("path//to///thing", "path/to/thing"),
("/path//to///thing", "/path/to/thing"),
];
for test in tests {
assert_eq!(
clean(test.0),
PathBuf::from(test.1),
"clean({}) == {}",
test.0,
test.1
);
}
}
#[test]
fn test_eliminate_current_dir() {
let tests = vec![
("./", "."),
("/./", "/"),
("./test", "test"),
("./test/./path", "test/path"),
("/test/./path/", "/test/path"),
("test/path/.", "test/path"),
];
for test in tests {
assert_eq!(
clean(test.0),
PathBuf::from(test.1),
"clean({}) == {}",
test.0,
test.1
);
}
}
#[test]
fn test_eliminate_parent_dir() {
let tests = vec![
("/..", "/"),
("/../test", "/test"),
("test/..", "."),
("test/path/..", "test"),
("test/../path", "path"),
("/test/../path", "/path"),
("test/path/../../", "."),
("test/path/../../..", ".."),
("/test/path/../../..", "/"),
("/test/path/../../../..", "/"),
("test/path/../../../..", "../.."),
("test/path/../../another/path", "another/path"),
("test/path/../../another/path/..", "another"),
("../test", "../test"),
("../test/", "../test"),
("../test/path", "../test/path"),
("../test/..", ".."),
];
for test in tests {
assert_eq!(
clean(test.0),
PathBuf::from(test.1),
"clean({}) == {}",
test.0,
test.1
);
}
}
#[test]
fn test_trait() {
assert_eq!(
PathBuf::from("/test/../path/").clean(),
PathBuf::from("/path")
);
}
}