diff --git a/src/common/validation.rs b/src/common/validation.rs index 58460941037..146fa4fda36 100644 --- a/src/common/validation.rs +++ b/src/common/validation.rs @@ -98,17 +98,35 @@ fn get_canonical_util_name(util_name: &str) -> &str { /// Gets the binary path from command line arguments /// Panics if the binary path cannot be determined -#[cfg(any( - not(any( - target_os = "linux", - all(target_os = "android", target_pointer_width = "64") - )), - target_env = "musl" +#[cfg(all( + not(windows), + any( + not(any( + target_os = "linux", + all(target_os = "android", target_pointer_width = "64") + )), + target_env = "musl" + ) ))] pub fn binary_path(args: &mut impl Iterator) -> PathBuf { PathBuf::from(args.next().unwrap()) // no fallback for empty args. current_exe() (/proc/self/exe) is valid only for hardlinks } + +/// Gets the path of the running executable instead of `argv[0]`. +/// +/// cmd.exe passes `argv[0]` the way it was typed, so `False` would not match +/// `false.exe`, while this function has the name as found on disk without resolving symlink, +/// so links named after a utility keep working. A path given explicitly in another case +/// keeps that spelling. +/// Panics if the binary path cannot be determined +#[cfg(windows)] +pub fn binary_path(args: &mut impl Iterator) -> PathBuf { + // argv[0] is always skipped, the caller reads the arguments after it. + let argv0 = args.next(); + // Backend is `GetModuleFileNameW` which does not resolve symlink. + std::env::current_exe().unwrap_or_else(|_| argv0.unwrap().into()) +} /// Get actual binary path from kernel, not argv0, to prevent `env -a` from bypassing /// AppArmor, SELinux policies on hard-linked binaries #[cfg(all( diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index e0dfa14ed98..7e2901f22d8 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -198,6 +198,49 @@ fn util_name_single() { ))); } +#[rstest::rstest] +#[case::util("basename.exe", &["BASENAME", "a/b"], false)] +#[case::multicall("coreutils.exe", &["COREUTILS", "basename", "a/b"], false)] +// Fails if `current_exe()` starts resolving symlinks and returns coreutils.exe. +#[case::symlink("basename.exe", &["BASENAME", "a/b"], true)] +#[cfg(all(windows, feature = "basename"))] +fn util_name_is_the_name_on_disk_on_windows( + #[case] name: &str, + #[case] typed: &[&str], + #[case] symlink: bool, +) { + use std::process::Command; + + let scenario = TestScenario::new("basename"); + let path = scenario.fixtures.plus(name); + if symlink { + // Needs Developer Mode or admin rights locally. CI has them, so it must not skip there. + if let Err(e) = std::os::windows::fs::symlink_file(&scenario.bin_path, path) { + assert!(!uutests::util::is_ci(), "cannot create a symlink: {e}"); + print!("Test skipped; cannot create a symlink: {e}"); + return; + } + } else { + // A copy, since hard links can't cross drives. + std::fs::copy(&scenario.bin_path, path).unwrap(); + } + // cmd.exe finds `basename.exe` for `BASENAME`, but passes argv[0] as typed. + // Its PATH only holds that file, so it can't run a standalone `basename.exe`. + let cmd = env::var_os("ComSpec").unwrap_or_else(|| "cmd.exe".into()); + let output = Command::new(cmd) + .arg("/C") + .args(typed) + .env("PATH", &scenario.fixtures.subdir) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(output.stdout, b"b\n"); +} + #[test] #[cfg(unix)] fn util_invalid_name_help() {