From 18dd009ca8b84b7b0d57b5b32423e7b21a400128 Mon Sep 17 00:00:00 2001 From: Kevin Del Castillo Date: Sat, 11 Apr 2020 14:05:29 -0500 Subject: [PATCH] Unified path expansion under new module and better canonicalize (#1571) * New 'path' module under nu-cli. Added normalize and canonicalize method. Added some unit tests. * Replace old usages of normalize and canonicalize. * Fix reading symlinks and existence logic. * Better explained --- crates/nu-cli/src/lib.rs | 1 + crates/nu-cli/src/path.rs | 184 ++++++++++++++++++++ crates/nu-cli/src/shell/filesystem_shell.rs | 61 +------ 3 files changed, 194 insertions(+), 52 deletions(-) create mode 100644 crates/nu-cli/src/path.rs diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index 11d5c3ea7f..9c4cf2d1bb 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -17,6 +17,7 @@ mod evaluate; mod format; mod futures; mod git; +mod path; mod shell; mod stream; mod utils; diff --git a/crates/nu-cli/src/path.rs b/crates/nu-cli/src/path.rs new file mode 100644 index 0000000000..ce384ae0ff --- /dev/null +++ b/crates/nu-cli/src/path.rs @@ -0,0 +1,184 @@ +// Copyright (C) 2020 Kevin Dc +// +// This file is part of nushell. +// +// nushell is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// nushell is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with nushell. If not, see . + +use std::io; +use std::path::{Component, Path, PathBuf}; + +pub fn normalize(path: impl AsRef) -> PathBuf { + let mut normalized = PathBuf::new(); + for component in path.as_ref().components() { + match component { + Component::Normal(normal) => { + if let Some(normal) = normal.to_str() { + if normal.chars().all(|c| c == '.') { + for _ in 0..(normal.len() - 1) { + normalized.push(".."); + } + } else { + normalized.push(normal); + } + } else { + normalized.push(normal); + } + } + c => normalized.push(c.as_os_str()), + } + } + + normalized +} + +pub struct AllowMissing(pub bool); + +pub fn canonicalize( + relative_to: P, + path: Q, + allow_missing: AllowMissing, +) -> io::Result +where + P: AsRef, + Q: AsRef, +{ + let path = normalize(path); + let (relative_to, path) = if path.is_absolute() { + 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(), path) + } + } else { + (relative_to.as_ref().to_path_buf(), path) + }; + + 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 + }; + + let path = match std::fs::read_link(&path) { + Ok(resolved) => resolved, + Err(e) => { + // We are here if path doesn't exist or isn't a symlink + if allow_missing.0 || path.exists() { + // Return if we allow missing paths or if the path + // actually exists, but wasn't a symlink + path + } else { + return Err(e); + } + } + }; + + // De-UNC paths + Ok(dunce::simplified(&path).to_path_buf()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io; + + #[test] + fn normalize_three_dots() { + assert_eq!(PathBuf::from("../.."), normalize("...")); + } + + #[test] + fn normalize_three_dots_with_redundant_dot() { + assert_eq!(PathBuf::from("./../.."), normalize("./...")); + } + + #[test] + fn canonicalize_two_dots_and_allow_missing() -> io::Result<()> { + let relative_to = Path::new("/foo/bar"); // does not exists + let path = Path::new(".."); + + assert_eq!( + PathBuf::from("/foo"), + canonicalize(relative_to, path, AllowMissing(true))? + ); + + Ok(()) + } + + #[test] + fn canonicalize_three_dots_and_allow_missing() -> io::Result<()> { + let relative_to = Path::new("/foo/bar/baz"); // missing path + let path = Path::new("..."); + + assert_eq!( + PathBuf::from("/foo"), + canonicalize(relative_to, path, AllowMissing(true))? + ); + + Ok(()) + } + + #[test] + fn canonicalize_three_dots_with_redundant_dot_and_allow_missing() -> io::Result<()> { + let relative_to = Path::new("/foo/bar/baz"); // missing path + let path = Path::new("./..."); + + assert_eq!( + PathBuf::from("/foo"), + canonicalize(relative_to, path, AllowMissing(true))? + ); + + Ok(()) + } + + #[test] + fn canonicalize_three_dots_and_disallow_missing() -> io::Result<()> { + let relative_to = Path::new("/foo/bar/"); // root is not missing + let path = Path::new("..."); + + assert_eq!( + PathBuf::from("/"), + canonicalize(relative_to, path, AllowMissing(false))? + ); + + Ok(()) + } + + #[test] + fn canonicalize_three_dots_and_disallow_missing_should_fail() { + let relative_to = Path::new("/foo/bar/baz"); // foo is missing + let path = Path::new("..."); + + assert!(canonicalize(relative_to, path, AllowMissing(false)).is_err()); + } +} diff --git a/crates/nu-cli/src/shell/filesystem_shell.rs b/crates/nu-cli/src/shell/filesystem_shell.rs index 33fa049e21..45a86c46fb 100644 --- a/crates/nu-cli/src/shell/filesystem_shell.rs +++ b/crates/nu-cli/src/shell/filesystem_shell.rs @@ -5,6 +5,7 @@ use crate::commands::mkdir::MkdirArgs; use crate::commands::mv::MoveArgs; use crate::commands::rm::RemoveArgs; use crate::data::dir_entry_dict; +use crate::path::{canonicalize, normalize, AllowMissing}; use crate::prelude::*; use crate::shell::completer::NuCompleter; use crate::shell::shell::Shell; @@ -187,13 +188,14 @@ impl Shell for FilesystemShell { if target == Path::new("-") { PathBuf::from(&self.last_path) } else { - let path = canonicalize(self.path(), target).map_err(|_| { - ShellError::labeled_error( - "Cannot change to directory", - "directory not found", - &v.tag, - ) - })?; + let path = + canonicalize(self.path(), target, AllowMissing(false)).map_err(|_| { + ShellError::labeled_error( + "Cannot change to directory", + "directory not found", + &v.tag, + ) + })?; if !path.is_dir() { return Err(ShellError::labeled_error( @@ -1164,48 +1166,3 @@ fn is_hidden_dir(dir: impl AsRef) -> bool { } } } - -fn normalize(path: impl AsRef) -> PathBuf { - let mut normalized = PathBuf::new(); - for component in path.as_ref().components() { - match component { - Component::Normal(normal) => { - if let Some(normal) = normal.to_str() { - if normal.chars().all(|c| c == '.') { - for _ in 0..(normal.len() - 1) { - normalized.push(".."); - } - } else { - normalized.push(normal); - } - } else { - normalized.push(normal); - } - } - c => normalized.push(c.as_os_str()), - } - } - - normalized -} - -fn canonicalize(relative_to: impl AsRef, path: impl AsRef) -> std::io::Result { - let path = if path.as_ref().is_relative() { - let mut result = relative_to.as_ref().to_path_buf(); - normalize(path.as_ref()) - .components() - .for_each(|component| match component { - Component::ParentDir => { - result.pop(); - } - Component::Normal(normal) => result.push(normal), - _ => {} - }); - - result - } else { - path.as_ref().into() - }; - - dunce::canonicalize(path) -}