cnf_lib/provider/
pacman.rs

1// Copyright (C) 2023 Andreas Hartmann <hartan@7x.de>
2// GNU General Public License v3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5//! Search packages with pacman
6use crate::provider::prelude::*;
7use std::str::FromStr;
8
9#[derive(Debug, ThisError)]
10pub enum Error {
11    #[error("pacman database files don't exist, please update database files")]
12    NoDatabase,
13    #[error("failed to parse stderr from pacman: '{0}'")]
14    ParseError(String),
15}
16
17/// Shorthand to get error type from stderr
18impl FromStr for Error {
19    type Err = Self;
20
21    fn from_str(s: &str) -> Result<Self, Self::Err> {
22        if s.contains("warning: database file") & s.contains("not exist (use '-Fy' to download)") {
23            Ok(Self::NoDatabase)
24        } else {
25            Err(Self::ParseError(s.to_string()))
26        }
27    }
28}
29
30#[derive(Default, Debug, PartialEq)]
31pub struct Pacman;
32
33impl fmt::Display for Pacman {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        write!(f, "pacman")
36    }
37}
38
39impl Pacman {
40    pub fn new() -> Self {
41        Default::default()
42    }
43
44    fn get_candidates_from_files_output(&self, output: String) -> ProviderResult<Vec<Candidate>> {
45        let mut results = vec![];
46
47        for line in output.lines() {
48            let mut candidate = Candidate::default();
49            for (index, piece) in line.splitn(4, '\0').enumerate() {
50                let piece = piece.to_string();
51                match index {
52                    0 => candidate.origin = piece,
53                    1 => candidate.package = piece,
54                    2 => candidate.version = piece,
55                    3 => candidate.actions.execute = cmd!(piece),
56                    _ => panic!("line contained superfluous piece {}", piece),
57                }
58            }
59            if !candidate.package.is_empty() {
60                results.push(candidate);
61            }
62        }
63
64        Ok(results)
65    }
66}
67
68#[async_trait]
69impl IsProvider for Pacman {
70    async fn search_internal(
71        &self,
72        command: &str,
73        target_env: Arc<Environment>,
74    ) -> ProviderResult<Vec<Candidate>> {
75        let stdout = match target_env
76            .output_of(cmd!(
77                "pacman",
78                "-F",
79                "--noconfirm",
80                "--machinereadable",
81                command
82            ))
83            .await
84        {
85            Ok(val) => val,
86            Err(ExecutionError::NonZero { ref output, .. })
87                if (output.stdout.is_empty() && output.stderr.is_empty()) =>
88            {
89                return Err(ProviderError::NotFound(command.to_string()))
90            }
91            Err(e) => return Err(ProviderError::from(e)),
92        };
93
94        let mut candidates = self.get_candidates_from_files_output(stdout)?;
95        candidates.iter_mut().for_each(|candidate| {
96            if candidate.actions.execute.is_empty() {
97                candidate.actions.execute = cmd!(command);
98            }
99        });
100
101        Ok(candidates)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::test::prelude::*;
109
110    #[test]
111    fn initialize() {
112        let _pacman = Pacman::new();
113    }
114
115    test::default_tests!(Pacman::new());
116
117    /// Searching without system cache
118    ///
119    /// - Searched with: pacman 6.0.2
120    /// - Search command: "pacman -F --noconfirm --machinereadable asdwasda"
121    #[test]
122    fn cache_empty() {
123        let query =
124            quick_test!(Pacman::new(), Err(ExecutionError::NonZero {
125            command: "pacman".to_string(),
126            output: std::process::Output {
127                stdout: r"".into(),
128                stderr: r"warning: database file for 'core' does not exist (use '-Fy' to download)
129warning: database file for 'extra' does not exist (use '-Fy' to download)
130warning: database file for 'community' does not exist (use '-Fy' to download)
131".into(),
132                status: ExitStatus::from_raw(1),
133            }
134        }));
135
136        assert::is_err!(query);
137        assert::err::execution!(query);
138    }
139
140    /// Searching nonexistent package
141    ///
142    /// - Searched with: pacman 6.0.2
143    /// - Search command: "pacman -F --noconfirm --machinereadable asdwasda"
144    #[test]
145    fn search_nonexistent() {
146        let query = quick_test!(
147            Pacman::new(),
148            Err(ExecutionError::NonZero {
149                command: "pacman".to_string(),
150                output: std::process::Output {
151                    stdout: r"".into(),
152                    stderr: r"".into(),
153                    status: ExitStatus::from_raw(1),
154                }
155            })
156        );
157
158        assert::is_err!(query);
159        assert::err::not_found!(query);
160    }
161
162    /// Searching existent package
163    ///
164    /// - Searched with: pacman 6.0.2
165    /// - Search command: "pacman -F --noconfirm --machinereadable htop"
166    #[test]
167    fn matches_htop() {
168        let query = quick_test!(
169            Pacman::new(),
170            Ok("
171extra\0bash-completion\x002.11-3\0usr/share/bash-completion/completions/htop
172extra\0htop\x003.2.2-1\0usr/bin/htop
173community\0pcp\x006.0.3-1\0etc/pcp/pmlogconf/tools/htop
174community\0pcp\x006.0.3-1\0var/lib/pcp/config/pmlogconf/tools/htop
175"
176            .to_string())
177        );
178
179        let result = query.results.unwrap();
180
181        assert_eq!(result.len(), 4);
182        assert!(result[0].package.starts_with("bash-completion"));
183        assert_eq!(result[0].version, "2.11-3");
184        assert_eq!(result[0].origin, "extra");
185        assert!(result[0].description.is_empty());
186        assert_eq!(
187            result[0].actions.execute,
188            vec!["usr/share/bash-completion/completions/htop"].into()
189        );
190        assert!(result[1].package.starts_with("htop"))
191    }
192}