utmpx 0.2.0

Rust bindings and wrapper around `utmpx.h`
Documentation
//! This library contains bindings to the types and functions in `utmpx.h`.
//!
//! The [`sys`] module contains the raw bindings. The root module contains idiomatic wrappers.
//!
//! The underlying `utmpx` functions work with per-thread cursors. Care should be taken when using
//! this in multiple threads, but all functions should be usable from different threads.
//!
//! For platforms that implement `utmp.h` but not `utmpx.h`, the intent is for wrappers to call the
//! `utmp.h` equivalents.
use std::{
    ffi::{c_char, c_int, c_uint, CStr},
    net::IpAddr,
};

use libc::{pid_t, timeval};

use sys::{ExitStatus, UtType, Utmpx, UT_HOSTSIZE, UT_LINESIZE, UT_NAMESIZE};

use crate::sys::pututxline;

pub mod sys;

/// Error creating Utmpx instance.
pub enum UtmpxError {
    LineTooLong,
    IdTooLong,
    UserTooLong,
    HostTooLong,
}

impl Utmpx {
    /// Create a new instance
    ///
    /// # Errors
    ///
    /// If any `Cstring` is longer that the allows values.
    pub fn new(
        ut_type: UtType,
        ut_pid: pid_t,
        ut_line: &CStr,
        ut_id: &CStr,
        ut_user: &CStr,
        ut_host: &CStr,
        ut_exit: ExitStatus,
        ut_session: i32,
        ut_tv: timeval,
        ut_addr_v6: IpAddr,
    ) -> Result<Self, UtmpxError> {
        Ok(Utmpx {
            ut_type,
            __ut_pad1: 0,
            ut_pid,
            ut_line: cast_cstring(ut_line).ok_or(UtmpxError::LineTooLong)?,
            ut_id: cast_cstring(ut_id).ok_or(UtmpxError::IdTooLong)?,
            ut_user: cast_cstring(ut_user).ok_or(UtmpxError::UserTooLong)?,
            ut_host: cast_cstring(ut_host).ok_or(UtmpxError::HostTooLong)?,
            ut_exit,
            ut_session,
            __ut_pad2: 0,
            ut_tv,
            ut_addr_v6: cast_addr(ut_addr_v6),
            __unused: [0; 20],
        })
    }
}

impl Default for Utmpx {
    fn default() -> Self {
        Utmpx {
            ut_type: UtType::EMPTY,
            __ut_pad1: 0,
            ut_pid: 0,
            ut_line: [0; UT_LINESIZE],
            ut_id: [0; 4],
            ut_user: [0; UT_NAMESIZE],
            ut_host: [0; UT_HOSTSIZE],
            ut_exit: ExitStatus::default(),
            ut_session: 0,
            __ut_pad2: 0,
            ut_tv: timeval {
                tv_sec: 0,
                tv_usec: 0,
            },
            ut_addr_v6: [0; 4],
            __unused: [0; 20],
        }
    }
}

//// Convert a `CStr` into an array of `c_char`.
///
/// Returns `None` if the input string is too long.
fn cast_cstring<const S: usize>(cstr: &CStr) -> Option<[c_char; S]> {
    let source = cstr.to_bytes_with_nul();
    if source.len() > S {
        return None;
    }
    let mut ret: [c_char; S] = [0; S];
    for (r, s) in ret.iter_mut().zip(source.iter()) {
        *r = *s as c_char;
    }

    Some(ret)
}

#[test]
fn test_cast_cstring() {
    use std::ffi::CString;

    let hello = CString::new("hello").unwrap();
    let array: [c_char; 6] = cast_cstring(hello.as_c_str()).unwrap();
    assert_eq!(unsafe { CStr::from_ptr(array.as_ptr()) }, hello.as_c_str());
}

/// Convert an `IpAddr` to the format expected by `Utmpx`.
fn cast_addr(addr: IpAddr) -> [c_uint; 4usize] {
    match addr {
        IpAddr::V4(ipv4addr) => [u32::from(ipv4addr), 0, 0, 0],
        IpAddr::V6(ipv6addr) => {
            let octets = ipv6addr.octets();
            let mut ret = [0u32; 4];
            for i in 0..4 {
                ret[i] = u32::from(octets[i * 4]) << 24
                    | u32::from(octets[i * 4 + 1]) << 16
                    | u32::from(octets[i * 4 + 2]) << 8
                    | u32::from(octets[i * 4 + 3]);
            }
            ret
        }
    }
}

/// Change the filename of the database opened by the current thread.
///
/// The default filename is `/etc/utmpx`.
pub fn set_filename(filename: &CStr) -> Result<(), c_int> {
    let ptr = filename.as_ptr();
    let r = unsafe { sys::utmpxname(ptr) };

    if r == 0 {
        Ok(())
    } else {
        Err(r)
    }
}

/// Resets the cursor for the current thread.
///
/// The cursor is reset to the beginning of the utmpx database.
pub fn reset_cursor() {
    unsafe { sys::setutxent() }
}

/// Write the provided structure into the utmpx database.
///
/// Replaces an entry based on `getutxid`, if any. Otherwise inserts a new one.
pub fn write_line(line: &Utmpx) -> Result<Utmpx, ()> {
    let ptr: *const Utmpx = line;
    let res = unsafe { pututxline(ptr) };
    if res.is_null() {
        return Err(());
    }
    // SAFETY: pointer is not null and points to a valid utmp structure.
    let r: Utmpx = unsafe { *res };
    Ok(r)
}

/// Close the utmpx database for the current thread.
pub fn close_database() {
    unsafe { sys::endutxent() }
}

/// Find the next entry by `ut_type` and `ut_id`.
///
/// The search begins at the current cursor position and ends at EOF.
pub fn find_by_id(b: &Utmpx) -> Result<Utmpx, ()> {
    let ptr: *const Utmpx = b;
    let res = unsafe { sys::getutxid(ptr) };
    if res.is_null() {
        return Err(());
    }
    // SAFETY: pointer is not null and points to a valid utmp structure.
    let r: Utmpx = unsafe { *res };
    Ok(r)
}

/// Find the next entry by `ut_line`.
///
/// The search begins at the current cursor position and ends at EOF.
pub fn find_by_line(b: &Utmpx) -> Result<Utmpx, ()> {
    let ptr: *const Utmpx = b;
    let res = unsafe { sys::getutxline(ptr) };
    if res.is_null() {
        return Err(());
    }
    // SAFETY: pointer is not null and points to a valid utmp structure.
    let r: Utmpx = unsafe { *res };
    Ok(r)
}

/// Read in the next entry from the utmpx database.
///
/// If the database is not already open, it opens it.
pub fn read_next_entry() -> Result<Utmpx, ()> {
    let res = unsafe { sys::getutxent() };
    if res.is_null() {
        return Err(());
    }
    // SAFETY: pointer is not null and points to a valid utmp structure.
    let r: Utmpx = unsafe { *res };
    Ok(r)
}