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
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
//! The core functionality
//!
//! See [crate-level documentation](../index.html) for more information on this module.

use {
    crate::{
        util::{is_executable, is_executable_path, split_path_env},
        IchwhResult,
    },
    async_std::{
        fs::read_dir,
        path::{Path, PathBuf},
    },
    cfg_if::cfg_if,
    futures::{
        future::join_all,
        stream::{FuturesOrdered, StreamExt},
    },
};

#[cfg(windows)]
use {
    crate::util::{filename_matches, pathext},
    std::collections::HashMap,
};

/// Searches `PATH` for an executable with the name `bin`
///
/// # Errors
///
/// * PATH is not defined as an environment variable
/// * An IO error occurs
pub async fn which(bin: &str) -> IchwhResult<Option<PathBuf>> {
    // Check if this is a local file. It is a local file if it contains a '/'
    if let local_expanded @ Ok(Some(_)) = check_local_file(bin).await {
        return local_expanded;
    }

    let dirs = split_path_env()?;

    // Read each directory in parallel
    let mut read_stream = dirs
        .into_iter()
        .map(|dir| async move { which_in_dir(bin, &dir).await })
        // Each read will be in the PATH order
        .collect::<FuturesOrdered<_>>();

    while let Some(res) = read_stream.next().await {
        // We ignore errors when reading the directory
        if let res @ Ok(Some(_)) = res {
            // TODO: abort/cancel the remaining reads
            return res;
        }
    }

    Ok(None)
}

/// Searches `PATH` for all executables with the name `bin`
///
/// Returns a list of paths, in the order of which they were found. The list may be empty,
/// indicating no binary was found.
pub async fn which_all(bin: &str) -> IchwhResult<Vec<PathBuf>> {
    if let Some(local_expanded) = check_local_file(bin).await? {
        return Ok(vec![local_expanded]);
    }

    // Make a closure to read the dir
    cfg_if! {
        if #[cfg(unix)] {
            let read_dir = |dir| async move {
                which_in_dir(bin, &dir).await
            };
        } else if #[cfg(windows)] {
            let read_dir = |dir| async move {
                which_all_in_dir(bin, &dir).await
            };

        }
    }

    // Get an iterator over a future for each dir
    let read_futures = split_path_env()?.into_iter().map(read_dir);

    // Poll all the futures concurrently
    cfg_if! {
        if #[cfg(unix)] {
            let rtn = join_all(read_futures)
                .await
                .into_iter()
                // Discard errors & drop `None`s
                .filter_map(|res| match res {
                    Ok(res) => res,
                    Err(_) => None
                })
                .collect();
        } else if #[cfg(windows)] {
            let rtn = join_all(read_futures)
                .await
                .into_iter()
                // `Result<Option<_>>` -> `Option<_>`
                .filter_map(|res| res.ok())
                // We have a `Vec<Vec<PathBuf>>`, want just `Vec<PathBuf>`
                .flatten()
                .collect();
        }
    };

    Ok(rtn)
}

/// Searches a directory for an exexcutable with the name `bin`
///
/// # Errors
///
/// * An IO error occurs
pub async fn which_in_dir<P: AsRef<Path>>(bin: &str, path: P) -> IchwhResult<Option<PathBuf>> {
    #[cfg(windows)]
    {
        let matching_entries = which_all_in_dir(bin, path).await?;

        Ok(matching_entries.get(0).cloned())
    }

    #[cfg(unix)]
    {
        let mut entries = read_dir(path).await?;

        while let Some(entry) = entries.next().await.transpose()? {
            if is_executable(&entry).await? && entry.file_name() == bin {
                return Ok(Some(entry.path()));
            }
        }

        Ok(None)
    }
}

