From fab837700283a7303273b086df93fabf0e67b311 Mon Sep 17 00:00:00 2001 From: Chih-Hung Hsieh Date: Tue, 7 Apr 2020 14:24:03 -0700 Subject: Import 'which' package version 3.1.1 * Add OWNERS Bug: 152884384 Test: make Change-Id: Ic643b3c23001db1124e626548e13e43b27b89c7b --- src/checker.rs | 70 +++++++++++++++ src/error.rs | 92 ++++++++++++++++++++ src/finder.rs | 155 +++++++++++++++++++++++++++++++++ src/helper.rs | 40 +++++++++ src/lib.rs | 271 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 628 insertions(+) create mode 100644 src/checker.rs create mode 100644 src/error.rs create mode 100644 src/finder.rs create mode 100644 src/helper.rs create mode 100644 src/lib.rs (limited to 'src') diff --git a/src/checker.rs b/src/checker.rs new file mode 100644 index 0000000..6021711 --- /dev/null +++ b/src/checker.rs @@ -0,0 +1,70 @@ +use finder::Checker; +#[cfg(unix)] +use libc; +#[cfg(unix)] +use std::ffi::CString; +use std::fs; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +use std::path::Path; + +pub struct ExecutableChecker; + +impl ExecutableChecker { + pub fn new() -> ExecutableChecker { + ExecutableChecker + } +} + +impl Checker for ExecutableChecker { + #[cfg(unix)] + fn is_valid(&self, path: &Path) -> bool { + CString::new(path.as_os_str().as_bytes()) + .and_then(|c| Ok(unsafe { libc::access(c.as_ptr(), libc::X_OK) == 0 })) + .unwrap_or(false) + } + + #[cfg(windows)] + fn is_valid(&self, _path: &Path) -> bool { + true + } +} + +pub struct ExistedChecker; + +impl ExistedChecker { + pub fn new() -> ExistedChecker { + ExistedChecker + } +} + +impl Checker for ExistedChecker { + fn is_valid(&self, path: &Path) -> bool { + fs::metadata(path) + .map(|metadata| metadata.is_file()) + .unwrap_or(false) + } +} + +pub struct CompositeChecker { + checkers: Vec>, +} + +impl CompositeChecker { + pub fn new() -> CompositeChecker { + CompositeChecker { + checkers: Vec::new(), + } + } + + pub fn add_checker(mut self, checker: Box) -> CompositeChecker { + self.checkers.push(checker); + self + } +} + +impl Checker for CompositeChecker { + fn is_valid(&self, path: &Path) -> bool { + self.checkers.iter().all(|checker| checker.is_valid(path)) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..c75fe4c --- /dev/null +++ b/src/error.rs @@ -0,0 +1,92 @@ +#[cfg(feature = "failure")] +use failure::{Backtrace, Context, Fail}; +use std; +use std::fmt::{self, Display}; + +#[derive(Debug)] +pub struct Error { + #[cfg(feature = "failure")] + inner: Context, + #[cfg(not(feature = "failure"))] + inner: ErrorKind, +} + +// To suppress false positives from cargo-clippy +#[cfg_attr(feature = "cargo-clippy", allow(empty_line_after_outer_attr))] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum ErrorKind { + BadAbsolutePath, + BadRelativePath, + CannotFindBinaryPath, + CannotGetCurrentDir, + CannotCanonicalize, +} + +#[cfg(feature = "failure")] +impl Fail for ErrorKind {} + +impl Display for ErrorKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let display = match *self { + ErrorKind::BadAbsolutePath => "Bad absolute path", + ErrorKind::BadRelativePath => "Bad relative path", + ErrorKind::CannotFindBinaryPath => "Cannot find binary path", + ErrorKind::CannotGetCurrentDir => "Cannot get current directory", + ErrorKind::CannotCanonicalize => "Cannot canonicalize path", + }; + f.write_str(display) + } +} + +#[cfg(feature = "failure")] +impl Fail for Error { + fn cause(&self) -> Option<&dyn Fail> { + self.inner.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } +} + +#[cfg(not(feature = "failure"))] +impl std::error::Error for Error {} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Display::fmt(&self.inner, f) + } +} + +impl Error { + pub fn kind(&self) -> ErrorKind { + #[cfg(feature = "failure")] + { + *self.inner.get_context() + } + #[cfg(not(feature = "failure"))] + { + self.inner + } + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Error { + Error { + #[cfg(feature = "failure")] + inner: Context::new(kind), + #[cfg(not(feature = "failure"))] + inner: kind, + } + } +} + +#[cfg(feature = "failure")] +impl From> for Error { + fn from(inner: Context) -> Error { + Error { inner } + } +} + +pub type Result = std::result::Result; diff --git a/src/finder.rs b/src/finder.rs new file mode 100644 index 0000000..2519aa8 --- /dev/null +++ b/src/finder.rs @@ -0,0 +1,155 @@ +use error::*; +#[cfg(windows)] +use helper::has_executable_extension; +use std::env; +use std::ffi::OsStr; +#[cfg(windows)] +use std::ffi::OsString; +use std::iter; +use std::path::{Path, PathBuf}; + +pub trait Checker { + fn is_valid(&self, path: &Path) -> bool; +} + +trait PathExt { + fn has_separator(&self) -> bool; + + fn to_absolute

