174 lines
5.5 KiB
Rust
174 lines
5.5 KiB
Rust
use std::ffi::OsStr;
|
|
#[cfg(unix)]
|
|
use std::ffi::OsString;
|
|
#[cfg(unix)]
|
|
use std::fs;
|
|
#[cfg(unix)]
|
|
use std::path::PathBuf;
|
|
|
|
use super::{known_bare_name, resolve_binary_in_wsl_safe_path};
|
|
|
|
#[cfg(unix)]
|
|
fn make_executable(path: &std::path::Path) {
|
|
use std::os::unix::fs::PermissionsExt as _;
|
|
let mut perms = fs::metadata(path).unwrap().permissions();
|
|
perms.set_mode(0o755);
|
|
fs::set_permissions(path, perms).unwrap();
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn write_exec(path: &std::path::Path) {
|
|
fs::write(path, b"#!/bin/sh\nexit 0\n").unwrap();
|
|
make_executable(path);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn join(parts: &[PathBuf]) -> OsString {
|
|
std::env::join_paths(parts).unwrap()
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn picks_first_linux_path_when_wsl() {
|
|
let linux_dir = tempfile::tempdir().unwrap();
|
|
write_exec(&linux_dir.path().join("git"));
|
|
|
|
// `/mnt/c/...` paths in the synthetic PATH that don't exist on
|
|
// disk simulate the WSL-with-Windows-git layout: the resolver
|
|
// should skip them on the prefix and never stat them.
|
|
let path_env = join(&[
|
|
PathBuf::from("/mnt/c/Program Files/Git/cmd"),
|
|
linux_dir.path().to_path_buf(),
|
|
]);
|
|
|
|
let resolved =
|
|
resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).unwrap();
|
|
assert_eq!(resolved, linux_dir.path().join("git"));
|
|
assert!(!resolved.starts_with("/mnt"));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn passes_through_first_match_when_not_wsl() {
|
|
// When not WSL, `/mnt/...` is just another directory; the resolver
|
|
// should pick the first dir on PATH that contains an exec match.
|
|
let mnt_dir = tempfile::tempdir().unwrap();
|
|
write_exec(&mnt_dir.path().join("git"));
|
|
let other_dir = tempfile::tempdir().unwrap();
|
|
write_exec(&other_dir.path().join("git"));
|
|
|
|
let path_env = join(&[mnt_dir.path().to_path_buf(), other_dir.path().to_path_buf()]);
|
|
|
|
let resolved =
|
|
resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), false).unwrap();
|
|
assert_eq!(resolved, mnt_dir.path().join("git"));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn falls_back_to_none_when_only_mnt_has_git() {
|
|
let path_env = join(&[
|
|
PathBuf::from("/mnt/c/Program Files/Git/cmd"),
|
|
PathBuf::from("/mnt/c/Windows/System32"),
|
|
]);
|
|
assert!(resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).is_none());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn picks_user_local_bin() {
|
|
let bin_dir = tempfile::tempdir().unwrap();
|
|
let empty_dir = bin_dir.path().join("empty");
|
|
fs::create_dir_all(&empty_dir).unwrap();
|
|
let local_bin = bin_dir.path().join("home/.local/bin");
|
|
fs::create_dir_all(&local_bin).unwrap();
|
|
write_exec(&local_bin.join("git"));
|
|
|
|
// PATH order: an empty dir, then a `/mnt/...` candidate that
|
|
// should be skipped on WSL, then the directory with the real
|
|
// exec. The resolver must walk past the first two and land on
|
|
// the third.
|
|
let path_env = join(&[
|
|
empty_dir,
|
|
PathBuf::from("/mnt/c/Program Files/Git/cmd"),
|
|
local_bin.clone(),
|
|
]);
|
|
let resolved =
|
|
resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).unwrap();
|
|
assert_eq!(resolved, local_bin.join("git"));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn follows_symlinks() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let real = dir.path().join("git-wrapper");
|
|
write_exec(&real);
|
|
let link = dir.path().join("git");
|
|
std::os::unix::fs::symlink(&real, &link).unwrap();
|
|
|
|
let path_env = join(&[dir.path().to_path_buf()]);
|
|
let resolved =
|
|
resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).unwrap();
|
|
assert_eq!(resolved, link);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn skips_non_executable() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
fs::write(dir.path().join("git"), b"not exec").unwrap();
|
|
// Intentionally do NOT chmod +x.
|
|
|
|
let path_env = join(&[dir.path().to_path_buf()]);
|
|
assert!(resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn handles_empty_path_env() {
|
|
assert!(resolve_binary_in_wsl_safe_path("git", None, true).is_none());
|
|
assert!(resolve_binary_in_wsl_safe_path("git", None, false).is_none());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn handles_non_utf8_path_components() {
|
|
use std::os::unix::ffi::OsStringExt as _;
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
write_exec(&dir.path().join("git"));
|
|
|
|
// Build a PATH whose first component is a non-UTF-8 byte sequence,
|
|
// followed by a real directory. The resolver must walk past the
|
|
// garbage entry without panicking and find the valid one.
|
|
let mut bytes = b"/\xff\xfe/bad:".to_vec();
|
|
bytes.extend_from_slice(dir.path().as_os_str().as_encoded_bytes());
|
|
let path_env = OsString::from_vec(bytes);
|
|
|
|
let resolved =
|
|
resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).unwrap();
|
|
assert_eq!(resolved, dir.path().join("git"));
|
|
}
|
|
|
|
#[test]
|
|
fn known_bare_name_recognizes_git_and_gh() {
|
|
assert_eq!(known_bare_name(OsStr::new("git")), Some("git"));
|
|
assert_eq!(known_bare_name(OsStr::new("gh")), Some("gh"));
|
|
}
|
|
|
|
#[test]
|
|
fn known_bare_name_skips_paths() {
|
|
assert_eq!(known_bare_name(OsStr::new("/usr/bin/git")), None);
|
|
assert_eq!(known_bare_name(OsStr::new("./git")), None);
|
|
assert_eq!(known_bare_name(OsStr::new("bin/git")), None);
|
|
#[cfg(windows)]
|
|
assert_eq!(known_bare_name(OsStr::new("C:\\git\\git.exe")), None);
|
|
}
|
|
|
|
#[test]
|
|
fn known_bare_name_skips_unknowns() {
|
|
assert_eq!(known_bare_name(OsStr::new("ls")), None);
|
|
assert_eq!(known_bare_name(OsStr::new("python")), None);
|
|
assert_eq!(known_bare_name(OsStr::new("")), None);
|
|
}
|