use crate::provider::prelude::*;
#[derive(Default, Debug, PartialEq)]
pub struct Dnf;
impl fmt::Display for Dnf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DNF")
}
}
impl Dnf {
pub fn new() -> Self {
Default::default()
}
fn get_candidates_from_provides_output(&self, output: String) -> Vec<Candidate> {
let lines = output
.lines()
.map(|s| s.to_string())
.collect::<Vec<String>>();
let mut results = vec![];
let mut found_empty = true;
let mut candidate = Candidate::default();
for line in lines {
if line.is_empty() {
found_empty = true;
continue;
}
let (before, after) = match line.split_once(':') {
Some((a, b)) => (a.trim(), b.trim()),
None => {
log::warn!("ignoring unexpected output from dnf: '{}'", line);
found_empty = true;
continue;
}
};
if found_empty {
if !candidate.package.is_empty() {
results.push(candidate);
}
candidate = Candidate::default();
candidate.package = before.to_string();
candidate.description = after.to_string();
candidate.actions.install = Some(cmd!("dnf", "install", "-y", before).privileged());
found_empty = false;
}
if before == "Repo" {
candidate.origin = after.to_string();
}
if before == "Provide" {
if let Some((package, version)) = after.split_once(" = ") {
candidate.actions.execute = cmd!(package);
candidate.version = version.to_string();
} else {
candidate.actions.execute = cmd!(after);
}
}
}
results.push(candidate);
results
}
async fn check_installed(
&self,
target_env: &Arc<Environment>,
mut candidates: Vec<Candidate>,
) -> Vec<Candidate> {
let mut futures = vec![];
for mut candidate in candidates.drain(0..) {
let cloned_env = target_env.clone();
let future = async_std::task::spawn(async move {
let is_installed = cloned_env
.output_of(cmd!("rpm", "-q", "--quiet", &candidate.package))
.await
.map(|_| true)
.unwrap_or(false);
if is_installed {
candidate.actions.install = None;
}
candidate
});
futures.push(future);
}
futures::future::join_all(futures).await
}
async fn search(
&self,
target_env: &Arc<Environment>,
command: &str,
user_only: bool,
) -> Result<String, Error> {
let mut cmd = cmd!("dnf", "-q", "--color", "never", "provides", command);
if !user_only {
cmd.append(&["-C"]);
}
target_env.output_of(cmd).await.map_err(Error::from)
}
async fn update_cache(&self, target_env: &Arc<Environment>) -> Result<(), CacheError> {
target_env
.output_of(cmd!("dnf", "makecache", "-q", "--color", "never").privileged())
.await
.map(|_| ())
.map_err(CacheError::from)
}
}
#[async_trait]
impl IsProvider for Dnf {
async fn search_internal(
&self,
command: &str,
target_env: Arc<Environment>,
) -> ProviderResult<Vec<Candidate>> {
let stdout = match self.search(&target_env, command, false).await {
Ok(val) => val,
Err(Error::NoCache) => async_std::task::block_on(async {
log::info!("dnf cache is outdated, trying to update");
let success = self.update_cache(&target_env).await.is_ok();
self.search(&target_env, command, !success).await
})
.map_err(|err| err.into_provider(command))?,
Err(err) => return Err(err.into_provider(command)),
};
let candidates = self.get_candidates_from_provides_output(stdout);
let mut candidates = self.check_installed(&target_env, candidates).await;
candidates.iter_mut().for_each(|candidate| {
if candidate.actions.execute.is_empty() {
candidate.actions.execute = cmd!(command.to_string());
}
});
Ok(candidates)
}
}
#[derive(Debug, ThisError)]
pub enum Error {
#[error("command not found")]
NotFound,
#[error("cannot query packages, please update system (root) cache")]
Cache(#[from] CacheError),
#[error("no package cache present, please update system cache (as root user)")]
NoCache,
#[error("'{0}' must be installed to use this provider")]
Requirements(String),
#[error(transparent)]
Execution(ExecutionError),
}
#[derive(Debug, ThisError)]
#[error("failed to update dnf system cache")]
pub struct CacheError(#[from] ExecutionError);
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),
_ => ProviderError::ApplicationError(anyhow::Error::new(self)),
}
}
}
impl From<ExecutionError> for Error {
fn from(value: ExecutionError) -> Self {
if let ExecutionError::NonZero { ref output, .. } = value {
let matcher = OutputMatcher::from(output);
if matcher.starts_with("Error: No matches found") {
Error::NotFound
} else if matcher.starts_with("Error: Cache-only enabled but no cache") {
Error::NoCache
} else {
Error::Execution(value)
}
} else if matches!(value, ExecutionError::NotFound(_)) {
Error::Requirements("dnf".to_string())
} else {
Error::Execution(value)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::prelude::*;
#[test]
fn initialize() {
let _dnf = Dnf::new();
}
test::default_tests!(Dnf::new());
#[test]
fn cache_empty() {
let query = quick_test!(
Dnf::new(),
Err(ExecutionError::NonZero {
command: "dnf".to_string(),
output: std::process::Output {
stdout: r"".into(),
stderr: r#"Error: Cache-only enabled but no cache for 'fedora'"#.into(),
status: ExitStatus::from_raw(1),
},
}),
Err(ExecutionError::NotFound("dnf".to_string())),
Ok("
htop-3.2.1-2.fc37.x86_64 : Interactive process viewer
Repo : fedora
Matched from:
Provide : htop = 3.2.1-2.fc37
htop-3.2.2-2.fc37.x86_64 : Interactive process viewer
Repo : updates
Matched from:
Provide : htop = 3.2.2-2.fc37
"
.to_string()),
Ok("".to_string()),
Ok("".to_string())
);
let result = query.results.expect("expected successful results");
assert_eq!(result.len(), 2);
assert!(result[0].package.starts_with("htop-3.2.1-2"));
}
#[test]
fn search_nonexistent() {
let query = quick_test!(Dnf::new(), Err(ExecutionError::NonZero {
command: "dnf".to_string(),
output: std::process::Output {
stdout: r"".into(),
stderr: r#"Error: No matches found. If searching for a file, try specifying the full path or using a wildcard prefix ("*/") at the beginning."#.into(),
status: ExitStatus::from_raw(1),
},
}));
assert::is_err!(query);
assert::err::not_found!(query);
}
#[test]
fn matches_htop() {
let query = quick_test!(
Dnf::new(),
Ok("
htop-3.2.1-2.fc37.x86_64 : Interactive process viewer
Repo : fedora
Matched from:
Provide : htop = 3.2.1-2.fc37
htop-3.2.2-2.fc37.x86_64 : Interactive process viewer
Repo : updates
Matched from:
Provide : htop = 3.2.2-2.fc37
"
.to_string()),
Ok("".to_string()),
Err(ExecutionError::NonZero {
command: "rpm".to_string(),
output: std::process::Output {
stdout: r"".into(),
stderr: r"".into(),
status: ExitStatus::from_raw(1),
},
})
);
let result = query.results.unwrap();
assert_eq!(result.len(), 2);
assert!(result[0].package.starts_with("htop-3.2.1-2"));
assert_eq!(result[0].version, "3.2.1-2.fc37");
assert_eq!(result[0].origin, "fedora");
assert_eq!(result[0].description, "Interactive process viewer");
assert_eq!(result[0].actions.execute, vec!["htop"].into());
assert_eq!(result[1].description, "Interactive process viewer");
let num_installable = result
.iter()
.fold(0, |acc, c| acc + (c.actions.install.is_some() as usize));
assert_eq!(num_installable, 1);
}
#[test]
fn matches_ping() {
let query = quick_test!(
Dnf::new(),
Ok("
iputils-20211215-3.fc37.x86_64 : Network monitoring tools including ping
Repo : fedora
Matched from:
Filename : /usr/bin/ping
Provide : /bin/ping
Filename : /usr/sbin/ping
iputils-20221126-1.fc37.x86_64 : Network monitoring tools including ping
Repo : @System
Matched from:
Filename : /usr/bin/ping
Provide : /bin/ping
Filename : /usr/sbin/ping
iputils-20221126-1.fc37.x86_64 : Network monitoring tools including ping
Repo : updates
Matched from:
Filename : /usr/bin/ping
Provide : /bin/ping
Filename : /usr/sbin/ping
"
.to_string()),
Ok("".to_string()),
Err(ExecutionError::NonZero {
command: "rpm".to_string(),
output: std::process::Output {
stdout: r"".into(),
stderr: r"".into(),
status: ExitStatus::from_raw(1),
},
}),
Err(ExecutionError::NotFound("rpm".to_string()))
);
let result = query.results.unwrap();
assert!(result.len() == 3);
assert!(result[0].package.starts_with("iputils"));
assert!(result[0].version.is_empty());
assert!(result[0].origin == "fedora");
assert!(result[1].origin == "@System");
assert!(result[0].description == "Network monitoring tools including ping");
assert!(result[0].actions.execute == vec!["/bin/ping"].into());
assert!(result[1].description == "Network monitoring tools including ping");
let num_installable = result
.iter()
.fold(0, |acc, c| acc + (c.actions.install.is_some() as usize));
assert_eq!(num_installable, 2);
}
}