1use crate::provider::prelude::*;
9use futures::StreamExt;
10use std::ops::Not;
11
12#[derive(Default, Debug, PartialEq, Clone)]
14#[allow(missing_copy_implementations)]
16pub struct Apt {}
17
18impl Apt {
19 pub fn new() -> Apt {
21 Default::default()
22 }
23
24 async fn search(&self, env: &Arc<Environment>, command: &str) -> Result<String, Error> {
26 env.output_of(cmd!(
27 "apt-file",
28 "search",
29 "--regexp",
30 &format!("bin.*/{}$", command)
31 ))
32 .await
33 .map_err(Error::from)
34 }
35
36 async fn update_cache(&self, env: &Arc<Environment>) -> Result<(), CacheError> {
38 env.output_of(cmd!("apt-file", "update").privileged())
39 .await
40 .map(|_| ())
41 .map_err(CacheError::from)
42 }
43
44 async fn get_candidates_from_output(
46 &self,
47 output: &str,
48 env: Arc<Environment>,
49 ) -> Vec<Candidate> {
50 futures::stream::iter(output.lines())
51 .then(|line| {
52 async {
53 let line = line.to_owned();
54 let cloned_env = env.clone();
55 let mut candidate = Candidate::default();
56
57 if let Some((package, bin)) = line.split_once(": ") {
58 candidate.package = package.to_string();
59 candidate.actions.execute = cmd!(bin);
60
61 if let Ok(output) = cloned_env
63 .output_of(cmd!("apt-cache", "show", package))
64 .await
65 .with_context(|| {
66 format!("failed to gather additional info for '{}'", line)
67 })
68 .to_log()
69 {
70 for line in output.lines() {
71 match line.split_once(": ") {
72 Some(("Version", version)) => {
73 candidate.version = version.to_string()
74 }
75 Some(("Origin", origin)) => {
76 candidate.origin = origin.to_string()
77 }
78 Some(("Description", text)) => {
79 candidate.description = text.to_string()
80 }
81 _ => continue,
82 }
83 }
84 }
85
86 candidate.actions.install = cloned_env
88 .output_of(cmd!("dpkg-query", "-W", package))
89 .await
90 .map(|_| true)
92 .unwrap_or(false)
94 .not()
97 .then_some(cmd!("apt", "install", "-y", package).privileged());
99 }
100 candidate
101 }
102 .in_current_span()
103 })
104 .collect::<Vec<Candidate>>()
105 .await
106 }
107}
108
109impl fmt::Display for Apt {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 write!(f, "APT")
112 }
113}
114
115#[async_trait]
116impl IsProvider for Apt {
117 async fn search_internal(
118 &self,
119 command: &str,
120 target_env: Arc<Environment>,
121 ) -> ProviderResult<Vec<Candidate>> {
122 let stdout = match self.search(&target_env, command).await {
123 Ok(val) => val,
124 Err(Error::NoCache) => {
125 self.update_cache(&target_env)
126 .await
127 .map_err(Error::Cache)
128 .map_err(|e| e.into_provider(command))?;
129 self.search(&target_env, command)
130 .await
131 .map_err(|e| e.into_provider(command))?
132 }
133 Err(e) => return Err(e.into_provider(command)),
134 };
135
136 Ok(self.get_candidates_from_output(&stdout, target_env).await)
137 }
138}
139
140#[derive(Debug, ThisError, Display)]
142pub enum Error {
143 Cache(#[from] CacheError),
145
146 NoCache,
148
149 Requirements(String),
151
152 NotFound,
154
155 Execution(ExecutionError),
157}
158
159impl From<ExecutionError> for Error {
160 fn from(value: ExecutionError) -> Self {
161 match value {
162 ExecutionError::NonZero { ref output, .. } => {
163 let matcher = OutputMatcher::new(output);
164 if matcher.contains("E: The cache is empty") {
165 Error::NoCache
166 } else if matcher.is_empty() {
167 Error::NotFound
168 } else {
169 Error::Execution(value)
170 }
171 }
172 ExecutionError::NotFound(val) => Error::Requirements(val),
173 _ => Error::Execution(value),
174 }
175 }
176}
177
178impl Error {
179 pub fn into_provider(self, command: &str) -> ProviderError {
181 match self {
182 Self::NotFound => ProviderError::NotFound(command.to_string()),
183 Self::Requirements(what) => ProviderError::Requirements(what),
184 Self::Execution(err) => ProviderError::Execution(err),
185 _ => ProviderError::ApplicationError(anyhow::Error::new(self)),
186 }
187 }
188}
189
190#[derive(Debug, ThisError, Display)]
191pub struct CacheError(#[from] ExecutionError);
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::test::prelude::*;
198
199 #[test]
200 fn initialize() {
201 let _apt = Apt::new();
202 }
203
204 test::default_tests!(Apt::new());
205
206 #[test]
211 fn cache_empty() {
212 let query = quick_test!(
213 Apt::new(),
214 Err(ExecutionError::NonZero {
215 command: "apt-file".to_string(),
216 output: std::process::Output {
217 stdout: r"Finding relevant cache files to search ...".into(),
218 stderr: r#"E: The cache is empty. You need to run "apt-file update" first.
219"#
220 .into(),
221 status: ExitStatus::from_raw(3),
222 },
223 }),
224 Err(ExecutionError::NonZero {
225 command: "sudo".to_string(),
226 output: std::process::Output {
227 stdout: r"".into(),
228 stderr: r#"sudo: a password is required\n"#.into(),
229 status: ExitStatus::from_raw(1),
230 },
231 })
232 );
233
234 assert::is_err!(query);
235 assert::err::application!(query);
236 }
237
238 #[test]
243 fn search_existent() {
244 let query = quick_test!(Apt::new(),
245 Ok("btrbk: /usr/bin/btrbk".to_string()),
247 Ok("
249Package: btrbk
250Architecture: all
251Version: 0.31.3-1
252Priority: optional
253Section: universe/utils
254Origin: Ubuntu
255Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
256Original-Maintainer: Axel Burri <axel@tty0.ch>
257Bugs: https://bugs.launchpad.net/ubuntu/+filebug
258Installed-Size: 404
259Depends: perl, btrfs-progs (>= 4.12)
260Recommends: openssh-client, mbuffer
261Suggests: openssl, python3
262Filename: pool/universe/b/btrbk/btrbk_0.31.3-1_all.deb
263Size: 107482
264MD5sum: ad4aaa293c91981fcde34a413f043f37
265SHA1: 88734d2e6f6c5bc6597edd4da22f67bf86ae45ad
266SHA256: b554489c952390da62c0c2de6012883f18a932b1b40157254846332fb6aaa889
267SHA512: 5dcd7015b325fcc5f6acd9b70c4f2c511826aa03426b594a278386091b1f36af6fef6a05a99f5bcc0866badf96fa2342afb6a44a0249d7d67199f0d877f3614c
268Homepage: https://digint.ch/btrbk/
269Description: backup tool for btrfs subvolumes
270Description-md5: 13434d9f502ec934b9db33ec622b0769
271".to_string()),
272 Err(ExecutionError::NonZero {
274 command: "dpkg-query".to_string(),
275 output: std::process::Output {
276 stdout: r"".into(),
277 stderr: r"dpkg-query: no packages found matching btrbk\n".into(),
278 status: ExitStatus::from_raw(1),
279 }
280 })
281 );
282
283 let result = query.results.unwrap();
284 assert_eq!(result.len(), 1);
285 assert_eq!(result[0].package, "btrbk".to_string());
286 assert_eq!(result[0].actions.execute, vec!["/usr/bin/btrbk"].into());
287 assert_eq!(result[0].version, "0.31.3-1".to_string());
288 assert_eq!(result[0].origin, "Ubuntu".to_string());
289 assert_eq!(
290 result[0].description,
291 "backup tool for btrfs subvolumes".to_string()
292 );
293 assert!(result[0].actions.install.is_some());
294 }
295
296 #[test]
301 fn search_nonexistent() {
302 let query = quick_test!(
303 Apt::new(),
304 Err(ExecutionError::NonZero {
305 command: "apt-file".to_string(),
306 output: std::process::Output {
307 stdout: r"".into(),
308 stderr: r"".into(),
309 status: ExitStatus::from_raw(1),
310 }
311 })
312 );
313
314 assert::is_err!(query);
315 assert::err::not_found!(query);
316 }
317}