diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 232266427..34abfc511 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -50,6 +50,7 @@ iflags kibi kibibytes lcase +lossily mebi mebibytes mergeable diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index aec4b4a42..82cbbe15f 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -321,3 +321,6 @@ uucore_procs uumain uutil uutils + +# * function names +getcwd diff --git a/src/uu/pwd/src/pwd.rs b/src/uu/pwd/src/pwd.rs index 75dc637e6..e8f0c0013 100644 --- a/src/uu/pwd/src/pwd.rs +++ b/src/uu/pwd/src/pwd.rs @@ -10,28 +10,115 @@ extern crate uucore; use clap::{crate_version, App, Arg}; use std::env; -use std::io; +use std::io::{self, Write}; use std::path::{Path, PathBuf}; -use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::error::{FromIo, UResult}; static ABOUT: &str = "Display the full filename of the current working directory."; static OPT_LOGICAL: &str = "logical"; static OPT_PHYSICAL: &str = "physical"; -pub fn absolute_path(path: &Path) -> io::Result { - let path_buf = path.canonicalize()?; +fn physical_path() -> io::Result { + // std::env::current_dir() is a thin wrapper around libc's getcwd(). + // On Unix, getcwd() must return the physical path: + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/getcwd.html + #[cfg(unix)] + { + env::current_dir() + } + + // On Windows we have to resolve it. + // On other systems we also resolve it, just in case. + #[cfg(not(unix))] + { + env::current_dir().and_then(|path| path.canonicalize()) + } +} + +fn logical_path() -> io::Result { + // getcwd() on Windows seems to include symlinks, so this is easy. #[cfg(windows)] - let path_buf = Path::new( - path_buf - .as_path() - .to_string_lossy() - .trim_start_matches(r"\\?\"), - ) - .to_path_buf(); + { + env::current_dir() + } - Ok(path_buf) + // If we're not on Windows we do things Unix-style. + // + // Typical Unix-like kernels don't actually keep track of the logical working + // directory. They know the precise directory a process is in, and the getcwd() + // syscall reconstructs a path from that. + // + // The logical working directory is maintained by the shell, in the $PWD + // environment variable. So we check carefully if that variable looks + // reasonable, and if not then we fall back to the physical path. + // + // POSIX: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pwd.html + #[cfg(not(windows))] + { + fn looks_reasonable(path: &Path) -> bool { + // First, check if it's an absolute path. + if !path.has_root() { + return false; + } + + // Then, make sure there are no . or .. components. + // Path::components() isn't useful here, it normalizes those out. + + // to_string_lossy() may allocate, but that's fine, we call this + // only once per run. It may also lose information, but not any + // information that we need for this check. + if path + .to_string_lossy() + .split(std::path::is_separator) + .any(|piece| piece == "." || piece == "..") + { + return false; + } + + // Finally, check if it matches the directory we're in. + #[cfg(unix)] + { + use std::fs::metadata; + use std::os::unix::fs::MetadataExt; + let path_info = match metadata(path) { + Ok(info) => info, + Err(_) => return false, + }; + let real_info = match metadata(".") { + Ok(info) => info, + Err(_) => return false, + }; + if path_info.dev() != real_info.dev() || path_info.ino() != real_info.ino() { + return false; + } + } + + #[cfg(not(unix))] + { + use std::fs::canonicalize; + let canon_path = match canonicalize(path) { + Ok(path) => path, + Err(_) => return false, + }; + let real_path = match canonicalize(".") { + Ok(path) => path, + Err(_) => return false, + }; + if canon_path != real_path { + return false; + } + } + + true + } + + match env::var_os("PWD").map(PathBuf::from) { + Some(value) if looks_reasonable(&value) => Ok(value), + _ => env::current_dir(), + } + } } fn usage() -> String { @@ -43,24 +130,48 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let usage = usage(); let matches = uu_app().usage(&usage[..]).get_matches_from(args); + let cwd = if matches.is_present(OPT_LOGICAL) { + logical_path() + } else { + physical_path() + } + .map_err_context(|| "failed to get current directory".to_owned())?; - match env::current_dir() { - Ok(logical_path) => { - if matches.is_present(OPT_LOGICAL) { - println!("{}", logical_path.display()); - } else { - let physical_path = absolute_path(&logical_path) - .map_err_context(|| "failed to get absolute path".to_string())?; - println!("{}", physical_path.display()); - } - } - Err(e) => { - return Err(USimpleError::new( - 1, - format!("failed to get current directory {}", e), - )) - } - }; + // \\?\ is a prefix Windows gives to paths under certain circumstances, + // including when canonicalizing them. + // With the right extension trait we can remove it non-lossily, but + // we print it lossily anyway, so no reason to bother. + #[cfg(windows)] + let cwd = cwd + .to_string_lossy() + .strip_prefix(r"\\?\") + .map(Into::into) + .unwrap_or(cwd); + + print_path(&cwd).map_err_context(|| "failed to print current directory".to_owned())?; + + Ok(()) +} + +fn print_path(path: &Path) -> io::Result<()> { + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + // On Unix we print non-lossily. + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + stdout.write_all(path.as_os_str().as_bytes())?; + stdout.write_all(b"\n")?; + } + + // On other platforms we potentially mangle it. + // There might be some clever way to do it correctly on Windows, but + // invalid unicode in filenames is rare there. + #[cfg(not(unix))] + { + writeln!(stdout, "{}", path.display())?; + } Ok(()) } @@ -79,6 +190,7 @@ pub fn uu_app() -> App<'static, 'static> { Arg::with_name(OPT_PHYSICAL) .short("P") .long(OPT_PHYSICAL) + .overrides_with(OPT_LOGICAL) .help("avoid all symlinks"), ) } diff --git a/tests/by-util/test_pwd.rs b/tests/by-util/test_pwd.rs index 2779b9e62..bc08ddbb0 100644 --- a/tests/by-util/test_pwd.rs +++ b/tests/by-util/test_pwd.rs @@ -1,9 +1,13 @@ +// spell-checker:ignore (words) symdir somefakedir + +use std::path::PathBuf; + use crate::common::util::*; #[test] fn test_default() { let (at, mut ucmd) = at_and_ucmd!(); - ucmd.run().stdout_is(at.root_dir_resolved() + "\n"); + ucmd.succeeds().stdout_is(at.root_dir_resolved() + "\n"); } #[test] @@ -11,3 +15,118 @@ fn test_failed() { let (_at, mut ucmd) = at_and_ucmd!(); ucmd.arg("will-fail").fails(); } + +#[cfg(unix)] +#[test] +fn test_deleted_dir() { + use std::process::Command; + + let ts = TestScenario::new(util_name!()); + let at = ts.fixtures.clone(); + let output = Command::new("sh") + .arg("-c") + .arg(format!( + "cd '{}'; mkdir foo; cd foo; rmdir ../foo; exec {} {}", + at.root_dir_resolved(), + ts.bin_path.to_str().unwrap(), + ts.util_name, + )) + .output() + .unwrap(); + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); + assert_eq!( + output.stderr, + b"pwd: failed to get current directory: No such file or directory\n" + ); +} + +struct Env { + ucmd: UCommand, + #[cfg(not(windows))] + root: String, + subdir: String, + symdir: String, +} + +fn symlinked_env() -> Env { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("subdir"); + // Note: on Windows this requires admin permissions + at.symlink_dir("subdir", "symdir"); + let root = PathBuf::from(at.root_dir_resolved()); + ucmd.raw.current_dir(root.join("symdir")); + #[cfg(not(windows))] + ucmd.env("PWD", root.join("symdir")); + Env { + ucmd, + #[cfg(not(windows))] + root: root.to_string_lossy().into_owned(), + subdir: root.join("subdir").to_string_lossy().into_owned(), + symdir: root.join("symdir").to_string_lossy().into_owned(), + } +} + +#[test] +fn test_symlinked_logical() { + let mut env = symlinked_env(); + env.ucmd.arg("-L").succeeds().stdout_is(env.symdir + "\n"); +} + +#[test] +fn test_symlinked_physical() { + let mut env = symlinked_env(); + env.ucmd.arg("-P").succeeds().stdout_is(env.subdir + "\n"); +} + +#[test] +fn test_symlinked_default() { + let mut env = symlinked_env(); + env.ucmd.succeeds().stdout_is(env.subdir + "\n"); +} + +#[cfg(not(windows))] +pub mod untrustworthy_pwd_var { + use std::path::Path; + + use super::*; + + #[test] + fn test_nonexistent_logical() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.arg("-L") + .env("PWD", "/somefakedir") + .succeeds() + .stdout_is(at.root_dir_resolved() + "\n"); + } + + #[test] + fn test_wrong_logical() { + let mut env = symlinked_env(); + env.ucmd + .arg("-L") + .env("PWD", env.root) + .succeeds() + .stdout_is(env.subdir + "\n"); + } + + #[test] + fn test_redundant_logical() { + let mut env = symlinked_env(); + env.ucmd + .arg("-L") + .env("PWD", Path::new(&env.symdir).join(".")) + .succeeds() + .stdout_is(env.subdir + "\n"); + } + + #[test] + fn test_relative_logical() { + let mut env = symlinked_env(); + env.ucmd + .arg("-L") + .env("PWD", ".") + .succeeds() + .stdout_is(env.subdir + "\n"); + } +} diff --git a/tests/common/macros.rs b/tests/common/macros.rs index 62b8c4824..108bc0fb7 100644 --- a/tests/common/macros.rs +++ b/tests/common/macros.rs @@ -31,7 +31,11 @@ macro_rules! path_concat { #[macro_export] macro_rules! util_name { () => { - module_path!().split("_").nth(1).expect("no test name") + module_path!() + .split("_") + .nth(1) + .and_then(|s| s.split("::").next()) + .expect("no test name") }; }