use std::borrow::Cow; use std::io; use std::path::{Component, Path, PathBuf}; // Utility for applying a function that can only be called on the borrowed type of the Cow // and also returns a ref. If the Cow is a borrow, we can return the same borrow but an // owned value needs extra handling because the returned valued has to be owned as well pub fn cow_map_by_ref(c: Cow<'_, B>, f: F) -> Cow<'_, B> where B: ToOwned + ?Sized, O: AsRef, F: FnOnce(&B) -> &B, { match c { Cow::Borrowed(b) => Cow::Borrowed(f(b)), Cow::Owned(o) => Cow::Owned(f(o.as_ref()).to_owned()), } } // Utility for applying a function over Cow<'a, Path> over a Cow<'a, str> while avoiding unnecessary conversions fn cow_map_str_path<'a, F>(c: Cow<'a, str>, f: F) -> Cow<'a, str> where F: FnOnce(Cow<'a, Path>) -> Cow<'a, Path>, { let ret = match c { Cow::Borrowed(b) => f(Cow::Borrowed(Path::new(b))), Cow::Owned(o) => f(Cow::Owned(PathBuf::from(o))), }; match ret { Cow::Borrowed(expanded) => expanded.to_string_lossy(), Cow::Owned(expanded) => Cow::Owned(expanded.to_string_lossy().to_string()), } } // Utility for applying a function over Cow<'a, str> over a Cow<'a, Path> while avoiding unnecessary conversions fn cow_map_path_str<'a, F>(c: Cow<'a, Path>, f: F) -> Cow<'a, Path> where F: FnOnce(Cow<'a, str>) -> Cow<'a, str>, { let ret = match c { Cow::Borrowed(path) => f(path.to_string_lossy()), Cow::Owned(buf) => f(Cow::Owned(buf.to_string_lossy().to_string())), }; match ret { Cow::Borrowed(expanded) => Cow::Borrowed(Path::new(expanded)), Cow::Owned(expanded) => Cow::Owned(PathBuf::from(expanded)), } } const EXPAND_STR: &str = if cfg!(windows) { r"..\" } else { "../" }; fn handle_dots_push(string: &mut String, count: u8) { if count < 1 { return; } if count == 1 { string.push('.'); return; } for _ in 0..(count - 1) { string.push_str(EXPAND_STR); } string.pop(); // remove last '/' } // Expands any occurence of more than two dots into a sequence of ../ (or ..\ on windows), e.g. // ... into ../.. // .... into ../../../ fn expand_ndots_string(path: Cow<'_, str>) -> Cow<'_, str> { use std::path::is_separator; // find if we need to expand any >2 dot paths and early exit if not let mut dots_count = 0u8; let ndots_present = { for chr in path.chars() { if chr == '.' { dots_count += 1; } else { if is_separator(chr) && (dots_count > 2) { // this path component had >2 dots break; } dots_count = 0; } } dots_count > 2 }; if !ndots_present { return path; } let mut dots_count = 0u8; let mut expanded = String::new(); for chr in path.chars() { if chr == '.' { dots_count += 1; } else { if is_separator(chr) { // check for dots expansion only at path component boundaries handle_dots_push(&mut expanded, dots_count); dots_count = 0; } else { // got non-dot within path component => do not expand any dots while dots_count > 0 { expanded.push('.'); dots_count -= 1; } } expanded.push(chr); } } handle_dots_push(&mut expanded, dots_count); expanded.into() } // Expands any occurence of more than two dots into a sequence of ../ (or ..\ on windows), e.g. // ... into ../.. // .... into ../../../ fn expand_ndots(path: Cow<'_, Path>) -> Cow<'_, Path> { cow_map_path_str(path, expand_ndots_string) } pub fn absolutize(relative_to: P, path: Q) -> PathBuf where P: AsRef, Q: AsRef, { let path = if path.as_ref() == Path::new(".") { // Joining a Path with '.' appends a '.' at the end, making the prompt // more ugly - so we don't do anything, which should result in an equal // path on all supported systems. relative_to.as_ref().to_owned() } else if path.as_ref().starts_with("~") { expand_tilde(Cow::Borrowed(path.as_ref())).to_path_buf() } else { relative_to.as_ref().join(path) }; let (relative_to, path) = { let components: Vec<_> = path.components().collect(); let separator = components .iter() .enumerate() .find(|(_, c)| c == &&Component::CurDir || c == &&Component::ParentDir); if let Some((index, _)) = separator { let (absolute, relative) = components.split_at(index); let absolute: PathBuf = absolute.iter().collect(); let relative: PathBuf = relative.iter().collect(); (absolute, relative) } else { ( relative_to.as_ref().to_path_buf(), components.iter().collect::(), ) } }; let path = if path.is_relative() { let mut result = relative_to; path.components().for_each(|component| match component { Component::ParentDir => { result.pop(); } Component::Normal(normal) => result.push(normal), _ => {} }); result } else { path }; dunce::simplified(&path).to_path_buf() } pub fn canonicalize(relative_to: P, path: Q) -> io::Result where P: AsRef, Q: AsRef, { let absolutized = absolutize(&relative_to, path); let path = match std::fs::read_link(&absolutized) { Ok(resolved) => { let parent = absolutized.parent().unwrap_or(&absolutized); absolutize(parent, resolved) } Err(e) => { if absolutized.exists() { absolutized } else { return Err(e); } } }; Ok(dunce::simplified(&path).to_path_buf()) } // Expansion logic lives here to enable testing without depending on dirs-next fn expand_tilde_with(path: Cow<'_, Path>, home: Option) -> Cow<'_, Path> { if !path.starts_with("~") { return path; } match home { None => path, Some(mut h) => { if h == Path::new("/") { // Corner case: `h` root directory; // don't prepend extra `/`, just drop the tilde. cow_map_by_ref(path, |p: &Path| { p.strip_prefix("~").expect("cannot strip ~ prefix") }) } else { h.push(path.strip_prefix("~/").expect("cannot strip ~/ prefix")); Cow::Owned(h) } } } } pub fn expand_tilde(path: Cow<'_, Path>) -> Cow<'_, Path> { expand_tilde_with(path, dirs_next::home_dir()) } pub fn expand_tilde_string(path: Cow<'_, str>) -> Cow<'_, str> { cow_map_str_path(path, expand_tilde) } // Remove "." and ".." in a path. Prefix ".." are not removed as we don't have access to the // current dir. This is merely 'string manipulation'. Does not handle "...+", see expand_ndots for that pub fn resolve_dots(path: Cow<'_, Path>) -> Cow<'_, Path> { debug_assert!(!path.components().any(|c| std::matches!(c, Component::Normal(os_str) if os_str.to_string_lossy().starts_with("..."))), "Unexpected ndots!"); if !path .components() .any(|c| std::matches!(c, Component::CurDir | Component::ParentDir)) { return path; } let mut result = PathBuf::with_capacity(path.as_os_str().len()); // Only pop/skip path elements if the previous one was an actual path element let prev_is_normal = |p: &Path| -> bool { p.components() .next_back() .map(|c| std::matches!(c, Component::Normal(_))) .unwrap_or(false) }; path.as_ref() .components() .for_each(|component| match component { Component::ParentDir if prev_is_normal(&result) => { result.pop(); } Component::CurDir if prev_is_normal(&result) => {} _ => result.push(component), }); Cow::Owned(dunce::simplified(&result).to_path_buf()) } // Expands ~ to home and shortens paths by removing unecessary ".." and "." // where possible. Also expands "...+" appropriately. pub fn expand_path(path: Cow<'_, Path>) -> Cow<'_, Path> { let path = expand_tilde(path); let path = expand_ndots(path); resolve_dots(path) } pub fn expand_path_string(path: Cow<'_, str>) -> Cow<'_, str> { cow_map_str_path(path, expand_path) } #[cfg(test)] mod tests { use super::*; use std::io; #[test] fn absolutize_two_dots() { let relative_to = Path::new("/foo/bar"); let path = Path::new(".."); assert_eq!( PathBuf::from("/foo"), // missing path absolutize(relative_to, path) ); } #[test] fn absolutize_with_curdir() { let relative_to = Path::new("/foo"); let path = Path::new("./bar/./baz"); assert!(!absolutize(relative_to, path) .to_str() .unwrap() .contains('.')); } #[test] fn canonicalize_should_succeed() -> io::Result<()> { let relative_to = Path::new("/foo/bar"); let path = Path::new("../.."); assert_eq!( PathBuf::from("/"), // existing path canonicalize(relative_to, path)?, ); Ok(()) } #[test] fn canonicalize_should_fail() { let relative_to = Path::new("/foo/bar/baz"); // '/foo' is missing let path = Path::new("../.."); assert!(canonicalize(relative_to, path).is_err()); } fn check_ndots_expansion(expected: &str, s: &str) { let expanded = expand_ndots(Cow::Borrowed(Path::new(s))); // If we don't expect expansion, verify that we get a borrow back and no PathBuf creation has been made if expected == s { assert!( std::matches!(expanded, Cow::Borrowed(_)), "No PathBuf should be needed here (unnecessary allocation)" ); } assert_eq!(Path::new(expected), &expanded); } // common tests #[test] fn string_without_ndots() { check_ndots_expansion("../hola", "../hola"); } #[test] fn string_with_three_ndots_and_chars() { check_ndots_expansion("a...b", "a...b"); } #[test] fn string_with_two_ndots_and_chars() { check_ndots_expansion("a..b", "a..b"); } #[test] fn string_with_one_dot_and_chars() { check_ndots_expansion("a.b", "a.b"); } #[test] fn resolve_dots_double_dots_no_change() { // Can't resolve this as we don't know our parent dir assert_eq!(Path::new(".."), resolve_dots(Path::new("..").into())); } #[test] fn resolve_dots_single_dot_no_change() { // Can't resolve this as we don't know our current dir assert_eq!(Path::new("."), resolve_dots(Path::new(".").into())); } #[test] fn resolve_dots_multi_single_dots_no_change() { assert_eq!(Path::new("././."), resolve_dots(Path::new("././.").into())); } #[test] fn resolve_multi_double_dots_no_change() { assert_eq!( Path::new("../../../"), resolve_dots(Path::new("../../../").into()) ); } #[test] fn resolve_dots_no_change_with_dirs() { // Can't resolve this as we don't know our parent dir assert_eq!( Path::new("../../../dir1/dir2/"), resolve_dots(Path::new("../../../dir1/dir2").into()) ); } #[test] fn resolve_dots_simple() { assert_eq!( Path::new("/foo"), resolve_dots(Path::new("/foo/bar/..").into()) ); } #[test] fn resolve_dots_complex() { assert_eq!( Path::new("/test"), resolve_dots(Path::new("/foo/./bar/../../test/././test2/../").into()) ); } // Windows tests #[cfg(windows)] mod windows { use super::*; #[test] fn string_with_three_ndots() { check_ndots_expansion(r"..\..", "..."); } #[test] fn string_with_mixed_ndots_and_chars() { check_ndots_expansion( r"a...b/./c..d/../e.f/..\..\..//.", "a...b/./c..d/../e.f/....//.", ); } #[test] fn string_with_three_ndots_and_final_slash() { check_ndots_expansion(r"..\../", ".../"); } #[test] fn string_with_three_ndots_and_garbage() { check_ndots_expansion(r"ls ..\../ garbage.*[", "ls .../ garbage.*["); } } // non-Windows tests #[cfg(not(windows))] mod non_windows { use super::*; #[test] fn string_with_three_ndots() { check_ndots_expansion(r"../..", "..."); } #[test] fn string_with_mixed_ndots_and_chars() { check_ndots_expansion( "a...b/./c..d/../e.f/../../..//.", "a...b/./c..d/../e.f/....//.", ); } #[test] fn string_with_three_ndots_and_final_slash() { check_ndots_expansion("../../", ".../"); } #[test] fn string_with_three_ndots_and_garbage() { check_ndots_expansion("ls ../../ garbage.*[", "ls .../ garbage.*["); } } mod tilde { use super::*; fn check_expanded(s: &str) { let home = Path::new("/home"); let buf = Some(PathBuf::from(home)); assert!(expand_tilde_with(Cow::Borrowed(Path::new(s)), buf).starts_with(&home)); // Tests the special case in expand_tilde for "/" as home let home = Path::new("/"); let buf = Some(PathBuf::from(home)); assert!(!expand_tilde_with(Cow::Borrowed(Path::new(s)), buf).starts_with("//")); } fn check_not_expanded(s: &str) { let home = PathBuf::from("/home"); let expanded = expand_tilde_with(Cow::Borrowed(Path::new(s)), Some(home)); assert!( std::matches!(expanded, Cow::Borrowed(_)), "No PathBuf should be needed here (unecessary allocation)" ); assert!(expanded == Path::new(s)); } #[test] fn string_with_tilde() { check_expanded("~"); } #[test] fn string_with_tilde_forward_slash() { check_expanded("~/test/"); } #[test] fn string_with_tilde_double_forward_slash() { check_expanded("~//test/"); } #[test] fn does_not_expand_tilde_if_tilde_is_not_first_character() { check_not_expanded("1~1"); } #[cfg(windows)] #[test] fn string_with_tilde_backslash() { check_expanded("~\\test/test2/test3"); } #[cfg(windows)] #[test] fn string_with_double_tilde_backslash() { check_expanded("~\\\\test\\test2/test3"); } } }