procutils-common 0.1.0

Shared utilities for procutils tools (utmp, signal, procmatch, fmt, uid)
Documentation
//! Reader for the `utmp(5)` binary file format used by `/var/run/utmp`.
//!
//! The on-disk struct layout is set by the system's libc; we use
//! [`libc::utmpx`] for the field offsets and entry stride so the parser
//! is correct on every glibc-supported architecture (the entry size is
//! 384 bytes on x86_64 but 400 bytes on aarch64, for example).

use std::{
    ffi::CStr,
    fs, io,
    path::Path,
    time::{Duration, SystemTime, UNIX_EPOCH},
};

pub const EMPTY: i16 = libc::EMPTY;
pub const RUN_LVL: i16 = libc::RUN_LVL;
pub const BOOT_TIME: i16 = libc::BOOT_TIME;
pub const NEW_TIME: i16 = libc::NEW_TIME;
pub const OLD_TIME: i16 = libc::OLD_TIME;
pub const INIT_PROCESS: i16 = libc::INIT_PROCESS;
pub const LOGIN_PROCESS: i16 = libc::LOGIN_PROCESS;
pub const USER_PROCESS: i16 = libc::USER_PROCESS;
pub const DEAD_PROCESS: i16 = libc::DEAD_PROCESS;

/// One parsed entry from a utmp file.
#[derive(Debug, Clone)]
pub struct UtmpEntry {
    pub ut_type: i16,
    pub pid: i32,
    /// Terminal device basename (e.g. `tty2`, `pts/0`). Trimmed of trailing NULs.
    pub line: String,
    /// 4-character session id.
    pub id: String,
    /// Login name.
    pub user: String,
    /// Remote host or display, if any.
    pub host: String,
    /// Login timestamp.
    pub login_time: SystemTime,
}

/// Default location of the system utmp database.
pub const DEFAULT_UTMP_PATH: &str = "/var/run/utmp";

/// Read and parse all entries from a utmp file.
pub fn read(path: impl AsRef<Path>) -> io::Result<Vec<UtmpEntry>> {
    let data = fs::read(path)?;
    Ok(parse(&data))
}

/// Parse a buffer of utmp entries. Trailing bytes that don't form a
/// complete entry are silently discarded.
pub fn parse(data: &[u8]) -> Vec<UtmpEntry> {
    let stride = std::mem::size_of::<libc::utmpx>();
    data.chunks_exact(stride).filter_map(parse_entry).collect()
}

fn parse_entry(buf: &[u8]) -> Option<UtmpEntry> {
    // SAFETY: libc::utmpx is repr(C) and we've verified `buf.len() == sizeof`
    // via chunks_exact. read_unaligned tolerates any alignment.
    let utx: libc::utmpx =
        unsafe { std::ptr::read_unaligned(buf.as_ptr().cast()) };

    if utx.ut_type == EMPTY {
        return None;
    }

    Some(UtmpEntry {
        ut_type: utx.ut_type,
        pid: utx.ut_pid,
        line: c_array_to_string(&utx.ut_line),
        id: c_array_to_string(&utx.ut_id),
        user: c_array_to_string(&utx.ut_user),
        host: c_array_to_string(&utx.ut_host),
        login_time: UNIX_EPOCH
            + Duration::new(
                utx.ut_tv.tv_sec as u64,
                utx.ut_tv.tv_usec as u32 * 1000,
            ),
    })
}

fn c_array_to_string(buf: &[libc::c_char]) -> String {
    // Reinterpret as bytes and trim at the first NUL.
    let bytes: &[u8] =
        unsafe { std::slice::from_raw_parts(buf.as_ptr().cast(), buf.len()) };
    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
    CStr::from_bytes_with_nul(&[&bytes[..end], &[0]].concat())
        .ok()
        .and_then(|s| s.to_str().ok().map(str::to_owned))
        .unwrap_or_default()
}

/// Count entries of type [`USER_PROCESS`] — convenient for the `N users`
/// header line shown by `uptime`, `top`, and `w`.
pub fn count_user_processes(entries: &[UtmpEntry]) -> usize {
    entries.iter().filter(|e| e.ut_type == USER_PROCESS).count()
}