Skip to main content

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