ferrix_lib/
soft.rs

1/* soft.rs
2 *
3 * Copyright 2025 Michail Krasnov <mskrasnov07@ya.ru>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21//! Installed software list
22
23use anyhow::{Result, anyhow};
24use serde::{Deserialize, Serialize};
25use std::{fmt::Display, path::Path, process::Command};
26
27use crate::traits::ToJson;
28
29/// Type of installed Linux distro packaging system or concrete package
30#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
31pub enum PkgType {
32    /// System with `dpkg` package manager only
33    Deb,
34
35    /// System with `rpm` package manager only
36    Rpm,
37
38    /// System with `dpkg` and `rpm` package managers
39    DebRpm, // If deb and rpm package managers is installed
40
41    /// Unknown and/or unsupported Linux distro
42    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    /// Detect Linux packaging system
74    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        // dbg!(value);
93        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/// List of installed software
117#[derive(Debug, Deserialize, Serialize, Clone)]
118pub struct InstalledPackages {
119    /// Package list
120    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        // dbg!(value);
190        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}