mirror of
https://github.com/nushell/nushell.git
synced 2025-06-30 14:40:06 +02:00
nu-path crate refactor (#3730)
* Resolve rebase artifacts * Remove leftover dependencies on removed feature * Remove unnecessary 'pub' * Start taking notes and fooling around * Split canonicalize to two versions; Add TODOs One that takes `relative_to` and one that doesn't. More TODO notes. * Merge absolutize to and rename resolve_dots * Add custom absolutize fn and use it in path expand * Convert a couple of dunce::canonicalize to ours * Update nu-path description * Replace all canonicalize with nu-path version * Remove leftover dunce dependencies * Fix broken autocd with trailing slash Trailing slash is preserved *only* in paths that do not contain "." or "..". This should be fixed in the future to cover all paths but for now it at least covers basic cases. * Use dunce::canonicalize for canonicalizing * Alow cd recovery from non-existent cwd * Disable removed canonicalize functionality tests Remove unused import * Break down nu-path into separate modules * Remove unused public imports * Remove abundant cow mapping * Fix clippy warning * Reformulate old canonicalize tests to expand_path They wouldn't work with the new canonicalize. * Canonicalize also ~ and ndots; Unify path joining Also, add doc comments in nu_path::expansions. * Add comment * Avoid expanding ndots if path is not valid UTF-8 With this change, no lossy path->string conversion should happen in the nu-path crate. * Fmt * Slight expand_tilde refactor; Add doc comments * Start nu-path integration tests * Add tests TODO * Fix docstring typo * Fix some doc strings * Add README for nu-path crate * Add a couple of canonicalize tests * Add nu-path integration tests * Add trim trailing slashes tests * Update nu-path dependency * Remove unused import * Regenerate lockfile
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
authors = ["The Nu Project Contributors"]
|
||||
description = "Nushell parser"
|
||||
description = "Path handling library for Nushell"
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
name = "nu-path"
|
||||
@ -9,3 +9,6 @@ version = "0.36.1"
|
||||
[dependencies]
|
||||
dirs-next = "2.0.0"
|
||||
dunce = "1.0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
nu-test-support = { version = "0.36.1", path="../nu-test-support" }
|
||||
|
3
crates/nu-path/README.md
Normal file
3
crates/nu-path/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# nu-path
|
||||
|
||||
This crate takes care of path handling in Nushell, such as canonicalization and component expansion, as well as other path-related utilities.
|
259
crates/nu-path/src/dots.rs
Normal file
259
crates/nu-path/src/dots.rs
Normal file
@ -0,0 +1,259 @@
|
||||
use std::path::{is_separator, 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 occurence of more than two dots into a sequence of ../ (or ..\ on windows), e.g.,
|
||||
/// "..." into "../..", "...." into "../../../", etc.
|
||||
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 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;
|
||||
}
|
||||
|
||||
dots_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
dots_count > 2
|
||||
};
|
||||
|
||||
if !ndots_present {
|
||||
return path.as_ref().into();
|
||||
}
|
||||
|
||||
let mut dots_count = 0u8;
|
||||
let mut expanded = String::new();
|
||||
for chr in path_str.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()
|
||||
}
|
||||
|
||||
/// 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());
|
||||
|
||||
// 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();
|
||||
}
|
||||
Component::CurDir if prev_is_normal(&result) => {}
|
||||
_ => result.push(component),
|
||||
});
|
||||
|
||||
dunce::simplified(&result).to_path_buf()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn expand_two_dots() {
|
||||
let path = Path::new("/foo/bar/..");
|
||||
|
||||
assert_eq!(
|
||||
PathBuf::from("/foo"), // missing path
|
||||
expand_dots(path)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_dots_with_curdir() {
|
||||
let path = Path::new("/foo/./bar/./baz");
|
||||
|
||||
assert_eq!(PathBuf::from("/foo/bar/baz"), expand_dots(path));
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
#[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 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"ls ..\../ garbage.*[", "ls .../ 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() {
|
||||
check_ndots_expansion("ls ../../ garbage.*[", "ls .../ garbage.*[");
|
||||
}
|
||||
}
|
||||
}
|
75
crates/nu-path/src/expansions.rs
Normal file
75
crates/nu-path/src/expansions.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::dots::{expand_dots, expand_ndots};
|
||||
use super::tilde::expand_tilde;
|
||||
|
||||
// Join a path relative to another path. Paths starting with tilde are considered as absolute.
|
||||
fn join_path_relative<P, Q>(path: P, relative_to: Q) -> PathBuf
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let relative_to = relative_to.as_ref();
|
||||
|
||||
if path == 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.into()
|
||||
} else if path.starts_with("~") {
|
||||
// do not end up with "/some/path/~"
|
||||
path.into()
|
||||
} else {
|
||||
relative_to.join(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve all symbolic links and all components (tilde, ., .., ...+) and return the path in its
|
||||
/// absolute form.
|
||||
///
|
||||
/// Fails under the same conditions as
|
||||
/// [std::fs::canonicalize](https://doc.rust-lang.org/std/fs/fn.canonicalize.html).
|
||||
pub fn canonicalize(path: impl AsRef<Path>) -> io::Result<PathBuf> {
|
||||
let path = expand_tilde(path);
|
||||
let path = expand_ndots(path);
|
||||
|
||||
dunce::canonicalize(path)
|
||||
}
|
||||
|
||||
/// Same as canonicalize() but the input path is specified relative to another path
|
||||
pub fn canonicalize_with<P, Q>(path: P, relative_to: Q) -> io::Result<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
let path = join_path_relative(path, relative_to);
|
||||
|
||||
canonicalize(path)
|
||||
}
|
||||
|
||||
/// Resolve only path components (tilde, ., .., ...+), if possible.
|
||||
///
|
||||
/// The function works in a "best effort" mode: It does not fail but rather returns the unexpanded
|
||||
/// version if the expansion is not possible.
|
||||
///
|
||||
/// Furthermore, unlike canonicalize(), it does not use sys calls (such as readlink).
|
||||
///
|
||||
/// Does not convert to absolute form nor does it resolve symlinks.
|
||||
pub fn expand_path(path: impl AsRef<Path>) -> PathBuf {
|
||||
let path = expand_tilde(path);
|
||||
let path = expand_ndots(path);
|
||||
expand_dots(path)
|
||||
}
|
||||
|
||||
/// Same as expand_path() but the input path is specified relative to another path
|
||||
pub fn expand_path_with<P, Q>(path: P, relative_to: Q) -> PathBuf
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
let path = join_path_relative(path, relative_to);
|
||||
|
||||
expand_path(path)
|
||||
}
|
@ -1,530 +1,8 @@
|
||||
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<B, O, F>(c: Cow<'_, B>, f: F) -> Cow<'_, B>
|
||||
where
|
||||
B: ToOwned<Owned = O> + ?Sized,
|
||||
O: AsRef<B>,
|
||||
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<P, Q>(relative_to: P, path: Q) -> PathBuf
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
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::<PathBuf>(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
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<P, Q>(relative_to: P, path: Q) -> io::Result<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
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<PathBuf>) -> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
mod dots;
|
||||
mod expansions;
|
||||
mod tilde;
|
||||
mod util;
|
||||
|
||||
pub use expansions::{canonicalize, canonicalize_with, expand_path, expand_path_with};
|
||||
pub use tilde::expand_tilde;
|
||||
pub use util::trim_trailing_slash;
|
||||
|
85
crates/nu-path/src/tilde.rs
Normal file
85
crates/nu-path/src/tilde.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn expand_tilde_with(path: impl AsRef<Path>, home: Option<PathBuf>) -> PathBuf {
|
||||
let path = path.as_ref();
|
||||
|
||||
if !path.starts_with("~") {
|
||||
return path.into();
|
||||
}
|
||||
|
||||
match home {
|
||||
None => path.into(),
|
||||
Some(mut h) => {
|
||||
if h == Path::new("/") {
|
||||
// Corner case: `h` is a root directory;
|
||||
// don't prepend extra `/`, just drop the tilde.
|
||||
path.strip_prefix("~").unwrap_or(path).into()
|
||||
} else {
|
||||
if let Ok(p) = path.strip_prefix("~/") {
|
||||
h.push(p)
|
||||
}
|
||||
h
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand tilde ("~") into a home directory if it is the first path component
|
||||
pub fn expand_tilde(path: impl AsRef<Path>) -> PathBuf {
|
||||
// TODO: Extend this to work with "~user" style of home paths
|
||||
expand_tilde_with(path, dirs_next::home_dir())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn check_expanded(s: &str) {
|
||||
let home = Path::new("/home");
|
||||
let buf = Some(PathBuf::from(home));
|
||||
assert!(expand_tilde_with(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(Path::new(s), buf).starts_with("//"));
|
||||
}
|
||||
|
||||
fn check_not_expanded(s: &str) {
|
||||
let home = PathBuf::from("/home");
|
||||
let expanded = expand_tilde_with(Path::new(s), Some(home));
|
||||
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");
|
||||
}
|
||||
}
|
4
crates/nu-path/src/util.rs
Normal file
4
crates/nu-path/src/util.rs
Normal file
@ -0,0 +1,4 @@
|
||||
/// Trim trailing path separator from a string
|
||||
pub fn trim_trailing_slash(s: &str) -> &str {
|
||||
s.trim_end_matches(std::path::is_separator)
|
||||
}
|
412
crates/nu-path/tests/canonicalize.rs
Normal file
412
crates/nu-path/tests/canonicalize.rs
Normal file
@ -0,0 +1,412 @@
|
||||
use std::path::Path;
|
||||
|
||||
use nu_test_support::fs::Stub::EmptyFile;
|
||||
use nu_test_support::playground::Playground;
|
||||
|
||||
use nu_path::{canonicalize, canonicalize_with};
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let mut spam = dirs.test().clone();
|
||||
spam.push("spam.txt");
|
||||
|
||||
let actual = canonicalize(spam).expect("Failed to canonicalize");
|
||||
|
||||
assert!(actual.ends_with("spam.txt"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_unicode_path() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![EmptyFile("🚒.txt")]);
|
||||
|
||||
let mut spam = dirs.test().clone();
|
||||
spam.push("🚒.txt");
|
||||
|
||||
let actual = canonicalize(spam).expect("Failed to canonicalize");
|
||||
|
||||
assert!(actual.ends_with("🚒.txt"));
|
||||
});
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn canonicalize_non_utf8_path() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let actual = canonicalize_with("spam.txt", dirs.test()).expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_unicode_path_relative_to_unicode_path_with_spaces() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.mkdir("e-$ èрт🚒♞中片-j");
|
||||
sandbox.with_files(vec![EmptyFile("e-$ èрт🚒♞中片-j/🚒.txt")]);
|
||||
|
||||
let mut relative_to = dirs.test().clone();
|
||||
relative_to.push("e-$ èрт🚒♞中片-j");
|
||||
|
||||
let actual = canonicalize_with("🚒.txt", relative_to).expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("e-$ èрт🚒♞中片-j/🚒.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn canonicalize_non_utf8_path_relative_to_non_utf8_path_with_spaces() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_absolute_path_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let mut absolute_path = dirs.test().clone();
|
||||
absolute_path.push("spam.txt");
|
||||
|
||||
let actual = canonicalize_with(&absolute_path, "non/existent/directory")
|
||||
.expect("Failed to canonicalize");
|
||||
let expected = absolute_path;
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_dot() {
|
||||
let actual = canonicalize(".").expect("Failed to canonicalize");
|
||||
let expected = std::env::current_dir().expect("Could not get current directory");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_many_dots() {
|
||||
let actual = canonicalize("././/.//////./././//.///").expect("Failed to canonicalize");
|
||||
let expected = std::env::current_dir().expect("Could not get current directory");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_with_dot_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let actual = canonicalize_with("./spam.txt", dirs.test()).expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_with_many_dots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let actual = canonicalize_with("././/.//////./././//.////spam.txt", dirs.test())
|
||||
.expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_double_dot() {
|
||||
let actual = canonicalize("..").expect("Failed to canonicalize");
|
||||
let cwd = std::env::current_dir().expect("Could not get current directory");
|
||||
let expected = cwd
|
||||
.parent()
|
||||
.expect("Could not get parent of current directory");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_with_double_dot_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.mkdir("foo");
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let actual =
|
||||
canonicalize_with("foo/../spam.txt", dirs.test()).expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_with_many_double_dots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.mkdir("foo/bar/baz");
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let actual = canonicalize_with("foo/bar/baz/../../../spam.txt", dirs.test())
|
||||
.expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_ndots() {
|
||||
let actual = canonicalize("...").expect("Failed to canonicalize");
|
||||
let cwd = std::env::current_dir().expect("Could not get current directory");
|
||||
let expected = cwd
|
||||
.parent()
|
||||
.expect("Could not get parent of current directory")
|
||||
.parent()
|
||||
.expect("Could not get parent of a parent of current directory");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_with_3_ndots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.mkdir("foo/bar");
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let actual =
|
||||
canonicalize_with("foo/bar/.../spam.txt", dirs.test()).expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_with_many_3_ndots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.mkdir("foo/bar/baz/eggs/sausage/bacon");
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let actual = canonicalize_with(
|
||||
"foo/bar/baz/eggs/sausage/bacon/.../.../.../spam.txt",
|
||||
dirs.test(),
|
||||
)
|
||||
.expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_with_4_ndots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.mkdir("foo/bar/baz");
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let actual = canonicalize_with("foo/bar/baz/..../spam.txt", dirs.test())
|
||||
.expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_with_many_4_ndots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.mkdir("foo/bar/baz/eggs/sausage/bacon");
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let actual = canonicalize_with(
|
||||
"foo/bar/baz/eggs/sausage/bacon/..../..../spam.txt",
|
||||
dirs.test(),
|
||||
)
|
||||
.expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_path_with_way_too_many_dots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.mkdir("foo/bar/baz/eggs/sausage/bacon/vikings");
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
|
||||
let mut relative_to = dirs.test().clone();
|
||||
relative_to.push("foo/bar/baz/eggs/sausage/bacon/vikings");
|
||||
|
||||
let actual = canonicalize_with("././..////././...///././.....///spam.txt", relative_to)
|
||||
.expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_unicode_path_with_way_too_many_dots_relative_to_unicode_path_with_spaces() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.mkdir("foo/áčěéí +šř=é/baz/eggs/e-$ èрт🚒♞中片-j/bacon/öäöä öäöä");
|
||||
sandbox.with_files(vec![EmptyFile("🚒.txt")]);
|
||||
|
||||
let mut relative_to = dirs.test().clone();
|
||||
relative_to.push("foo/áčěéí +šř=é/baz/eggs/e-$ èрт🚒♞中片-j/bacon/öäöä öäöä");
|
||||
|
||||
let actual = canonicalize_with("././..////././...///././.....///🚒.txt", relative_to)
|
||||
.expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("🚒.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_tilde() {
|
||||
let tilde_path = "~";
|
||||
|
||||
let actual = canonicalize(tilde_path).expect("Failed to canonicalize");
|
||||
|
||||
assert!(actual.is_absolute());
|
||||
assert!(!actual.starts_with("~"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_tilde_relative_to() {
|
||||
let tilde_path = "~";
|
||||
|
||||
let actual =
|
||||
canonicalize_with(tilde_path, "non/existent/path").expect("Failed to canonicalize");
|
||||
|
||||
assert!(actual.is_absolute());
|
||||
assert!(!actual.starts_with("~"));
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn canonicalize_symlink() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
sandbox.symlink("spam.txt", "link_to_spam.txt");
|
||||
|
||||
let mut symlink_path = dirs.test().clone();
|
||||
symlink_path.push("link_to_spam.txt");
|
||||
|
||||
let actual = canonicalize(symlink_path).expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn canonicalize_symlink_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
sandbox.symlink("spam.txt", "link_to_spam.txt");
|
||||
|
||||
let actual =
|
||||
canonicalize_with("link_to_spam.txt", dirs.test()).expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(not(windows))] // seems like Windows symlink requires existing file or dir
|
||||
#[test]
|
||||
fn canonicalize_symlink_loop_relative_to_should_fail() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
// sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
sandbox.symlink("spam.txt", "link_to_spam.txt");
|
||||
sandbox.symlink("link_to_spam.txt", "spam.txt");
|
||||
|
||||
let actual = canonicalize_with("link_to_spam.txt", dirs.test());
|
||||
|
||||
assert!(actual.is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn canonicalize_nested_symlink_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.with_files(vec![EmptyFile("spam.txt")]);
|
||||
sandbox.symlink("spam.txt", "link_to_spam.txt");
|
||||
sandbox.symlink("link_to_spam.txt", "link_to_link_to_spam.txt");
|
||||
|
||||
let actual = canonicalize_with("link_to_link_to_spam.txt", dirs.test())
|
||||
.expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn canonicalize_nested_symlink_within_symlink_dir_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, sandbox| {
|
||||
sandbox.mkdir("foo/bar/baz");
|
||||
sandbox.with_files(vec![EmptyFile("foo/bar/baz/spam.txt")]);
|
||||
sandbox.symlink("foo/bar/baz/spam.txt", "foo/bar/link_to_spam.txt");
|
||||
sandbox.symlink("foo/bar/link_to_spam.txt", "foo/link_to_link_to_spam.txt");
|
||||
sandbox.symlink("foo", "link_to_foo");
|
||||
|
||||
let actual = canonicalize_with("link_to_foo/link_to_link_to_spam.txt", dirs.test())
|
||||
.expect("Failed to canonicalize");
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("foo/bar/baz/spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_should_fail() {
|
||||
let path = Path::new("/foo/bar/baz"); // hopefully, this path does not exist
|
||||
|
||||
assert!(canonicalize(path).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_with_should_fail() {
|
||||
let relative_to = "/foo";
|
||||
let path = "bar/baz";
|
||||
|
||||
assert!(canonicalize_with(path, relative_to).is_err());
|
||||
}
|
294
crates/nu-path/tests/expand_path.rs
Normal file
294
crates/nu-path/tests/expand_path.rs
Normal file
@ -0,0 +1,294 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use nu_test_support::playground::Playground;
|
||||
|
||||
use nu_path::{expand_path, expand_path_with};
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_and_without_relative() {
|
||||
let relative_to = "/foo/bar";
|
||||
let path = "../..";
|
||||
let full_path = "/foo/bar/../..";
|
||||
|
||||
assert_eq!(expand_path(full_path), expand_path_with(path, relative_to),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_relative() {
|
||||
let relative_to = "/foo/bar";
|
||||
let path = "../..";
|
||||
|
||||
assert_eq!(PathBuf::from("/"), expand_path_with(path, relative_to),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_no_change() {
|
||||
let path = "/foo/bar";
|
||||
|
||||
let actual = expand_path(&path);
|
||||
|
||||
assert_eq!(actual, PathBuf::from(path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_unicode_path_no_change() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let mut spam = dirs.test().clone();
|
||||
spam.push("🚒.txt");
|
||||
|
||||
let actual = expand_path(spam);
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("🚒.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn expand_non_utf8_path() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let actual = expand_path_with("spam.txt", dirs.test());
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_unicode_path_relative_to_unicode_path_with_spaces() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let mut relative_to = dirs.test().clone();
|
||||
relative_to.push("e-$ èрт🚒♞中片-j");
|
||||
|
||||
let actual = expand_path_with("🚒.txt", relative_to);
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("e-$ èрт🚒♞中片-j/🚒.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn expand_non_utf8_path_relative_to_non_utf8_path_with_spaces() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_absolute_path_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let mut absolute_path = dirs.test().clone();
|
||||
absolute_path.push("spam.txt");
|
||||
|
||||
let actual = expand_path_with(&absolute_path, "non/existent/directory");
|
||||
let expected = absolute_path;
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_dot() {
|
||||
let actual = expand_path(".");
|
||||
let expected = PathBuf::from(".");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_many_dots() {
|
||||
let actual = expand_path("././/.//////./././//.///");
|
||||
let expected = PathBuf::from("././././././.");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_dot_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let actual = expand_path_with("./spam.txt", dirs.test());
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_double_dot() {
|
||||
let actual = expand_path("..");
|
||||
let expected = PathBuf::from("..");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_dot_double_dot() {
|
||||
let actual = expand_path("./..");
|
||||
let expected = PathBuf::from("./..");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_double_dot_dot() {
|
||||
let actual = expand_path("../.");
|
||||
let expected = PathBuf::from("..");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_many_dots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let actual = expand_path_with("././/.//////./././//.////spam.txt", dirs.test());
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_double_dot_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let actual = expand_path_with("foo/../spam.txt", dirs.test());
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_many_double_dots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let actual = expand_path_with("foo/bar/baz/../../../spam.txt", dirs.test());
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_ndots() {
|
||||
let actual = expand_path("...");
|
||||
let mut expected = PathBuf::from("..");
|
||||
expected.push("..");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_normal_path_ndots() {
|
||||
let actual = expand_path("foo/bar/baz/...");
|
||||
let expected = PathBuf::from("foo");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_3_ndots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let actual = expand_path_with("foo/bar/.../spam.txt", dirs.test());
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_many_3_ndots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let actual = expand_path_with(
|
||||
"foo/bar/baz/eggs/sausage/bacon/.../.../.../spam.txt",
|
||||
dirs.test(),
|
||||
);
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_4_ndots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let actual = expand_path_with("foo/bar/baz/..../spam.txt", dirs.test());
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_many_4_ndots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let actual = expand_path_with(
|
||||
"foo/bar/baz/eggs/sausage/bacon/..../..../spam.txt",
|
||||
dirs.test(),
|
||||
);
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_way_too_many_dots_relative_to() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let mut relative_to = dirs.test().clone();
|
||||
relative_to.push("foo/bar/baz/eggs/sausage/bacon/vikings");
|
||||
|
||||
let actual = expand_path_with("././..////././...///././.....///spam.txt", relative_to);
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("spam.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_unicode_path_with_way_too_many_dots_relative_to_unicode_path_with_spaces() {
|
||||
Playground::setup("nu_path_test_1", |dirs, _| {
|
||||
let mut relative_to = dirs.test().clone();
|
||||
relative_to.push("foo/áčěéí +šř=é/baz/eggs/e-$ èрт🚒♞中片-j/bacon/öäöä öäöä");
|
||||
|
||||
let actual = expand_path_with("././..////././...///././.....///🚒.txt", relative_to);
|
||||
let mut expected = dirs.test().clone();
|
||||
expected.push("🚒.txt");
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_tilde() {
|
||||
let tilde_path = "~";
|
||||
|
||||
let actual = expand_path(tilde_path);
|
||||
|
||||
assert!(actual.is_absolute());
|
||||
assert!(!actual.starts_with("~"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_tilde_relative_to() {
|
||||
let tilde_path = "~";
|
||||
|
||||
let actual = expand_path_with(tilde_path, "non/existent/path");
|
||||
|
||||
assert!(actual.is_absolute());
|
||||
assert!(!actual.starts_with("~"));
|
||||
}
|
3
crates/nu-path/tests/mod.rs
Normal file
3
crates/nu-path/tests/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod canonicalize;
|
||||
mod expand_path;
|
||||
mod util;
|
45
crates/nu-path/tests/util.rs
Normal file
45
crates/nu-path/tests/util.rs
Normal file
@ -0,0 +1,45 @@
|
||||
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, "")
|
||||
}
|
Reference in New Issue
Block a user