(self, cwd: P) -> PathBuf + where + P: AsRef; +} + +impl PathExt for PathBuf { + fn has_separator(&self) -> bool { + self.components().count() > 1 + } + + fn to_absolute

(self, cwd: P) -> PathBuf + where + P: AsRef, + { + if self.is_absolute() { + self + } else { + let mut new_path = PathBuf::from(cwd.as_ref()); + new_path.push(self); + new_path + } + } +} + +pub struct Finder; + +impl Finder { + pub fn new() -> Finder { + Finder + } + + pub fn find( + &self, + binary_name: T, + paths: Option, + cwd: V, + binary_checker: &dyn Checker, + ) -> Result + where + T: AsRef, + U: AsRef, + V: AsRef, + { + let path = PathBuf::from(&binary_name); + + let binary_path_candidates: Box> = if path.has_separator() { + // Search binary in cwd if the path have a path separator. + let candidates = Self::cwd_search_candidates(path, cwd).into_iter(); + Box::new(candidates) + } else { + // Search binary in PATHs(defined in environment variable). + let p = paths.ok_or(ErrorKind::CannotFindBinaryPath)?; + let paths: Vec<_> = env::split_paths(&p).collect(); + + let candidates = Self::path_search_candidates(path, paths).into_iter(); + + Box::new(candidates) + }; + + for p in binary_path_candidates { + // find a valid binary + if binary_checker.is_valid(&p) { + return Ok(p); + } + } + + // can't find any binary + return Err(ErrorKind::CannotFindBinaryPath.into()); + } + + fn cwd_search_candidates(binary_name: PathBuf, cwd: C) -> impl IntoIterator + where + C: AsRef, + { + let path = binary_name.to_absolute(cwd); + + Self::append_extension(iter::once(path)) + } + + fn path_search_candidates

( + binary_name: PathBuf, + paths: P, + ) -> impl IntoIterator + where + P: IntoIterator, + { + let new_paths = paths.into_iter().map(move |p| p.join(binary_name.clone())); + + Self::append_extension(new_paths) + } + + #[cfg(unix)] + fn append_extension

(paths: P) -> impl IntoIterator + where + P: IntoIterator, + { + paths + } + + #[cfg(windows)] + fn append_extension

(paths: P) -> impl IntoIterator + where + P: IntoIterator, + { + // Read PATHEXT env variable and split it into vector of String + let path_exts = + env::var_os("PATHEXT").unwrap_or(OsString::from(env::consts::EXE_EXTENSION)); + + let exe_extension_vec = env::split_paths(&path_exts) + .filter_map(|e| e.to_str().map(|e| e.to_owned())) + .collect::>(); + + paths + .into_iter() + .flat_map(move |p| -> Box> { + // Check if path already have executable extension + if has_executable_extension(&p, &exe_extension_vec) { + Box::new(iter::once(p)) + } else { + // Appended paths with windows executable extensions. + // e.g. path `c:/windows/bin` will expend to: + // c:/windows/bin.COM + // c:/windows/bin.EXE + // c:/windows/bin.CMD + // ... + let ps = exe_extension_vec.clone().into_iter().map(move |e| { + // Append the extension. + let mut p = p.clone().to_path_buf().into_os_string(); + p.push(e); + + PathBuf::from(p) + }); + + Box::new(ps) + } + }) + } +} diff --git a/src/helper.rs b/src/helper.rs new file mode 100644 index 0000000..71658a0 --- /dev/null +++ b/src/helper.rs @@ -0,0 +1,40 @@ +use std::path::Path; + +/// Check if given path has extension which in the given vector. +pub fn has_executable_extension, S: AsRef>(path: T, exts_vec: &Vec) -> bool { + let ext = path.as_ref().extension().and_then(|e| e.to_str()); + match ext { + Some(ext) => exts_vec + .iter() + .any(|e| ext.eq_ignore_ascii_case(&e.as_ref()[1..])), + _ => false, + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::path::PathBuf; + + #[test] + fn test_extension_in_extension_vector() { + // Case insensitive + assert!(has_executable_extension( + PathBuf::from("foo.exe"), + &vec![".COM", ".EXE", ".CMD"] + )); + + assert!(has_executable_extension( + PathBuf::from("foo.CMD"), + &vec![".COM", ".EXE", ".CMD"] + )); + } + + #[test] + fn test_extension_not_in_extension_vector() { + assert!(!has_executable_extension( + PathBuf::from("foo.bar"), + &vec![".COM", ".EXE", ".CMD"] + )); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..42a6963 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,271 @@ +//! which +//! +//! A Rust equivalent of Unix command `which(1)`. +//! # Example: +//! +//! To find which rustc executable binary is using: +//! +//! ``` norun +//! use which::which; +//! +//! let result = which::which("rustc").unwrap(); +//! assert_eq!(result, PathBuf::from("/usr/bin/rustc")); +//! +//! ``` + +#[cfg(feature = "failure")] +extern crate failure; +extern crate libc; + +#[cfg(feature = "failure")] +use failure::ResultExt; +mod checker; +mod error; +mod finder; +#[cfg(windows)] +mod helper; + +use std::env; +use std::fmt; +use std::path; + +use std::ffi::OsStr; + +use checker::CompositeChecker; +use checker::ExecutableChecker; +use checker::ExistedChecker; +pub use error::*; +use finder::Finder; + +/// Find a exectable binary's path by name. +/// +/// If given an absolute path, returns it if the file exists and is executable. +/// +/// If given a relative path, returns an absolute path to the file if +/// it exists and is executable. +/// +/// If given a string without path separators, looks for a file named +/// `binary_name` at each directory in `$PATH` and if it finds an executable +/// file there, returns it. +/// +/// # Example +/// +/// ``` norun +/// use which::which; +/// use std::path::PathBuf; +/// +/// let result = which::which("rustc").unwrap(); +/// assert_eq!(result, PathBuf::from("/usr/bin/rustc")); +/// +/// ``` +pub fn which>(binary_name: T) -> Result { + #[cfg(feature = "failure")] + let cwd = env::current_dir().context(ErrorKind::CannotGetCurrentDir)?; + #[cfg(not(feature = "failure"))] + let cwd = env::current_dir().map_err(|_| ErrorKind::CannotGetCurrentDir)?; + + which_in(binary_name, env::var_os("PATH"), &cwd) +} + +/// Find `binary_name` in the path list `paths`, using `cwd` to resolve relative paths. +pub fn which_in(binary_name: T, paths: Option, cwd: V) -> Result +where + T: AsRef, + U: AsRef, + V: AsRef, +{ + let binary_checker = CompositeChecker::new() + .add_checker(Box::new(ExistedChecker::new())) + .add_checker(Box::new(ExecutableChecker::new())); + + let finder = Finder::new(); + + finder.find(binary_name, paths, cwd, &binary_checker) +} + +/// An owned, immutable wrapper around a `PathBuf` containing the path of an executable. +/// +/// The constructed `PathBuf` is the output of `which` or `which_in`, but `which::Path` has the +/// advantage of being a type distinct from `std::path::Path` and `std::path::PathBuf`. +/// +/// It can be beneficial to use `which::Path` instead of `std::path::Path` when you want the type +/// system to enforce the need for a path that exists and points to a binary that is executable. +/// +/// Since `which::Path` implements `Deref` for `std::path::Path`, all methods on `&std::path::Path` +/// are also available to `&which::Path` values. +#[derive(Clone, PartialEq)] +pub struct Path { + inner: path::PathBuf, +} + +impl Path { + /// Returns the path of an executable binary by name. + /// + /// This calls `which` and maps the result into a `Path`. + pub fn new>(binary_name: T) -> Result { + which(binary_name).map(|inner| Path { inner }) + } + + /// Returns the path of an executable binary by name in the path list `paths` and using the + /// current working directory `cwd` to resolve relative paths. + /// + /// This calls `which_in` and maps the result into a `Path`. + pub fn new_in(binary_name: T, paths: Option, cwd: V) -> Result + where + T: AsRef, + U: AsRef, + V: AsRef, + { + which_in(binary_name, paths, cwd).map(|inner| Path { inner }) + } + + /// Returns a reference to a `std::path::Path`. + pub fn as_path(&self) -> &path::Path { + self.inner.as_path() + } + + /// Consumes the `which::Path`, yielding its underlying `std::path::PathBuf`. + pub fn into_path_buf(self) -> path::PathBuf { + self.inner + } +} + +impl fmt::Debug for Path { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.inner, f) + } +} + +impl std::ops::Deref for Path { + type Target = path::Path; + + fn deref(&self) -> &path::Path { + self.inner.deref() + } +} + +impl AsRef for Path { + fn as_ref(&self) -> &path::Path { + self.as_path() + } +} + +impl AsRef for Path { + fn as_ref(&self) -> &OsStr { + self.as_os_str() + } +} + +impl Eq for Path {} + +impl PartialEq for Path { + fn eq(&self, other: &path::PathBuf) -> bool { + self.inner == *other + } +} + +impl PartialEq for path::PathBuf { + fn eq(&self, other: &Path) -> bool { + *self == other.inner + } +} + +/// An owned, immutable wrapper around a `PathBuf` containing the _canonical_ path of an +/// executable. +/// +/// The constructed `PathBuf` is the result of `which` or `which_in` followed by +/// `Path::canonicalize`, but `CanonicalPath` has the advantage of being a type distinct from +/// `std::path::Path` and `std::path::PathBuf`. +/// +/// It can be beneficial to use `CanonicalPath` instead of `std::path::Path` when you want the type +/// system to enforce the need for a path that exists, points to a binary that is executable, is +/// absolute, has all components normalized, and has all symbolic links resolved +/// +/// Since `CanonicalPath` implements `Deref` for `std::path::Path`, all methods on +/// `&std::path::Path` are also available to `&CanonicalPath` values. +#[derive(Clone, PartialEq)] +pub struct CanonicalPath { + inner: path::PathBuf, +} + +impl CanonicalPath { + /// Returns the canonical path of an executable binary by name. + /// + /// This calls `which` and `Path::canonicalize` and maps the result into a `CanonicalPath`. + pub fn new>(binary_name: T) -> Result { + which(binary_name) + .and_then(|p| { + p.canonicalize() + .map_err(|_| ErrorKind::CannotCanonicalize.into()) + }) + .map(|inner| CanonicalPath { inner }) + } + + /// Returns the canonical path of an executable binary by name in the path list `paths` and + /// using the current working directory `cwd` to resolve relative paths. + /// + /// This calls `which` and `Path::canonicalize` and maps the result into a `CanonicalPath`. + pub fn new_in(binary_name: T, paths: Option, cwd: V) -> Result + where + T: AsRef, + U: AsRef, + V: AsRef, + { + which_in(binary_name, paths, cwd) + .and_then(|p| { + p.canonicalize() + .map_err(|_| ErrorKind::CannotCanonicalize.into()) + }) + .map(|inner| CanonicalPath { inner }) + } + + /// Returns a reference to a `std::path::Path`. + pub fn as_path(&self) -> &path::Path { + self.inner.as_path() + } + + /// Consumes the `which::CanonicalPath`, yielding its underlying `std::path::PathBuf`. + pub fn into_path_buf(self) -> path::PathBuf { + self.inner + } +} + +impl fmt::Debug for CanonicalPath { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.inner, f) + } +} + +impl std::ops::Deref for CanonicalPath { + type Target = path::Path; + + fn deref(&self) -> &path::Path { + self.inner.deref() + } +} + +impl AsRef for CanonicalPath { + fn as_ref(&self) -> &path::Path { + self.as_path() + } +} + +impl AsRef for CanonicalPath { + fn as_ref(&self) -> &OsStr { + self.as_os_str() + } +} + +impl Eq for CanonicalPath {} + +impl PartialEq for CanonicalPath { + fn eq(&self, other: &path::PathBuf) -> bool { + self.inner == *other + } +} + +impl PartialEq for path::PathBuf { + fn eq(&self, other: &CanonicalPath) -> bool { + *self == other.inner + } +} -- cgit v1.2.3