Skip to main content

procutils_common/
utmp.rs

1//! Reader for the `utmp(5)` binary file format used by `/var/run/utmp`.
2//!
3//! The on-disk struct layout is set by the system's libc; we use
4//! [`libc::utmpx`] for the field offsets and entry stride so the parser
5//! is correct on every glibc-supported architecture (the entry size is
6//! 384 bytes on x86_64 but 400 bytes on aarch64, for example).
7
8use std::{
9    ffi::CStr,
10    fs, io,
11    path::Path,
12    time::{Duration, SystemTime, UNIX_EPOCH},
13};
14
15pub const EMPTY: i16 = libc::EMPTY;
16pub const RUN_LVL: i16 = libc::RUN_LVL;
17pub const BOOT_TIME: i16 = libc::BOOT_TIME;
18pub const NEW_TIME: i16 = libc::NEW_TIME;
19pub const OLD_TIME: i16 = libc::OLD_TIME;
20pub const INIT_PROCESS: i16 = libc::INIT_PROCESS;
21pub const LOGIN_PROCESS: i16 = libc::LOGIN_PROCESS;
22pub const USER_PROCESS: i16 = libc::USER_PROCESS;
23pub const DEAD_PROCESS: i16 = libc::DEAD_PROCESS;
24
25/// One parsed entry from a utmp file.
26#[derive(Debug, Clone)]
27pub struct UtmpEntry {
28    pub ut_type: i16,
29    pub pid: i32,
30    /// Terminal device basename (e.g. `tty2`, `pts/0`). Trimmed of trailing NULs.
31    pub line: String,
32    /// 4-character session id.
33    pub id: String,
34    /// Login name.
35    pub user: String,
36    /// Remote host or display, if any.
37    pub host: String,
38    /// Login timestamp.
39    pub login_time: SystemTime,
40}
41
42/// Default location of the system utmp database.
43pub const DEFAULT_UTMP_PATH: &str = "/var/run/utmp";
44
45/// Read and parse all entries from a utmp file.
46pub fn read(path: impl AsRef<Path>) -> io::Result<Vec<UtmpEntry>> {
47    let data = fs::read(path)?;
48    Ok(parse(&data))
49}
50
51/// Parse a buffer of utmp entries. Trailing bytes that don't form a
52/// complete entry are silently discarded.
53pub fn parse(data: &[u8]) -> Vec<UtmpEntry> {
54    let stride = std::mem::size_of::<libc::utmpx>();
55    data.chunks_exact(stride).filter_map(parse_entry).collect()
56}
57
58fn parse_entry(buf: &[u8]) -> Option<UtmpEntry> {
59    // SAFETY: libc::utmpx is repr(C) and we've verified `buf.len() == sizeof`
60    // via chunks_exact. read_unaligned tolerates any alignment.
61    let utx: libc::utmpx =
62        unsafe { std::ptr::read_unaligned(buf.as_ptr().cast()) };
63
64    if utx.ut_type == EMPTY {
65        return None;
66    }
67
68    Some(UtmpEntry {
69        ut_type: utx.ut_type,
70        pid: utx.ut_pid,
71        line: c_array_to_string(&utx.ut_line),
72        id: c_array_to_string(&utx.ut_id),
73        user: c_array_to_string(&utx.ut_user),
74        host: c_array_to_string(&utx.ut_host),
75        login_time: UNIX_EPOCH
76            + Duration::new(
77                utx.ut_tv.tv_sec as u64,
78                utx.ut_tv.tv_usec as u32 * 1000,
79            ),
80    })
81}
82
83fn c_array_to_string(buf: &[libc::c_char]) -> String {
84    // Reinterpret as bytes and trim at the first NUL.
85    let bytes: &[u8] =
86        unsafe { std::slice::from_raw_parts(buf.as_ptr().cast(), buf.len()) };
87    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
88    CStr::from_bytes_with_nul(&[&bytes[..end], &[0]].concat())
89        .ok()
90        .and_then(|s| s.to_str().ok().map(str::to_owned))
91        .unwrap_or_default()
92}
93
94/// Count entries of type [`USER_PROCESS`] — convenient for the `N users`
95/// header line shown by `uptime`, `top`, and `w`.
96pub fn count_user_processes(entries: &[UtmpEntry]) -> usize {
97    entries.iter().filter(|e| e.ut_type == USER_PROCESS).count()
98}