utmp_rs/
entry.rs

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/// Parsed utmp entry.
11#[derive(Clone, Debug, Eq, PartialEq)]
12#[non_exhaustive]
13pub enum UtmpEntry {
14    /// Record does not contain valid info
15    Empty,
16    /// Change in system run-level (see `init(8)`)
17    RunLevel {
18        /// PID of the init process
19        pid: pid_t,
20        /// Kernel version
21        kernel_version: String,
22        /// Time entry was made
23        time: OffsetDateTime,
24    },
25    /// Time of system boot
26    BootTime {
27        /// Kernel version
28        kernel_version: String,
29        /// Time entry was made
30        time: OffsetDateTime,
31    },
32    /// Time of system shutdown
33    ShutdownTime {
34        /// Kernel version
35        kernel_version: String,
36        /// Time entry was made
37        time: OffsetDateTime,
38    },
39    /// Time after system clock change
40    NewTime(OffsetDateTime),
41    /// Time before system clock change
42    OldTime(OffsetDateTime),
43    /// Process spawned by `init(8)`
44    InitProcess {
45        /// PID of the init process
46        pid: pid_t,
47        /// Time entry was made
48        time: OffsetDateTime,
49    },
50    /// Session leader process for user login
51    LoginProcess {
52        /// PID of the login process
53        pid: pid_t,
54        /// Device name of tty
55        line: String,
56        /// Username
57        user: String,
58        /// Hostname for remote login
59        host: String,
60        /// Time entry was made
61        time: OffsetDateTime,
62    },
63    /// Normal process
64    UserProcess {
65        /// PID of login process
66        pid: pid_t,
67        /// Device name of tty
68        line: String,
69        /// Username
70        user: String,
71        /// Hostname for remote login
72        host: String,
73        /// Session ID (`getsid(2)`)
74        session: pid_t,
75        /// Time entry was made
76        time: OffsetDateTime,
77        // TODO: Figure out the correct byte order to parse the address
78        // address: IpAddr,
79    },
80    /// Terminated process
81    DeadProcess {
82        /// PID of the terminated process
83        pid: pid_t,
84        /// Device name of tty
85        line: String,
86        /// Time entry was made
87        time: OffsetDateTime,
88    },
89    /// Not implemented
90    #[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}