1use crate::traits::*;
24use crate::utils::read_to_string;
25use anyhow::{Result, anyhow};
26use serde::Serialize;
27use std::env::vars;
28
29#[derive(Debug, Serialize)]
32pub struct Sys {
33 pub kernel: Kernel,
35
36 pub os_release: OsRelease,
38
39 pub machine_id: Option<String>,
41
42 pub timezone: Option<String>,
44
45 pub env_vars: Vec<(String, String)>,
47
48 pub uptime: Uptime,
50
51 pub loadavg: LoadAVG,
53
54 pub users: Users,
56
57 pub groups: Groups,
59
60 pub shells: Shells,
62
63 pub hostname: Option<HostName>,
65 }
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 })
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#[derive(Debug, Serialize, Clone)]
98pub struct Kernel {
99 pub uname: Option<String>, pub cmdline: Option<String>, pub arch: Option<String>, pub version: Option<String>, pub build_info: Option<String>, pub pid_max: u32, pub threads_max: u32, pub user_events_max: Option<u32>, pub enthropy_avail: Option<u16>, }
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#[derive(Debug, Serialize, Default, Clone)]
155pub struct OsRelease {
156 pub name: String,
160
161 pub id: Option<String>,
164
165 pub id_like: Option<String>,
168
169 pub pretty_name: Option<String>,
173
174 pub cpe_name: Option<String>,
176
177 pub variant: Option<String>,
180
181 pub variant_id: Option<String>,
184
185 pub version: Option<String>,
189
190 pub version_id: Option<String>,
193
194 pub version_codename: Option<String>,
197
198 pub build_id: Option<String>,
201
202 pub image_id: Option<String>,
207
208 pub image_version: Option<String>,
212
213 pub home_url: Option<String>,
215
216 pub documentation_url: Option<String>,
218
219 pub support_url: Option<String>,
221
222 pub bug_report_url: Option<String>,
224
225 pub privacy_policy_url: Option<String>,
227
228 pub logo: Option<String>,
231
232 pub default_hostname: Option<String>,
235
236 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#[derive(Debug, Serialize, Clone)]
301pub struct Uptime(
302 pub f32,
304 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#[derive(Debug, Serialize, Clone)]
330pub struct LoadAVG(
331 pub f32,
333 pub f32,
335 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#[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#[derive(Debug, Serialize, Clone)]
385pub struct User {
386 pub name: String,
388
389 pub uid: u32,
398
399 pub gid: u32,
402
403 pub gecos: Option<String>,
412
413 pub home_dir: String,
415
416 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#[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#[derive(Debug, Serialize, Clone)]
485pub struct Group {
486 pub name: String,
488
489 pub gid: u32,
491
492 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
526pub 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
540pub 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#[derive(Debug, Serialize, Clone)]
552pub struct Locale {}
553
554fn sanitize_str(s: &str) -> String {
555 s.trim().replace('"', "").replace('\'', "")
556}