mirror of
https://github.com/nushell/nushell.git
synced 2024-12-25 00:19:39 +01:00
Path expansion no longer removes trailing slashes (#12662)
This PR changes `nu_path::expand_path_with()` to no longer remove trailing slashes. It also fixes bugs in the current implementation due to ineffective tests (Fixes #12602).
This commit is contained in:
parent
b22d131279
commit
f184a77fe1
@ -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);
|
||||
|
||||
|
50
crates/nu-path/src/assert_path_eq.rs
Normal file
50
crates/nu-path/src/assert_path_eq.rs
Normal file
@ -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::<Path>::as_ref(&$left).to_str().unwrap(),
|
||||
AsRef::<Path>::as_ref(&$right).to_str().unwrap()
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_path_ne {
|
||||
($left:expr, $right:expr $(,)?) => {
|
||||
assert_ne!(
|
||||
AsRef::<Path>::as_ref(&$left).to_str().unwrap(),
|
||||
AsRef::<Path>::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");
|
||||
}
|
||||
}
|
242
crates/nu-path/src/components.rs
Normal file
242
crates/nu-path/src/components.rs
Normal file
@ -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<Item = Component> {
|
||||
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);
|
||||
}
|
||||
}
|
@ -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<Path>) -> 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<Path>) -> 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<Path>) -> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -151,6 +151,7 @@ pub fn expand_tilde(path: impl AsRef<Path>) -> 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\\");
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -1 +0,0 @@
|
||||
mod util;
|
@ -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, "")
|
||||
}
|
Loading…
Reference in New Issue
Block a user