use crate::error::{ArchToolkitError, Result};
use std::collections::HashSet;
use std::hash::BuildHasher;
use std::process::{Command, Stdio};
pub fn get_installed_packages() -> Result<HashSet<String>> {
tracing::debug!("Running: pacman -Qq");
let output = Command::new("pacman")
.args(["-Qq"])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output();
match output {
Ok(output) => {
if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout);
let packages: HashSet<String> = text
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
tracing::debug!(
"Successfully retrieved {} installed packages",
packages.len()
);
Ok(packages)
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::error!(
"pacman -Qq failed with status {:?}: {}",
output.status.code(),
stderr
);
Ok(HashSet::new())
}
}
Err(e) => {
tracing::error!("Failed to execute pacman -Qq: {}", e);
Ok(HashSet::new())
}
}
}
pub fn get_upgradable_packages() -> Result<HashSet<String>> {
tracing::debug!("Running: pacman -Qu");
let output = Command::new("pacman")
.args(["-Qu"])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output();
match output {
Ok(output) => {
if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout);
let packages: HashSet<String> = text
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() {
return None;
}
Some(line.find(' ').map_or_else(
|| line.to_string(),
|space_pos| line[..space_pos].trim().to_string(),
))
})
.collect();
tracing::debug!(
"Successfully retrieved {} upgradable packages",
packages.len()
);
Ok(packages)
} else {
tracing::debug!("pacman -Qu returned non-zero status (no upgrades or error)");
Ok(HashSet::new())
}
}
Err(e) => {
tracing::debug!("Failed to execute pacman -Qu: {} (assuming no upgrades)", e);
Ok(HashSet::new())
}
}
}
#[must_use]
pub fn get_provided_packages<S: BuildHasher + Default>(
_installed: &HashSet<String, S>,
) -> HashSet<String> {
HashSet::default()
}
fn check_if_provided<S: BuildHasher>(
name: &str,
_installed: &HashSet<String, S>,
) -> Option<String> {
let output = Command::new("pacman")
.args(["-Qqo", name])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output();
match output {
Ok(output) if output.status.success() => {
let text = String::from_utf8_lossy(&output.stdout);
let providing_pkg = text.lines().next().map(|s| s.trim().to_string());
if let Some(providing_pkg) = &providing_pkg {
tracing::debug!("{} is provided by {}", name, providing_pkg);
}
providing_pkg
}
_ => None,
}
}
#[must_use]
pub fn is_package_installed_or_provided<S: BuildHasher>(
name: &str,
installed: &HashSet<String, S>,
_provided: &HashSet<String, S>,
) -> bool {
if installed.contains(name) {
return true;
}
check_if_provided(name, installed).is_some()
}
pub fn get_installed_version(name: &str) -> Result<String> {
let output = Command::new("pacman")
.args(["-Q", name])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| ArchToolkitError::Parse(format!("pacman -Q failed: {e}")))?;
if !output.status.success() {
return Err(ArchToolkitError::PackageNotFound {
package: name.to_string(),
});
}
let text = String::from_utf8_lossy(&output.stdout);
if let Some(line) = text.lines().next() {
if let Some(space_pos) = line.find(' ') {
let version = line[space_pos + 1..].trim();
let version = version.split('-').next().unwrap_or(version);
return Ok(version.to_string());
}
}
Err(ArchToolkitError::Parse(format!(
"Could not parse version from pacman -Q output for package '{name}'"
)))
}
#[must_use]
pub fn get_available_version(name: &str) -> Option<String> {
let output = Command::new("pacman")
.args(["-Si", name])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
if line.starts_with("Version")
&& let Some(colon_pos) = line.find(':')
{
let version = line[colon_pos + 1..].trim();
let version = version.split('-').next().unwrap_or(version);
return Some(version.to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_installed_packages_output() {
let sample_output = "pacman\nfirefox\nvim\n";
let packages: HashSet<String> = sample_output
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
assert_eq!(packages.len(), 3);
assert!(packages.contains("pacman"));
assert!(packages.contains("firefox"));
assert!(packages.contains("vim"));
}
#[test]
fn test_parse_upgradable_packages_output() {
let sample_output =
"firefox 121.0-1 -> 122.0-1\nvim 9.0.0000-1 -> 9.0.1000-1\npackage-name\n";
let packages: HashSet<String> = sample_output
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() {
return None;
}
Some(line.find(' ').map_or_else(
|| line.to_string(),
|space_pos| line[..space_pos].trim().to_string(),
))
})
.collect();
assert_eq!(packages.len(), 3);
assert!(packages.contains("firefox"));
assert!(packages.contains("vim"));
assert!(packages.contains("package-name"));
}
#[test]
fn test_parse_installed_version_output() {
let sample_output = "pacman 6.1.0-1\n";
if let Some(line) = sample_output.lines().next()
&& let Some(space_pos) = line.find(' ')
{
let version = line[space_pos + 1..].trim();
let version = version.split('-').next().unwrap_or(version);
assert_eq!(version, "6.1.0");
}
}
#[test]
fn test_parse_available_version_output() {
let sample_output =
"Repository : extra\nName : pacman\nVersion : 6.1.0-1\n";
for line in sample_output.lines() {
if line.starts_with("Version")
&& let Some(colon_pos) = line.find(':')
{
let version = line[colon_pos + 1..].trim();
let version = version.split('-').next().unwrap_or(version);
assert_eq!(version, "6.1.0");
return;
}
}
panic!("Version line not found");
}
#[test]
fn test_get_provided_packages_returns_empty() {
let installed = HashSet::from(["pacman".to_string()]);
let provided = get_provided_packages(&installed);
assert!(provided.is_empty());
}
#[test]
fn test_is_package_installed_or_provided_direct_install() {
let installed = HashSet::from(["pacman".to_string(), "vim".to_string()]);
let provided = HashSet::new();
assert!(is_package_installed_or_provided(
"pacman", &installed, &provided
));
assert!(is_package_installed_or_provided(
"vim", &installed, &provided
));
assert!(!is_package_installed_or_provided(
"nonexistent",
&installed,
&provided
));
}
#[test]
#[ignore = "Requires pacman to be available"]
fn test_get_installed_packages_integration() {
if let Ok(packages) = get_installed_packages() {
println!("Found {} installed packages", packages.len());
}
}
#[test]
#[ignore = "Requires pacman to be available"]
fn test_get_upgradable_packages_integration() {
if let Ok(packages) = get_upgradable_packages() {
println!("Found {} upgradable packages", packages.len());
}
}
#[test]
#[ignore = "Requires pacman to be available and package to be installed"]
fn test_get_installed_version_integration() {
if let Ok(version) = get_installed_version("pacman") {
assert!(!version.is_empty());
println!("Installed pacman version: {version}");
}
}
#[test]
#[ignore = "Requires pacman to be available and package in repos"]
fn test_get_available_version_integration() {
if let Some(version) = get_available_version("pacman") {
assert!(!version.is_empty());
println!("Available pacman version: {version}");
}
}
}