use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use std::{fmt::Display, path::Path, process::Command};
use crate::traits::ToJson;
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub enum PkgType {
Deb,
Rpm,
DebRpm,
Other,
}
impl PkgType {
const BINARY_PATHES: &[&str] = &[
"/bin/",
"/usr/bin/",
"/usr/local/bin/",
"/sbin/",
"/usr/sbin/",
"/usr/local/sbin/",
];
fn is_deb() -> bool {
for dir in Self::BINARY_PATHES {
if Path::new(*dir).join("dpkg-query").exists() {
return true;
}
}
false
}
fn is_rpm() -> bool {
for dir in Self::BINARY_PATHES {
if Path::new(*dir).join("rpm").exists() {
return true;
}
}
false
}
pub fn detect() -> Self {
let deb = Self::is_deb();
let rpm = Self::is_rpm();
if deb && rpm {
Self::DebRpm
} else if deb {
Self::Deb
} else if rpm {
Self::Rpm
} else {
Self::Other
}
}
}
impl From<&str> for PkgType {
fn from(value: &str) -> Self {
match &value.replace("'", "") as &str {
"<DEB>" => Self::Deb,
"<RPM>" => Self::Rpm,
_ => Self::Other,
}
}
}
impl Display for PkgType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Deb => "deb",
Self::Rpm => "rpm",
Self::DebRpm => "deb+rpm",
Self::Other => "unknown",
}
)
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct InstalledPackages {
pub packages: Vec<Package>,
}
impl ToJson for InstalledPackages {}
impl InstalledPackages {
pub fn get() -> Result<Self> {
let pkg_type = PkgType::detect();
match pkg_type {
PkgType::Deb => Self::get_deb_packages(),
PkgType::Rpm => Self::get_rpm_packages(),
PkgType::DebRpm => {
let mut deb = Self::get_deb_packages()?;
let mut rpm = Self::get_rpm_packages()?;
deb.packages.append(&mut rpm.packages);
Ok(deb)
}
PkgType::Other => Err(anyhow!(
"Unsupported packaging system type! Supports only `deb` and `rpm` package types."
)),
}
}
fn command(args: &[&str]) -> Result<Self> {
let pkglist = Command::new("/bin/env").args(args).output()?;
let pkglist_stdout = String::from_utf8(pkglist.stdout)?;
let mut packages = Vec::new();
for pkg_str in pkglist_stdout.lines().map(|line| line.trim()) {
let package = Package::try_from(pkg_str);
if let Ok(pkg) = package {
packages.push(pkg);
}
}
Ok(Self { packages })
}
fn get_deb_packages() -> Result<Self> {
Self::command(&[
"dpkg-query",
"-W",
"-f='<DEB>\t${Package}\t${Version}\t${Architecture}\n",
])
}
fn get_rpm_packages() -> Result<Self> {
Self::command(&[
"rpm",
"-qa",
"--queryformat='<RPM>\t%{NAME}\t%{VERSION}\t%{ARCH}\n'",
])
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Package {
pub name: String,
pub version: String,
pub arch: String,
pub pkg_type: PkgType,
}
impl TryFrom<&str> for Package {
type Error = anyhow::Error;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
let mut chunks = value.trim().split('\t').map(|s| s.trim().replace("'", ""));
match (chunks.next(), chunks.next(), chunks.next(), chunks.next()) {
(Some(pkg), Some(name), Some(version), Some(arch)) => Ok(Self {
pkg_type: PkgType::from(pkg.as_str()),
name,
version,
arch,
}),
_ => Err(anyhow!("String \"{value}\" has incorrect format!")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pkg_list_test() {
let pkgs = InstalledPackages::get();
dbg!(&pkgs);
assert!(pkgs.is_ok());
}
}