Skip to main content

cnf_lib/provider/
path.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// SPDX-FileCopyrightText: (C) 2023 Andreas Hartmann <hartan@7x.de>
3// This file is part of cnf-lib, available at <https://gitlab.com/hartang/rust/cnf>
4
5//! # Path provider
6//!
7//! Searches for an executable in the `$PATH`. Since this provider runs on all environments, it is
8//! useful to e.g. discover executables available in a toolbx container.
9use crate::provider::prelude::*;
10
11/// Provider for the system `$PATH`
12#[derive(Debug, Default, PartialEq)]
13// In the future these may get (mutable) internal state.
14#[allow(missing_copy_implementations)]
15pub struct Path {}
16
17impl fmt::Display for Path {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        write!(f, "$PATH")
20    }
21}
22
23impl Path {
24    /// Create a new instance of the `$PATH` provider.
25    pub fn new() -> Path {
26        Default::default()
27    }
28}
29
30#[async_trait]
31impl IsProvider for Path {
32    async fn search_internal(
33        &self,
34        command: &str,
35        target_env: Arc<Environment>,
36    ) -> ProviderResult<Vec<Candidate>> {
37        let result_success = |package: String| {
38            Ok(vec![Candidate {
39                package: package.clone(),
40                actions: Actions {
41                    install: None,
42                    execute: cmd!(package),
43                },
44                ..Candidate::default()
45            }])
46        };
47
48        if &crate::environment::current() == target_env.as_ref() {
49            match which::which(command) {
50                Ok(path) => result_success(path.display().to_string()),
51                Err(_) => Err(ProviderError::NotFound(command.to_string())),
52            }
53        } else {
54            let stdout = target_env
55                .output_of(cmd!("command", "-v", command))
56                .await
57                .map_err(|e| {
58                    if let ExecutionError::NonZero { .. } = e {
59                        ProviderError::NotFound(command.to_string())
60                    } else {
61                        ProviderError::from(e)
62                    }
63                })?;
64
65            result_success(stdout.trim_end().to_string())
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::test::prelude::*;
74
75    test::default_tests!(Path::new());
76
77    #[test]
78    fn search_nonexistent() {
79        let query = quick_test!(
80            Path::new(),
81            Err(ExecutionError::NonZero {
82                command: "command".to_string(),
83                output: std::process::Output {
84                    stdout: "".into(),
85                    stderr: "".into(),
86                    status: ExitStatus::from_raw(1),
87                }
88            })
89        );
90
91        assert::is_err!(query);
92        assert::err::not_found!(query);
93    }
94
95    #[test]
96    fn search_existent() {
97        let query = quick_test!(Path::new(), Ok("/usr/bin/top".to_string()));
98
99        let result = query.results.unwrap();
100
101        assert_eq!(result.len(), 1);
102        assert_eq!(result[0].package, "/usr/bin/top".to_string());
103        assert_eq!(result[0].actions.execute, vec!["/usr/bin/top"].into());
104    }
105}