1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
use std::{env, ffi, fs, path};

use std::os::unix::fs::PermissionsExt;

extern crate libc;

pub fn which<S: AsRef<ffi::OsStr>>(name: S) -> Option<path::PathBuf> {
    let name: &ffi::OsStr = name.as_ref();

    let var = match env::var_os("PATH") {
        Some(var) => var,
        None => return None,
    };

    // Separate PATH value into paths
    let paths_iter = env::split_paths(&var);

    // Attempt to read each path as a directory
    let dirs_iter = paths_iter.filter_map(|path| fs::read_dir(path).ok());

    for dir in dirs_iter {
        let mut matches_iter = dir.filter_map(|file| file.ok())
                                  .filter(|file| file.file_name() == name)
                                  .filter(is_executable);
        if let Some(file) = matches_iter.next() {
            return Some(file.path());
        }
    }

    None
}

fn is_executable(file: &fs::DirEntry) -> bool {
    // Don't use `file.metadata()` directly since it doesn't follow symlinks.
    let file_metadata = match file.path().metadata() {
        Ok(metadata) => metadata,
        Err(..) => return false,
    };
    let file_path = match file.path()
                              .to_str()
                              .and_then(|p| ffi::CString::new(p).ok()) {
        Some(path) => path,
        None => return false,
    };
    let is_executable_by_user = unsafe {
        libc::access(file_path.into_raw(), libc::X_OK) == libc::EXIT_SUCCESS
    };
    static EXECUTABLE_FLAGS: u32 =
        (libc::S_IEXEC | libc::S_IXGRP | libc::S_IXOTH) as u32;
    let has_executable_flag =
        file_metadata.permissions().mode() & EXECUTABLE_FLAGS != 0;
    is_executable_by_user && has_executable_flag && file_metadata.is_file()
}

#[cfg(test)]
mod tests {
    use std::path;
    use super::which;

    /// FIXME: this is not a good test since it relies on PATH and the
    ///        filesystem being in a certain state.
    #[test]
    fn test_sh() {
        let expected = path::PathBuf::from("/bin/sh");
        let actual = which("sh");
        assert_eq!(Some(expected), actual);
    }

    #[test]
    fn test_none() {
        let actual = which("foofoofoobar");
        assert_eq!(None, actual);
    }
}