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