use std::path::{Component, Path, PathBuf};
use super::windows::search_path;
const EXECUTABLE_EXTENSION: &str = ".exe";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum HowResolved {
Absolute,
RelativeToLaunchDirectory,
SearchPath,
NotFound,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ResolvedCommand {
pub path: PathBuf,
pub how: HowResolved,
}
pub(crate) fn resolve_executable(command: &str, launch_directory: &Path) -> ResolvedCommand {
let path = Path::new(command);
if path.is_absolute() {
return ResolvedCommand {
path: path.to_path_buf(),
how: HowResolved::Absolute,
};
}
if has_separator(command) || is_drive_relative(path) {
let relative = strip_drive_prefix(path);
return ResolvedCommand {
path: launch_directory.join(relative),
how: HowResolved::RelativeToLaunchDirectory,
};
}
match search_path(command, EXECUTABLE_EXTENSION) {
Some(found) => ResolvedCommand {
path: found,
how: HowResolved::SearchPath,
},
None => ResolvedCommand {
path: path.to_path_buf(),
how: HowResolved::NotFound,
},
}
}
fn has_separator(command: &str) -> bool {
command.contains('/') || command.contains('\\')
}
fn is_drive_relative(path: &Path) -> bool {
matches!(path.components().next(), Some(Component::Prefix(_)))
}
fn strip_drive_prefix(path: &Path) -> PathBuf {
path.components()
.skip_while(|component| matches!(component, Component::Prefix(_) | Component::RootDir))
.collect()
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
const LAUNCH: &str = r"C:\work";
fn resolve(command: &str) -> ResolvedCommand {
resolve_executable(command, Path::new(LAUNCH))
}
#[test]
fn an_absolute_path_is_taken_as_written() {
let resolved = resolve(r"D:\tools\app.exe");
assert_eq!(resolved.path, PathBuf::from(r"D:\tools\app.exe"));
assert_eq!(resolved.how, HowResolved::Absolute);
}
#[test]
fn a_path_with_a_separator_is_anchored_at_the_launch_directory() {
for command in [r"bin\app.exe", "bin/app.exe", r".\app.exe"] {
let resolved = resolve(command);
assert_eq!(resolved.how, HowResolved::RelativeToLaunchDirectory);
assert!(
resolved.path.starts_with(LAUNCH),
"{command} resolved to {}",
resolved.path.display()
);
}
}
#[test]
fn a_drive_relative_path_is_anchored_at_the_launch_directory() {
let resolved = resolve(r"C:tools\app.exe");
assert_eq!(resolved.how, HowResolved::RelativeToLaunchDirectory);
assert_eq!(resolved.path, PathBuf::from(r"C:\work\tools\app.exe"));
}
#[test]
fn a_drive_prefix_alone_is_enough_to_anchor_a_path() {
let resolved = resolve("C:app.exe");
assert_eq!(resolved.how, HowResolved::RelativeToLaunchDirectory);
assert_eq!(resolved.path, PathBuf::from(r"C:\work\app.exe"));
}
#[test]
#[cfg_attr(miri, ignore)]
fn a_bare_name_is_looked_up_on_the_search_path() {
let resolved = resolve("cmd");
assert_eq!(resolved.how, HowResolved::SearchPath);
assert!(resolved.path.is_absolute());
assert!(!resolved.path.starts_with(LAUNCH));
}
#[test]
#[cfg_attr(miri, ignore)]
fn a_bare_name_that_is_nowhere_keeps_its_own_spelling() {
let resolved = resolve("no-such-executable-anywhere");
assert_eq!(resolved.how, HowResolved::NotFound);
assert_eq!(resolved.path, PathBuf::from("no-such-executable-anywhere"));
}
}