lazy_badger/
read.rs

1use std::{
2    fs::Permissions,
3    os::unix::fs::PermissionsExt,
4    path::{Path, PathBuf},
5};
6
7/// All errors triggered by the find script feature
8#[derive(Debug, thiserror::Error)]
9pub enum FindScriptError {
10    /// Multiple scripts were found
11    #[error("Multiple({1}) scripts found with name '{0}'")]
12    MultipleFound(String, usize),
13    /// List scripts error
14    #[error(transparent)]
15    ListScriptsError(#[from] ListScriptsError),
16}
17
18/// All errors triggered by the list scripts feature
19#[derive(Debug, thiserror::Error)]
20pub enum ListScriptsError {
21    /// Path is not a directory
22    #[error("Path '{0:?}' is not a directory")]
23    NotADir(PathBuf),
24    /// Error reading the path
25    #[error("Error reading directory: {0}")]
26    ReadDirFailure(std::io::Error),
27}
28
29/// Finds a script from its name and the root directory
30///
31/// Returns the path to the script if found
32///
33/// # Errors
34///
35/// - Error reading any sub-directory;
36/// - Passed-in path is not a directory;
37/// - More than one script was found;
38pub fn find_script(root: &Path, name: String) -> Result<Option<PathBuf>, FindScriptError> {
39    let all_scripts = list_scripts(root)?;
40    let matching_scripts = all_scripts
41        .into_iter()
42        .filter(|(n, _)| *n == name)
43        .collect::<Vec<_>>();
44    match matching_scripts.as_slice() {
45        [(_, path)] => Ok(Some(path.to_path_buf())),
46        [] => Ok(None),
47        multiple => Err(FindScriptError::MultipleFound(name, multiple.len())),
48    }
49}
50
51/// Lists all available scripts in a directory and its sub-directories
52///
53/// Returns the name of the script and its full path
54///
55/// Transverse the passed-in directory and find all files that:
56///   - Have the [`BASH_EXTENSION`] extension;
57///   - Are executable;
58///
59/// # Notes
60///
61/// - Ignores files that fail to be read
62///
63/// # Errors
64///
65/// - Error reading any sub-directory;
66/// - Passed-in path is not a directory;
67pub fn list_scripts(root: &Path) -> Result<Vec<(String, PathBuf)>, ListScriptsError> {
68    let mut found_scripts = Vec::new();
69    let sub_dirs = root
70        .read_dir()
71        .map_err(ListScriptsError::ReadDirFailure)?
72        .filter_map(|f| f.ok())
73        .filter(|f| f.path().is_dir())
74        .collect::<Vec<_>>();
75    for sub_dir in sub_dirs {
76        let sub_scripts = list_scripts(&sub_dir.path())?;
77        found_scripts.extend(sub_scripts);
78    }
79    found_scripts.extend(list_scripts_at_directory(root)?);
80
81    Ok(found_scripts)
82}
83
84/// Lists all scripts in the given directory
85///
86/// Ignores files that cannot be read
87fn list_scripts_at_directory(dir: &Path) -> Result<Vec<(String, PathBuf)>, ListScriptsError> {
88    if !dir.is_dir() {
89        return Err(ListScriptsError::NotADir(dir.to_path_buf()));
90    }
91    let scripts = dir
92        .read_dir()
93        .map_err(ListScriptsError::ReadDirFailure)?
94        .filter_map(|f| f.ok())
95        .filter(|f| f.path().is_file())
96        .filter(|f| {
97            let Ok(m) = f.metadata() else {
98                return false;
99            };
100            is_path_executable(m.permissions())
101        })
102        .filter(|f| {
103            let Some(e) = f
104                .path()
105                .extension()
106                .map(|e| e.to_string_lossy().to_string())
107            else {
108                return false;
109            };
110            e == super::BASH_EXTENSION
111        })
112        .filter_map(|f| {
113            let name = f.path().file_stem()?.to_string_lossy().to_string();
114            Some((name, f.path()))
115        })
116        .collect::<Vec<_>>();
117
118    Ok(scripts)
119}
120
121/// Returns if the given path is executable
122pub(crate) fn is_path_executable(permissions: Permissions) -> bool {
123    permissions.mode() & 0o111 != 0
124}