mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 08:23:24 +01:00
709b2479d9
# Description Fixes #12758. #12662 introduced a bug where calling `cd` with a path with a trailing slash would cause `PWD` to be set to a path including a trailing slash, which is not allowed. This adds a helper to `nu_path` to remove this, and uses it in the `cd` command to clean it up before setting `PWD`. # Tests + Formatting I added some tests to make sure we don't regress on this in the future. - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib`
230 lines
6.7 KiB
Rust
230 lines
6.7 KiB
Rust
//! 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.
|
|
|
|
use std::{
|
|
ffi::OsStr,
|
|
path::{Component, Path},
|
|
};
|
|
|
|
use crate::trailing_slash::has_trailing_slash;
|
|
|
|
/// 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(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);
|
|
}
|
|
}
|