use anyhow::Result;
use regex::Regex;
use std::collections::HashMap;
use std::process::Command;
use std::sync::OnceLock;
#[derive(Debug, Clone, Default)]
pub struct PackageInfo {
pub name: String,
pub installed_version: Option<String>,
pub candidate_version: Option<String>,
pub installed_size_kb: Option<u64>,
pub download_size_bytes: Option<u64>,
pub description: Option<String>,
pub section: Option<String>,
pub homepage: Option<String>,
pub depends: Vec<String>,
}
static SIZE_RE: OnceLock<Regex> = OnceLock::new();
fn size_re() -> &'static Regex {
SIZE_RE.get_or_init(|| Regex::new(r"(\d+)").unwrap())
}
fn parse_apt_cache_show(output: &str, name: &str) -> PackageInfo {
let mut info = PackageInfo {
name: name.to_string(),
..Default::default()
};
let fields: HashMap<&str, &str> = output
.lines()
.filter_map(|l| {
let mut parts = l.splitn(2, ": ");
let key = parts.next()?.trim();
let val = parts.next()?.trim();
Some((key, val))
})
.collect();
info.description = fields.get("Description").map(|s| s.to_string());
info.section = fields.get("Section").map(|s| s.to_string());
info.homepage = fields.get("Homepage").map(|s| s.to_string());
info.depends = fields
.get("Depends")
.map(|d| d.split(',').map(|s| s.trim().to_string()).collect())
.unwrap_or_default();
if let Some(size_str) = fields.get("Installed-Size") {
info.installed_size_kb = size_str.parse().ok();
}
if let Some(size_str) = fields.get("Size") {
info.download_size_bytes = size_str.parse().ok();
}
info
}
fn parse_apt_cache_policy(output: &str) -> (Option<String>, Option<String>) {
let mut installed = None;
let mut candidate = None;
for line in output.lines() {
let trimmed = line.trim();
if let Some(v) = trimmed.strip_prefix("Installed: ") {
if v != "(none)" {
installed = Some(v.to_string());
}
} else if let Some(v) = trimmed.strip_prefix("Candidate: ") {
if v != "(none)" {
candidate = Some(v.to_string());
}
}
}
(installed, candidate)
}
pub fn show_package(name: &str) -> Result<PackageInfo> {
let show_out = Command::new("apt-cache")
.args(["show", name])
.output()?;
let policy_out = Command::new("apt-cache")
.args(["policy", name])
.output()?;
let show_str = String::from_utf8_lossy(&show_out.stdout);
let policy_str = String::from_utf8_lossy(&policy_out.stdout);
let mut info = parse_apt_cache_show(&show_str, name);
let (installed, candidate) = parse_apt_cache_policy(&policy_str);
info.installed_version = installed;
info.candidate_version = candidate;
Ok(info)
}
pub fn search_packages(term: &str, names_only: bool) -> Result<Vec<PackageInfo>> {
let mut cmd = Command::new("apt-cache");
cmd.arg("search");
if names_only {
cmd.arg("--names-only");
}
cmd.arg(term);
let out = cmd.output()?;
let stdout = String::from_utf8_lossy(&out.stdout);
let mut results = Vec::new();
for line in stdout.lines() {
let mut parts = line.splitn(2, " - ");
let name = parts.next().unwrap_or("").trim().to_string();
let desc = parts.next().map(|s| s.to_string());
if !name.is_empty() {
results.push(PackageInfo {
name,
description: desc,
..Default::default()
});
}
}
Ok(results)
}
pub fn list_packages(installed: bool, upgradable: bool) -> Result<Vec<PackageInfo>> {
let mut args = vec!["list"];
if installed {
args.push("--installed");
}
if upgradable {
args.push("--upgradable");
}
let out = Command::new("apt").args(&args).output()?;
let stdout = String::from_utf8_lossy(&out.stdout);
let re = Regex::new(r"^([^/]+)/\S+\s+(\S+)\s+\S+(?:\s+\[(.+)\])?").unwrap();
let mut packages = Vec::new();
for line in stdout.lines().skip(1) {
if line.starts_with("WARNING") || line.is_empty() {
continue;
}
if let Some(caps) = re.captures(line) {
let name = caps[1].to_string();
let ver = caps[2].to_string();
let status = caps.get(3).map(|m| m.as_str());
let (installed_ver, candidate_ver) = if status.map_or(false, |s| s.starts_with("installed")) {
if let Some(upg) = status.and_then(|s| s.strip_prefix("installed,upgradable to ")) {
(Some(ver.clone()), Some(upg.to_string()))
} else {
(Some(ver.clone()), None)
}
} else {
(None, Some(ver))
};
packages.push(PackageInfo {
name,
installed_version: installed_ver,
candidate_version: candidate_ver,
..Default::default()
});
}
}
let _ = size_re();
Ok(packages)
}