use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
pub fn into_collapsed(path: impl Into<Utf8PathBuf>) -> Utf8PathBuf {
let path_buf = path.into();
if path_buf.as_str().is_empty() {
return path_buf;
}
if is_collapsed(&path_buf) {
return path_buf;
}
let mut components = Vec::new();
let mut normal_seen = false;
for component in path_buf.components() {
match component {
Utf8Component::Prefix(prefix) => {
components.push(Utf8Component::Prefix(prefix));
}
Utf8Component::RootDir => {
components.push(Utf8Component::RootDir);
normal_seen = false; }
Utf8Component::CurDir => {
if components.is_empty() {
components.push(component);
}
}
Utf8Component::ParentDir => {
if normal_seen && !components.is_empty() {
match components.last() {
Some(Utf8Component::Normal(_)) => {
components.pop();
normal_seen = components.iter().any(|c| matches!(c, Utf8Component::Normal(_)));
continue;
}
Some(Utf8Component::ParentDir) => {}
Some(Utf8Component::RootDir) | Some(Utf8Component::Prefix(_)) => {
continue;
}
_ => {}
}
}
components.push(component);
}
Utf8Component::Normal(name) => {
components.push(Utf8Component::Normal(name));
normal_seen = true;
}
}
}
if components.is_empty() {
if path_buf.as_str().starts_with("./") {
return Utf8PathBuf::from(".");
} else {
return Utf8PathBuf::from("");
}
}
let mut result = Utf8PathBuf::new();
for component in components {
result.push(component.as_str());
}
result
}
pub fn try_into_collapsed(path: impl Into<Utf8PathBuf>) -> Option<Utf8PathBuf> {
let path_buf = path.into();
if is_collapsed(&path_buf) && !contains_problematic_components(&path_buf) {
return Some(path_buf);
}
let mut components = Vec::new();
let mut normal_seen = false;
let mut parent_count = 0;
for component in path_buf.components() {
match component {
Utf8Component::Prefix(_) => {
return None;
}
Utf8Component::RootDir => {
return None;
}
Utf8Component::CurDir => {
if components.is_empty() {
components.push(component);
}
}
Utf8Component::ParentDir => {
if normal_seen {
if let Some(Utf8Component::Normal(_)) = components.last() {
components.pop();
normal_seen = components.iter().any(|c| matches!(c, Utf8Component::Normal(_)));
continue;
}
} else {
parent_count += 1;
}
components.push(component);
}
Utf8Component::Normal(name) => {
components.push(Utf8Component::Normal(name));
normal_seen = true;
}
}
}
if parent_count > 0 && components.iter().filter(|c| matches!(c, Utf8Component::Normal(_))).count() < parent_count {
return None;
}
if components.is_empty() {
if path_buf.as_str().starts_with("./") {
return Some(Utf8PathBuf::from("."));
} else {
return Some(Utf8PathBuf::from(""));
}
}
let mut result = Utf8PathBuf::new();
for component in components {
result.push(component.as_str());
}
Some(result)
}
pub fn is_collapsed(path: impl AsRef<Utf8Path>) -> bool {
let path = path.as_ref();
let mut components = path.components().peekable();
let mut is_absolute = false;
let mut previous_was_normal = false;
while let Some(component) = components.next() {
match component {
Utf8Component::Prefix(_) | Utf8Component::RootDir => {
is_absolute = true;
}
Utf8Component::CurDir => {
if previous_was_normal || is_absolute || components.peek().is_some() {
return false;
}
}
Utf8Component::ParentDir => {
if is_absolute {
return false;
}
if previous_was_normal {
return false;
}
}
Utf8Component::Normal(_) => {
previous_was_normal = true;
}
}
}
true
}
fn contains_problematic_components(path: &Utf8Path) -> bool {
let mut has_parent_after_normal = false;
let mut has_prefix_or_root = false;
let mut normal_seen = false;
for component in path.components() {
match component {
Utf8Component::Prefix(_) | Utf8Component::RootDir => {
has_prefix_or_root = true;
}
Utf8Component::ParentDir => {
if normal_seen {
has_parent_after_normal = true;
}
}
Utf8Component::Normal(_) => {
normal_seen = true;
}
_ => {}
}
}
has_prefix_or_root || has_parent_after_normal
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use super::*;
#[test]
fn test_reshape_collapser_into_collapsed_simple() -> Result<()> {
let data = &[
("a/b/c", "a/b/c"),
("a/./b", "a/b"),
("./a/b", "./a/b"),
("./a/b", "./a/b"),
("a/./b/.", "a/b"),
("/a/./b/.", "/a/b"),
("a/../b", "b"),
("../a/b", "../a/b"), ("../a/b/..", "../a"), ("../a/b/../../..", "../.."), ("a/b/..", "a"),
("a/b/../..", ""), ("../../a/b", "../../a/b"), (".", "."), ("..", ".."), ];
for (input, expected) in data {
let input_path = Utf8PathBuf::from(input);
let result_path = into_collapsed(input_path);
let expected_path = Utf8PathBuf::from(expected);
assert_eq!(
result_path, expected_path,
"Input: '{input}', Expected: '{expected}', Got: '{result_path}'"
);
}
Ok(())
}
}