use std::collections::HashMap;
use std::io::BufRead;
use std::time::{SystemTime, UNIX_EPOCH};
use chrono::{Local, TimeZone};
#[cfg(unix)]
use std::ffi::CStr;
pub const DEFAULT_WATCHFMT: &str = "%n has %a %l from %m.";
pub const DEFAULT_WATCHFMT_NOHOST: &str = "%n has %a %l.";
#[cfg(unix)]
pub type WATCH_STRUCT_UTMP = libc::utmpx;
pub fn utmp_user(u: &libc::utmpx) -> String { unsafe { CStr::from_ptr(u.ut_user.as_ptr()).to_string_lossy().into_owned() }
}
pub fn utmp_line(u: &libc::utmpx) -> String { unsafe { CStr::from_ptr(u.ut_line.as_ptr()).to_string_lossy().into_owned() }
}
pub fn utmp_host(u: &libc::utmpx) -> String { unsafe { CStr::from_ptr(u.ut_host.as_ptr()).to_string_lossy().into_owned() }
}
pub fn utmp_is_active(u: &libc::utmpx) -> bool { u.ut_type == libc::USER_PROCESS as i16
&& u.ut_user.first().copied().unwrap_or(0) != 0
}
#[cfg(test)]
pub fn utmp_make(user: &str, line: &str, host: &str, time: i64, pid: i32, ut_type: i16) -> libc::utmpx {
let mut u: libc::utmpx = unsafe { std::mem::zeroed() };
let mut copy = |dst: &mut [libc::c_char], src: &str| {
let bytes = src.as_bytes();
let n = bytes.len().min(dst.len().saturating_sub(1));
for (i, &b) in bytes[..n].iter().enumerate() {
dst[i] = b as libc::c_char;
}
};
copy(&mut u.ut_user, user);
copy(&mut u.ut_line, line);
copy(&mut u.ut_host, host);
u.ut_tv.tv_sec = time as libc::time_t;
u.ut_pid = pid;
u.ut_type = ut_type;
u
}
thread_local! {
static WTAB: std::cell::RefCell<Vec<libc::utmpx>> = const {
std::cell::RefCell::new(Vec::new())
};
static LASTWATCH: std::cell::Cell<i64> = const {
std::cell::Cell::new(0)
};
static LASTUTMPCHECK: std::cell::Cell<i64> = const {
std::cell::Cell::new(0)
};
static WATCH: std::cell::RefCell<Vec<String>> = const {
std::cell::RefCell::new(Vec::new())
};
}
pub fn set_watch_list(list: Vec<String>) {
WATCH.with(|w| *w.borrow_mut() = list);
}
pub fn should_check() -> bool {
if WATCH.with(|w| w.borrow().is_empty()) {
return false;
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let interval = {
let raw = crate::ported::params::getiparam("LOGCHECK");
if raw > 0 { raw } else { 60 }
};
now - LASTWATCH.with(|t| t.get()) > interval
}
pub fn check_entry(entry: &libc::utmpx, current_user: &str) -> bool {
let user = utmp_user(entry);
let line = utmp_line(entry);
let host = utmp_host(entry);
WATCH.with(|w| {
let watch_list = w.borrow();
if watch_list.is_empty() {
return false;
}
if watch_list.first().map(|s| s.as_str()) == Some("all") {
return true;
}
let mut iter = watch_list.iter().peekable();
if iter.peek().map(|s| s.as_str()) == Some("notme") {
if user == current_user {
return false;
}
iter.next();
if iter.peek().is_none() {
return true;
}
}
for pattern in iter {
let mut rest = pattern.as_str();
let mut matched = true;
if !rest.starts_with('@') && !rest.starts_with('%') {
let end = rest.find(['@', '%']).unwrap_or(rest.len());
let user_pat = &rest[..end];
if !watchlog_match(user_pat, &user) {
matched = false;
}
rest = &rest[end..];
}
while !rest.is_empty() && matched {
if let Some(rest1) = rest.strip_prefix('%') {
let end = rest1.find('@').unwrap_or(rest1.len());
let line_pat = &rest1[..end];
if !watchlog_match(line_pat, &line) {
matched = false;
}
rest = &rest1[end..];
} else if let Some(rest1) = rest.strip_prefix('@') {
let end = rest1.find('%').unwrap_or(rest1.len());
let host_pat = &rest1[..end];
if !watchlog_match(host_pat, &host) {
matched = false;
}
rest = &rest1[end..];
} else {
break;
}
}
if matched {
return true;
}
}
false
})
}
pub fn watchlog_match(pattern: &str, value: &str) -> bool { if pattern == value {
return true;
}
if pattern.contains('*') || pattern.contains('?') {
crate::glob::matchpat(pattern, value, false, true)
} else {
false
}
}
pub fn watch3ary(entry: &libc::utmpx, logged_in: bool, fmt: &str) -> String { let mut result = String::new();
let mut chars = fmt.chars().peekable();
let user = utmp_user(entry);
let line = utmp_line(entry);
let host = utmp_host(entry);
let time = entry.ut_tv.tv_sec as i64;
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(next) = chars.next() {
result.push(next);
}
} else if c == '%' {
if let Some(&next) = chars.peek() {
chars.next();
match next {
'n' => result.push_str(&user),
'a' => {
if logged_in {
result.push_str("logged on");
} else {
result.push_str("logged off");
}
}
'l' => {
let line = if line.starts_with("tty") {
&line[3..]
} else {
&line
};
result.push_str(line);
}
'm' => {
let host = host.split('.').next().unwrap_or(&host);
result.push_str(host);
}
'M' => result.push_str(&host),
't' | '@' => {
if let Some(dt) = Local.timestamp_opt(time, 0).single() {
result.push_str(&dt.format("%l:%M%p").to_string());
}
}
'T' => {
if let Some(dt) = Local.timestamp_opt(time, 0).single() {
result.push_str(&dt.format("%H:%M").to_string());
}
}
'w' => {
if let Some(dt) = Local.timestamp_opt(time, 0).single() {
result.push_str(&dt.format("%a %e").to_string());
}
}
'W' => {
if let Some(dt) = Local.timestamp_opt(time, 0).single() {
result.push_str(&dt.format("%m/%d/%y").to_string());
}
}
'D' => {
if chars.peek() == Some(&'{') {
chars.next();
let mut custom_fmt = String::new();
for fc in chars.by_ref() {
if fc == '}' {
break;
}
custom_fmt.push(fc);
}
if let Some(dt) = Local.timestamp_opt(time, 0).single() {
result.push_str(&dt.format(&custom_fmt).to_string());
}
} else {
if let Some(dt) = Local.timestamp_opt(time, 0).single() {
result.push_str(&dt.format("%y-%m-%d").to_string());
}
}
}
'%' => result.push('%'),
'(' => {
if let (Some(condition), Some(separator)) = (chars.next(), chars.next()) {
let truth = match condition {
'n' => !user.is_empty(),
'a' => logged_in,
'l' => {
if line.starts_with("tty") {
line.len() > 3
} else {
!line.is_empty()
}
}
'm' | 'M' => !host.is_empty(),
_ => false,
};
let mut true_branch = String::new();
let mut false_branch = String::new();
let mut depth = 1;
let mut in_true = true;
while let Some(c) = chars.next() {
if c == ')' {
depth -= 1;
if depth == 0 {
break;
}
}
if c == separator && depth == 1 {
in_true = false;
continue;
}
if c == '%' && chars.peek() == Some(&'(') {
depth += 1;
}
if in_true {
true_branch.push(c);
} else {
false_branch.push(c);
}
}
let branch = if truth { &true_branch } else { &false_branch };
result.push_str(&watch3ary(entry, logged_in, branch));
}
}
_ => {
result.push('%');
result.push(next);
}
}
}
} else {
result.push(c);
}
}
result
}
pub fn dowatch(current_user: &str) -> Vec<(libc::utmpx, bool)> { let mut events: Vec<libc::utmpx> = Vec::new();
let mut new_entries: Vec<libc::utmpx> = Vec::new();
#[cfg(any(target_os = "linux", target_os = "macos"))]
unsafe {
libc::setutxent();
loop {
let entry = libc::getutxent();
if entry.is_null() {
break;
}
new_entries.push(std::ptr::read(entry));
}
libc::endutxent();
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let old_entries = WTAB.with(|t| t.borrow().clone());
let key_of = |e: &libc::utmpx| format!("{}:{}", utmp_user(e), utmp_line(e));
let old_active: HashMap<String, libc::utmpx> = old_entries
.iter()
.filter(|e| utmp_is_active(e))
.map(|e| (key_of(e), *e))
.collect();
let new_active: HashMap<String, libc::utmpx> = new_entries
.iter()
.filter(|e| utmp_is_active(e))
.map(|e| (key_of(e), *e))
.collect();
for (key, entry) in &new_active {
if !old_active.contains_key(key) && check_entry(entry, current_user) {
events.push(*entry);
}
}
for (key, entry) in &old_active {
if !new_active.contains_key(key) && check_entry(entry, current_user) {
events.push(*entry);
}
}
let login_keys: std::collections::HashSet<String> = new_active
.keys()
.filter(|k| !old_active.contains_key(*k))
.cloned()
.collect();
let result: Vec<(libc::utmpx, bool)> = events
.into_iter()
.map(|e| {
let key = key_of(&e);
let is_login = login_keys.contains(&key);
(e, is_login)
})
.collect();
WTAB.with(|t| *t.borrow_mut() = new_entries);
LASTWATCH.with(|t| t.set(now));
result
}
pub fn bin_log(current_user: &str, fmt: Option<&str>) -> String { let fmt_str = fmt
.map(|s| s.to_string())
.unwrap_or_else(|| {
crate::ported::params::getsparam("WATCHFMT").unwrap_or_else(|| DEFAULT_WATCHFMT.to_string())
});
WTAB.with(|t| t.borrow_mut().clear());
LASTUTMPCHECK.with(|t| t.set(0));
let events = dowatch(current_user);
let mut output = String::new();
for (entry, logged_in) in events {
output.push_str(&watch3ary(&entry, logged_in, &fmt_str));
output.push('\n');
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_watch_initial_empty() {
WATCH.with(|w| assert!(w.borrow().is_empty()));
}
#[test]
fn test_glob_match() {
use crate::glob::matchpat;
assert!(matchpat("*", "anything", false, true));
assert!(matchpat("user*", "username", false, true));
assert!(matchpat("*name", "username", false, true));
assert!(matchpat("user?ame", "username", false, true));
assert!(!matchpat("user", "username", false, true));
}
#[test]
fn test_watch_match() {
assert!(watchlog_match("root", "root"));
assert!(watchlog_match("*", "anyuser"));
assert!(!watchlog_match("root", "admin"));
}
#[test]
fn test_format_watch_basic() {
let entry = utmp_make("testuser", "tty1", "localhost", 0, 1234, libc::USER_PROCESS as i16);
let result = watch3ary(&entry, true, "%n has %a %l");
assert!(result.contains("testuser"));
assert!(result.contains("logged on"));
assert!(result.contains("1"));
let result = watch3ary(&entry, false, "%n has %a");
assert!(result.contains("logged off"));
}
#[test]
fn test_format_watch_host() {
let entry = utmp_make("user", "pts/0", "host.example.com", 0, 1, libc::USER_PROCESS as i16);
let result = watch3ary(&entry, true, "%m");
assert_eq!(result, "host");
let result = watch3ary(&entry, true, "%M");
assert_eq!(result, "host.example.com");
}
#[test]
fn test_check_watch_entry_all() {
let entry = utmp_make("anyone", "pts/0", "", 0, 1, libc::USER_PROCESS as i16);
set_watch_list(vec!["all".to_string()]);
assert!(check_entry(&entry, "me"));
}
#[test]
fn test_check_watch_entry_notme() {
let entry = utmp_make("me", "pts/0", "", 0, 1, libc::USER_PROCESS as i16);
set_watch_list(vec!["notme".to_string()]);
assert!(!check_entry(&entry, "me"));
let other = utmp_make("other", "pts/0", "", 0, 1, libc::USER_PROCESS as i16);
assert!(check_entry(&other, "me"));
}
#[test]
fn test_matches_watch_pattern() {
let entry = utmp_make("admin", "pts/0", "server.local", 0, 1, libc::USER_PROCESS as i16);
set_watch_list(vec!["admin".to_string()]);
assert!(check_entry(&entry, "me"));
set_watch_list(vec!["admin@server.local".to_string()]);
assert!(check_entry(&entry, "me"));
set_watch_list(vec!["admin%pts/0".to_string()]);
assert!(check_entry(&entry, "me"));
set_watch_list(vec!["root".to_string()]);
assert!(!check_entry(&entry, "me"));
}
#[test]
fn test_session_type() {
let entry = utmp_make("user", "pts/0", "", 0, 1, libc::USER_PROCESS as i16);
assert!(utmp_is_active(&entry));
let dead = utmp_make("user", "pts/0", "", 0, 1, libc::DEAD_PROCESS as i16);
assert!(!utmp_is_active(&dead));
}
}
use crate::ported::zsh_h::module;
#[allow(unused_variables)]
pub fn setup_(m: *const module) -> i32 { 0
}
pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 { *features = featuresarray(m, module_features());
0
}
pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 { handlefeatures(m, module_features(), enables)
}
#[allow(unused_variables)]
pub fn boot_(m: *const module) -> i32 { if crate::ported::params::getsparam("WATCHFMT").is_none() {
crate::ported::params::setsparam("WATCHFMT", DEFAULT_WATCHFMT); }
if crate::ported::params::getsparam("LOGCHECK").is_none() {
crate::ported::params::setsparam("LOGCHECK", "60"); }
0
}
pub fn cleanup_(m: *const module) -> i32 { setfeatureenables(m, module_features(), None)
}
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 { 0
}
pub fn getlogtime(u_line: &str, u_time: i64, inout: i32) -> i64 { if inout != 0 { return u_time; }
let _ = u_line;
unsafe { libc::time(std::ptr::null_mut()) as i64 } }
pub fn ucmp(u_time: i64, u_line: &str, v_time: i64, v_line: &str) -> i32 { if u_time == v_time { return match u_line.cmp(v_line) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
};
}
(u_time - v_time) as i32 }
pub fn readwtab() -> Vec<libc::utmpx> { let mut entries: Vec<libc::utmpx> = Vec::new(); #[cfg(any(target_os = "linux", target_os = "macos"))]
unsafe {
libc::setutxent(); loop {
let entry = libc::getutxent(); if entry.is_null() { break; }
let ut = &*entry;
if ut.ut_type != libc::USER_PROCESS { continue; }
entries.push(std::ptr::read(entry));
}
libc::endutxent(); }
entries.sort_by(|a, b| {
let at = a.ut_tv.tv_sec as i64;
let bt = b.ut_tv.tv_sec as i64;
match ucmp(at, &utmp_line(a), bt, &utmp_line(b)) {
n if n < 0 => std::cmp::Ordering::Less,
n if n > 0 => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
}
});
entries }
pub fn watchlog(inout: i32, u: &libc::utmpx, w: &[String], fmt: &str) { let current_user = crate::ported::params::getsparam("USERNAME")
.or_else(|| crate::ported::params::getsparam("USER"))
.unwrap_or_default();
if !check_entry(u, ¤t_user) { return;
}
let _ = w; let line = watch3ary(u, inout != 0, fmt);
eprintln!("{}", line); }
pub fn watchlog2(_inout: i32, _u: &libc::utmpx, fmt: &str, _prnt: i32, _fini: i32) -> String { fmt.to_string() }
pub fn checksched() { let watch_set = WATCH.with(|w| !w.borrow().is_empty());
if !watch_set { return; }
let now = unsafe { libc::time(std::ptr::null_mut()) as i64 }; let last = LASTWATCH.with(|t| t.get()); let logcheck: i64 = {
let raw = crate::ported::params::getiparam("LOGCHECK");
if raw > 0 { raw } else { 60 }
};
if (now - last) > logcheck { let user = crate::ported::params::getsparam("USERNAME")
.or_else(|| crate::ported::params::getsparam("USER"))
.unwrap_or_default();
let _ = dowatch(&user); }
}
use crate::ported::zsh_h::features as features_t;
use std::sync::{Mutex, OnceLock};
static MODULE_FEATURES: OnceLock<Mutex<features_t>> = OnceLock::new();
fn module_features() -> &'static Mutex<features_t> {
MODULE_FEATURES.get_or_init(|| Mutex::new(features_t {
bn_list: None,
bn_size: 1,
cd_list: None,
cd_size: 0,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 2,
n_abstract: 0,
}))
}
fn featuresarray(_m: *const module, _f: &Mutex<features_t>) -> Vec<String> {
vec!["b:log".to_string(), "p:WATCH".to_string(), "p:watch".to_string()]
}
fn handlefeatures(
_m: *const module,
_f: &Mutex<features_t>,
enables: &mut Option<Vec<i32>>,
) -> i32 {
if enables.is_none() {
*enables = Some(vec![1; 3]);
}
0
}
fn setfeatureenables(
_m: *const module,
_f: &Mutex<features_t>,
_e: Option<&[i32]>,
) -> i32 {
0
}