ferrix_lib/
sys.rs

1/* sys.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//! Get information about installed system
22
23use crate::traits::*;
24use crate::utils::read_to_string;
25use anyhow::{Result, anyhow};
26use serde::Serialize;
27use std::env::vars;
28
29/// A structure containing all collected information about
30/// installed system
31#[derive(Debug, Serialize)]
32pub struct Sys {
33    /// Information about kernel
34    pub kernel: Kernel,
35
36    /// Information about installed distro
37    pub os_release: OsRelease,
38
39    /// Machine ID
40    pub machine_id: Option<String>,
41
42    /// Timezone
43    pub timezone: Option<String>,
44
45    /// Environment variables for current user
46    pub env_vars: Vec<(String, String)>,
47
48    /// Uptime
49    pub uptime: Uptime,
50
51    /// System load (average)
52    pub loadavg: LoadAVG,
53
54    /// Information about users
55    pub users: Users,
56
57    /// List of user groups
58    pub groups: Groups,
59
60    /// List of installed shells
61    pub shells: Shells,
62
63    /// Host name
64    pub hostname: Option<HostName>,
65    // /// Current locale
66    // pub locale: Locale,
67}
68
69impl Sys {
70    pub fn new() -> Result<Self> {
71        Ok(Self {
72            kernel: Kernel::new()?,
73            os_release: OsRelease::new()?,
74            machine_id: read_to_string("/etc/machine-id").ok(),
75            timezone: read_to_string("/etc/timezone").ok(),
76            env_vars: vars().collect(),
77            uptime: Uptime::new()?,
78            loadavg: LoadAVG::new()?,
79            users: Users::new()?,
80            groups: Groups::new()?,
81            shells: get_shells()?,
82            hostname: get_hostname(),
83            // locale: todo!(),
84        })
85    }
86
87    pub fn update(&mut self) -> Result<()> {
88        self.uptime = Uptime::new()?;
89        self.loadavg = LoadAVG::new()?;
90        Ok(())
91    }
92}
93
94impl ToJson for Sys {}
95
96/// Information about Linux kernel
97#[derive(Debug, Serialize, Clone)]
98pub struct Kernel {
99    /// All data about kernel
100    pub uname: Option<String>, // /proc/version
101
102    /// Kernel command line
103    pub cmdline: Option<String>, // /proc/cmdline
104
105    /// Kernel architecture
106    pub arch: Option<String>, // /proc/sys/kernel/arch
107
108    /// Kernel version
109    pub version: Option<String>, // /proc/sys/kernel/osrelease
110
111    /// Kernel build info
112    pub build_info: Option<String>, // /proc/sys/kernel/version
113
114    /// Max processes count
115    pub pid_max: u32, // /proc/sys/kernel/pid_max
116
117    /// Max threads count
118    pub threads_max: u32, // /proc/sys/kernel/threads-max
119
120    /// Max user events
121    pub user_events_max: Option<u32>, // /proc/sys/kernel/user_events_max
122
123    /// Available enthropy
124    pub enthropy_avail: Option<u16>, // /proc/sys/kernel/random/entropy_avail
125}
126
127impl Kernel {
128    pub fn new() -> Result<Self> {
129        Ok(Self {
130            uname: read_to_string("/proc/version").ok(),
131            cmdline: read_to_string("/proc/cmdline").ok(),
132            arch: read_to_string("/proc/sys/kernel/arch").ok(),
133            version: read_to_string("/proc/sys/kernel/osrelease").ok(),
134            build_info: read_to_string("/proc/sys/kernel/version").ok(),
135            pid_max: read_to_string("/proc/sys/kernel/pid_max")?.parse()?,
136            threads_max: read_to_string("/proc/sys/kernel/threads-max")?.parse()?,
137            user_events_max: match read_to_string("/proc/sys/kernel/user_events_max").ok() {
138                Some(uem) => uem.parse().ok(),
139                None => None,
140            },
141            enthropy_avail: match read_to_string("/proc/sys/kernel/random/entropy_avail").ok() {
142                Some(ea) => ea.parse().ok(),
143                None => None,
144            },
145        })
146    }
147}
148
149impl ToJson for Kernel {}
150
151/// Information about installed distro from `/etc/os-release`
152///
153/// > Information from *[freedesktop](https://www.freedesktop.org/software/systemd/man/249/os-release.html)* portal.
154#[derive(Debug, Serialize, Default, Clone)]
155pub struct OsRelease {
156    /// The operating system name without a version component
157    ///
158    /// If not set, a default `Linux` value may be used
159    pub name: String,
160
161    /// A lower-case string identifying the OS, excluding any
162    /// version information
163    pub id: Option<String>,
164
165    /// A space-separated list of operating system identifiers in the
166    /// same syntax as the `id` param.
167    pub id_like: Option<String>,
168
169    /// A pretty OS name in a format suitable for presentation to
170    /// the user. May or may not contain a release code or OS version
171    /// of some kind, as suitable
172    pub pretty_name: Option<String>,
173
174    /// A CPE name for the OS, in URI binding syntax
175    pub cpe_name: Option<String>,
176
177    /// Specific variant or edition of the OS suitable for
178    /// presentation to the user
179    pub variant: Option<String>,
180
181    /// Lower-case string identifying a specific variant or edition
182    /// of the OS
183    pub variant_id: Option<String>,
184
185    /// The OS version, excluding any OS name information, possibly
186    /// including a release code name, and suitable for presentation
187    /// to the user
188    pub version: Option<String>,
189
190    /// A lower-case string identifying the OS version, excluding any
191    /// OS name information or release code name
192    pub version_id: Option<String>,
193
194    /// A lower-case string identifying the OS release code name,
195    /// excluding any OS name information or release version
196    pub version_codename: Option<String>,
197
198    /// A string uniquely identifying the system image originally
199    /// used as the installation base
200    pub build_id: Option<String>,
201
202    /// A lower-case string, identifying a specific image of the OS.
203    /// This is supposed to be used for envs where OS images are
204    /// prepared, built, shipped and updated as comprehensive,
205    /// consistent OS images
206    pub image_id: Option<String>,
207
208    /// A lower-case string identifying the OS image version. This is
209    /// supposed to be used together with `image_id` describes above,
210    /// to discern different versions of the same image
211    pub image_version: Option<String>,
212
213    /// Home URL of installed OS
214    pub home_url: Option<String>,
215
216    /// Documentation URL of installed OS
217    pub documentation_url: Option<String>,
218
219    /// Support URL of installed OS
220    pub support_url: Option<String>,
221
222    /// URL for bug reports
223    pub bug_report_url: Option<String>,
224
225    /// URL with information about privacy policy of the installed OS
226    pub privacy_policy_url: Option<String>,
227
228    /// A string, specifying the name of an icon as defined by
229    /// [freedesktop.org Icon Theme Specification](http://standards.freedesktop.org/icon-theme-spec/latest)
230    pub logo: Option<String>,
231
232    /// Default hostname if `hostname(5)` isn't present and no other
233    /// configuration source specifies the hostname
234    pub default_hostname: Option<String>,
235
236    /// A lower-case string identifying the OS extensions support
237    /// level, to indicate which extension images are supported.
238    ///
239    /// See [systemd-sysext(8)](https://www.freedesktop.org/software/systemd/man/249/systemd-sysext.html#) for more information
240    pub sysext_level: Option<String>,
241}
242
243impl OsRelease {
244    pub fn new() -> Result<Self> {
245        let chunks = get_chunks_osrelease(read_to_string("/etc/os-release")?);
246        let mut osr = Self::default();
247        for chunk in chunks {
248            parse_osrelease(&mut osr, chunk);
249        }
250        Ok(osr)
251    }
252}
253
254impl ToJson for OsRelease {}
255
256fn get_chunks_osrelease(contents: String) -> Vec<(Option<String>, Option<String>)> {
257    contents
258        .lines()
259        .map(|item| {
260            let mut items = item.split('=').map(sanitize_str);
261            (items.next(), items.next())
262        })
263        .collect::<Vec<_>>()
264}
265
266fn parse_osrelease(osr: &mut OsRelease, chunk: (Option<String>, Option<String>)) {
267    match chunk {
268        (Some(key), Some(val)) => {
269            let key = &key as &str;
270            match key {
271                "NAME" => osr.name = val.to_string(),
272                "ID" => osr.id = Some(val.to_string()),
273                "ID_LIKE" => osr.id_like = Some(val.to_string()),
274                "PRETTY_NAME" => osr.pretty_name = Some(val.to_string()),
275                "CPE_NAME" => osr.cpe_name = Some(val.to_string()),
276                "VARIANT" => osr.variant = Some(val.to_string()),
277                "VARIANT_ID" => osr.variant_id = Some(val.to_string()),
278                "VERSION" => osr.version = Some(val.to_string()),
279                "VERSION_CODENAME" => osr.version_codename = Some(val.to_string()),
280                "VERSION_ID" => osr.version_id = Some(val.to_string()),
281                "BUILD_ID" => osr.build_id = Some(val.to_string()),
282                "IMAGE_ID" => osr.image_id = Some(val.to_string()),
283                "IMAGE_VERSION" => osr.image_version = Some(val.to_string()),
284                "HOME_URL" => osr.home_url = Some(val.to_string()),
285                "DOCUMENTATION_URL" => osr.documentation_url = Some(val.to_string()),
286                "SUPPORT_URL" => osr.support_url = Some(val.to_string()),
287                "BUG_REPORT_URL" => osr.bug_report_url = Some(val.to_string()),
288                "PRIVACY_POLICY_URL" => osr.privacy_policy_url = Some(val.to_string()),
289                "LOGO" => osr.logo = Some(val.to_string()),
290                "DEFAULT_HOSTNAME" => osr.default_hostname = Some(val.to_string()),
291                "SYSEXT_LEVEL" => osr.sysext_level = Some(val.to_string()),
292                _ => {}
293            }
294        }
295        _ => {}
296    }
297}
298
299/// System uptime
300#[derive(Debug, Serialize, Clone)]
301pub struct Uptime(
302    /// Uptime
303    pub f32,
304    /// Downtime
305    pub f32,
306);
307
308impl Uptime {
309    pub fn new() -> Result<Self> {
310        let data = read_to_string("/proc/uptime")?;
311        let mut chunks = data.split_whitespace();
312        match (chunks.next(), chunks.next()) {
313            (Some(a), Some(b)) => Ok(Self(a.parse()?, b.parse()?)),
314            _ => Err(anyhow!("`/proc/uptime` file format is incorrect!")),
315        }
316    }
317}
318
319impl ToPlainText for Uptime {
320    fn to_plain(&self) -> String {
321        format!(
322            "\nUptime: {} seconds; downtime: {} seconds\n",
323            self.0, self.1
324        )
325    }
326}
327
328/// System load (average)
329#[derive(Debug, Serialize, Clone)]
330pub struct LoadAVG(
331    /// 1minute
332    pub f32,
333    /// 5minutes
334    pub f32,
335    /// 15minutes
336    pub f32,
337);
338
339impl LoadAVG {
340    pub fn new() -> Result<Self> {
341        let data = read_to_string("/proc/loadavg")?;
342        let mut chunks = data.split_whitespace();
343        match (chunks.next(), chunks.next(), chunks.next()) {
344            (Some(a), Some(b), Some(c)) => Ok(Self(a.parse()?, b.parse()?, c.parse()?)),
345            _ => Err(anyhow!("`/proc/loadavg` file format is incorrect!")),
346        }
347    }
348}
349
350impl ToPlainText for LoadAVG {
351    fn to_plain(&self) -> String {
352        let mut s = format!("\nAverage system load:\n");
353        s += &print_val("1 minute", &self.0);
354        s += &print_val("5 minutes", &self.1);
355        s += &print_val("15 minutes", &self.2);
356
357        s
358    }
359}
360
361/// Information about users
362#[derive(Debug, Serialize, Clone)]
363pub struct Users {
364    pub users: Vec<User>,
365}
366
367impl ToJson for Users {}
368
369impl Users {
370    pub fn new() -> Result<Self> {
371        let mut users = vec![];
372        for user in read_to_string("/etc/passwd")?.lines() {
373            match User::try_from(user) {
374                Ok(user) => users.push(user),
375                Err(_) => continue,
376            }
377        }
378
379        Ok(Self { users })
380    }
381}
382
383/// Information about followed user
384#[derive(Debug, Serialize, Clone)]
385pub struct User {
386    /// User's login name (case-sensitive, 1-32 characters)
387    pub name: String,
388
389    /// User ID
390    ///
391    /// ## Examples
392    /// | UID   | User name     |
393    /// |:-----:|---------------|
394    /// | 0     | `root`        |
395    /// | 1-999 | System users  |
396    /// | 1000+ | Regular users |
397    pub uid: u32,
398
399    /// Group ID links to `/etc/group` ([`Groups`]). Defines default
400    /// group ownership for new files
401    pub gid: u32,
402
403    /// Optional comment field (traditionally for user info). Often
404    /// holds:
405    ///
406    /// - Full name;
407    /// - Room number;
408    /// - Contact info;
409    ///
410    ///  Multiple entries comma-separated.
411    pub gecos: Option<String>,
412
413    /// Absolute path to the user's home directory
414    pub home_dir: String,
415
416    /// Absolute path to the user's default shell (e.g., `/bin/bash`).
417    /// If set to `/usr/sbin/nologin` or `/bin/false`, the user cannot
418    /// log in
419    pub login_shell: String,
420}
421
422impl TryFrom<&str> for User {
423    type Error = anyhow::Error;
424
425    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
426        let chunks = value
427            .trim()
428            .split(':')
429            .map(sanitize_str)
430            .collect::<Vec<_>>();
431        if chunks.len() != 7 {
432            return Err(anyhow!("Field \"{value}\" is incorrect user entry"));
433        }
434
435        Ok(Self {
436            name: sanitize_str(&chunks[0]),
437            uid: chunks[2].parse()?,
438            gid: chunks[3].parse()?,
439            gecos: match chunks[4].is_empty() {
440                true => None,
441                false => Some(sanitize_str(&chunks[4])),
442            },
443            home_dir: sanitize_str(&chunks[5]),
444            login_shell: sanitize_str(&chunks[6]),
445        })
446    }
447}
448
449impl ToPlainText for User {
450    fn to_plain(&self) -> String {
451        let mut s = format!("\nUser '{}':\n", &self.name);
452        s += &print_val("User ID", &self.uid);
453        s += &print_val("Group ID", &self.gid);
454        s += &print_opt_val("GECOS", &self.gecos);
455        s += &print_val("Home directory", &self.home_dir);
456        s += &print_val("Login shell", &self.login_shell);
457
458        s
459    }
460}
461
462/// Information about groups
463#[derive(Debug, Serialize, Clone)]
464pub struct Groups {
465    pub groups: Vec<Group>,
466}
467
468impl ToJson for Groups {}
469
470impl Groups {
471    pub fn new() -> Result<Self> {
472        let mut groups = vec![];
473        for group in read_to_string("/etc/group")?.lines() {
474            match Group::try_from(group) {
475                Ok(group) => groups.push(group),
476                Err(_) => continue,
477            }
478        }
479        Ok(Self { groups })
480    }
481}
482
483/// Information about followed group
484#[derive(Debug, Serialize, Clone)]
485pub struct Group {
486    /// Group name
487    pub name: String,
488
489    /// Group ID
490    pub gid: u32,
491
492    /// List of users (links to `/etc/passwd` ([`Users`])) in this
493    /// group
494    pub users: Vec<String>,
495}
496
497impl TryFrom<&str> for Group {
498    type Error = anyhow::Error;
499
500    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
501        let chunks = value
502            .trim()
503            .split(':')
504            .map(sanitize_str)
505            .collect::<Vec<_>>();
506        if chunks.len() != 4 {
507            return Err(anyhow!("Field \"{value}\" is incorrect group entry"));
508        }
509
510        Ok(Self {
511            name: chunks[0].to_string(),
512            gid: chunks[2].parse()?,
513            users: {
514                let mut users = vec![];
515                for user in chunks[3].split(',') {
516                    if !user.is_empty() {
517                        users.push(user.to_string());
518                    }
519                }
520                users
521            },
522        })
523    }
524}
525
526/// List of installed console shells
527pub type Shells = Vec<String>;
528
529fn get_shells() -> Result<Shells> {
530    let mut shells = vec![];
531    for shell in read_to_string("/etc/shells")?
532        .lines()
533        .filter(|line| !line.is_empty() && !line.starts_with('#'))
534    {
535        shells.push(shell.to_string());
536    }
537    Ok(shells)
538}
539
540/// Host name
541pub type HostName = String;
542
543pub fn get_hostname() -> Option<HostName> {
544    match read_to_string("/etc/hostname") {
545        Ok(s) => Some(sanitize_str(&s)),
546        Err(_) => None,
547    }
548}
549
550/// Information about current locale
551#[derive(Debug, Serialize, Clone)]
552pub struct Locale {}
553
554fn sanitize_str(s: &str) -> String {
555    s.trim().replace('"', "").replace('\'', "")
556}