diff options
-rw-r--r-- | .cargo_vcs_info.json | 5 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .travis.yml | 18 | ||||
-rw-r--r-- | Android.bp | 14 | ||||
-rw-r--r-- | Cargo.toml | 36 | ||||
-rw-r--r-- | Cargo.toml.orig | 26 | ||||
l--------- | LICENSE | 1 | ||||
-rw-r--r-- | LICENSE.txt | 19 | ||||
-rw-r--r-- | METADATA | 19 | ||||
-rw-r--r-- | MODULE_LICENSE_BSD_LIKE | 0 | ||||
-rw-r--r-- | OWNERS | 1 | ||||
-rw-r--r-- | README.md | 34 | ||||
-rw-r--r-- | appveyor.yml | 29 | ||||
-rw-r--r-- | src/checker.rs | 70 | ||||
-rw-r--r-- | src/error.rs | 92 | ||||
-rw-r--r-- | src/finder.rs | 155 | ||||
-rw-r--r-- | src/helper.rs | 40 | ||||
-rw-r--r-- | src/lib.rs | 271 | ||||
-rw-r--r-- | tests/basic.rs | 307 |
19 files changed, 1139 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..98aa420 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,5 @@ +{ + "git": { + "sha1": "1ffb2479652982ab8aaa1706bba6de5281a7ba22" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0de7621 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: rust + +os: + - linux + - osx + +rust: + - stable + +cache: + directories: + - $HOME/.cargo + +script: + - cargo build --all + - cargo test + - cargo test --no-default-features + - cargo doc --all --no-deps
\ No newline at end of file diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..fc7db87 --- /dev/null +++ b/Android.bp @@ -0,0 +1,14 @@ +// This file is generated by cargo2android.py. + +rust_library_host_rlib { + name: "libwhich", + crate_name: "which", + srcs: ["src/lib.rs"], + edition: "2015", + rlibs: [ + "liblibc", + ], +} + +// dependent_library ["feature_list"] +// libc-0.2.68 "default,std" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..567792b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,36 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies +# +# If you believe there's an error in this file please file an +# issue against the rust-lang/cargo repository. If you're +# editing this file be aware that the upstream Cargo.toml +# will likely look very different (and much more reasonable) + +[package] +name = "which" +version = "3.1.1" +authors = ["Harry Fei <tiziyuanfang@gmail.com>"] +description = "A Rust equivalent of Unix command \"which\". Locate installed executable in cross platforms." +documentation = "https://docs.rs/which/" +readme = "README.md" +keywords = ["which", "which-rs", "unix", "command"] +categories = ["os", "filesystem"] +license = "MIT" +repository = "https://github.com/harryfei/which-rs.git" +[dependencies.failure] +version = "0.1.7" +features = ["std"] +optional = true +default-features = false + +[dependencies.libc] +version = "0.2.65" +[dev-dependencies.tempdir] +version = "0.3.7" + +[features] +default = ["failure"] diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..2c353c8 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,26 @@ +[package] +name = "which" +version = "3.1.1" +authors = ["Harry Fei <tiziyuanfang@gmail.com>"] +repository = "https://github.com/harryfei/which-rs.git" +documentation = "https://docs.rs/which/" +license = "MIT" +description = "A Rust equivalent of Unix command \"which\". Locate installed executable in cross platforms." +readme = "README.md" +categories = ["os", "filesystem"] +keywords = ["which", "which-rs", "unix", "command"] + +[dependencies] +libc = "0.2.65" + +[dependencies.failure] +version = "0.1.7" +default-features = false +features = ["std"] +optional = true + +[dev-dependencies] +tempdir = "0.3.7" + +[features] +default = ["failure"] @@ -0,0 +1 @@ +LICENSE.txt
\ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..369139b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2015 fangyuanziti + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..9595f0d --- /dev/null +++ b/METADATA @@ -0,0 +1,19 @@ +name: "which" +description: "A Rust equivalent of Unix command \"which\". Locate installed executable in cross platforms." +third_party { + url { + type: HOMEPAGE + value: "https://crates.io/crates/which" + } + url { + type: GIT + value: "https://github.com/harryfei/which-rs.git" + } + version: "3.1.1" + license_type: NOTICE + last_upgrade_date { + year: 2020 + month: 3 + day: 31 + } +} diff --git a/MODULE_LICENSE_BSD_LIKE b/MODULE_LICENSE_BSD_LIKE new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_BSD_LIKE @@ -0,0 +1 @@ +include platform/prebuilts/rust:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c8d7e1 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +[![Travis Build Status](https://travis-ci.org/harryfei/which-rs.svg?branch=master)](https://travis-ci.org/harryfei/which-rs) +[![Appveyor Build status](https://ci.appveyor.com/api/projects/status/1y40b135iaixs9x6?svg=true)](https://ci.appveyor.com/project/HarryFei/which-rs) + +# which + +A Rust equivalent of Unix command "which". Locate installed executable in cross platforms. + +## Support platforms + +* Linux +* Windows +* macOS + +## Example + +To find which rustc exectable binary is using. + +``` rust +use which::which; + +let result = which::which("rustc").unwrap(); +assert_eq!(result, PathBuf::from("/usr/bin/rustc")); +``` + +## Errors + +By default this crate exposes a [`failure`] based error. This is optional, disable the default +features to get an error type implementing the standard library `Error` trait. + +[`failure`]: https://crates.io/crates/failure + +## Documentation + +The documentation is [available online](https://docs.rs/which/). diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..e8beb07 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,29 @@ +os: Visual Studio 2015 + +environment: + matrix: + - channel: stable + target: x86_64-pc-windows-msvc + - channel: stable + target: i686-pc-windows-msvc + - channel: stable + target: x86_64-pc-windows-gnu + - channel: stable + target: i686-pc-windows-gnu +install: + # Set PATH for MinGW toolset + - if %target% == x86_64-pc-windows-gnu set PATH=%PATH%;C:\msys64\mingw64\bin + - if %target% == i686-pc-windows-gnu set PATH=%PATH%;C:\msys64\mingw32\bin + + # Install Rust toolset + - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe + - rustup-init -yv --default-toolchain %channel% --default-host %target% + - set PATH=%PATH%;%USERPROFILE%\.cargo\bin + - rustc -vV + - cargo -vV + +build: false + +test_script: + - cargo test + - cargo test --no-default-features 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<Box<dyn Checker>>, +} + +impl CompositeChecker { + pub fn new() -> CompositeChecker { + CompositeChecker { + checkers: Vec::new(), + } + } + + pub fn add_checker(mut self, checker: Box<dyn Checker>) -> 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<ErrorKind>, + #[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<ErrorKind> 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<Context<ErrorKind>> for Error { + fn from(inner: Context<ErrorKind>) -> Error { + Error { inner } + } +} + +pub type Result<T> = std::result::Result<T, Error>; 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<P>(self, cwd: P) -> PathBuf + where + P: AsRef<Path>; +} + +impl PathExt for PathBuf { + fn has_separator(&self) -> bool { + self.components().count() > 1 + } + + fn to_absolute<P>(self, cwd: P) -> PathBuf + where + P: AsRef<Path>, + { + 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<T, U, V>( + &self, + binary_name: T, + paths: Option<U>, + cwd: V, + binary_checker: &dyn Checker, + ) -> Result<PathBuf> + where + T: AsRef<OsStr>, + U: AsRef<OsStr>, + V: AsRef<Path>, + { + let path = PathBuf::from(&binary_name); + + let binary_path_candidates: Box<dyn Iterator<Item = _>> = 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<C>(binary_name: PathBuf, cwd: C) -> impl IntoIterator<Item = PathBuf> + where + C: AsRef<Path>, + { + let path = binary_name.to_absolute(cwd); + + Self::append_extension(iter::once(path)) + } + + fn path_search_candidates<P>( + binary_name: PathBuf, + paths: P, + ) -> impl IntoIterator<Item = PathBuf> + where + P: IntoIterator<Item = PathBuf>, + { + let new_paths = paths.into_iter().map(move |p| p.join(binary_name.clone())); + + Self::append_extension(new_paths) + } + + #[cfg(unix)] + fn append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf> + where + P: IntoIterator<Item = PathBuf>, + { + paths + } + + #[cfg(windows)] + fn append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf> + where + P: IntoIterator<Item = PathBuf>, + { + // 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::<Vec<_>>(); + + paths + .into_iter() + .flat_map(move |p| -> Box<dyn Iterator<Item = _>> { + // 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<T: AsRef<Path>, S: AsRef<str>>(path: T, exts_vec: &Vec<S>) -> 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<T: AsRef<OsStr>>(binary_name: T) -> Result<path::PathBuf> { + #[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<T, U, V>(binary_name: T, paths: Option<U>, cwd: V) -> Result<path::PathBuf> +where + T: AsRef<OsStr>, + U: AsRef<OsStr>, + V: AsRef<path::Path>, +{ + 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<T: AsRef<OsStr>>(binary_name: T) -> Result<Path> { + 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<T, U, V>(binary_name: T, paths: Option<U>, cwd: V) -> Result<Path> + where + T: AsRef<OsStr>, + U: AsRef<OsStr>, + V: AsRef<path::Path>, + { + 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<path::Path> for Path { + fn as_ref(&self) -> &path::Path { + self.as_path() + } +} + +impl AsRef<OsStr> for Path { + fn as_ref(&self) -> &OsStr { + self.as_os_str() + } +} + +impl Eq for Path {} + +impl PartialEq<path::PathBuf> for Path { + fn eq(&self, other: &path::PathBuf) -> bool { + self.inner == *other + } +} + +impl PartialEq<Path> 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<T: AsRef<OsStr>>(binary_name: T) -> Result<CanonicalPath> { + 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<T, U, V>(binary_name: T, paths: Option<U>, cwd: V) -> Result<CanonicalPath> + where + T: AsRef<OsStr>, + U: AsRef<OsStr>, + V: AsRef<path::Path>, + { + 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<path::Path> for CanonicalPath { + fn as_ref(&self) -> &path::Path { + self.as_path() + } +} + +impl AsRef<OsStr> for CanonicalPath { + fn as_ref(&self) -> &OsStr { + self.as_os_str() + } +} + +impl Eq for CanonicalPath {} + +impl PartialEq<path::PathBuf> for CanonicalPath { + fn eq(&self, other: &path::PathBuf) -> bool { + self.inner == *other + } +} + +impl PartialEq<CanonicalPath> for path::PathBuf { + fn eq(&self, other: &CanonicalPath) -> bool { + *self == other.inner + } +} diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..b6ba30d --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,307 @@ +extern crate tempdir; +extern crate which; + +use std::env; +use std::ffi::{OsStr, OsString}; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use tempdir::TempDir; + +struct TestFixture { + /// Temp directory. + pub tempdir: TempDir, + /// $PATH + pub paths: OsString, + /// Binaries created in $PATH + pub bins: Vec<PathBuf>, +} + +const SUBDIRS: &'static [&'static str] = &["a", "b", "c"]; +const BIN_NAME: &'static str = "bin"; + +#[cfg(unix)] +fn mk_bin(dir: &Path, path: &str, extension: &str) -> io::Result<PathBuf> { + use std::os::unix::fs::OpenOptionsExt; + let bin = dir.join(path).with_extension(extension); + fs::OpenOptions::new() + .write(true) + .create(true) + .mode(0o666 | (libc::S_IXUSR as u32)) + .open(&bin) + .and_then(|_f| bin.canonicalize()) +} + +fn touch(dir: &Path, path: &str, extension: &str) -> io::Result<PathBuf> { + let b = dir.join(path).with_extension(extension); + fs::File::create(&b).and_then(|_f| b.canonicalize()) +} + +#[cfg(windows)] +fn mk_bin(dir: &Path, path: &str, extension: &str) -> io::Result<PathBuf> { + touch(dir, path, extension) +} + +impl TestFixture { + // tmp/a/bin + // tmp/a/bin.exe + // tmp/a/bin.cmd + // tmp/b/bin + // tmp/b/bin.exe + // tmp/b/bin.cmd + // tmp/c/bin + // tmp/c/bin.exe + // tmp/c/bin.cmd + pub fn new() -> TestFixture { + let tempdir = TempDir::new("which_tests").unwrap(); + let mut builder = fs::DirBuilder::new(); + builder.recursive(true); + let mut paths = vec![]; + let mut bins = vec![]; + for d in SUBDIRS.iter() { + let p = tempdir.path().join(d); + builder.create(&p).unwrap(); + bins.push(mk_bin(&p, &BIN_NAME, "").unwrap()); + bins.push(mk_bin(&p, &BIN_NAME, "exe").unwrap()); + bins.push(mk_bin(&p, &BIN_NAME, "cmd").unwrap()); + paths.push(p); + } + TestFixture { + tempdir: tempdir, + paths: env::join_paths(paths).unwrap(), + bins: bins, + } + } + + #[allow(dead_code)] + pub fn touch(&self, path: &str, extension: &str) -> io::Result<PathBuf> { + touch(self.tempdir.path(), &path, &extension) + } + + pub fn mk_bin(&self, path: &str, extension: &str) -> io::Result<PathBuf> { + mk_bin(self.tempdir.path(), &path, &extension) + } +} + +fn _which<T: AsRef<OsStr>>(f: &TestFixture, path: T) -> which::Result<which::CanonicalPath> { + which::CanonicalPath::new_in(path, Some(f.paths.clone()), f.tempdir.path()) +} + +#[test] +#[cfg(unix)] +fn it_works() { + use std::process::Command; + let result = which::Path::new("rustc"); + assert!(result.is_ok()); + + let which_result = Command::new("which").arg("rustc").output(); + + assert_eq!( + String::from(result.unwrap().to_str().unwrap()), + String::from_utf8(which_result.unwrap().stdout) + .unwrap() + .trim() + ); +} + +#[test] +#[cfg(unix)] +fn test_which() { + let f = TestFixture::new(); + assert_eq!(_which(&f, &BIN_NAME).unwrap(), f.bins[0]) +} + +#[test] +#[cfg(windows)] +fn test_which() { + let f = TestFixture::new(); + assert_eq!(_which(&f, &BIN_NAME).unwrap(), f.bins[1]) +} + +#[test] +#[cfg(unix)] +fn test_which_extension() { + let f = TestFixture::new(); + let b = Path::new(&BIN_NAME).with_extension(""); + assert_eq!(_which(&f, &b).unwrap(), f.bins[0]) +} + +#[test] +#[cfg(windows)] +fn test_which_extension() { + let f = TestFixture::new(); + let b = Path::new(&BIN_NAME).with_extension("cmd"); + assert_eq!(_which(&f, &b).unwrap(), f.bins[2]) +} + +#[test] +fn test_which_not_found() { + let f = TestFixture::new(); + assert!(_which(&f, "a").is_err()); +} + +#[test] +fn test_which_second() { + let f = TestFixture::new(); + let b = f.mk_bin("b/another", env::consts::EXE_EXTENSION).unwrap(); + assert_eq!(_which(&f, "another").unwrap(), b); +} + +#[test] +#[cfg(unix)] +fn test_which_absolute() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, &f.bins[3]).unwrap(), + f.bins[3].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(windows)] +fn test_which_absolute() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, &f.bins[4]).unwrap(), + f.bins[4].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(windows)] +fn test_which_absolute_path_case() { + // Test that an absolute path with an uppercase extension + // is accepted. + let f = TestFixture::new(); + let p = &f.bins[4]; + assert_eq!(_which(&f, &p).unwrap(), f.bins[4].canonicalize().unwrap()); +} + +#[test] +#[cfg(unix)] +fn test_which_absolute_extension() { + let f = TestFixture::new(); + // Don't append EXE_EXTENSION here. + let b = f.bins[3].parent().unwrap().join(&BIN_NAME); + assert_eq!(_which(&f, &b).unwrap(), f.bins[3].canonicalize().unwrap()); +} + +#[test] +#[cfg(windows)] +fn test_which_absolute_extension() { + let f = TestFixture::new(); + // Don't append EXE_EXTENSION here. + let b = f.bins[4].parent().unwrap().join(&BIN_NAME); + assert_eq!(_which(&f, &b).unwrap(), f.bins[4].canonicalize().unwrap()); +} + +#[test] +#[cfg(unix)] +fn test_which_relative() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, "b/bin").unwrap(), + f.bins[3].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(windows)] +fn test_which_relative() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, "b/bin").unwrap(), + f.bins[4].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(unix)] +fn test_which_relative_extension() { + // test_which_relative tests a relative path without an extension, + // so test a relative path with an extension here. + let f = TestFixture::new(); + let b = Path::new("b/bin").with_extension(env::consts::EXE_EXTENSION); + assert_eq!(_which(&f, &b).unwrap(), f.bins[3].canonicalize().unwrap()); +} + +#[test] +#[cfg(windows)] +fn test_which_relative_extension() { + // test_which_relative tests a relative path without an extension, + // so test a relative path with an extension here. + let f = TestFixture::new(); + let b = Path::new("b/bin").with_extension("cmd"); + assert_eq!(_which(&f, &b).unwrap(), f.bins[5].canonicalize().unwrap()); +} + +#[test] +#[cfg(windows)] +fn test_which_relative_extension_case() { + // Test that a relative path with an uppercase extension + // is accepted. + let f = TestFixture::new(); + let b = Path::new("b/bin").with_extension("EXE"); + assert_eq!(_which(&f, &b).unwrap(), f.bins[4].canonicalize().unwrap()); +} + +#[test] +#[cfg(unix)] +fn test_which_relative_leading_dot() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, "./b/bin").unwrap(), + f.bins[3].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(windows)] +fn test_which_relative_leading_dot() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, "./b/bin").unwrap(), + f.bins[4].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(unix)] +fn test_which_non_executable() { + // Shouldn't return non-executable files. + let f = TestFixture::new(); + f.touch("b/another", "").unwrap(); + assert!(_which(&f, "another").is_err()); +} + +#[test] +#[cfg(unix)] +fn test_which_absolute_non_executable() { + // Shouldn't return non-executable files, even if given an absolute path. + let f = TestFixture::new(); + let b = f.touch("b/another", "").unwrap(); + assert!(_which(&f, &b).is_err()); +} + +#[test] +#[cfg(unix)] +fn test_which_relative_non_executable() { + // Shouldn't return non-executable files. + let f = TestFixture::new(); + f.touch("b/another", "").unwrap(); + assert!(_which(&f, "b/another").is_err()); +} + +#[test] +#[cfg(feature = "failure")] +fn test_failure() { + let f = TestFixture::new(); + + let run = || -> std::result::Result<PathBuf, failure::Error> { + // Test the conversion to failure + let p = _which(&f, "./b/bin")?; + Ok(p.into_path_buf()) + }; + + let _ = run(); +} |