Skip to main content

sysinfo/unix/linux/
system.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use crate::sys::cpu::{CpusWrapper, get_physical_core_count};
4use crate::sys::process::{compute_cpu_usage, refresh_procs};
5use crate::sys::utils::{get_all_utf8_data, to_u64};
6use crate::{
7    Cpu, CpuRefreshKind, Error, LoadAvg, MemoryRefreshKind, Pid, Process, ProcessRefreshKind,
8    ProcessesToUpdate,
9};
10
11use libc::{self, _SC_CLK_TCK, _SC_HOST_NAME_MAX, _SC_PAGESIZE, c_char, sysconf};
12
13use std::collections::HashMap;
14use std::ffi::CStr;
15use std::fs::File;
16use std::io::Read;
17use std::mem::MaybeUninit;
18use std::path::Path;
19use std::str::FromStr;
20use std::time::Duration;
21
22unsafe fn getrlimit() -> Option<libc::rlimit> {
23    let mut limits = libc::rlimit {
24        rlim_cur: 0,
25        rlim_max: 0,
26    };
27
28    if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limits) } != 0 {
29        None
30    } else {
31        Some(limits)
32    }
33}
34
35declare_signals! {
36    libc::c_int,
37    Signal::Hangup => libc::SIGHUP,
38    Signal::Interrupt => libc::SIGINT,
39    Signal::Quit => libc::SIGQUIT,
40    Signal::Illegal => libc::SIGILL,
41    Signal::Trap => libc::SIGTRAP,
42    Signal::Abort => libc::SIGABRT,
43    Signal::IOT => libc::SIGIOT,
44    Signal::Bus => libc::SIGBUS,
45    Signal::FloatingPointException => libc::SIGFPE,
46    Signal::Kill => libc::SIGKILL,
47    Signal::User1 => libc::SIGUSR1,
48    Signal::Segv => libc::SIGSEGV,
49    Signal::User2 => libc::SIGUSR2,
50    Signal::Pipe => libc::SIGPIPE,
51    Signal::Alarm => libc::SIGALRM,
52    Signal::Term => libc::SIGTERM,
53    Signal::Child => libc::SIGCHLD,
54    Signal::Continue => libc::SIGCONT,
55    Signal::Stop => libc::SIGSTOP,
56    Signal::TSTP => libc::SIGTSTP,
57    Signal::TTIN => libc::SIGTTIN,
58    Signal::TTOU => libc::SIGTTOU,
59    Signal::Urgent => libc::SIGURG,
60    Signal::XCPU => libc::SIGXCPU,
61    Signal::XFSZ => libc::SIGXFSZ,
62    Signal::VirtualAlarm => libc::SIGVTALRM,
63    Signal::Profiling => libc::SIGPROF,
64    Signal::Winch => libc::SIGWINCH,
65    Signal::IO => libc::SIGIO,
66    Signal::Poll => libc::SIGPOLL,
67    Signal::Power => libc::SIGPWR,
68    Signal::Sys => libc::SIGSYS,
69}
70
71#[doc = include_str!("../../../md_doc/supported_signals.md")]
72pub const SUPPORTED_SIGNALS: &[crate::Signal] = supported_signals();
73#[doc = include_str!("../../../md_doc/minimum_cpu_update_interval.md")]
74pub const MINIMUM_CPU_UPDATE_INTERVAL: Duration = Duration::from_millis(200);
75
76fn boot_time() -> Result<u64, Error> {
77    if let Ok(buf) = File::open("/proc/stat").and_then(|mut f| {
78        let mut buf = Vec::new();
79        f.read_to_end(&mut buf)?;
80        Ok(buf)
81    }) {
82        let line = buf.split(|c| *c == b'\n').find(|l| l.starts_with(b"btime"));
83
84        if let Some(line) = line
85            && let Some(btime) = line
86                .split(|x| *x == b' ')
87                .filter(|s| !s.is_empty())
88                .nth(1)
89                .map(to_u64)
90        {
91            return Ok(btime);
92        }
93    }
94    // Either we didn't find "btime" or "/proc/stat" wasn't available for some reason...
95    unsafe {
96        let mut up: libc::timespec = std::mem::zeroed();
97        if libc::clock_gettime(libc::CLOCK_BOOTTIME, &mut up) == 0 {
98            Ok(up.tv_sec as u64)
99        } else {
100            Err(Error::from(
101                "clock_gettime failed: boot time cannot be retrieved",
102            ))
103        }
104    }
105}
106
107pub(crate) struct SystemInfo {
108    pub(crate) page_size_b: u64,
109    pub(crate) clock_cycle: u64,
110    pub(crate) boot_time: u64,
111}
112
113impl SystemInfo {
114    fn new() -> Self {
115        unsafe {
116            Self {
117                page_size_b: sysconf(_SC_PAGESIZE) as _,
118                clock_cycle: sysconf(_SC_CLK_TCK) as _,
119                boot_time: boot_time().unwrap_or(0),
120            }
121        }
122    }
123}
124
125pub(crate) struct SystemInner {
126    process_list: HashMap<Pid, Process>,
127    mem_total: u64,
128    mem_free: u64,
129    mem_available: u64,
130    mem_buffers: u64,
131    mem_page_cache: u64,
132    mem_shmem: u64,
133    mem_slab_reclaimable: u64,
134    swap_total: u64,
135    swap_free: u64,
136    info: SystemInfo,
137    cpus: CpusWrapper,
138}
139
140impl SystemInner {
141    /// It is sometime possible that a CPU usage computation is bigger than
142    /// `"number of CPUs" * 100`.
143    ///
144    /// To prevent that, we compute ahead of time this maximum value and ensure that processes'
145    /// CPU usage don't go over it.
146    fn get_max_process_cpu_usage(&self) -> f32 {
147        self.cpus.len() as f32 * 100.
148    }
149
150    fn update_procs_cpu(&mut self, refresh_kind: ProcessRefreshKind) {
151        if !refresh_kind.cpu() {
152            return;
153        }
154        self.cpus
155            .refresh_if_needed(true, CpuRefreshKind::nothing().with_usage());
156
157        if self.cpus.is_empty() {
158            sysinfo_debug!("cannot compute processes CPU usage: no CPU found...");
159            return;
160        }
161        let (new, old) = self.cpus.get_global_raw_times();
162        let total_time = if old > new { 1 } else { new - old };
163        let total_time = total_time as f32 / self.cpus.len() as f32;
164        let max_value = self.get_max_process_cpu_usage();
165
166        for proc_ in self.process_list.values_mut() {
167            compute_cpu_usage(&mut proc_.inner, total_time, max_value);
168        }
169    }
170
171    fn refresh_cpus(&mut self, only_update_global_cpu: bool, refresh_kind: CpuRefreshKind) {
172        self.cpus.refresh(only_update_global_cpu, refresh_kind);
173    }
174}
175
176impl SystemInner {
177    pub(crate) fn new() -> Result<Self, Error> {
178        Ok(Self {
179            process_list: HashMap::new(),
180            mem_total: 0,
181            mem_free: 0,
182            mem_available: 0,
183            mem_buffers: 0,
184            mem_page_cache: 0,
185            mem_shmem: 0,
186            mem_slab_reclaimable: 0,
187            swap_total: 0,
188            swap_free: 0,
189            cpus: CpusWrapper::new(),
190            info: SystemInfo::new(),
191        })
192    }
193
194    pub(crate) fn refresh_memory_specifics(&mut self, refresh_kind: MemoryRefreshKind) {
195        if !refresh_kind.ram() && !refresh_kind.swap() {
196            return;
197        }
198        let mut mem_available_found = false;
199        read_table("/proc/meminfo", ':', |key, value_kib| {
200            let field = match key {
201                "MemTotal" => &mut self.mem_total,
202                "MemFree" => &mut self.mem_free,
203                "MemAvailable" => {
204                    mem_available_found = true;
205                    &mut self.mem_available
206                }
207                "Buffers" => &mut self.mem_buffers,
208                "Cached" => &mut self.mem_page_cache,
209                "Shmem" => &mut self.mem_shmem,
210                "SReclaimable" => &mut self.mem_slab_reclaimable,
211                "SwapTotal" => &mut self.swap_total,
212                "SwapFree" => &mut self.swap_free,
213                _ => return,
214            };
215            // /proc/meminfo reports KiB, though it says "kB". Convert it.
216            *field = value_kib.saturating_mul(1_024);
217        });
218
219        // Linux < 3.14 may not have MemAvailable in /proc/meminfo
220        // So it should fallback to the old way of estimating available memory
221        // https://github.com/KittyKatt/screenFetch/issues/386#issuecomment-249312716
222        if !mem_available_found {
223            self.mem_available = self
224                .mem_free
225                .saturating_add(self.mem_buffers)
226                .saturating_add(self.mem_page_cache)
227                .saturating_add(self.mem_slab_reclaimable)
228                .saturating_sub(self.mem_shmem);
229        }
230    }
231
232    pub(crate) fn cgroup_limits(&self) -> Option<crate::CGroupLimits> {
233        crate::sys::cgroup::limits_for_system()
234    }
235
236    pub(crate) fn refresh_cpu_specifics(&mut self, refresh_kind: CpuRefreshKind) {
237        self.refresh_cpus(false, refresh_kind);
238    }
239
240    pub(crate) fn refresh_processes_specifics(
241        &mut self,
242        processes_to_update: ProcessesToUpdate<'_>,
243        refresh_kind: ProcessRefreshKind,
244    ) -> usize {
245        let uptime = Self::uptime().unwrap_or(0);
246        let nb_updated = refresh_procs(
247            &mut self.process_list,
248            Path::new("/proc"),
249            uptime,
250            &self.info,
251            processes_to_update,
252            refresh_kind,
253        );
254        self.update_procs_cpu(refresh_kind);
255        nb_updated
256    }
257
258    // COMMON PART
259    //
260    // Need to be moved into a "common" file to avoid duplication.
261
262    pub(crate) fn processes(&self) -> &HashMap<Pid, Process> {
263        &self.process_list
264    }
265
266    pub(crate) fn processes_mut(&mut self) -> &mut HashMap<Pid, Process> {
267        &mut self.process_list
268    }
269
270    pub(crate) fn process(&self, pid: Pid) -> Option<&Process> {
271        self.process_list.get(&pid)
272    }
273
274    pub(crate) fn global_cpu_usage(&self) -> f32 {
275        self.cpus.global_cpu.usage()
276    }
277
278    pub(crate) fn cpus(&self) -> &[Cpu] {
279        &self.cpus.cpus
280    }
281
282    pub(crate) fn total_memory(&self) -> u64 {
283        self.mem_total
284    }
285
286    pub(crate) fn free_memory(&self) -> u64 {
287        self.mem_free
288    }
289
290    pub(crate) fn available_memory(&self) -> u64 {
291        self.mem_available
292    }
293
294    pub(crate) fn used_memory(&self) -> u64 {
295        self.mem_total - self.mem_available
296    }
297
298    pub(crate) fn total_swap(&self) -> u64 {
299        self.swap_total
300    }
301
302    pub(crate) fn free_swap(&self) -> u64 {
303        self.swap_free
304    }
305
306    // need to be checked
307    pub(crate) fn used_swap(&self) -> u64 {
308        self.swap_total - self.swap_free
309    }
310
311    pub(crate) fn uptime() -> Result<u64, Error> {
312        if cfg!(not(target_os = "android"))
313            && let Ok(content) = get_all_utf8_data("/proc/uptime", 50)
314            && let Some(uptime) = content.split('.').next().and_then(|t| t.parse().ok())
315        {
316            Ok(uptime)
317        } else {
318            Self::uptime_with_sysinfo()
319        }
320    }
321
322    fn uptime_with_sysinfo() -> Result<u64, Error> {
323        unsafe {
324            let mut s = MaybeUninit::<libc::sysinfo>::uninit();
325            if libc::sysinfo(s.as_mut_ptr()) != 0 {
326                Err(Error::from("sysinfo call failed"))
327            } else {
328                let s = s.assume_init();
329                Ok(if s.uptime < 1 { 0 } else { s.uptime as u64 })
330            }
331        }
332    }
333
334    pub(crate) fn boot_time() -> Result<u64, Error> {
335        boot_time()
336    }
337
338    pub(crate) fn load_average() -> Result<LoadAvg, Error> {
339        let mut s = String::new();
340        File::open("/proc/loadavg").and_then(|mut f| f.read_to_string(&mut s))?;
341        let loads = s
342            .trim()
343            .split(' ')
344            .take(3)
345            .filter_map(|val| val.parse::<f64>().ok())
346            .collect::<Vec<f64>>();
347        Ok(match *loads.as_slice() {
348            [one, five, fifteen, ..] => LoadAvg { one, five, fifteen },
349            [one, five] => LoadAvg {
350                one,
351                five,
352                fifteen: 0.,
353            },
354            [one] => LoadAvg {
355                one,
356                five: 0.,
357                fifteen: 0.,
358            },
359            [] => LoadAvg {
360                one: 0.,
361                five: 0.,
362                fifteen: 0.,
363            },
364        })
365    }
366
367    #[cfg(not(target_os = "android"))]
368    pub(crate) fn name() -> Result<String, Error> {
369        match get_system_info_linux(
370            InfoType::Name,
371            Path::new("/etc/os-release"),
372            Path::new("/etc/lsb-release"),
373        ) {
374            Some(info) => Ok(info),
375            None => Err(Error::Other("failed to retrieve system name".into())),
376        }
377    }
378
379    #[cfg(target_os = "android")]
380    pub(crate) fn name() -> Result<String, Error> {
381        match get_system_info_android(InfoType::Name) {
382            Some(info) => Ok(info),
383            None => Err(Error::Other("failed to retrieve system name".into())),
384        }
385    }
386
387    #[cfg(not(target_os = "android"))]
388    pub(crate) fn long_os_version() -> Result<String, Error> {
389        let mut long_name = "Linux".to_owned();
390
391        let distro_name = Self::name();
392        let distro_version = Self::os_version();
393        if let Ok(distro_version) = &distro_version {
394            // "Linux (Ubuntu 24.04)"
395            long_name.push_str(" (");
396            long_name.push_str(distro_name.as_deref().unwrap_or("unknown"));
397            long_name.push(' ');
398            long_name.push_str(distro_version);
399            long_name.push(')');
400        } else if let Ok(distro_name) = &distro_name {
401            // "Linux (Ubuntu)"
402            long_name.push_str(" (");
403            long_name.push_str(distro_name);
404            long_name.push(')');
405        }
406
407        Ok(long_name)
408    }
409
410    #[cfg(target_os = "android")]
411    pub(crate) fn long_os_version() -> Result<String, Error> {
412        let mut long_name = "Android".to_owned();
413
414        if let Ok(os_version) = Self::os_version() {
415            long_name.push(' ');
416            long_name.push_str(&os_version);
417        }
418
419        // Android's name() is extracted from the system property "ro.product.model"
420        // which is documented as "The end-user-visible name for the end product."
421        // So this produces a long_os_version like "Android 15 on Pixel 9 Pro".
422        if let Ok(product_name) = Self::name() {
423            long_name.push_str(" on ");
424            long_name.push_str(&product_name);
425        }
426
427        Ok(long_name)
428    }
429
430    pub(crate) fn host_name() -> Result<String, Error> {
431        unsafe {
432            let hostname_max = sysconf(_SC_HOST_NAME_MAX);
433            let mut buffer = vec![0_u8; hostname_max as usize];
434            if libc::gethostname(buffer.as_mut_ptr() as *mut c_char, buffer.len()) == 0 {
435                if let Some(pos) = buffer.iter().position(|x| *x == 0) {
436                    // Shrink buffer to terminate the null bytes
437                    buffer.resize(pos, 0);
438                }
439                if let Ok(info) = String::from_utf8(buffer) {
440                    return Ok(info);
441                }
442            } else {
443                sysinfo_debug!("gethostname failed: hostname cannot be retrieved...");
444            }
445        }
446        Err(Error::Other("failed to retrieve host name".into()))
447    }
448
449    pub(crate) fn kernel_version() -> Result<String, Error> {
450        let mut raw = MaybeUninit::<libc::utsname>::zeroed();
451
452        unsafe {
453            if libc::uname(raw.as_mut_ptr()) == 0 {
454                let info = raw.assume_init();
455
456                let release = info
457                    .release
458                    .iter()
459                    .filter(|c| **c != 0)
460                    .map(|c| *c as u8 as char)
461                    .collect::<String>();
462
463                Ok(release)
464            } else {
465                Err(Error::Other("failed to retrieve kernel version".into()))
466            }
467        }
468    }
469
470    #[cfg(not(target_os = "android"))]
471    pub(crate) fn os_version() -> Result<String, Error> {
472        match get_system_info_linux(
473            InfoType::OsVersion,
474            Path::new("/etc/os-release"),
475            Path::new("/etc/lsb-release"),
476        ) {
477            Some(info) => Ok(info),
478            None => Err(Error::Other("failed to retrieve OS version".into())),
479        }
480    }
481
482    #[cfg(target_os = "android")]
483    pub(crate) fn os_version() -> Result<String, Error> {
484        match get_system_info_android(InfoType::OsVersion) {
485            Some(info) => Ok(info),
486            None => Err(Error::Other("failed to retrieve OS version".into())),
487        }
488    }
489
490    #[cfg(not(target_os = "android"))]
491    pub(crate) fn distribution_id() -> String {
492        get_system_info_linux(
493            InfoType::DistributionID,
494            Path::new("/etc/os-release"),
495            Path::new(""),
496        )
497        .unwrap_or_else(|| std::env::consts::OS.to_owned())
498    }
499
500    #[cfg(target_os = "android")]
501    pub(crate) fn distribution_id() -> String {
502        // Currently get_system_info_android doesn't support InfoType::DistributionID and always
503        // returns None. This call is done anyway for consistency with non-Android implementation
504        // and to suppress dead-code warning for DistributionID on Android.
505        get_system_info_android(InfoType::DistributionID)
506            .unwrap_or_else(|| std::env::consts::OS.to_owned())
507    }
508
509    #[cfg(not(target_os = "android"))]
510    pub(crate) fn distribution_id_like() -> Vec<String> {
511        system_info_as_list(get_system_info_linux(
512            InfoType::DistributionIDLike,
513            Path::new("/etc/os-release"),
514            Path::new(""),
515        ))
516    }
517
518    #[cfg(target_os = "android")]
519    pub(crate) fn distribution_id_like() -> Vec<String> {
520        // Currently get_system_info_android doesn't support InfoType::DistributionIDLike and always
521        // returns None. This call is done anyway for consistency with non-Android implementation
522        // and to suppress dead-code warning for DistributionIDLike on Android.
523        system_info_as_list(get_system_info_android(InfoType::DistributionIDLike))
524    }
525
526    #[cfg(not(target_os = "android"))]
527    pub(crate) fn kernel_name() -> Option<&'static str> {
528        Some("Linux")
529    }
530
531    #[cfg(target_os = "android")]
532    pub(crate) fn kernel_name() -> Option<&'static str> {
533        Some("Android kernel")
534    }
535
536    pub(crate) fn cpu_arch() -> Option<String> {
537        let mut raw = MaybeUninit::<libc::utsname>::uninit();
538
539        unsafe {
540            if libc::uname(raw.as_mut_ptr()) != 0 {
541                return None;
542            }
543            let info = raw.assume_init();
544            // Converting `&[i8]` to `&[u8]`.
545            let machine: &[u8] =
546                std::slice::from_raw_parts(info.machine.as_ptr() as *const _, info.machine.len());
547
548            CStr::from_bytes_until_nul(machine)
549                .ok()
550                .and_then(|res| match res.to_str() {
551                    Ok(arch) => Some(arch.to_string()),
552                    Err(_) => None,
553                })
554        }
555    }
556
557    pub(crate) fn physical_core_count() -> Result<usize, Error> {
558        get_physical_core_count()
559    }
560
561    pub(crate) fn refresh_cpu_list(&mut self, refresh_kind: CpuRefreshKind) {
562        self.cpus = CpusWrapper::new();
563        self.refresh_cpu_specifics(refresh_kind);
564    }
565
566    pub(crate) fn open_files_limit() -> Result<usize, Error> {
567        unsafe {
568            match getrlimit() {
569                Some(limits) => Ok(limits.rlim_cur as _),
570                None => {
571                    sysinfo_debug!("getrlimit failed");
572                    Err(Error::Other("failed to retrieve open files limit".into()))
573                }
574            }
575        }
576    }
577}
578
579fn read_table<F>(filename: &str, colsep: char, mut f: F)
580where
581    F: FnMut(&str, u64),
582{
583    if let Ok(content) = get_all_utf8_data(filename, 16_635) {
584        content
585            .split('\n')
586            .flat_map(|line| {
587                let mut split = line.split(colsep);
588                let key = split.next()?;
589                let value = split.next()?;
590                let value0 = value.trim_start().split(' ').next()?;
591                let value0_u64 = u64::from_str(value0).ok()?;
592                Some((key, value0_u64))
593            })
594            .for_each(|(k, v)| f(k, v));
595    }
596}
597
598#[derive(PartialEq, Eq)]
599enum InfoType {
600    /// The end-user friendly name of:
601    /// - Android: The device model
602    /// - Linux: The distributions name
603    Name,
604    OsVersion,
605    /// Machine-parseable ID of a distribution, see
606    /// https://www.freedesktop.org/software/systemd/man/os-release.html#ID=
607    DistributionID,
608    /// Machine-parseable ID_LIKE of related distributions, see
609    /// <https://www.freedesktop.org/software/systemd/man/latest/os-release.html#ID_LIKE=>
610    DistributionIDLike,
611}
612
613#[cfg(not(target_os = "android"))]
614fn get_system_info_linux(info: InfoType, path: &Path, fallback_path: &Path) -> Option<String> {
615    if let Ok(buf) = File::open(path).and_then(|mut f| {
616        let mut buf = String::new();
617        f.read_to_string(&mut buf)?;
618        Ok(buf)
619    }) {
620        let info_str = match info {
621            InfoType::Name => "NAME=",
622            InfoType::OsVersion => "VERSION_ID=",
623            InfoType::DistributionID => "ID=",
624            InfoType::DistributionIDLike => "ID_LIKE=",
625        };
626
627        for line in buf.lines() {
628            if let Some(stripped) = line.strip_prefix(info_str) {
629                return Some(stripped.replace('"', ""));
630            }
631        }
632    }
633
634    // Fallback to `/etc/lsb-release` file for systems where VERSION_ID is not included.
635    // VERSION_ID is not required in the `/etc/os-release` file
636    // per https://www.linux.org/docs/man5/os-release.html
637    // If this fails for some reason, fallback to None
638    let buf = File::open(fallback_path)
639        .and_then(|mut f| {
640            let mut buf = String::new();
641            f.read_to_string(&mut buf)?;
642            Ok(buf)
643        })
644        .ok()?;
645
646    let info_str = match info {
647        InfoType::OsVersion => "DISTRIB_RELEASE=",
648        InfoType::Name => "DISTRIB_ID=",
649        InfoType::DistributionID => {
650            // lsb-release is inconsistent with os-release and unsupported.
651            return None;
652        }
653        InfoType::DistributionIDLike => {
654            // lsb-release doesn't support ID_LIKE.
655            return None;
656        }
657    };
658    for line in buf.lines() {
659        if let Some(stripped) = line.strip_prefix(info_str) {
660            return Some(stripped.replace('"', ""));
661        }
662    }
663    None
664}
665
666/// Returns a system info value as a list of strings.
667/// Absence of a value is treated as an empty list.
668fn system_info_as_list(sysinfo: Option<String>) -> Vec<String> {
669    match sysinfo {
670        Some(value) => value.split_ascii_whitespace().map(String::from).collect(),
671        // For list fields absence of a field is equivalent to an empty list.
672        None => Vec::new(),
673    }
674}
675
676#[cfg(target_os = "android")]
677fn get_system_info_android(info: InfoType) -> Option<String> {
678    // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/os/Build.java#58
679    let name: &'static [u8] = match info {
680        InfoType::Name => b"ro.product.model\0",
681        InfoType::OsVersion => b"ro.build.version.release\0",
682        InfoType::DistributionID => {
683            // Not supported.
684            return None;
685        }
686        InfoType::DistributionIDLike => {
687            // Not supported.
688            return None;
689        }
690    };
691
692    let mut value_buffer = vec![0u8; libc::PROP_VALUE_MAX as usize];
693    unsafe {
694        let len = libc::__system_property_get(
695            name.as_ptr() as *const c_char,
696            value_buffer.as_mut_ptr() as *mut c_char,
697        );
698
699        if len != 0 {
700            if let Some(pos) = value_buffer.iter().position(|c| *c == 0) {
701                value_buffer.resize(pos, 0);
702            }
703            String::from_utf8(value_buffer).ok()
704        } else {
705            None
706        }
707    }
708}
709
710#[cfg(test)]
711mod test {
712    use super::InfoType;
713    #[cfg(target_os = "android")]
714    use super::get_system_info_android;
715    #[cfg(not(target_os = "android"))]
716    use super::get_system_info_linux;
717    use super::read_table;
718    use super::system_info_as_list;
719    use std::collections::HashMap;
720    use std::io::Write;
721    use tempfile::NamedTempFile;
722
723    #[test]
724    fn test_read_table() {
725        // Create a temporary file with test content
726        let mut file = NamedTempFile::new().unwrap();
727        writeln!(file, "KEY1:100 kB").unwrap();
728        writeln!(file, "KEY2:200 kB").unwrap();
729        writeln!(file, "KEY3:300 kB").unwrap();
730        writeln!(file, "KEY4:invalid").unwrap();
731
732        let file_path = file.path().to_str().unwrap();
733
734        // Test reading the table
735        let mut result = HashMap::new();
736        read_table(file_path, ':', |key, value| {
737            result.insert(key.to_string(), value);
738        });
739
740        assert_eq!(result.get("KEY1"), Some(&100));
741        assert_eq!(result.get("KEY2"), Some(&200));
742        assert_eq!(result.get("KEY3"), Some(&300));
743        assert_eq!(result.get("KEY4"), None);
744
745        // Test with different separator and units
746        let mut file = NamedTempFile::new().unwrap();
747        writeln!(file, "KEY1 400 MB").unwrap();
748        writeln!(file, "KEY2 500 GB").unwrap();
749        writeln!(file, "KEY3 600").unwrap();
750
751        let file_path = file.path().to_str().unwrap();
752
753        let mut result = HashMap::new();
754        read_table(file_path, ' ', |key, value| {
755            result.insert(key.to_string(), value);
756        });
757
758        assert_eq!(result.get("KEY1"), Some(&400));
759        assert_eq!(result.get("KEY2"), Some(&500));
760        assert_eq!(result.get("KEY3"), Some(&600));
761
762        // Test with empty file
763        let file = NamedTempFile::new().unwrap();
764        let file_path = file.path().to_str().unwrap();
765
766        let mut result = HashMap::new();
767        read_table(file_path, ':', |key, value| {
768            result.insert(key.to_string(), value);
769        });
770
771        assert!(result.is_empty());
772
773        // Test with non-existent file
774        let mut result = HashMap::new();
775        read_table("/nonexistent/file", ':', |key, value| {
776            result.insert(key.to_string(), value);
777        });
778
779        assert!(result.is_empty());
780    }
781
782    #[test]
783    #[cfg(target_os = "android")]
784    fn lsb_release_fallback_android() {
785        assert!(get_system_info_android(InfoType::OsVersion).is_some());
786        assert!(get_system_info_android(InfoType::Name).is_some());
787        assert!(get_system_info_android(InfoType::DistributionID).is_none());
788        assert!(get_system_info_android(InfoType::DistributionIDLike).is_none());
789    }
790
791    #[test]
792    #[cfg(not(target_os = "android"))]
793    fn lsb_release_fallback_not_android() {
794        use std::path::Path;
795
796        let dir = tempfile::tempdir().expect("failed to create temporary directory");
797        let tmp1 = dir.path().join("tmp1");
798        let tmp2 = dir.path().join("tmp2");
799
800        // /etc/os-release
801        std::fs::write(
802            &tmp1,
803            r#"NAME="Ubuntu"
804VERSION="20.10 (Groovy Gorilla)"
805ID=ubuntu
806ID_LIKE=debian
807PRETTY_NAME="Ubuntu 20.10"
808VERSION_ID="20.10"
809VERSION_CODENAME=groovy
810UBUNTU_CODENAME=groovy
811"#,
812        )
813        .expect("Failed to create tmp1");
814
815        // /etc/lsb-release
816        std::fs::write(
817            &tmp2,
818            r#"DISTRIB_ID=Ubuntu
819DISTRIB_RELEASE=20.10
820DISTRIB_CODENAME=groovy
821DISTRIB_DESCRIPTION="Ubuntu 20.10"
822"#,
823        )
824        .expect("Failed to create tmp2");
825
826        // Check for the "normal" path: "/etc/os-release"
827        assert_eq!(
828            get_system_info_linux(InfoType::OsVersion, &tmp1, Path::new("")),
829            Some("20.10".to_owned())
830        );
831        assert_eq!(
832            get_system_info_linux(InfoType::Name, &tmp1, Path::new("")),
833            Some("Ubuntu".to_owned())
834        );
835        assert_eq!(
836            get_system_info_linux(InfoType::DistributionID, &tmp1, Path::new("")),
837            Some("ubuntu".to_owned())
838        );
839        assert_eq!(
840            get_system_info_linux(InfoType::DistributionIDLike, &tmp1, Path::new("")),
841            Some("debian".to_owned())
842        );
843
844        // Check for the "fallback" path: "/etc/lsb-release"
845        assert_eq!(
846            get_system_info_linux(InfoType::OsVersion, Path::new(""), &tmp2),
847            Some("20.10".to_owned())
848        );
849        assert_eq!(
850            get_system_info_linux(InfoType::Name, Path::new(""), &tmp2),
851            Some("Ubuntu".to_owned())
852        );
853        assert_eq!(
854            get_system_info_linux(InfoType::DistributionID, Path::new(""), &tmp2),
855            None
856        );
857        assert_eq!(
858            get_system_info_linux(InfoType::DistributionIDLike, Path::new(""), &tmp2),
859            None
860        );
861    }
862
863    #[test]
864    fn test_system_info_as_list() {
865        // No value.
866        assert_eq!(system_info_as_list(None), Vec::<String>::new());
867        // Empty value.
868        assert_eq!(
869            system_info_as_list(Some("".to_string())),
870            Vec::<String>::new(),
871        );
872        // Whitespaces only.
873        assert_eq!(
874            system_info_as_list(Some(" ".to_string())),
875            Vec::<String>::new(),
876        );
877        // Single value.
878        assert_eq!(
879            system_info_as_list(Some("debian".to_string())),
880            vec!["debian".to_string()],
881        );
882        // Multiple values.
883        assert_eq!(
884            system_info_as_list(Some("rhel fedora".to_string())),
885            vec!["rhel".to_string(), "fedora".to_string()],
886        );
887        // Multiple spaces.
888        assert_eq!(
889            system_info_as_list(Some("rhel        fedora".to_string())),
890            vec!["rhel".to_string(), "fedora".to_string()],
891        );
892    }
893}