1use libc::pid_t;
2use std::convert::TryFrom;
3use std::os::raw::c_short;
4use std::str;
5use thiserror::Error;
6use time::OffsetDateTime;
7use utmp_raw::x32::utmp as utmp32;
8use utmp_raw::x64::{timeval as timeval64, utmp as utmp64};
9
10#[derive(Clone, Debug, Eq, PartialEq)]
12#[non_exhaustive]
13pub enum UtmpEntry {
14 Empty,
16 RunLevel {
18 pid: pid_t,
20 kernel_version: String,
22 time: OffsetDateTime,
24 },
25 BootTime {
27 kernel_version: String,
29 time: OffsetDateTime,
31 },
32 ShutdownTime {
34 kernel_version: String,
36 time: OffsetDateTime,
38 },
39 NewTime(OffsetDateTime),
41 OldTime(OffsetDateTime),
43 InitProcess {
45 pid: pid_t,
47 time: OffsetDateTime,
49 },
50 LoginProcess {
52 pid: pid_t,
54 line: String,
56 user: String,
58 host: String,
60 time: OffsetDateTime,
62 },
63 UserProcess {
65 pid: pid_t,
67 line: String,
69 user: String,
71 host: String,
73 session: pid_t,
75 time: OffsetDateTime,
77 },
80 DeadProcess {
82 pid: pid_t,
84 line: String,
86 time: OffsetDateTime,
88 },
89 #[non_exhaustive]
91 Accounting,
92}
93
94impl<'a> TryFrom<&'a utmp32> for UtmpEntry {
95 type Error = UtmpError;
96
97 fn try_from(from: &utmp32) -> Result<Self, UtmpError> {
98 UtmpEntry::try_from(&utmp64 {
99 ut_type: from.ut_type,
100 ut_pid: from.ut_pid,
101 ut_line: from.ut_line,
102 ut_id: from.ut_id,
103 ut_user: from.ut_user,
104 ut_host: from.ut_host,
105 ut_exit: from.ut_exit,
106 ut_session: i64::from(from.ut_session),
107 ut_tv: timeval64 {
108 tv_sec: i64::from(from.ut_tv.tv_sec),
109 tv_usec: i64::from(from.ut_tv.tv_usec),
110 },
111 ut_addr_v6: from.ut_addr_v6,
112 __unused: from.__unused,
113 })
114 }
115}
116
117impl<'a> TryFrom<&'a utmp64> for UtmpEntry {
118 type Error = UtmpError;
119
120 fn try_from(from: &utmp64) -> Result<Self, UtmpError> {
121 Ok(match from.ut_type {
122 utmp_raw::EMPTY => UtmpEntry::Empty,
123 utmp_raw::RUN_LVL => {
124 let kernel_version =
125 string_from_bytes(&from.ut_host).map_err(UtmpError::InvalidHost)?;
126 let time = time_from_tv(from.ut_tv)?;
127 if from.ut_line[0] == b'~' && from.ut_user.starts_with(b"shutdown\0") {
128 UtmpEntry::ShutdownTime {
129 kernel_version,
130 time,
131 }
132 } else {
133 UtmpEntry::RunLevel {
134 pid: from.ut_pid,
135 kernel_version,
136 time,
137 }
138 }
139 }
140 utmp_raw::BOOT_TIME => UtmpEntry::BootTime {
141 kernel_version: string_from_bytes(&from.ut_host).map_err(UtmpError::InvalidHost)?,
142 time: time_from_tv(from.ut_tv)?,
143 },
144 utmp_raw::NEW_TIME => UtmpEntry::NewTime(time_from_tv(from.ut_tv)?),
145 utmp_raw::OLD_TIME => UtmpEntry::OldTime(time_from_tv(from.ut_tv)?),
146 utmp_raw::INIT_PROCESS => UtmpEntry::InitProcess {
147 pid: from.ut_pid,
148 time: time_from_tv(from.ut_tv)?,
149 },
150 utmp_raw::LOGIN_PROCESS => UtmpEntry::LoginProcess {
151 pid: from.ut_pid,
152 time: time_from_tv(from.ut_tv)?,
153 line: string_from_bytes(&from.ut_line).map_err(UtmpError::InvalidLine)?,
154 user: string_from_bytes(&from.ut_user).map_err(UtmpError::InvalidUser)?,
155 host: string_from_bytes(&from.ut_host).map_err(UtmpError::InvalidHost)?,
156 },
157 utmp_raw::USER_PROCESS => UtmpEntry::UserProcess {
158 pid: from.ut_pid,
159 line: string_from_bytes(&from.ut_line).map_err(UtmpError::InvalidLine)?,
160 user: string_from_bytes(&from.ut_user).map_err(UtmpError::InvalidUser)?,
161 host: string_from_bytes(&from.ut_host).map_err(UtmpError::InvalidHost)?,
162 session: from.ut_session as pid_t,
163 time: time_from_tv(from.ut_tv)?,
164 },
165 utmp_raw::DEAD_PROCESS => UtmpEntry::DeadProcess {
166 pid: from.ut_pid,
167 line: string_from_bytes(&from.ut_line).map_err(UtmpError::InvalidLine)?,
168 time: time_from_tv(from.ut_tv)?,
169 },
170 utmp_raw::ACCOUNTING => UtmpEntry::Accounting,
171 _ => return Err(UtmpError::UnknownType(from.ut_type)),
172 })
173 }
174}
175
176#[derive(Debug, Error)]
177#[non_exhaustive]
178pub enum UtmpError {
179 #[error("unknown type {0}")]
180 UnknownType(c_short),
181 #[error("invalid time value {0:?}")]
182 InvalidTime(timeval64),
183 #[error("invalid line value `{0:?}`")]
184 InvalidLine(Box<[u8]>),
185 #[error("invalid user value `{0:?}`")]
186 InvalidUser(Box<[u8]>),
187 #[error("invalid host value `{0:?}`")]
188 InvalidHost(Box<[u8]>),
189}
190
191fn time_from_tv(tv: timeval64) -> Result<OffsetDateTime, UtmpError> {
192 let timeval64 { tv_sec, tv_usec } = tv;
193 if tv_usec < 0 {
194 return Err(UtmpError::InvalidTime(tv));
195 }
196 let usec = i128::from(tv_sec) * 1_000_000 + i128::from(tv_usec);
197 OffsetDateTime::from_unix_timestamp_nanos(usec * 1000).map_err(|_| UtmpError::InvalidTime(tv))
198}
199
200fn string_from_bytes(bytes: &[u8]) -> Result<String, Box<[u8]>> {
201 let trimmed = match bytes.iter().position(|b| *b == 0) {
202 Some(pos) => &bytes[..pos],
203 None => bytes,
204 };
205 str::from_utf8(trimmed)
206 .map(|s| s.into())
207 .map_err(|_| bytes.into())
208}