mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 19:47:45 +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:
parent
b0becf0054
commit
b4c95d49d8
5 changed files with 270 additions and 31 deletions
|
@ -50,6 +50,7 @@ iflags
|
|||
kibi
|
||||
kibibytes
|
||||
lcase
|
||||
lossily
|
||||
mebi
|
||||
mebibytes
|
||||
mergeable
|
||||
|
|
|
@ -321,3 +321,6 @@ uucore_procs
|
|||
uumain
|
||||
uutil
|
||||
uutils
|
||||
|
||||
# * function names
|
||||
getcwd
|
||||
|
|
|
@ -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);
|
||||
|
||||
match env::current_dir() {
|
||||
Ok(logical_path) => {
|
||||
if matches.is_present(OPT_LOGICAL) {
|
||||
println!("{}", logical_path.display());
|
||||
let cwd = if matches.is_present(OPT_LOGICAL) {
|
||||
logical_path()
|
||||
} else {
|
||||
let physical_path = absolute_path(&logical_path)
|
||||
.map_err_context(|| "failed to get absolute path".to_string())?;
|
||||
println!("{}", physical_path.display());
|
||||
physical_path()
|
||||
}
|
||||
.map_err_context(|| "failed to get current directory".to_owned())?;
|
||||
|
||||
// \\?\ 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")?;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(USimpleError::new(
|
||||
1,
|
||||
format!("failed to get current directory {}", e),
|
||||
))
|
||||
|
||||
// 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"),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue