use libc::pid_t;
use std::convert::TryFrom;
use std::ffi::CStr;
use std::os::raw::c_short;
use thiserror::Error;
use utwt_raw::x32::utmp as utmp32;
use utwt_raw::x64::{timeval as timeval64, utmp as utmp64};
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum UtmpEntry {
Empty,
RunLevel {
kernel_version: String,
time_in_micros: i64,
},
BootTime {
kernel_version: String,
time_in_micros: i64,
},
ShutdownTime {
kernel_version: String,
time_in_micros: i64,
},
NewTime(i64),
OldTime(i64),
InitProcess {
pid: pid_t,
time_in_micros: i64,
},
LoginProcess {
pid: pid_t,
time_in_micros: i64,
},
UserProcess {
pid: pid_t,
line: String,
user: String,
host: String,
session: pid_t,
time_in_micros: i64,
},
DeadProcess {
pid: pid_t,
line: String,
time_in_micros: i64,
},
#[non_exhaustive]
Accounting,
}
impl<'a> TryFrom<&'a utmp32> for UtmpEntry {
type Error = UtmpError;
fn try_from(from: &utmp32) -> Result<Self, UtmpError> {
UtmpEntry::try_from(&utmp64 {
ut_type: from.ut_type,
ut_pid: from.ut_pid,
ut_line: from.ut_line,
ut_id: from.ut_id,
ut_user: from.ut_user,
ut_host: from.ut_host,
ut_exit: from.ut_exit,
ut_session: i64::from(from.ut_session),
ut_tv: timeval64 {
tv_sec: i64::from(from.ut_tv.tv_sec),
tv_usec: i64::from(from.ut_tv.tv_usec),
},
ut_addr_v6: from.ut_addr_v6,
__unused: from.__unused,
})
}
}
impl<'a> TryFrom<&'a utmp64> for UtmpEntry {
type Error = UtmpError;
fn try_from(from: &utmp64) -> Result<Self, UtmpError> {
Ok(match from.ut_type {
utwt_raw::EMPTY => UtmpEntry::Empty,
utwt_raw::RUN_LVL => {
let kernel_version =
string_from_bytes(&from.ut_host).map_err(UtmpError::InvalidHost)?;
let time_in_micros = time_from_tv(from.ut_tv)?;
if from.ut_line[0] == b'~' && from.ut_user.starts_with(b"shutdown\0") {
UtmpEntry::ShutdownTime {
kernel_version,
time_in_micros,
}
} else {
UtmpEntry::RunLevel {
kernel_version,
time_in_micros,
}
}
}
utwt_raw::BOOT_TIME => UtmpEntry::BootTime {
kernel_version: string_from_bytes(&from.ut_host).map_err(UtmpError::InvalidHost)?,
time_in_micros: time_from_tv(from.ut_tv)?,
},
utwt_raw::NEW_TIME => UtmpEntry::NewTime(time_from_tv(from.ut_tv)?),
utwt_raw::OLD_TIME => UtmpEntry::OldTime(time_from_tv(from.ut_tv)?),
utwt_raw::INIT_PROCESS => UtmpEntry::InitProcess {
pid: from.ut_pid,
time_in_micros: time_from_tv(from.ut_tv)?,
},
utwt_raw::LOGIN_PROCESS => UtmpEntry::LoginProcess {
pid: from.ut_pid,
time_in_micros: time_from_tv(from.ut_tv)?,
},
utwt_raw::USER_PROCESS => UtmpEntry::UserProcess {
pid: from.ut_pid,
line: string_from_bytes(&from.ut_line).map_err(UtmpError::InvalidLine)?,
user: string_from_bytes(&from.ut_user).map_err(UtmpError::InvalidUser)?,
host: string_from_bytes(&from.ut_host).map_err(UtmpError::InvalidHost)?,
session: from.ut_session as pid_t,
time_in_micros: time_from_tv(from.ut_tv)?,
},
utwt_raw::DEAD_PROCESS => UtmpEntry::DeadProcess {
pid: from.ut_pid,
line: string_from_bytes(&from.ut_line).map_err(UtmpError::InvalidLine)?,
time_in_micros: time_from_tv(from.ut_tv)?,
},
utwt_raw::ACCOUNTING => UtmpEntry::Accounting,
_ => return Err(UtmpError::UnknownType(from.ut_type)),
})
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum UtmpError {
#[error("unknown type {0}")]
UnknownType(c_short),
#[error("invalid time value {0:?}")]
InvalidTime(timeval64),
#[error("invalid line value `{0:?}`")]
InvalidLine(Box<[u8]>),
#[error("invalid user value `{0:?}`")]
InvalidUser(Box<[u8]>),
#[error("invalid host value `{0:?}`")]
InvalidHost(Box<[u8]>),
}
fn time_from_tv(tv: timeval64) -> Result<i64, UtmpError> {
let timeval64 { tv_sec, tv_usec } = tv;
if tv_usec < 0 {
return Err(UtmpError::InvalidTime(tv));
}
Ok(tv_sec * 1_000_000 + tv_usec)
}
fn string_from_bytes(bytes: &[u8]) -> Result<String, Box<[u8]>> {
bytes
.iter()
.position(|b| *b == 0)
.and_then(|pos| {
let cstr = unsafe { CStr::from_bytes_with_nul_unchecked(&bytes[..=pos]) };
Some(cstr.to_str().ok()?.to_string())
})
.ok_or_else(|| bytes.to_owned().into_boxed_slice())
}