use crate::provider::prelude::*;
use std::ops::Not;
#[derive(Default, Debug, PartialEq, Clone)]
pub struct Apt {}
impl Apt {
pub fn new() -> Apt {
Default::default()
}
async fn search(&self, env: &Arc<Environment>, command: &str) -> Result<String, Error> {
env.output_of(cmd!(
"apt-file",
"search",
"--regexp",
&format!("bin.*/{}$", command)
))
.await
.map_err(Error::from)
}
async fn update_cache(&self, env: &Arc<Environment>) -> Result<(), CacheError> {
env.output_of(cmd!("apt-file", "update").privileged())
.await
.map(|_| ())
.map_err(CacheError::from)
}
pub(crate) async fn get_candidates_from_output(
&self,
output: &str,
env: Arc<Environment>,
) -> ProviderResult<Vec<Candidate>> {
let mut futures = vec![];
for line in output.lines() {
let line = line.to_owned();
let cloned_env = env.clone();
let future = async_std::task::spawn(async move {
let mut candidate = Candidate::default();
if let Some((package, bin)) = line.split_once(": ") {
candidate.package = package.to_string();
candidate.actions.execute = cmd!(bin);
if let Ok(output) = cloned_env
.output_of(cmd!("apt-cache", "show", package))
.await
.with_context(|| format!("failed to gather additional info for '{}'", line))
.to_log()
{
for line in output.lines() {
match line.split_once(": ") {
Some(("Version", version)) => {
candidate.version = version.to_string()
}
Some(("Origin", origin)) => candidate.origin = origin.to_string(),
Some(("Description", text)) => {
candidate.description = text.to_string()
}
_ => continue,
}
}
}
candidate.actions.install = cloned_env
.output_of(cmd!("dpkg-query", "-W", package))
.await
.map(|_| true)
.unwrap_or(false)
.not()
.then_some(cmd!("apt", "install", "-y", package).privileged());
}
candidate
});
futures.push(future);
}
Ok(futures::future::join_all(futures).await)
}
}
impl fmt::Display for Apt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "APT")
}
}
#[async_trait]
impl IsProvider for Apt {
async fn search_internal(
&self,
command: &str,
target_env: Arc<Environment>,
) -> ProviderResult<Vec<Candidate>> {
let stdout = match self.search(&target_env, command).await {
Ok(val) => val,
Err(Error::NoCache) => async_std::task::block_on(async {
self.update_cache(&target_env).await?;
self.search(&target_env, command).await
})
.map_err(|e| e.into_provider(command))?,
Err(e) => return Err(e.into_provider(command)),
};
self.get_candidates_from_output(&stdout, target_env).await
}
}
#[derive(Debug, ThisError)]
pub enum Error {
#[error(transparent)]
Cache(#[from] CacheError),
#[error("system cache is empty, please run 'apt-file update' as root first")]
NoCache,
#[error("requirement not fulfilled: '{0}'")]
Requirements(String),
#[error("command not found")]
NotFound,
#[error(transparent)]
Execution(ExecutionError),
}
impl From<ExecutionError> for Error {
fn from(value: ExecutionError) -> Self {
match value {
ExecutionError::NonZero { ref output, .. } => {
let matcher = OutputMatcher::new(output);
if matcher.contains("E: The cache is empty") {
Error::NoCache
} else if matcher.is_empty() {
Error::NotFound
} else {
Error::Execution(value)
}
}
ExecutionError::NotFound(val) => Error::Requirements(val),
_ => Error::Execution(value),
}
}
}
impl Error {
pub fn into_provider(self, command: &str) -> ProviderError {
match self {
Self::NotFound => ProviderError::NotFound(command.to_string()),
Self::Requirements(what) => ProviderError::Requirements(what),
Self::Execution(err) => ProviderError::Execution(err),
_ => ProviderError::ApplicationError(anyhow::Error::new(self)),
}
}
}
#[derive(Debug, ThisError)]
#[error("failed to update apt-file cache")]
pub struct CacheError(#[from] ExecutionError);
#[cfg(test)]
mod tests {
use super::*;
use crate::test::prelude::*;
#[test]
fn initialize() {
let _apt = Apt::new();
}
test::default_tests!(Apt::new());
#[test]
fn cache_empty() {
let query = quick_test!(
Apt::new(),
Err(ExecutionError::NonZero {
command: "apt-file".to_string(),
output: std::process::Output {
stdout: r"Finding relevant cache files to search ...".into(),
stderr: r#"E: The cache is empty. You need to run "apt-file update" first.
"#
.into(),
status: ExitStatus::from_raw(3),
},
}),
Err(ExecutionError::NonZero {
command: "sudo".to_string(),
output: std::process::Output {
stdout: r"".into(),
stderr: r#"sudo: a password is required\n"#.into(),
status: ExitStatus::from_raw(1),
},
})
);
assert::is_err!(query);
assert::err::application!(query);
}
#[test]
fn search_existent() {
let query = quick_test!(Apt::new(),
Ok("btrbk: /usr/bin/btrbk".to_string()),
Ok("
Package: btrbk
Architecture: all
Version: 0.31.3-1
Priority: optional
Section: universe/utils
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Axel Burri <axel@tty0.ch>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 404
Depends: perl, btrfs-progs (>= 4.12)
Recommends: openssh-client, mbuffer
Suggests: openssl, python3
Filename: pool/universe/b/btrbk/btrbk_0.31.3-1_all.deb
Size: 107482
MD5sum: ad4aaa293c91981fcde34a413f043f37
SHA1: 88734d2e6f6c5bc6597edd4da22f67bf86ae45ad
SHA256: b554489c952390da62c0c2de6012883f18a932b1b40157254846332fb6aaa889
SHA512: 5dcd7015b325fcc5f6acd9b70c4f2c511826aa03426b594a278386091b1f36af6fef6a05a99f5bcc0866badf96fa2342afb6a44a0249d7d67199f0d877f3614c
Homepage: https://digint.ch/btrbk/
Description: backup tool for btrfs subvolumes
Description-md5: 13434d9f502ec934b9db33ec622b0769
".to_string()),
Err(ExecutionError::NonZero {
command: "dpkg-query".to_string(),
output: std::process::Output {
stdout: r"".into(),
stderr: r"dpkg-query: no packages found matching btrbk\n".into(),
status: ExitStatus::from_raw(1),
}
})
);
let result = query.results.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].package, "btrbk".to_string());
assert_eq!(result[0].actions.execute, vec!["/usr/bin/btrbk"].into());
assert_eq!(result[0].version, "0.31.3-1".to_string());
assert_eq!(result[0].origin, "Ubuntu".to_string());
assert_eq!(
result[0].description,
"backup tool for btrfs subvolumes".to_string()
);
assert!(result[0].actions.install.is_some());
}
#[test]
fn search_nonexistent() {
let query = quick_test!(
Apt::new(),
Err(ExecutionError::NonZero {
command: "apt-file".to_string(),
output: std::process::Output {
stdout: r"".into(),
stderr: r"".into(),
status: ExitStatus::from_raw(1),
}
})
);
assert::is_err!(query);
assert::err::not_found!(query);
}
}