From 0e04f959c2adbcced6b2711b433f823b7fbeddd3 Mon Sep 17 00:00:00 2001 From: James Robson Date: Sun, 27 Jun 2021 11:47:23 +0100 Subject: [PATCH] Add Physical mode to realpath This adds the 'Physical Mode' and 'Logical Mode' switches to realpath, which control when symlinks are resolved. --- src/uu/cp/src/cp.rs | 6 +-- src/uu/ln/src/ln.rs | 10 +++-- src/uu/readlink/src/readlink.rs | 27 +++++++----- src/uu/realpath/src/realpath.rs | 33 +++++++++++---- src/uu/relpath/src/relpath.rs | 8 ++-- src/uucore/src/lib/features/fs.rs | 70 +++++++++++++++++++++++-------- tests/by-util/test_readlink.rs | 11 +++++ tests/by-util/test_realpath.rs | 33 +++++++++++++++ 8 files changed, 153 insertions(+), 45 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 91ea7ef37..dfed4e0fc 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -48,7 +48,7 @@ use std::path::{Path, PathBuf, StripPrefixError}; use std::str::FromStr; use std::string::ToString; use uucore::backup_control::{self, BackupMode}; -use uucore::fs::{canonicalize, CanonicalizeMode}; +use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; use walkdir::WalkDir; #[cfg(unix)] @@ -1431,8 +1431,8 @@ pub fn localize_to_target(root: &Path, source: &Path, target: &Path) -> CopyResu pub fn paths_refer_to_same_file(p1: &Path, p2: &Path) -> io::Result { // We have to take symlinks and relative paths into account. - let pathbuf1 = canonicalize(p1, CanonicalizeMode::Normal)?; - let pathbuf2 = canonicalize(p2, CanonicalizeMode::Normal)?; + let pathbuf1 = canonicalize(p1, MissingHandling::Normal, ResolveMode::Logical)?; + let pathbuf2 = canonicalize(p2, MissingHandling::Normal, ResolveMode::Logical)?; Ok(pathbuf1 == pathbuf2) } diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index d354acce9..3df859d8f 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -23,7 +23,7 @@ use std::os::unix::fs::symlink; use std::os::windows::fs::{symlink_dir, symlink_file}; use std::path::{Path, PathBuf}; use uucore::backup_control::{self, BackupMode}; -use uucore::fs::{canonicalize, CanonicalizeMode}; +use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; pub struct Settings { overwrite: OverwriteMode, @@ -361,8 +361,12 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) } fn relative_path<'a>(src: &Path, dst: &Path) -> Result> { - let src_abs = canonicalize(src, CanonicalizeMode::Normal)?; - let mut dst_abs = canonicalize(dst.parent().unwrap(), CanonicalizeMode::Normal)?; + let src_abs = canonicalize(src, MissingHandling::Normal, ResolveMode::Logical)?; + let mut dst_abs = canonicalize( + dst.parent().unwrap(), + MissingHandling::Normal, + ResolveMode::Logical, + )?; dst_abs.push(dst.components().last().unwrap()); let suffix_pos = src_abs .components() diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index 826fa0254..f9885939a 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -14,7 +14,7 @@ use clap::{crate_version, App, Arg}; use std::fs; use std::io::{stdout, Write}; use std::path::{Path, PathBuf}; -use uucore::fs::{canonicalize, CanonicalizeMode}; +use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; const NAME: &str = "readlink"; const ABOUT: &str = "Print value of a symbolic link or canonical file name."; @@ -42,14 +42,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let silent = matches.is_present(OPT_SILENT) || matches.is_present(OPT_QUIET); let verbose = matches.is_present(OPT_VERBOSE); - let can_mode = if matches.is_present(OPT_CANONICALIZE) { - CanonicalizeMode::Normal - } else if matches.is_present(OPT_CANONICALIZE_EXISTING) { - CanonicalizeMode::Existing - } else if matches.is_present(OPT_CANONICALIZE_MISSING) { - CanonicalizeMode::Missing + let res_mode = if matches.is_present(OPT_CANONICALIZE) + || matches.is_present(OPT_CANONICALIZE_EXISTING) + || matches.is_present(OPT_CANONICALIZE_MISSING) + { + ResolveMode::Logical } else { - CanonicalizeMode::None + ResolveMode::None + }; + + let can_mode = if matches.is_present(OPT_CANONICALIZE_EXISTING) { + MissingHandling::Existing + } else if matches.is_present(OPT_CANONICALIZE_MISSING) { + MissingHandling::Missing + } else { + MissingHandling::Normal }; let files: Vec = matches @@ -71,7 +78,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { for f in &files { let p = PathBuf::from(f); - if can_mode == CanonicalizeMode::None { + if res_mode == ResolveMode::None { match fs::read_link(&p) { Ok(path) => show(&path, no_newline, use_zero), Err(err) => { @@ -82,7 +89,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } } else { - match canonicalize(&p, can_mode) { + match canonicalize(&p, can_mode, res_mode) { Ok(path) => show(&path, no_newline, use_zero), Err(err) => { if verbose { diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index fe2ad4ccc..40cd94383 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -12,13 +12,15 @@ extern crate uucore; use clap::{crate_version, App, Arg}; use std::path::{Path, PathBuf}; -use uucore::fs::{canonicalize, CanonicalizeMode}; +use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; static ABOUT: &str = "print the resolved path"; static OPT_QUIET: &str = "quiet"; static OPT_STRIP: &str = "strip"; static OPT_ZERO: &str = "zero"; +static OPT_PHYSICAL: &str = "physical"; +static OPT_LOGICAL: &str = "logical"; static ARG_FILES: &str = "files"; @@ -42,9 +44,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let strip = matches.is_present(OPT_STRIP); let zero = matches.is_present(OPT_ZERO); let quiet = matches.is_present(OPT_QUIET); + let logical = matches.is_present(OPT_LOGICAL); let mut retcode = 0; for path in &paths { - if let Err(e) = resolve_path(path, strip, zero) { + if let Err(e) = resolve_path(path, strip, zero, logical) { if !quiet { show_error!("{}: {}", e, path.display()); } @@ -76,6 +79,19 @@ pub fn uu_app() -> App<'static, 'static> { .long(OPT_ZERO) .help("Separate output filenames with \\0 rather than newline"), ) + .arg( + Arg::with_name(OPT_LOGICAL) + .short("L") + .long(OPT_LOGICAL) + .help("resolve '..' components before symlinks"), + ) + .arg( + Arg::with_name(OPT_PHYSICAL) + .short("P") + .long(OPT_PHYSICAL) + .overrides_with_all(&[OPT_STRIP, OPT_LOGICAL]) + .help("resolve symlinks as encountered (default)"), + ) .arg( Arg::with_name(ARG_FILES) .multiple(true) @@ -96,14 +112,17 @@ pub fn uu_app() -> App<'static, 'static> { /// /// This function returns an error if there is a problem resolving /// symbolic links. -fn resolve_path(p: &Path, strip: bool, zero: bool) -> std::io::Result<()> { - let mode = if strip { - CanonicalizeMode::None +fn resolve_path(p: &Path, strip: bool, zero: bool, logical: bool) -> std::io::Result<()> { + let resolve = if strip { + ResolveMode::None + } else if logical { + ResolveMode::Logical } else { - CanonicalizeMode::Normal + ResolveMode::Physical }; - let abs = canonicalize(p, mode)?; + let abs = canonicalize(p, MissingHandling::Normal, resolve)?; let line_ending = if zero { '\0' } else { '\n' }; + print!("{}{}", abs.display(), line_ending); Ok(()) } diff --git a/src/uu/relpath/src/relpath.rs b/src/uu/relpath/src/relpath.rs index cb0fba7cc..48d02414d 100644 --- a/src/uu/relpath/src/relpath.rs +++ b/src/uu/relpath/src/relpath.rs @@ -13,7 +13,7 @@ extern crate uucore; use clap::{crate_version, App, Arg}; use std::env; use std::path::{Path, PathBuf}; -use uucore::fs::{canonicalize, CanonicalizeMode}; +use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; use uucore::InvalidEncodingHandling; static ABOUT: &str = "Convert TO destination to the relative path from the FROM dir. @@ -42,12 +42,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Some(p) => Path::new(p).to_path_buf(), None => env::current_dir().unwrap(), }; - let absto = canonicalize(to, CanonicalizeMode::Normal).unwrap(); - let absfrom = canonicalize(from, CanonicalizeMode::Normal).unwrap(); + let absto = canonicalize(to, MissingHandling::Normal, ResolveMode::Logical).unwrap(); + let absfrom = canonicalize(from, MissingHandling::Normal, ResolveMode::Logical).unwrap(); if matches.is_present(options::DIR) { let base = Path::new(&matches.value_of(options::DIR).unwrap()).to_path_buf(); - let absbase = canonicalize(base, CanonicalizeMode::Normal).unwrap(); + let absbase = canonicalize(base, MissingHandling::Normal, ResolveMode::Logical).unwrap(); if !absto.as_path().starts_with(absbase.as_path()) || !absfrom.as_path().starts_with(absbase.as_path()) { diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 36bdbfed0..ea1743880 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -56,11 +56,8 @@ pub fn resolve_relative_path(path: &Path) -> Cow { /// Controls how symbolic links should be handled when canonicalizing a path. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum CanonicalizeMode { - /// Do not resolve any symbolic links. - None, - - /// Resolve all symbolic links. +pub enum MissingHandling { + /// Return an error if any part of the path is missing. Normal, /// Resolve symbolic links, ignoring errors on the final component. @@ -70,6 +67,19 @@ pub enum CanonicalizeMode { Missing, } +/// Controls when symbolic links are resolved +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ResolveMode { + /// Do not resolve any symbolic links. + None, + + /// Resolve symlinks as encountered when processing the path + Physical, + + /// Resolve '..' elements before symlinks + Logical, +} + // copied from https://github.com/rust-lang/cargo/blob/2e4cfc2b7d43328b207879228a2ca7d427d188bb/src/cargo/util/paths.rs#L65-L90 // both projects are MIT https://github.com/rust-lang/cargo/blob/master/LICENSE-MIT // for std impl progress see rfc https://github.com/rust-lang/rfcs/issues/2208 @@ -130,20 +140,32 @@ fn resolve>(original: P) -> IOResult { /// This function is a generalization of [`std::fs::canonicalize`] that /// allows controlling how symbolic links are resolved and how to deal /// with missing components. It returns the canonical, absolute form of -/// a path. The `can_mode` parameter controls how symbolic links are -/// resolved: +/// a path. +/// The `miss_mode` parameter controls how missing path elements are handled /// -/// * [`CanonicalizeMode::Normal`] makes this function behave like +/// * [`MissingHandling::Normal`] makes this function behave like /// [`std::fs::canonicalize`], resolving symbolic links and returning /// an error if the path does not exist. -/// * [`CanonicalizeMode::Missing`] makes this function ignore non-final +/// * [`MissingHandling::Missing`] makes this function ignore non-final /// components of the path that could not be resolved. -/// * [`CanonicalizeMode::Existing`] makes this function return an error +/// * [`MissingHandling::Existing`] makes this function return an error /// if the final component of the path does not exist. -/// * [`CanonicalizeMode::None`] makes this function not try to resolve -/// any symbolic links. /// -pub fn canonicalize>(original: P, can_mode: CanonicalizeMode) -> IOResult { +/// The `res_mode` parameter controls how symbolic links are +/// resolved: +/// +/// * [`ResolveMode::None`] makes this function not try to resolve +/// any symbolic links. +/// * [`ResolveMode::Physical`] makes this function resolve symlinks as they +/// are encountered +/// * [`ResolveMode::Logical`] makes this function resolve '..' components +/// before symlinks +/// +pub fn canonicalize>( + original: P, + miss_mode: MissingHandling, + res_mode: ResolveMode, +) -> IOResult { // Create an absolute path let original = original.as_ref(); let original = if original.is_absolute() { @@ -167,7 +189,11 @@ pub fn canonicalize>(original: P, can_mode: CanonicalizeMode) -> } Component::CurDir => (), Component::ParentDir => { - parts.pop(); + if res_mode == ResolveMode::Logical { + parts.pop(); + } else { + parts.push(part.as_os_str()); + } } Component::Normal(_) => { parts.push(part.as_os_str()); @@ -180,12 +206,17 @@ pub fn canonicalize>(original: P, can_mode: CanonicalizeMode) -> for part in parts[..parts.len() - 1].iter() { result.push(part); - if can_mode == CanonicalizeMode::None { + //resolve as we go to handle long relative paths on windows + if res_mode == ResolveMode::Physical { + result = normalize_path(&result); + } + + if res_mode == ResolveMode::None { continue; } match resolve(&result) { - Err(_) if can_mode == CanonicalizeMode::Missing => continue, + Err(_) if miss_mode == MissingHandling::Missing => continue, Err(e) => return Err(e), Ok(path) => { result.pop(); @@ -196,12 +227,12 @@ pub fn canonicalize>(original: P, can_mode: CanonicalizeMode) -> result.push(parts.last().unwrap()); - if can_mode == CanonicalizeMode::None { + if res_mode == ResolveMode::None { return Ok(result); } match resolve(&result) { - Err(e) if can_mode == CanonicalizeMode::Existing => { + Err(e) if miss_mode == MissingHandling::Existing => { return Err(e); } Ok(path) => { @@ -210,6 +241,9 @@ pub fn canonicalize>(original: P, can_mode: CanonicalizeMode) -> } Err(_) => (), } + if res_mode == ResolveMode::Physical { + result = normalize_path(&result); + } } Ok(result) } diff --git a/tests/by-util/test_readlink.rs b/tests/by-util/test_readlink.rs index 51aebbed2..25f29004f 100644 --- a/tests/by-util/test_readlink.rs +++ b/tests/by-util/test_readlink.rs @@ -2,6 +2,17 @@ use crate::common::util::*; static GIBBERISH: &str = "supercalifragilisticexpialidocious"; +#[test] +fn test_resolve() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("foo"); + at.symlink_file("foo", "bar"); + + scene.ucmd().arg("bar").succeeds().stdout_contains("foo\n"); +} + #[test] fn test_canonicalize() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index e1384ac74..8cb1551f0 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -106,3 +106,36 @@ fn test_realpath_file_and_links_strip_zero() { .succeeds() .stdout_contains("bar\u{0}"); } + +#[test] +fn test_realpath_physical_mode() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("dir1"); + at.mkdir_all("dir2/bar"); + at.symlink_dir("dir2/bar", "dir1/foo"); + + scene + .ucmd() + .arg("dir1/foo/..") + .succeeds() + .stdout_contains("dir2\n"); +} + +#[test] +fn test_realpath_logical_mode() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("dir1"); + at.mkdir("dir2"); + at.symlink_dir("dir2", "dir1/foo"); + + scene + .ucmd() + .arg("-L") + .arg("dir1/foo/..") + .succeeds() + .stdout_contains("dir1\n"); +}