use std::{fs, io};
use camino::{Utf8Path, Utf8PathBuf};
use indexmap::IndexSet;
use super::options::CwdMode;
#[cfg(windows)]
use super::env;
use super::{
env::EnvSnapshot,
options::WhichOptions,
resolve_error::{ResolveError, direct_not_found_error, not_found},
};
mod workspace;
use workspace::search_workspace;
pub(crate) use workspace::{WORKSPACE_SKIP_DIRS, WorkspaceSkipList};
pub(super) fn lookup(
command: &str,
env: &EnvSnapshot,
options: &WhichOptions,
workspace_skips: &WorkspaceSkipList,
) -> Result<Vec<Utf8PathBuf>, ResolveError> {
if is_direct_path(command) {
return resolve_direct(command, env, options);
}
let dirs = env.resolved_dirs(options.cwd_mode);
let mut matches = Vec::new();
for dir in &dirs {
let candidates = candidates_for_dir(env, dir, command);
if push_matches(&mut matches, candidates, options.all)? {
break;
}
}
if matches.is_empty() {
return handle_miss(HandleMissContext {
env,
command,
options,
dirs: &dirs,
workspace_skips,
});
}
if options.canonical {
canonicalize(matches)
} else {
Ok(matches)
}
}
#[cfg(windows)]
pub(super) fn resolve_direct(
command: &str,
env: &EnvSnapshot,
options: &WhichOptions,
) -> Result<Vec<Utf8PathBuf>, ResolveError> {
let resolved = normalize_direct_path(command, env);
let candidates = direct_candidates(&resolved, env);
let mut matches = Vec::new();
let _ = push_matches(&mut matches, candidates, options.all)?;
if matches.is_empty() {
return Err(direct_not_found_error(command, &resolved));
}
if options.canonical {
canonicalize(matches)
} else {
Ok(matches)
}
}
#[cfg(not(windows))]
pub(super) fn resolve_direct(
command: &str,
env: &EnvSnapshot,
options: &WhichOptions,
) -> Result<Vec<Utf8PathBuf>, ResolveError> {
let resolved = normalize_direct_path(command, env);
if !is_executable(&resolved)? {
return Err(direct_not_found_error(command, &resolved));
}
if options.canonical {
canonicalize(vec![resolved])
} else {
Ok(vec![resolved])
}
}
fn normalize_direct_path(command: &str, env: &EnvSnapshot) -> Utf8PathBuf {
let raw = Utf8Path::new(command);
if raw.is_absolute() {
raw.to_path_buf()
} else {
env.cwd.join(raw)
}
}
#[cfg(windows)]
fn direct_candidates(resolved: &Utf8PathBuf, env: &EnvSnapshot) -> Vec<Utf8PathBuf> {
if resolved.extension().is_some() {
vec![resolved.clone()]
} else {
env.pathext()
.iter()
.map(|ext| {
let mut candidate = resolved.as_str().to_owned();
candidate.push_str(ext);
Utf8PathBuf::from(candidate)
})
.collect()
}
}
pub(super) fn push_matches(
matches: &mut Vec<Utf8PathBuf>,
candidates: Vec<Utf8PathBuf>,
collect_all: bool,
) -> Result<bool, ResolveError> {
for candidate in candidates {
if !is_executable(&candidate)? {
continue;
}
matches.push(candidate);
if !collect_all {
return Ok(true);
}
}
Ok(false)
}
pub(super) fn is_direct_path(command: &str) -> bool {
#[cfg(windows)]
{
command.contains(['\\', '/', ':'])
}
#[cfg(not(windows))]
{
command.contains('/')
}
}
fn candidates_for_dir(env: &EnvSnapshot, dir: &Utf8Path, command: &str) -> Vec<Utf8PathBuf> {
#[cfg(windows)]
{
env::candidate_paths(dir, command, env.pathext())
}
#[cfg(not(windows))]
{
let _ = env;
vec![dir.join(command)]
}
}
pub(super) fn is_executable(path: &Utf8Path) -> Result<bool, ResolveError> {
match fs::metadata(path.as_std_path()) {
Ok(metadata) => Ok(is_executable_metadata(&metadata)),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),
Err(source) => Err(ResolveError::IsExecutable {
path: path.to_owned(),
source,
}),
}
}
#[cfg(unix)]
fn is_executable_metadata(metadata: &fs::Metadata) -> bool {
use std::os::unix::fs::PermissionsExt;
metadata.is_file() && metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
fn is_executable_metadata(metadata: &fs::Metadata) -> bool {
metadata.is_file()
}
#[derive(Clone, Copy)]
struct HandleMissContext<'a> {
env: &'a EnvSnapshot,
command: &'a str,
options: &'a WhichOptions,
dirs: &'a [&'a Utf8Path],
workspace_skips: &'a WorkspaceSkipList,
}
fn handle_miss(ctx: HandleMissContext<'_>) -> Result<Vec<Utf8PathBuf>, ResolveError> {
let path_empty = ctx.env.raw_path.as_ref().is_none_or(|path| path.is_empty());
if path_empty && !matches!(ctx.options.cwd_mode, CwdMode::Never) {
let discovered =
search_workspace(ctx.env, ctx.command, ctx.options.all, ctx.workspace_skips)?;
if !discovered.is_empty() {
return if ctx.options.canonical {
canonicalize(discovered)
} else {
Ok(discovered)
};
}
}
Err(not_found(ctx.command, ctx.dirs, ctx.options.cwd_mode))
}
pub(super) fn canonicalize(paths: Vec<Utf8PathBuf>) -> Result<Vec<Utf8PathBuf>, ResolveError> {
let mut unique = IndexSet::new();
let mut resolved = Vec::new();
for path in paths {
let canonical =
fs::canonicalize(path.as_std_path()).map_err(|source| ResolveError::Canonicalize {
path: path.clone(),
source,
})?;
let utf8 =
Utf8PathBuf::from_path_buf(canonical).map_err(|_| ResolveError::CanonicalizeNonUtf8)?;
if unique.insert(utf8.clone()) {
resolved.push(utf8);
}
}
Ok(resolved)
}
#[cfg(test)]
mod tests;