use std::collections::HashMap;
use std::fs::{metadata, File};
use std::io::{Error, ErrorKind, Read, Result};
use super::common::*;
static ST_SIZE: usize = std::mem::size_of::<RStruct>();
static USER_PROCESS: i32 = 7;
#[repr(C, packed)]
#[derive(Debug, Copy, Clone)]
struct RStruct {
rtype: i32,
pid: i32,
line: [u8; 32],
id: [u8; 4],
user: [u8; 32],
host: [u8; 256],
exit: [i16; 2],
session: i32,
sec: i32,
usec: i32,
addr: [i32; 4],
unused: [u8; 20],
}
#[inline]
fn stringify<'a>(name: &str, string: &'a [u8]) -> Result<&'a str> {
Ok(std::str::from_utf8(string)
.map_err(|_| Error::new(ErrorKind::InvalidData, format!("invalid {name}")))?
.trim_matches('\0'))
}
fn map_record(umap: &HashMap<String, u32>, st: RStruct) -> Result<Record> {
let tty = stringify("tty", &st.line)?;
let name = stringify("username", &st.user)?;
Ok(Record {
uid: *umap.get(name).unwrap_or(&0),
name: name.to_owned(),
tty: tty.trim_matches('\0').to_owned(),
last_login: unix_timestamp(st.sec as u32),
})
}
fn set_latest(all: &mut HashMap<u32, Record>, new: Record) {
if let Some(rec) = all.get(&new.uid) {
if let LoginTime::Last(old) = rec.last_login {
if let LoginTime::Last(new) = new.last_login {
if old > new {
return;
}
}
}
}
all.insert(new.uid, new);
}
#[inline]
fn read_utmp(f: &mut File, buf: &mut Vec<u8>) -> Result<RStruct> {
f.read(buf)?;
let st = read_struct::<RStruct, _>(&buf[..])?;
if st.rtype < 0 || st.rtype > 10 || st.sec == 0 {
return Err(Error::new(ErrorKind::InvalidData, "read invalid struct"));
}
Ok(st)
}
fn read_until<F>(umap: &HashMap<String, u32>, fname: &str, until: F) -> Result<Vec<Record>>
where
F: Fn(&Record) -> bool,
{
let mut f = File::open(fname)?;
let fsize = metadata(fname)?.len() as usize;
let mut seek = 0;
let mut buffer = vec![0; ST_SIZE];
let mut records = HashMap::new();
while seek < fsize {
let st = read_utmp(&mut f, &mut buffer)?;
seek += ST_SIZE;
if st.rtype != USER_PROCESS {
continue;
}
let rec = map_record(&umap, st)?;
if until(&rec) {
set_latest(&mut records, rec);
break;
}
set_latest(&mut records, rec);
}
for (user, uid) in umap.iter() {
if !records.contains_key(&uid) {
records.insert(*uid, new_record(*uid, user.to_owned()));
}
}
Ok(records.into_values().collect())
}
pub struct Utmp {}
impl Module for Utmp {
fn is_valid(&self, f: &mut File) -> bool {
let mut buffer = vec![0; ST_SIZE];
read_utmp(f, &mut buffer).is_ok()
}
fn primary_file(&self) -> Result<&'static str> {
for fpath in vec!["/var/log/wtmp", "/var/log/utmp", "/var/run/utmp"].iter() {
let Ok(meta) = metadata(fpath) else { continue };
if meta.is_file() {
return Ok(fpath);
}
}
Err(Error::new(
ErrorKind::NotFound,
"cannot find valid utmp/wtmp path",
))
}
fn iter_accounts(&self, fname: &str) -> Result<Vec<Record>> {
let users = read_passwd_nmap();
let mut results = HashMap::new();
let records = read_until(&users, fname, |_| false)?;
for rec in records.into_iter() {
results.insert(rec.uid, rec);
}
Ok(results.into_values().collect())
}
fn search_uid(&self, uid: u32, fname: &str) -> Result<Record> {
let users = read_passwd_nmap();
let records = read_until(&users, fname, |r| r.uid == uid)?;
for record in records.into_iter() {
if record.uid == uid {
return Ok(record);
}
}
Err(Error::new(ErrorKind::InvalidInput, "no such user"))
}
fn search_username(&self, username: &str, fname: &str) -> Result<Record> {
let users = read_passwd_nmap();
let records = read_until(&users, fname, |r| r.name == username)?;
for record in records.into_iter() {
if record.name == username {
return Ok(record);
}
}
Err(Error::new(ErrorKind::InvalidInput, "no such user"))
}
}