diff --git a/crates/nu-path/fuzz/fuzz_targets/path_fuzzer.rs b/crates/nu-path/fuzz/fuzz_targets/path_fuzzer.rs index d3ccb7cdc7..1a265cc44d 100644 --- a/crates/nu-path/fuzz/fuzz_targets/path_fuzzer.rs +++ b/crates/nu-path/fuzz/fuzz_targets/path_fuzzer.rs @@ -1,7 +1,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use nu_path::{expand_path_with, expand_tilde, expand_to_real_path, trim_trailing_slash}; +use nu_path::{expand_path_with, expand_tilde, expand_to_real_path}; fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { @@ -10,9 +10,6 @@ fuzz_target!(|data: &[u8]| { // Fuzzing expand_to_real_path function let _ = expand_to_real_path(path); - // Fuzzing trim_trailing_slash function - let _ = trim_trailing_slash(s); - // Fuzzing expand_tilde function let _ = expand_tilde(path); diff --git a/crates/nu-path/src/assert_path_eq.rs b/crates/nu-path/src/assert_path_eq.rs new file mode 100644 index 0000000000..e5023b98bc --- /dev/null +++ b/crates/nu-path/src/assert_path_eq.rs @@ -0,0 +1,50 @@ +//! Path equality in Rust is defined by comparing their `components()`. However, +//! `Path::components()` will perform its own normalization, which makes +//! `assert_eq!` not suitable testing. +//! +//! This module provides two macros, `assert_path_eq!` and `assert_path_ne!`, +//! which converts path to string before comparison. They accept PathBuf, Path, +//! String, and &str as parameters. + +#[macro_export] +macro_rules! assert_path_eq { + ($left:expr, $right:expr $(,)?) => { + assert_eq!( + AsRef::::as_ref(&$left).to_str().unwrap(), + AsRef::::as_ref(&$right).to_str().unwrap() + ) + }; +} + +#[macro_export] +macro_rules! assert_path_ne { + ($left:expr, $right:expr $(,)?) => { + assert_ne!( + AsRef::::as_ref(&$left).to_str().unwrap(), + AsRef::::as_ref(&$right).to_str().unwrap() + ) + }; +} + +#[cfg(test)] +mod test { + use std::path::{Path, PathBuf}; + + #[test] + fn assert_path_eq_works() { + assert_path_eq!(PathBuf::from("/foo/bar"), Path::new("/foo/bar")); + assert_path_eq!(PathBuf::from("/foo/bar"), String::from("/foo/bar")); + assert_path_eq!(PathBuf::from("/foo/bar"), "/foo/bar"); + assert_path_eq!(Path::new("/foo/bar"), String::from("/foo/bar")); + assert_path_eq!(Path::new("/foo/bar"), "/foo/bar"); + assert_path_eq!(Path::new(r"\foo\bar"), r"\foo\bar"); + + assert_path_ne!(PathBuf::from("/foo/bar/."), Path::new("/foo/bar")); + assert_path_ne!(PathBuf::from("/foo/bar/."), String::from("/foo/bar")); + assert_path_ne!(PathBuf::from("/foo/bar/."), "/foo/bar"); + assert_path_ne!(Path::new("/foo/./bar"), String::from("/foo/bar")); + assert_path_ne!(Path::new("/foo/./bar"), "/foo/bar"); + assert_path_ne!(Path::new(r"\foo\bar"), r"/foo/bar"); + assert_path_ne!(Path::new(r"/foo/bar"), r"\foo\bar"); + } +} diff --git a/crates/nu-path/src/components.rs b/crates/nu-path/src/components.rs new file mode 100644 index 0000000000..b7f60598c2 --- /dev/null +++ b/crates/nu-path/src/components.rs @@ -0,0 +1,242 @@ +//! A wrapper around `Path::components()` that preserves trailing slashes. +//! +//! Trailing slashes are semantically important for us. For example, POSIX says +//! that path resolution should always follow the final symlink if it has +//! trailing slashes. Here's a demonstration: +//! +//! ```sh +//! mkdir foo +//! ln -s foo link +//! +//! cp -r link bar # This copies the symlink, so bar is now a symlink to foo +//! cp -r link/ baz # This copies the directory, so baz is now a directory +//! ``` +//! +//! However, `Path::components()` normalizes trailing slashes away, and so does +//! other APIs that uses `Path::components()` under the hood, such as +//! `Path::parent()`. This is not ideal for path manipulation. +//! +//! This module provides a wrapper around `Path::components()` that produces an +//! empty component when there's a trailing slash. +//! +//! You can reconstruct a path with a trailing slash by concatenating the +//! components returned by this function using `PathBuf::push()` or +//! `Path::join()`. It works because `PathBuf::push("")` will add a trailing +//! slash when the original path doesn't have one. + +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +#[cfg(windows)] +use std::os::windows::ffi::OsStrExt; +use std::{ + ffi::OsStr, + path::{Component, Path}, +}; + +/// Like `Path::components()`, but produces an extra empty component at the end +/// when `path` contains a trailing slash. +/// +/// Example: +/// +/// ``` +/// # use std::path::{Path, Component}; +/// # use std::ffi::OsStr; +/// use nu_path::components; +/// +/// let path = Path::new("/foo/bar/"); +/// let mut components = components(path); +/// +/// assert_eq!(components.next(), Some(Component::RootDir)); +/// assert_eq!(components.next(), Some(Component::Normal(OsStr::new("foo")))); +/// assert_eq!(components.next(), Some(Component::Normal(OsStr::new("bar")))); +/// assert_eq!(components.next(), Some(Component::Normal(OsStr::new("")))); +/// assert_eq!(components.next(), None); +/// ``` +pub fn components(path: &Path) -> impl Iterator { + let mut final_component = Some(Component::Normal(OsStr::new(""))); + path.components().chain(std::iter::from_fn(move || { + if has_trailing_slash(path) { + final_component.take() + } else { + None + } + })) +} + +#[cfg(windows)] +fn has_trailing_slash(path: &Path) -> bool { + let last = path.as_os_str().encode_wide().last(); + last == Some(b'\\' as u16) || last == Some(b'/' as u16) +} +#[cfg(unix)] +fn has_trailing_slash(path: &Path) -> bool { + let last = path.as_os_str().as_bytes().last(); + last == Some(&b'/') +} + +#[cfg(test)] +mod test { + //! We'll go through every variant of Component, with or without trailing + //! slashes. Then we'll try reconstructing the path on some typical use cases. + + use crate::assert_path_eq; + use std::{ + ffi::OsStr, + path::{Component, Path, PathBuf}, + }; + + #[test] + fn empty_path() { + let path = Path::new(""); + let mut components = crate::components(path); + + assert_eq!(components.next(), None); + } + + #[test] + #[cfg(windows)] + fn prefix_only() { + let path = Path::new("C:"); + let mut components = crate::components(path); + + assert!(matches!(components.next(), Some(Component::Prefix(_)))); + assert_eq!(components.next(), None); + } + + #[test] + #[cfg(windows)] + fn prefix_with_trailing_slash() { + let path = Path::new("C:\\"); + let mut components = crate::components(path); + + assert!(matches!(components.next(), Some(Component::Prefix(_)))); + assert!(matches!(components.next(), Some(Component::RootDir))); + assert_eq!(components.next(), Some(Component::Normal(OsStr::new("")))); + assert_eq!(components.next(), None); + } + + #[test] + fn root() { + let path = Path::new("/"); + let mut components = crate::components(path); + + assert!(matches!(components.next(), Some(Component::RootDir))); + assert_eq!(components.next(), Some(Component::Normal(OsStr::new("")))); + assert_eq!(components.next(), None); + } + + #[test] + fn cur_dir_only() { + let path = Path::new("."); + let mut components = crate::components(path); + + assert!(matches!(components.next(), Some(Component::CurDir))); + assert_eq!(components.next(), None); + } + + #[test] + fn cur_dir_with_trailing_slash() { + let path = Path::new("./"); + let mut components = crate::components(path); + + assert!(matches!(components.next(), Some(Component::CurDir))); + assert_eq!(components.next(), Some(Component::Normal(OsStr::new("")))); + assert_eq!(components.next(), None); + } + + #[test] + fn parent_dir_only() { + let path = Path::new(".."); + let mut components = crate::components(path); + + assert!(matches!(components.next(), Some(Component::ParentDir))); + assert_eq!(components.next(), None); + } + + #[test] + fn parent_dir_with_trailing_slash() { + let path = Path::new("../"); + let mut components = crate::components(path); + + assert!(matches!(components.next(), Some(Component::ParentDir))); + assert_eq!(components.next(), Some(Component::Normal(OsStr::new("")))); + assert_eq!(components.next(), None); + } + + #[test] + fn normal_only() { + let path = Path::new("foo"); + let mut components = crate::components(path); + + assert_eq!( + components.next(), + Some(Component::Normal(OsStr::new("foo"))) + ); + assert_eq!(components.next(), None); + } + + #[test] + fn normal_with_trailing_slash() { + let path = Path::new("foo/"); + let mut components = crate::components(path); + + assert_eq!( + components.next(), + Some(Component::Normal(OsStr::new("foo"))) + ); + assert_eq!(components.next(), Some(Component::Normal(OsStr::new("")))); + assert_eq!(components.next(), None); + } + + #[test] + #[cfg(not(windows))] + fn reconstruct_unix_only() { + let path = Path::new("/home/Alice"); + + let mut buf = PathBuf::new(); + for component in crate::components(path) { + buf.push(component); + } + + assert_path_eq!(path, buf); + } + + #[test] + #[cfg(not(windows))] + fn reconstruct_unix_with_trailing_slash() { + let path = Path::new("/home/Alice/"); + + let mut buf = PathBuf::new(); + for component in crate::components(path) { + buf.push(component); + } + + assert_path_eq!(path, buf); + } + + #[test] + #[cfg(windows)] + fn reconstruct_windows_only() { + let path = Path::new("C:\\WINDOWS\\System32"); + + let mut buf = PathBuf::new(); + for component in crate::components(path) { + buf.push(component); + } + + assert_path_eq!(path, buf); + } + + #[test] + #[cfg(windows)] + fn reconstruct_windows_with_trailing_slash() { + let path = Path::new("C:\\WINDOWS\\System32\\"); + + let mut buf = PathBuf::new(); + for component in crate::components(path) { + buf.push(component); + } + + assert_path_eq!(path, buf); + } +} diff --git a/crates/nu-path/src/dots.rs b/crates/nu-path/src/dots.rs index b503744a92..53330eec1d 100644 --- a/crates/nu-path/src/dots.rs +++ b/crates/nu-path/src/dots.rs @@ -1,341 +1,202 @@ -use std::path::{is_separator, Component, Path, PathBuf}; - use super::helpers; +use std::path::{Component, Path, PathBuf}; -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 occurrence of more than two dots into a sequence of ../ (or ..\ on windows), e.g., -/// "..." into "../..", "...." into "../../../", etc. +/// Normalize the path, expanding occurrences of n-dots. +/// +/// It performs the same normalization as `nu_path::components()`, except it also expands n-dots, +/// such as "..." and "....", into multiple "..". +/// +/// The resulting path will use platform-specific path separators, regardless of what path separators was used in the input. pub fn expand_ndots(path: impl AsRef) -> PathBuf { - // Check if path is valid UTF-8 and if not, return it as it is to avoid breaking it via string - // conversion. - let path_str = match path.as_ref().to_str() { - Some(s) => s, - None => return path.as_ref().into(), - }; - - // find if we need to expand any >2 dot paths and early exit if not - let mut dots_count = 0u8; - let mut not_separator_before_dot = false; - let ndots_present = { - for chr in path_str.chars() { - if chr == '.' { - dots_count += 1; - } else { - if is_separator(chr) && (dots_count > 2) { - // this path component had >2 dots - break; - } - not_separator_before_dot = !(is_separator(chr) || chr.is_whitespace()); - dots_count = 0; - } - } - - dots_count > 2 - }; - - if !ndots_present || not_separator_before_dot { - return path.as_ref().into(); + // Returns whether a path component is n-dots. + fn is_ndots(s: &std::ffi::OsStr) -> bool { + s.as_encoded_bytes().iter().all(|c| *c == b'.') && s.len() >= 3 } - enum Segment { - Empty, - OnlyDots, - OtherChars, - } - let mut dots_count = 0u8; - let mut path_segment = Segment::Empty; - let mut expanded = String::with_capacity(path_str.len() + 10); - for chr in path_str.chars() { - if chr == '.' { - if matches!(path_segment, Segment::Empty) { - path_segment = Segment::OnlyDots; - } - dots_count += 1; - } else { - if is_separator(chr) { - if matches!(path_segment, Segment::OnlyDots) { - // check for dots expansion only at path component boundaries - handle_dots_push(&mut expanded, dots_count); - dots_count = 0; - } else { - // if at a path component boundary a secment consists of not only dots - // don't expand the dots and only append the appropriate number of . - while dots_count > 0 { - expanded.push('.'); - dots_count -= 1; - } - } - path_segment = Segment::Empty; - } else { - // got non-dot within path component => do not expand any dots - path_segment = Segment::OtherChars; - while dots_count > 0 { - expanded.push('.'); - dots_count -= 1; - } - } - expanded.push(chr); - } - } - - // Here only the final dots without any following characters are handled - if matches!(path_segment, Segment::OnlyDots) { - handle_dots_push(&mut expanded, dots_count); - } else { - for _ in 0..dots_count { - expanded.push('.'); - } - } - - expanded.into() -} - -/// Expand "." and ".." into nothing and parent directory, respectively. -pub fn expand_dots(path: impl AsRef) -> PathBuf { let path = path.as_ref(); - // Early-exit if path does not contain '.' or '..' - if !path - .components() - .any(|c| std::matches!(c, Component::CurDir | Component::ParentDir)) - { - return path.into(); + let mut result = PathBuf::with_capacity(path.as_os_str().len()); + for component in crate::components(path) { + match component { + Component::Normal(s) if is_ndots(s) => { + let n = s.len(); + // Push ".." to the path (n - 1) times. + for _ in 0..n - 1 { + result.push(".."); + } + } + _ => result.push(component), + } } + result +} + +/// Normalize the path, expanding occurrences of "." and "..". +/// +/// It performs the same normalization as `nu_path::components()`, except it also expands ".." +/// when its preceding component is a normal component, ignoring the possibility of symlinks. +/// In other words, it operates on the lexical structure of the path. +/// +/// This won't expand "/.." even though the parent directory of "/" is often +/// considered to be itself. +/// +/// The resulting path will use platform-specific path separators, regardless of what path separators was used in the input. +pub fn expand_dots(path: impl AsRef) -> PathBuf { + // Check if the last component of the path is a normal component. + fn last_component_is_normal(path: &Path) -> bool { + matches!(path.components().last(), Some(Component::Normal(_))) + } + + let path = path.as_ref(); + 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.components().for_each(|component| match component { - Component::ParentDir if prev_is_normal(&result) => { - result.pop(); + for component in crate::components(path) { + match component { + Component::ParentDir if last_component_is_normal(&result) => { + result.pop(); + } + Component::CurDir if last_component_is_normal(&result) => { + // no-op + } + _ => result.push(component), } - Component::CurDir if prev_is_normal(&result) => {} - _ => result.push(component), - }); + } helpers::simiplified(&result) } #[cfg(test)] -mod tests { +mod test_expand_ndots { use super::*; + use crate::assert_path_eq; #[test] - fn expand_two_dots() { - let path = Path::new("/foo/bar/.."); - - assert_eq!( - PathBuf::from("/foo"), // missing path - expand_dots(path) - ); + fn empty_path() { + let path = Path::new(""); + assert_path_eq!(expand_ndots(path), ""); } #[test] - fn expand_dots_with_curdir() { - let path = Path::new("/foo/./bar/./baz"); - - assert_eq!(PathBuf::from("/foo/bar/baz"), expand_dots(path)); - } - - // track_caller refers, in the panic-message, to the line of the function call and not - // inside of the function, which is nice for a test-helper-function - #[track_caller] - fn check_ndots_expansion(expected: &str, s: &str) { - let expanded = expand_ndots(Path::new(s)); - assert_eq!(Path::new(expected), &expanded); - } - - // common tests - #[test] - fn string_without_ndots() { - check_ndots_expansion("../hola", "../hola"); + fn root_dir() { + let path = Path::new("/"); + let expected = if cfg!(windows) { "\\" } else { "/" }; + assert_path_eq!(expand_ndots(path), expected); } #[test] - fn string_with_three_ndots_and_chars() { - check_ndots_expansion("a...b", "a...b"); + fn two_dots() { + let path = Path::new(".."); + assert_path_eq!(expand_ndots(path), ".."); } #[test] - fn string_with_two_ndots_and_chars() { - check_ndots_expansion("a..b", "a..b"); + fn three_dots() { + let path = Path::new("..."); + let expected = if cfg!(windows) { r"..\.." } else { "../.." }; + assert_path_eq!(expand_ndots(path), expected); } #[test] - fn string_with_one_dot_and_chars() { - check_ndots_expansion("a.b", "a.b"); + fn five_dots() { + let path = Path::new("....."); + let expected = if cfg!(windows) { + r"..\..\..\.." + } else { + "../../../.." + }; + assert_path_eq!(expand_ndots(path), expected); } #[test] - fn string_starts_with_dots() { - check_ndots_expansion(".file", ".file"); - check_ndots_expansion("..file", "..file"); - check_ndots_expansion("...file", "...file"); - check_ndots_expansion("....file", "....file"); - check_ndots_expansion(".....file", ".....file"); + fn three_dots_with_trailing_slash() { + let path = Path::new("/tmp/.../"); + let expected = if cfg!(windows) { + r"\tmp\..\..\" + } else { + "/tmp/../../" + }; + assert_path_eq!(expand_ndots(path), expected); } #[test] - fn string_ends_with_dots() { - check_ndots_expansion("file.", "file."); - check_ndots_expansion("file..", "file.."); - check_ndots_expansion("file...", "file..."); - check_ndots_expansion("file....", "file...."); - check_ndots_expansion("file.....", "file....."); + fn filenames_with_dots() { + let path = Path::new("...foo.../"); + let expected = if cfg!(windows) { + r"...foo...\" + } else { + "...foo.../" + }; + assert_path_eq!(expand_ndots(path), expected); } #[test] - fn string_starts_and_ends_with_dots() { - check_ndots_expansion(".file.", ".file."); - check_ndots_expansion("..file..", "..file.."); - check_ndots_expansion("...file...", "...file..."); - check_ndots_expansion("....file....", "....file...."); - check_ndots_expansion(".....file.....", ".....file....."); - } - #[test] - fn expand_multiple_dots() { - check_ndots_expansion("../..", "..."); - check_ndots_expansion("../../..", "...."); - check_ndots_expansion("../../../..", "....."); - check_ndots_expansion("../../../../", ".../..."); - check_ndots_expansion("../../file name/../../", ".../file name/..."); - check_ndots_expansion("../../../file name/../../../", "..../file name/...."); - } - - #[test] - fn expand_dots_double_dots_no_change() { - // Can't resolve this as we don't know our parent dir - assert_eq!(Path::new(".."), expand_dots(Path::new(".."))); - } - - #[test] - fn expand_dots_single_dot_no_change() { - // Can't resolve this as we don't know our current dir - assert_eq!(Path::new("."), expand_dots(Path::new("."))); - } - - #[test] - fn expand_dots_multi_single_dots_no_change() { - assert_eq!(Path::new("././."), expand_dots(Path::new("././."))); - } - - #[test] - fn expand_multi_double_dots_no_change() { - assert_eq!(Path::new("../../../"), expand_dots(Path::new("../../../"))); - } - - #[test] - fn expand_dots_no_change_with_dirs() { - // Can't resolve this as we don't know our parent dir - assert_eq!( - Path::new("../../../dir1/dir2/"), - expand_dots(Path::new("../../../dir1/dir2")) - ); - } - - #[test] - fn expand_dots_simple() { - assert_eq!(Path::new("/foo"), expand_dots(Path::new("/foo/bar/.."))); - } - - #[test] - fn expand_dots_complex() { - assert_eq!( - Path::new("/test"), - expand_dots(Path::new("/foo/./bar/../../test/././test2/../")) - ); - } - - #[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"not_a_cmd.../ garbage.*[", "not_a_cmd.../ garbage.*["); - } - } - - #[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() { - // filenames can contain spaces, in these cases the ... .... etc. - // that are part of a filepath should not be expanded - check_ndots_expansion("not_a_cmd.../ garbage.*[", "not_a_cmd.../ garbage.*["); - check_ndots_expansion("/not_a_cmd.../ garbage.*[", "/not_a_cmd.../ garbage.*["); - check_ndots_expansion("./not_a_cmd.../ garbage.*[", "./not_a_cmd.../ garbage.*["); - check_ndots_expansion( - "../../not a cmd.../ garbage.*[", - ".../not a cmd.../ garbage.*[", - ); - check_ndots_expansion( - "../../not a cmd.../ garbage.*[...", - ".../not a cmd.../ garbage.*[...", - ); - check_ndots_expansion("../../ not a cmd garbage.*[", ".../ not a cmd garbage.*["); - } + fn multiple_ndots() { + let path = Path::new("..././..."); + let expected = if cfg!(windows) { + r"..\..\..\.." + } else { + "../../../.." + }; + assert_path_eq!(expand_ndots(path), expected); + } +} + +#[cfg(test)] +mod test_expand_dots { + use super::*; + use crate::assert_path_eq; + + #[test] + fn empty_path() { + let path = Path::new(""); + assert_path_eq!(expand_dots(path), ""); + } + + #[test] + fn single_dot() { + let path = Path::new("./"); + let expected = if cfg!(windows) { r".\" } else { "./" }; + assert_path_eq!(expand_dots(path), expected); + } + + #[test] + fn more_single_dots() { + let path = Path::new("././."); + let expected = "."; + assert_path_eq!(expand_dots(path), expected); + } + + #[test] + fn double_dots() { + let path = Path::new("../../.."); + let expected = if cfg!(windows) { + r"..\..\.." + } else { + "../../.." + }; + assert_path_eq!(expand_dots(path), expected); + } + + #[test] + fn backtrack_once() { + let path = Path::new("/foo/bar/../baz/"); + let expected = if cfg!(windows) { + r"\foo\baz\" + } else { + "/foo/baz/" + }; + assert_path_eq!(expand_dots(path), expected); + } + + #[test] + fn backtrack_to_root() { + let path = Path::new("/foo/bar/../../../../baz"); + let expected = if cfg!(windows) { + r"\..\..\baz" + } else { + "/../../baz" + }; + assert_path_eq!(expand_dots(path), expected); } } diff --git a/crates/nu-path/src/lib.rs b/crates/nu-path/src/lib.rs index eb52962f03..e4baf36e76 100644 --- a/crates/nu-path/src/lib.rs +++ b/crates/nu-path/src/lib.rs @@ -1,10 +1,11 @@ +mod assert_path_eq; +mod components; pub mod dots; pub mod expansions; mod helpers; mod tilde; -mod util; +pub use components::components; pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path, locate_in_dirs}; pub use helpers::{config_dir, config_dir_old, home_dir}; pub use tilde::expand_tilde; -pub use util::trim_trailing_slash; diff --git a/crates/nu-path/src/tilde.rs b/crates/nu-path/src/tilde.rs index b7df144237..60cc7d11eb 100644 --- a/crates/nu-path/src/tilde.rs +++ b/crates/nu-path/src/tilde.rs @@ -151,6 +151,7 @@ pub fn expand_tilde(path: impl AsRef) -> PathBuf { #[cfg(test)] mod tests { use super::*; + use crate::assert_path_eq; use std::path::MAIN_SEPARATOR; fn check_expanded(s: &str) { @@ -244,4 +245,23 @@ mod tests { assert_eq!(expected_home, actual_home, "wrong home"); } + + #[test] + #[cfg(not(windows))] + fn expand_tilde_preserve_trailing_slash() { + let path = PathBuf::from("~/foo/"); + let home = PathBuf::from("/home"); + + let actual = expand_tilde_with_home(path, Some(home)); + assert_path_eq!(actual, "/home/foo/"); + } + #[test] + #[cfg(windows)] + fn expand_tilde_preserve_trailing_slash() { + let path = PathBuf::from("~\\foo\\"); + let home = PathBuf::from("C:\\home"); + + let actual = expand_tilde_with_home(path, Some(home)); + assert_path_eq!(actual, "C:\\home\\foo\\"); + } } diff --git a/crates/nu-path/src/util.rs b/crates/nu-path/src/util.rs deleted file mode 100644 index 63351e6aef..0000000000 --- a/crates/nu-path/src/util.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// Trim trailing path separator from a string -pub fn trim_trailing_slash(s: &str) -> &str { - s.trim_end_matches(std::path::is_separator) -} diff --git a/crates/nu-path/tests/mod.rs b/crates/nu-path/tests/mod.rs deleted file mode 100644 index 83c8c0aa0a..0000000000 --- a/crates/nu-path/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod util; diff --git a/crates/nu-path/tests/util.rs b/crates/nu-path/tests/util.rs deleted file mode 100644 index 601d9dd437..0000000000 --- a/crates/nu-path/tests/util.rs +++ /dev/null @@ -1,45 +0,0 @@ -use nu_path::trim_trailing_slash; -use std::path::MAIN_SEPARATOR; - -/// Helper function that joins string literals with '/' or '\', based on the host OS -fn join_path_sep(pieces: &[&str]) -> String { - let sep_string = String::from(MAIN_SEPARATOR); - pieces.join(&sep_string) -} - -#[test] -fn trims_trailing_slash_without_trailing_slash() { - let path = join_path_sep(&["some", "path"]); - - let actual = trim_trailing_slash(&path); - - assert_eq!(actual, &path) -} - -#[test] -fn trims_trailing_slash() { - let path = join_path_sep(&["some", "path", ""]); - - let actual = trim_trailing_slash(&path); - let expected = join_path_sep(&["some", "path"]); - - assert_eq!(actual, &expected) -} - -#[test] -fn trims_many_trailing_slashes() { - let path = join_path_sep(&["some", "path", "", "", "", ""]); - - let actual = trim_trailing_slash(&path); - let expected = join_path_sep(&["some", "path"]); - - assert_eq!(actual, &expected) -} - -#[test] -fn trims_trailing_slash_empty() { - let path = String::from(MAIN_SEPARATOR); - let actual = trim_trailing_slash(&path); - - assert_eq!(actual, "") -}