use std::borrow::Cow;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use thiserror::Error;
use super::ShellState;
#[derive(Error, Debug, PartialEq)]
pub enum CommandPathResolutionError {
#[error("{}: command not found", .0.to_string_lossy())]
CommandNotFound(OsString),
#[error("{}: failed canonicalizing", .0.to_string_lossy())]
FailedCanonicalizing(OsString),
#[error("command name was empty")]
CommandEmpty,
}
impl CommandPathResolutionError {
pub fn exit_code(&self) -> i32 {
match self {
CommandPathResolutionError::CommandNotFound(_) => 127,
CommandPathResolutionError::CommandEmpty
| CommandPathResolutionError::FailedCanonicalizing(_) => 1,
}
}
}
pub fn resolve_command_path(
command_name: &OsStr,
base_dir: &Path,
state: &ShellState,
) -> Result<PathBuf, CommandPathResolutionError> {
if command_name.is_empty() {
return Err(CommandPathResolutionError::CommandEmpty);
}
let path = Path::new(command_name);
if path.is_absolute() {
return Ok(path.to_path_buf());
} else if path.components().count() > 1 {
return Ok(base_dir.join(command_name));
}
let result = which::WhichConfig::new_with_sys(state.clone())
.binary_name(command_name.to_os_string())
.custom_cwd(base_dir.to_path_buf())
.first_result();
result.map_err(|err| match err {
which::Error::CannotFindBinaryPath
| which::Error::CannotGetCurrentDirAndPathListEmpty => {
CommandPathResolutionError::CommandNotFound(command_name.into())
}
which::Error::CannotCanonicalize => {
CommandPathResolutionError::FailedCanonicalizing(command_name.into())
}
})
}
impl which::sys::Sys for ShellState {
type ReadDirEntry = std::fs::DirEntry;
type Metadata = std::fs::Metadata;
fn is_windows(&self) -> bool {
cfg!(windows)
}
fn current_dir(&self) -> std::io::Result<PathBuf> {
Ok(self.cwd().to_path_buf())
}
fn home_dir(&self) -> Option<PathBuf> {
None
}
fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf> {
std::env::split_paths(paths).collect()
}
fn env_path(&self) -> Option<OsString> {
self.get_var(OsStr::new("PATH")).cloned()
}
fn env_path_ext(&self) -> Option<OsString> {
self.get_var(OsStr::new("PATHEXT")).cloned()
}
fn env_windows_path_ext(&self) -> Cow<'static, [String]> {
Cow::Owned(
self
.get_var(OsStr::new("PATHEXT"))
.and_then(|pathext| {
Some(
pathext
.to_str()?
.split(';')
.filter_map(|s| {
if s.as_bytes().first() == Some(&b'.') {
Some(s.to_owned())
} else {
None
}
})
.collect::<Vec<_>>(),
)
})
.unwrap_or_else(|| {
vec![
".EXE".to_string(),
".CMD".to_string(),
".BAT".to_string(),
".COM".to_string(),
]
}),
)
}
fn metadata(&self, path: &Path) -> std::io::Result<Self::Metadata> {
std::fs::metadata(path)
}
fn symlink_metadata(&self, path: &Path) -> std::io::Result<Self::Metadata> {
std::fs::symlink_metadata(path)
}
fn read_dir(
&self,
path: &Path,
) -> std::io::Result<
Box<dyn Iterator<Item = std::io::Result<Self::ReadDirEntry>>>,
> {
let iter = std::fs::read_dir(path)?;
Ok(Box::new(iter))
}
#[cfg(unix)]
fn is_valid_executable(
&self,
path: &std::path::Path,
) -> std::io::Result<bool> {
use nix::unistd::AccessFlags;
use nix::unistd::access;
match access(path, AccessFlags::X_OK) {
Ok(()) => Ok(true),
Err(nix::errno::Errno::ENOENT) => Ok(false),
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
}
}
#[cfg(windows)]
fn is_valid_executable(
&self,
path: &std::path::Path,
) -> std::io::Result<bool> {
use std::os::windows::ffi::OsStrExt;
let name = path
.as_os_str()
.encode_wide()
.chain(Some(0))
.collect::<Vec<u16>>();
let mut bt: u32 = 0;
unsafe {
Ok(
windows_sys::Win32::Storage::FileSystem::GetBinaryTypeW(
name.as_ptr(),
&mut bt,
) != 0,
)
}
}
}