#[cfg(test)]
mod tests;
use std::{
fmt, fs, io,
os::unix::{ffi::OsStrExt as _, fs::PermissionsExt as _},
path::{Path, PathBuf},
};
#[derive(Debug)]
pub enum ResolutionError {
InvalidRequest,
InvalidWorkingDirectory,
InvalidSearchDirectory {
index: usize,
},
NotFound,
NotRegularFile,
NotExecutable,
Io {
directory: Option<usize>,
source: io::Error,
},
}
impl fmt::Display for ResolutionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidRequest => f.write_str("invalid executable request"),
Self::InvalidWorkingDirectory => f.write_str(
"executable resolution requires an absolute working directory without NUL",
),
Self::InvalidSearchDirectory { index } => {
write!(f, "executable search directory {index} contains NUL")
}
Self::NotFound => {
f.write_str("executable was not found in the supplied search directories")
}
Self::NotRegularFile => f.write_str("requested executable is not a regular file"),
Self::NotExecutable => f.write_str("requested file has no executable permission bits"),
Self::Io {
directory: Some(index),
..
} => write!(
f,
"executable filesystem resolution failed in search directory {index}"
),
Self::Io {
directory: None, ..
} => f.write_str("requested executable filesystem resolution failed"),
}
}
}
impl std::error::Error for ResolutionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
_ => None,
}
}
}
pub fn resolve_executable(
requested: &Path,
current_dir: &Path,
search_directories: &[PathBuf],
) -> Result<PathBuf, ResolutionError> {
if !current_dir.is_absolute() || contains_nul(current_dir) {
return Err(ResolutionError::InvalidWorkingDirectory);
}
if requested.as_os_str().is_empty() || contains_nul(requested) {
return Err(ResolutionError::InvalidRequest);
}
if requested.as_os_str().as_bytes().contains(&b'/') {
return candidate(¤t_dir.join(requested), None);
}
if requested == Path::new(".") || requested == Path::new("..") {
return Err(ResolutionError::InvalidRequest);
}
for (index, directory) in search_directories.iter().enumerate() {
if contains_nul(directory) {
return Err(ResolutionError::InvalidSearchDirectory { index });
}
}
for (index, directory) in search_directories.iter().enumerate() {
let path = current_dir.join(directory).join(requested);
match candidate(&path, Some(index)) {
Ok(path) => return Ok(path),
Err(
ResolutionError::NotFound
| ResolutionError::NotRegularFile
| ResolutionError::NotExecutable,
) => {}
Err(error) => return Err(error),
}
}
Err(ResolutionError::NotFound)
}
fn contains_nul(path: &Path) -> bool {
path.as_os_str().as_bytes().contains(&0)
}
fn candidate(path: &Path, directory: Option<usize>) -> Result<PathBuf, ResolutionError> {
let metadata = fs::metadata(path).map_err(|source| {
if directory.is_some() && source.kind() == io::ErrorKind::NotFound {
ResolutionError::NotFound
} else {
ResolutionError::Io { directory, source }
}
})?;
if !metadata.is_file() {
return Err(ResolutionError::NotRegularFile);
}
if metadata.permissions().mode() & 0o111 == 0 {
return Err(ResolutionError::NotExecutable);
}
fs::canonicalize(path).map_err(|source| ResolutionError::Io { directory, source })
}