1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

pwd: Properly resolve logical working directory (#2604)

* pwd: Properly resolve logical working directory

* fixup! pwd: Properly resolve logical working directory

* fixup! pwd: Properly resolve logical working directory
This commit is contained in:
Jan Verbeek 2021-08-28 22:31:20 +02:00 committed by GitHub
parent b0becf0054
commit b4c95d49d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 270 additions and 31 deletions

View file

@ -50,6 +50,7 @@ iflags
kibi
kibibytes
lcase
lossily
mebi
mebibytes
mergeable

View file

@ -321,3 +321,6 @@ uucore_procs
uumain
uutil
uutils
# * function names
getcwd

View file

@ -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<PathBuf> {
let path_buf = path.canonicalize()?;
fn physical_path() -> io::Result<PathBuf> {
// 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<PathBuf> {
// 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"),
)
}

View file

@ -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");
}
}

View file

@ -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")
};
}