use std::ffi::OsStr;
use std::path::{Path, PathBuf};
#[must_use]
pub(super) fn find_program(program: &str, path: Option<&OsStr>, probe: impl Fn(&Path) -> bool) -> Option<PathBuf> {
if program.is_empty() {
return None;
}
if program.contains('/') {
let direct = PathBuf::from(program);
return probe(&direct).then_some(direct);
}
std::env::split_paths(path?)
.filter(|folder| folder.is_absolute())
.map(|folder| folder.join(program))
.find(|candidate| probe(candidate))
}
#[must_use]
pub(super) fn is_executable(path: &Path) -> bool {
let Ok(metadata) = std::fs::metadata(path) else {
return false;
};
if !metadata.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
{
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::desktop::fixture::Tree;
use std::ffi::{OsStr, OsString};
use std::path::PathBuf;
fn program(tree: &Tree, rel: &str, mode: u32) -> PathBuf {
let path = tree.program(rel);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)).expect("permissions");
}
#[cfg(not(unix))]
let _ = mode;
path
}
fn joined(folders: &[&Path]) -> OsString {
std::env::join_paths(folders).expect("a PATH")
}
#[test]
fn finds_a_program_in_the_first_folder_that_has_it() {
let tree = Tree::new();
let first = tree.root().join("a");
let second = tree.root().join("b");
let later = program(&tree, "b/htop", 0o755);
std::fs::create_dir_all(&first).expect("a folder");
let path = joined(&[&first, &second]);
assert_eq!(find_program("htop", Some(&path), is_executable), Some(later));
let earlier = program(&tree, "a/htop", 0o700);
assert_eq!(find_program("htop", Some(&path), is_executable), Some(earlier));
}
#[test]
fn a_missing_program_is_not_found() {
let tree = Tree::new();
let path = joined(&[tree.root()]);
assert_eq!(find_program("htop", Some(&path), is_executable), None);
assert_eq!(find_program("htop", None, is_executable), None);
assert_eq!(find_program("", Some(&path), is_executable), None);
}
#[cfg(unix)]
#[test]
fn a_file_without_an_execute_bit_is_not_a_program() {
let tree = Tree::new();
program(&tree, "notes", 0o644);
let path = joined(&[tree.root()]);
assert_eq!(find_program("notes", Some(&path), is_executable), None);
}
#[test]
fn a_folder_is_not_a_program() {
let tree = Tree::new();
std::fs::create_dir_all(tree.root().join("htop")).expect("a folder");
let path = joined(&[tree.root()]);
assert_eq!(find_program("htop", Some(&path), is_executable), None);
}
#[test]
fn a_name_with_a_slash_is_checked_where_it_points() {
let tree = Tree::new();
let tool = program(&tree, "bin/tool", 0o755);
let tool_text = tool.to_str().expect("a UTF-8 path");
assert_eq!(find_program(tool_text, Some(OsStr::new("/nonexistent")), is_executable), Some(tool.clone()));
assert_eq!(find_program(&format!("{tool_text}-gone"), None, is_executable), None);
}
#[test]
fn empty_and_relative_path_folders_are_skipped() {
let seen = std::cell::RefCell::new(Vec::new());
let probe = |candidate: &Path| {
seen.borrow_mut().push(candidate.to_path_buf());
false
};
assert_eq!(find_program("htop", Some(OsStr::new(":/usr/bin::relative:/bin:")), probe), None);
assert_eq!(seen.into_inner(), vec![PathBuf::from("/usr/bin/htop"), PathBuf::from("/bin/htop")]);
}
}