use libc::pid_t;
use std::convert::TryFrom;
use std::os::raw::c_short;
use std::str;
use thiserror::Error;
use time::OffsetDateTime;
use utmp_raw::x32::utmp as utmp32;
use utmp_raw::x64::{timeval as timeval64, utmp as utmp64};
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum UtmpEntry {
Empty,
RunLevel {
pid: pid_t,
kernel_version: String,
time: OffsetDateTime,
},
BootTime {
kernel_version: String,
time: OffsetDateTime,
},
ShutdownTime {
kernel_version: String,
time: OffsetDateTime,
},
NewTime(OffsetDateTime),
OldTime(OffsetDateTime),
InitProcess {
pid: pid_t,
time: OffsetDateTime,
},
LoginProcess {
pid: pid_t,
line: String,
user: String,
host: String,
time: OffsetDateTime,
},
UserProcess {
pid: pid_t,
line: String,
user: String,
host: String,
session: pid_t,
time: OffsetDateTime,
},
DeadProcess {
pid: pid_t,
line: String,
time: OffsetDateTime,
},
#[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 {
utmp_raw::EMPTY => UtmpEntry::Empty,
utmp_raw::RUN_LVL => {
let kernel_version =
string_from_bytes(&from.ut_host).map_err(UtmpError::InvalidHost)?;
let time = 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,
}
} else {
UtmpEntry::RunLevel {
pid: from.ut_pid,
kernel_version,
time,
}
}
}
utmp_raw::BOOT_TIME => UtmpEntry::BootTime {
kernel_version: string_from_bytes(&from.ut_host).map_err(UtmpError::InvalidHost)?,
time: time_from_tv(from.ut_tv)?,
},
utmp_raw::NEW_TIME => UtmpEntry::NewTime(time_from_tv(from.ut_tv)?),
utmp_raw::OLD_TIME => UtmpEntry::OldTime(time_from_tv(from.ut_tv)?),
utmp_raw::INIT_PROCESS => UtmpEntry::InitProcess {
pid: from.ut_pid,
time: time_from_tv(from.ut_tv)?,
},
utmp_raw::LOGIN_PROCESS => UtmpEntry::LoginProcess {
pid: from.ut_pid,
time: time_from_tv(from.ut_tv)?,
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)?,
},
utmp_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: time_from_tv(from.ut_tv)?,
},
utmp_raw::DEAD_PROCESS => UtmpEntry::DeadProcess {
pid: from.ut_pid,
line: string_from_bytes(&from.ut_line).map_err(UtmpError::InvalidLine)?,
time: time_from_tv(from.ut_tv)?,
},
utmp_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<OffsetDateTime, UtmpError> {
let timeval64 { tv_sec, tv_usec } = tv;
if tv_usec < 0 {
return Err(UtmpError::InvalidTime(tv));
}
let usec = i128::from(tv_sec) * 1_000_000 + i128::from(tv_usec);
OffsetDateTime::from_unix_timestamp_nanos(usec * 1000).map_err(|_| UtmpError::InvalidTime(tv))
}
fn string_from_bytes(bytes: &[u8]) -> Result<String, Box<[u8]>> {
let trimmed = match bytes.iter().position(|b| *b == 0) {
Some(pos) => &bytes[..pos],
None => bytes,
};
str::from_utf8(trimmed)
.map(|s| s.into())
.map_err(|_| bytes.into())
}