/// Find all executable files that could possibly match in a given directory, sorted by their
/// extensions' appearance in %PATHEXT%
#[cfg(windows)]
pub(crate) async fn which_all_in_dir<P: AsRef<Path>>(
    bin: &str,
    path: P,
) -> IchwhResult<Vec<PathBuf>> {
    let mut matches = read_dir(path)
        .await?
        .filter_map(|entry| async {
            if let Ok(entry) = entry {
                let is_exec = is_executable(&entry).await;
                if is_exec.is_ok() && is_exec.unwrap() && filename_matches(&bin, &entry) {
                    return Some(entry.path());
                }
            }
            None
        })
        .collect::<Vec<_>>()
        .await;

    // Create a lookup table for executable extensions (extension --maps-> index)
    let exts = pathext()?
        .into_iter()
        .enumerate()
        .map(|(a, b)| (b, a))
        .collect::<HashMap<_, _>>();

    // Sort the matches by their extensions' appearance in PATHEXT
    matches.sort_by(|a, b| {
        let a_ext = a
            .extension()
            // Safe to unwrap, because we know that each of these entries must have an extension
            .unwrap()
            .to_string_lossy()
            .to_ascii_uppercase();
        let b_ext = b
            .extension()
            .unwrap()
            .to_string_lossy()
            .to_ascii_uppercase();

        exts[&a_ext].cmp(&exts[&b_ext])
    });

    Ok(matches)
}

/// If the binary is a local or direct path (eg, `./foo`, `/usr/bin/python`), return the full path
/// to it.
async fn check_local_file(bin: &str) -> IchwhResult<Option<PathBuf>> {
    cfg_if! {
        if #[cfg(unix)] {
            let is_path = bin.contains('/');
        } else if #[cfg(windows)] {
            // On windows (unlike unix), an executable file can be invoked
            // without a `.\` prefix, so we unconditionally check for local
            // files on windows.
            let is_path = true;
        }
    }

    if is_path {
        // It's an absolute or relative path, so see if it points to an executable file
        let path = Path::new(bin);
        if let Some(actual_path) = is_executable_path(&path).await? {
            return Ok(Some(actual_path));
        }
    }

    Ok(None)
}

/// Find a binary and, if it's a symlink, all intermediate files in its chain. Returns an ordered
/// `Vec`, with the first simlink at index 0 and the final binary at the end. If a symlink is
/// encountered with a relative path, it assumes the path is relative to the symlink's parent.
#[cfg(unix)]
pub async fn symlink_chain(bin: &str) -> IchwhResult<Vec<PathBuf>> {
    // First, find the executable
    if let Some(path) = which(bin).await? {
        let rtn = follow_symlink_chain(&path).await?;
        Ok(rtn)
    } else {
        Ok(vec![])
    }
}

/// Find a binary in a specific directory and, if it's a symlink, all intermediate files in its
/// chain. Returns an ordered `Vec`, with the first simlink at index 0 and the final binary at the
/// end. If a symlink is encountered with a relative path, it assumes the path is relative to the
/// symlink's parent.
#[cfg(unix)]
pub async fn symlink_chain_in_dir<P: AsRef<Path>>(bin: &str, dir: P) -> IchwhResult<Vec<PathBuf>> {
    if let Some(path) = which_in_dir(bin, dir).await? {
        let rtn = follow_symlink_chain(&path).await?;
        Ok(rtn)
    } else {
        Ok(vec![])
    }
}

/// Starting from a root, follows the chain of symlinks. Assumes that `root` exists. It does not
/// matter whether or not `root` points to an executable
#[cfg(unix)]
async fn follow_symlink_chain<P: AsRef<Path>>(root: P) -> IchwhResult<Vec<PathBuf>> {
    let mut path = root.as_ref().to_path_buf();
    let mut rtn = vec![path.clone()];

    while path.symlink_metadata().await?.file_type().is_symlink() {
        // Follow the symlink
        let pointee = async_std::fs::read_link(&path).await?;

        // If the link is relative, assume it it relative to the parent's current directory.
        // This accounts for situations like on my machine, where /usr/bin/python -> python3 ->
        // python3.8, instead of the absolute paths.
        path = if pointee.is_relative() {
            // Ok to unwrap, because that will only fail for "/", which cannot be symlinked
            let mut full = path.parent().unwrap().to_path_buf();
            full.push(pointee);
            full
        } else {
            pointee
        };

        // Add it to the rtn vec
        rtn.push(path.clone());
    }

    Ok(rtn)
}