1use anyhow::{Result, anyhow};
24use serde::{Deserialize, Serialize};
25use std::{fmt::Display, path::Path, process::Command};
26
27use crate::traits::ToJson;
28
29#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
31pub enum PkgType {
32 Deb,
34
35 Rpm,
37
38 DebRpm, Other,
43}
44
45impl PkgType {
46 const BINARY_PATHES: &[&str] = &[
47 "/bin/",
48 "/usr/bin/",
49 "/usr/local/bin/",
50 "/sbin/",
51 "/usr/sbin/",
52 "/usr/local/sbin/",
53 ];
54
55 fn is_deb() -> bool {
56 for dir in Self::BINARY_PATHES {
57 if Path::new(*dir).join("dpkg-query").exists() {
58 return true;
59 }
60 }
61 false
62 }
63
64 fn is_rpm() -> bool {
65 for dir in Self::BINARY_PATHES {
66 if Path::new(*dir).join("rpm").exists() {
67 return true;
68 }
69 }
70 false
71 }
72
73 pub fn detect() -> Self {
75 let deb = Self::is_deb();
76 let rpm = Self::is_rpm();
77
78 if deb && rpm {
79 Self::DebRpm
80 } else if deb {
81 Self::Deb
82 } else if rpm {
83 Self::Rpm
84 } else {
85 Self::Other
86 }
87 }
88}
89
90impl From<&str> for PkgType {
91 fn from(value: &str) -> Self {
92 match &value.replace("'", "") as &str {
94 "<DEB>" => Self::Deb,
95 "<RPM>" => Self::Rpm,
96 _ => Self::Other,
97 }
98 }
99}
100
101impl Display for PkgType {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 write!(
104 f,
105 "{}",
106 match self {
107 Self::Deb => "deb",
108 Self::Rpm => "rpm",
109 Self::DebRpm => "deb+rpm",
110 Self::Other => "unknown",
111 }
112 )
113 }
114}
115
116#[derive(Debug, Deserialize, Serialize, Clone)]
118pub struct InstalledPackages {
119 pub packages: Vec<Package>,
121}
122
123impl ToJson for InstalledPackages {}
124
125impl InstalledPackages {
126 pub fn get() -> Result<Self> {
127 let pkg_type = PkgType::detect();
128
129 match pkg_type {
130 PkgType::Deb => Self::get_deb_packages(),
131 PkgType::Rpm => Self::get_rpm_packages(),
132 PkgType::DebRpm => {
133 let mut deb = Self::get_deb_packages()?;
134 let mut rpm = Self::get_rpm_packages()?;
135 deb.packages.append(&mut rpm.packages);
136
137 Ok(deb)
138 }
139 PkgType::Other => Err(anyhow!(
140 "Unsupported packaging system type! Supports only `deb` and `rpm` package types."
141 )),
142 }
143 }
144
145 fn command(args: &[&str]) -> Result<Self> {
146 let pkglist = Command::new("/bin/env").args(args).output()?;
147 let pkglist_stdout = String::from_utf8(pkglist.stdout)?;
148 let mut packages = Vec::new();
149
150 for pkg_str in pkglist_stdout.lines().map(|line| line.trim()) {
151 let package = Package::try_from(pkg_str);
152 if let Ok(pkg) = package {
153 packages.push(pkg);
154 }
155 }
156
157 Ok(Self { packages })
158 }
159
160 fn get_deb_packages() -> Result<Self> {
161 Self::command(&[
162 "dpkg-query",
163 "-W",
164 "-f='<DEB>\t${Package}\t${Version}\t${Architecture}\n",
165 ])
166 }
167
168 fn get_rpm_packages() -> Result<Self> {
169 Self::command(&[
170 "rpm",
171 "-qa",
172 "--queryformat='<RPM>\t%{NAME}\t%{VERSION}\t%{ARCH}\n'",
173 ])
174 }
175}
176
177#[derive(Debug, Deserialize, Serialize, Clone)]
178pub struct Package {
179 pub name: String,
180 pub version: String,
181 pub arch: String,
182 pub pkg_type: PkgType,
183}
184
185impl TryFrom<&str> for Package {
186 type Error = anyhow::Error;
187
188 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
189 let mut chunks = value.trim().split('\t').map(|s| s.trim().replace("'", ""));
191
192 match (chunks.next(), chunks.next(), chunks.next(), chunks.next()) {
193 (Some(pkg), Some(name), Some(ver), Some(arch)) => Ok(Self {
194 pkg_type: PkgType::from(pkg.as_str()),
195 name: name,
196 version: ver,
197 arch: arch,
198 }),
199 _ => Err(anyhow!("String \"{value}\" has incorrect format!")),
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn pkg_list_test() {
210 let pkgs = InstalledPackages::get();
211 dbg!(&pkgs);
212 assert!(pkgs.is_ok());
213 }
214}