utmp_classic/
entry.rs

1use std::ffi::CStr;
2use std::os::raw::c_short;
3use thiserror::Error;
4use time::OffsetDateTime;
5use utmp_classic_raw::x32::utmp as utmp32;
6use utmp_classic_raw::x64::{timeval as timeval64, utmp as utmp64};
7
8/// Parsed utmp entry.
9#[derive(Clone, Debug, Eq, PartialEq)]
10#[non_exhaustive]
11pub enum UtmpEntry {
12    UTMP {
13        /// Device name of tty
14        line: String,
15        /// Username
16        user: String,
17        /// Hostname for remote login
18        host: String,
19        /// Session ID (`getsid(2)`)
20        time: OffsetDateTime,
21        // TODO: Figure out the correct byte order to parse the address
22    },    
23}
24
25impl<'a> TryFrom<&'a utmp32> for UtmpEntry {
26    type Error = UtmpError;
27
28    fn try_from(from: &utmp32) -> Result<Self, UtmpError> {
29        UtmpEntry::try_from(&utmp64 {
30            ut_line: from.ut_line,
31            ut_user: from.ut_user,
32            ut_host: from.ut_host,
33            ut_tv: timeval64 {
34                tv_sec: i64::from(from.ut_tv.tv_sec),
35                tv_usec: i64::from(from.ut_tv.tv_usec),
36            },
37        })
38    }
39}
40
41impl<'a> TryFrom<&'a utmp64> for UtmpEntry {
42    type Error = UtmpError;
43
44    fn try_from(from: &utmp64) -> Result<Self, UtmpError> {
45        Ok(UtmpEntry::UTMP {
46                line: string_from_bytes(&from.ut_line).map_err(UtmpError::InvalidLine)?,
47                user: string_from_bytes(&from.ut_user).map_err(UtmpError::InvalidUser)?,
48                host: string_from_bytes(&from.ut_host).map_err(UtmpError::InvalidHost)?,
49                time: time_from_tv(from.ut_tv)?,
50        })
51    }
52}
53
54#[derive(Debug, Error)]
55#[non_exhaustive]
56pub enum UtmpError {
57    #[error("unknown type {0}")]
58    UnknownType(c_short),
59    #[error("invalid time value {0:?}")]
60    InvalidTime(timeval64),
61    #[error("invalid line value `{0:?}`")]
62    InvalidLine(Box<[u8]>),
63    #[error("invalid user value `{0:?}`")]
64    InvalidUser(Box<[u8]>),
65    #[error("invalid host value `{0:?}`")]
66    InvalidHost(Box<[u8]>),
67}
68
69fn time_from_tv(tv: timeval64) -> Result<OffsetDateTime, UtmpError> {
70    let timeval64 { tv_sec, tv_usec } = tv;
71    if tv_usec < 0 {
72        return Err(UtmpError::InvalidTime(tv));
73    }
74    let usec = i128::from(tv_sec) * 1_000_000 + i128::from(tv_usec);
75    OffsetDateTime::from_unix_timestamp_nanos(usec * 1000).map_err(|_| UtmpError::InvalidTime(tv))
76}
77
78fn string_from_bytes(bytes: &[u8]) -> Result<String, Box<[u8]>> {
79    bytes
80        .iter()
81        .position(|b| *b == 0)
82        .and_then(|pos| {
83            // This is safe because we manually located the first zero byte above.
84            let cstr = unsafe { CStr::from_bytes_with_nul_unchecked(&bytes[..=pos]) };
85            Some(cstr.to_str().ok()?.to_string())
86        })
87        .ok_or_else(|| bytes.to_owned().into_boxed_slice())
88}