use std::borrow::Cow;
use std::cell::OnceCell;
use std::fmt;
use std::net::IpAddr;
use serde::Serialize;
pub const SEL_CMD: u16 = 0x0001;
pub const SEL_FD: u16 = 0x0004;
pub const SEL_NA: u16 = 0x0008;
pub const SEL_NET: u16 = 0x0010;
pub const SEL_NFS: u16 = 0x0020;
pub const SEL_NLINK: u16 = 0x0040;
pub const SEL_NM: u16 = 0x0080;
pub const SEL_PGID: u16 = 0x0100;
pub const SEL_PID: u16 = 0x0200;
pub const SEL_UID: u16 = 0x0400;
pub const SEL_UNX: u16 = 0x0800;
pub const SEL_EXCL_F: u16 = 0x2000;
pub const SEL_PROC: u16 = SEL_CMD | SEL_PGID | SEL_PID | SEL_UID;
pub const SEL_FILE: u16 = SEL_FD | SEL_NFS | SEL_NLINK | SEL_NM;
pub const SEL_NW: u16 = SEL_NA | SEL_NET | SEL_UNX;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub enum FileType {
Reg,
Dir,
Chr,
Blk,
Fifo,
Sock,
Link,
Pipe,
Kqueue,
Unix,
IPv4,
IPv6,
Systm,
Psem,
Pshm,
Atalk,
Fsevents,
Npolicy,
Channel,
Nexus,
Rte,
Ndrv,
Unknown(String),
}
impl FileType {
pub fn as_str(&self) -> &str {
match self {
Self::Reg => "REG",
Self::Dir => "DIR",
Self::Chr => "CHR",
Self::Blk => "BLK",
Self::Fifo => "FIFO",
Self::Sock => "sock",
Self::Link => "LINK",
Self::Pipe => "PIPE",
Self::Kqueue => "KQUEUE",
Self::Unix => "unix",
Self::IPv4 => "IPv4",
Self::IPv6 => "IPv6",
Self::Systm => "systm",
Self::Psem => "PSXSEM",
Self::Pshm => "PSXSHM",
Self::Atalk => "ATALK",
Self::Fsevents => "FSEVENT",
Self::Npolicy => "NPOLICY",
Self::Channel => "CHAN",
Self::Nexus => "NEXUS",
Self::Rte => "rte",
Self::Ndrv => "ndrv",
Self::Unknown(s) => s.as_str(),
}
}
}
impl fmt::Display for FileType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum Access {
Read,
Write,
ReadWrite,
None,
}
impl Access {
pub fn as_char(&self) -> char {
match self {
Self::Read => 'r',
Self::Write => 'w',
Self::ReadWrite => 'u',
Self::None => ' ',
}
}
}
#[derive(Debug, Clone, Default)]
pub struct InetAddr {
pub addr: Option<IpAddr>,
pub port: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum TcpState {
Closed,
Listen,
SynSent,
SynRecv,
Established,
CloseWait,
FinWait1,
Closing,
LastAck,
FinWait2,
TimeWait,
Unknown(i32),
}
impl TcpState {
pub fn from_raw(state: i32) -> Self {
match state {
0 => Self::Closed,
1 => Self::Listen,
2 => Self::SynSent,
3 => Self::SynRecv,
4 => Self::Established,
5 => Self::CloseWait,
6 => Self::FinWait1,
7 => Self::Closing,
8 => Self::LastAck,
9 => Self::FinWait2,
10 => Self::TimeWait,
n => Self::Unknown(n),
}
}
pub fn as_str(&self) -> &str {
match self {
Self::Closed => "CLOSED",
Self::Listen => "LISTEN",
Self::SynSent => "SYN_SENT",
Self::SynRecv => "SYN_RCVD",
Self::Established => "ESTABLISHED",
Self::CloseWait => "CLOSE_WAIT",
Self::FinWait1 => "FIN_WAIT_1",
Self::Closing => "CLOSING",
Self::LastAck => "LAST_ACK",
Self::FinWait2 => "FIN_WAIT_2",
Self::TimeWait => "TIME_WAIT",
Self::Unknown(_) => "UNKNOWN",
}
}
}
impl fmt::Display for TcpState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Default)]
pub struct SocketInfo {
pub local: InetAddr,
pub foreign: InetAddr,
pub protocol: String,
pub tcp_state: Option<TcpState>,
pub recv_queue: Option<u64>,
pub send_queue: Option<u64>,
pub recv_buf_size: Option<u64>,
pub send_buf_size: Option<u64>,
pub socket_options: Option<u32>,
pub socket_state: Option<u32>,
}
pub fn make_dev(major: u32, minor: u32) -> u64 {
if cfg!(target_os = "macos") {
((major as u64) << 24) | (minor as u64 & 0x00ff_ffff)
} else {
let (major, minor) = (major as u64, minor as u64);
(minor & 0xff) | ((major & 0xfff) << 8) | ((minor & !0xff) << 12) | ((major & !0xfff) << 32)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum FdName {
Cwd,
Rtd,
Txt,
Mem,
Err,
FilePort,
Number(i32),
Other(String),
}
impl FdName {
pub fn as_display(&self) -> Cow<'_, str> {
match self {
Self::Cwd => Cow::Borrowed("cwd"),
Self::Rtd => Cow::Borrowed("rtd"),
Self::Txt => Cow::Borrowed("txt"),
Self::Mem => Cow::Borrowed("mem"),
Self::Err => Cow::Borrowed("err"),
Self::FilePort => Cow::Borrowed("fp."),
Self::Number(n) => Cow::Owned(format!("{n}")),
Self::Other(s) => Cow::Borrowed(s),
}
}
pub fn with_access(&self, access: Access) -> String {
match self {
Self::Number(_) | Self::FilePort => {
let suffix = match access {
Access::Read => "r",
Access::Write => "w",
Access::ReadWrite => "u",
Access::None => "",
};
format!("{}{suffix}", self.as_display())
}
_ => self.as_display().into_owned(),
}
}
}
#[derive(Debug, Clone)]
pub struct OpenFile {
pub fd: FdName,
pub access: Access,
pub lock: char,
pub file_type: FileType,
pub device: Option<(u32, u32)>,
pub size: Option<u64>,
pub offset: Option<u64>,
pub inode: Option<u64>,
pub nlink: Option<u64>,
pub name: String,
pub name_append: Option<String>,
pub socket_info: Option<SocketInfo>,
pub sel_flags: u16,
pub is_nfs: bool,
pub rdev: Option<(u32, u32)>,
pub device_label: Option<String>,
pub file_flags: Option<i64>,
pub file_port: Option<u32>,
pub file_status: Option<u32>,
pub file_struct_addr: Option<u64>,
}
impl Default for OpenFile {
fn default() -> Self {
Self {
fd: FdName::Other(String::new()),
access: Access::None,
lock: ' ',
file_type: FileType::Unknown(String::new()),
device: None,
size: None,
offset: None,
inode: None,
nlink: None,
name: String::new(),
name_append: None,
socket_info: None,
sel_flags: 0,
is_nfs: false,
rdev: None,
device_label: None,
file_flags: None,
file_port: None,
file_status: None,
file_struct_addr: None,
}
}
}
impl OpenFile {
pub fn full_name(&self) -> String {
let base = match &self.socket_info {
Some(si)
if si.local.addr.is_some()
&& (crate::resolve::host_lookup() || crate::resolve::port_lookup()) =>
{
crate::resolve::inet_name(si)
}
_ => self.name.clone(),
};
match &self.name_append {
Some(extra) => format!("{base} {extra}"),
None => base,
}
}
pub fn display_name(&self) -> String {
let mut name = self.full_name();
if let Some(port) = self.file_port {
name = format!("{name} (fileport=0x{port:x})")
.trim_start()
.to_string();
}
match self
.socket_info
.as_ref()
.and_then(|si| si.tcp_state.as_ref())
{
Some(state) => format!("{name} ({state})"),
None => name,
}
}
pub fn size_or_offset_str(&self) -> String {
if let Some(sz) = self.size {
format!("{sz}")
} else if let Some(off) = self.offset {
format!("0t{off}")
} else {
String::new()
}
}
pub fn device_str(&self) -> String {
let dev = match self.file_type {
FileType::Chr | FileType::Blk => self.rdev.or(self.device),
_ => self.device,
};
match (dev, &self.device_label) {
(Some((maj, min)), _) => format!("{maj},{min}"),
(None, Some(label)) => label.clone(),
(None, None) => String::new(),
}
}
pub fn raw_device(&self) -> Option<u64> {
self.device.map(|(maj, min)| make_dev(maj, min))
}
pub fn raw_rdev(&self) -> Option<u64> {
self.rdev.map(|(maj, min)| make_dev(maj, min))
}
pub fn nlink_str(&self) -> String {
match self.nlink {
Some(n) => n.to_string(),
None => String::new(),
}
}
pub fn node_str(&self) -> String {
match self.inode {
Some(ino) => format!("{ino}"),
None => match &self.socket_info {
Some(si) if !si.protocol.is_empty() => si.protocol.clone(),
_ => String::new(),
},
}
}
}
#[derive(Debug)]
pub struct Process {
pub pid: i32,
pub ppid: i32,
pub pgid: i32,
pub uid: u32,
pub command: String,
pub files: Vec<OpenFile>,
pub sel_flags: u16,
pub sel_state: u8,
username_cache: OnceCell<String>,
}
impl Clone for Process {
fn clone(&self) -> Self {
Self {
pid: self.pid,
ppid: self.ppid,
pgid: self.pgid,
uid: self.uid,
command: self.command.clone(),
files: self.files.clone(),
sel_flags: self.sel_flags,
sel_state: self.sel_state,
username_cache: match self.username_cache.get() {
Some(s) => {
let cell = OnceCell::new();
let _ = cell.set(s.clone());
cell
}
None => OnceCell::new(),
},
}
}
}
impl Process {
pub fn new(
pid: i32,
ppid: i32,
pgid: i32,
uid: u32,
command: String,
files: Vec<OpenFile>,
) -> Self {
Self {
pid,
ppid,
pgid,
uid,
command,
files,
sel_flags: 0,
sel_state: 0,
username_cache: OnceCell::new(),
}
}
pub fn username(&self) -> &str {
self.username_cache.get_or_init(|| {
uzers::get_user_by_uid(self.uid)
.map(|u| u.name().to_string_lossy().into_owned())
.unwrap_or_else(|| self.uid.to_string())
})
}
}
#[derive(Debug, Clone)]
pub struct NetworkFilter {
pub protocol: Option<String>,
pub addr_family: Option<u8>,
pub addr: Option<IpAddr>,
pub host: Option<String>,
pub parsed_host: Option<IpAddr>,
pub port_start: Option<u16>,
pub port_end: Option<u16>,
}
#[derive(Debug, Clone)]
pub enum FdFilter {
Name(String),
Range(i32, i32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeltaStatus {
Unchanged,
New,
Gone,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sel_proc_matches_documented_union() {
assert_eq!(SEL_PROC, SEL_CMD | SEL_PGID | SEL_PID | SEL_UID);
}
#[test]
fn sel_file_matches_documented_union() {
assert_eq!(SEL_FILE, SEL_FD | SEL_NFS | SEL_NLINK | SEL_NM);
}
#[test]
fn sel_nw_matches_documented_union() {
assert_eq!(SEL_NW, SEL_NA | SEL_NET | SEL_UNX);
}
#[test]
fn file_type_unknown_empty_string_display() {
let ft = FileType::Unknown(String::new());
assert_eq!(ft.as_str(), "");
assert_eq!(format!("{ft}"), "");
}
#[test]
fn open_file_default_lock_is_space() {
let f = OpenFile::default();
assert_eq!(f.lock, ' ');
}
#[test]
fn open_file_lock_non_space_preserved() {
let f = OpenFile {
lock: 'X',
..Default::default()
};
assert_eq!(f.lock, 'X');
}
#[test]
fn file_type_as_str_all_variants() {
assert_eq!(FileType::Reg.as_str(), "REG");
assert_eq!(FileType::Dir.as_str(), "DIR");
assert_eq!(FileType::Chr.as_str(), "CHR");
assert_eq!(FileType::Blk.as_str(), "BLK");
assert_eq!(FileType::Fifo.as_str(), "FIFO");
assert_eq!(FileType::Sock.as_str(), "sock");
assert_eq!(FileType::Link.as_str(), "LINK");
assert_eq!(FileType::Pipe.as_str(), "PIPE");
assert_eq!(FileType::Kqueue.as_str(), "KQUEUE");
assert_eq!(FileType::Unix.as_str(), "unix");
assert_eq!(FileType::IPv4.as_str(), "IPv4");
assert_eq!(FileType::IPv6.as_str(), "IPv6");
assert_eq!(FileType::Systm.as_str(), "systm");
assert_eq!(FileType::Psem.as_str(), "PSXSEM");
assert_eq!(FileType::Pshm.as_str(), "PSXSHM");
assert_eq!(FileType::Atalk.as_str(), "ATALK");
assert_eq!(FileType::Fsevents.as_str(), "FSEVENT");
assert_eq!(FileType::Unknown("0014".to_string()).as_str(), "0014");
}
#[test]
fn file_type_reg_display_matches_as_str() {
let ft = FileType::Reg;
assert_eq!(format!("{ft}"), ft.as_str());
}
#[test]
fn file_type_dir_display_matches_as_str() {
let ft = FileType::Dir;
assert_eq!(format!("{ft}"), ft.as_str());
}
#[test]
fn fd_name_as_display_special_fds() {
assert_eq!(FdName::Cwd.as_display(), "cwd");
assert_eq!(FdName::Rtd.as_display(), "rtd");
assert_eq!(FdName::Txt.as_display(), "txt");
assert_eq!(FdName::Mem.as_display(), "mem");
assert_eq!(FdName::Err.as_display(), "err");
assert_eq!(FdName::Other("foo".into()).as_display(), "foo");
}
#[test]
fn fd_name_number_zero_and_negative_display() {
assert_eq!(FdName::Number(0).as_display(), "0");
assert_eq!(FdName::Number(-1).as_display(), "-1");
}
#[test]
fn fd_name_with_access_number_suffixes() {
assert_eq!(FdName::Number(3).with_access(Access::Read), "3r");
assert_eq!(FdName::Number(3).with_access(Access::Write), "3w");
assert_eq!(FdName::Number(3).with_access(Access::ReadWrite), "3u");
assert_eq!(FdName::Number(3).with_access(Access::None), "3");
}
#[test]
fn fd_name_with_access_non_numeric_ignores_access() {
assert_eq!(FdName::Cwd.with_access(Access::ReadWrite), "cwd");
assert_eq!(FdName::Mem.with_access(Access::Write), "mem");
}
#[test]
fn tcp_state_from_raw_maps_kernel_codes() {
assert_eq!(TcpState::from_raw(0).as_str(), "CLOSED");
assert_eq!(TcpState::from_raw(1).as_str(), "LISTEN");
assert_eq!(TcpState::from_raw(4).as_str(), "ESTABLISHED");
assert_eq!(TcpState::from_raw(10).as_str(), "TIME_WAIT");
}
#[test]
fn tcp_state_from_raw_unknown_numeric() {
let s = TcpState::from_raw(99);
assert!(matches!(s, TcpState::Unknown(99)));
assert_eq!(s.as_str(), "UNKNOWN");
}
#[test]
fn tcp_state_display_matches_as_str() {
let s = TcpState::Established;
assert_eq!(format!("{s}"), s.as_str());
}
#[test]
fn tcp_state_unknown_display_matches_as_str() {
let s = TcpState::Unknown(42);
assert_eq!(format!("{s}"), s.as_str());
}
#[test]
fn delta_status_equality() {
assert_eq!(DeltaStatus::New, DeltaStatus::New);
assert_ne!(DeltaStatus::New, DeltaStatus::Unchanged);
assert_ne!(DeltaStatus::Gone, DeltaStatus::Unchanged);
}
#[test]
fn file_type_display() {
assert_eq!(format!("{}", FileType::Reg), "REG");
assert_eq!(format!("{}", FileType::IPv4), "IPv4");
assert_eq!(format!("{}", FileType::Sock), FileType::Sock.as_str());
}
#[test]
fn file_type_as_str_fixed_variants() {
assert_eq!(FileType::Fifo.as_str(), "FIFO");
assert_eq!(FileType::Blk.as_str(), "BLK");
assert_eq!(FileType::Sock.as_str(), "sock");
assert_eq!(FileType::Link.as_str(), "LINK");
assert_eq!(FileType::Pipe.as_str(), "PIPE");
assert_eq!(FileType::Kqueue.as_str(), "KQUEUE");
assert_eq!(FileType::Systm.as_str(), "systm");
assert_eq!(FileType::Psem.as_str(), "PSXSEM");
assert_eq!(FileType::Pshm.as_str(), "PSXSHM");
assert_eq!(FileType::Atalk.as_str(), "ATALK");
assert_eq!(FileType::Fsevents.as_str(), "FSEVENT");
}
#[test]
fn access_as_char() {
assert_eq!(Access::Read.as_char(), 'r');
assert_eq!(Access::Write.as_char(), 'w');
assert_eq!(Access::ReadWrite.as_char(), 'u');
assert_eq!(Access::None.as_char(), ' ');
}
#[test]
fn tcp_state_from_raw_unknown_low_code() {
assert_eq!(TcpState::from_raw(11), TcpState::Unknown(11));
}
#[test]
fn tcp_state_syn_sent_as_str() {
assert_eq!(TcpState::SynSent.as_str(), "SYN_SENT");
}
#[test]
fn tcp_state_syn_recv_as_str() {
assert_eq!(TcpState::SynRecv.as_str(), "SYN_RCVD");
}
#[test]
fn tcp_state_fin_wait_1_as_str() {
assert_eq!(TcpState::FinWait1.as_str(), "FIN_WAIT_1");
}
#[test]
fn tcp_state_closing_and_fin_wait2_as_str() {
assert_eq!(TcpState::Closing.as_str(), "CLOSING");
assert_eq!(TcpState::FinWait2.as_str(), "FIN_WAIT_2");
}
#[test]
fn tcp_state_close_wait_and_last_ack_as_str() {
assert_eq!(TcpState::CloseWait.as_str(), "CLOSE_WAIT");
assert_eq!(TcpState::LastAck.as_str(), "LAST_ACK");
}
#[test]
fn tcp_state_from_raw_all() {
assert_eq!(TcpState::from_raw(0), TcpState::Closed);
assert_eq!(TcpState::from_raw(1), TcpState::Listen);
assert_eq!(TcpState::from_raw(2), TcpState::SynSent);
assert_eq!(TcpState::from_raw(3), TcpState::SynRecv);
assert_eq!(TcpState::from_raw(4), TcpState::Established);
assert_eq!(TcpState::from_raw(5), TcpState::CloseWait);
assert_eq!(TcpState::from_raw(6), TcpState::FinWait1);
assert_eq!(TcpState::from_raw(7), TcpState::Closing);
assert_eq!(TcpState::from_raw(8), TcpState::LastAck);
assert_eq!(TcpState::from_raw(9), TcpState::FinWait2);
assert_eq!(TcpState::from_raw(10), TcpState::TimeWait);
assert_eq!(TcpState::from_raw(99), TcpState::Unknown(99));
}
#[test]
fn tcp_state_as_str() {
assert_eq!(TcpState::Listen.as_str(), "LISTEN");
assert_eq!(TcpState::Established.as_str(), "ESTABLISHED");
assert_eq!(TcpState::Unknown(42).as_str(), "UNKNOWN");
}
#[test]
fn tcp_state_display() {
assert_eq!(format!("{}", TcpState::Established), "ESTABLISHED");
}
#[test]
fn fd_name_as_display() {
assert_eq!(FdName::Cwd.as_display(), "cwd");
assert_eq!(FdName::Rtd.as_display(), "rtd");
assert_eq!(FdName::Txt.as_display(), "txt");
assert_eq!(FdName::Mem.as_display(), "mem");
assert_eq!(FdName::Err.as_display(), "err");
assert_eq!(FdName::Number(42).as_display(), "42");
assert_eq!(FdName::Other("jld".to_string()).as_display(), "jld");
}
#[test]
fn fd_name_with_access() {
assert_eq!(FdName::Number(3).with_access(Access::Read), "3r");
assert_eq!(FdName::Number(3).with_access(Access::Write), "3w");
assert_eq!(FdName::Number(3).with_access(Access::ReadWrite), "3u");
assert_eq!(FdName::Number(3).with_access(Access::None), "3");
assert_eq!(FdName::Cwd.with_access(Access::Read), "cwd");
}
#[test]
fn inet_addr_default() {
let ia = InetAddr::default();
assert!(ia.addr.is_none());
assert_eq!(ia.port, 0);
}
#[test]
fn inet_addr_ipv4_with_port() {
use std::net::{IpAddr, Ipv4Addr};
let ia = InetAddr {
addr: Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 9))),
port: 65535,
};
assert_eq!(ia.port, 65535);
assert_eq!(ia.addr.unwrap().to_string(), "203.0.113.9");
}
#[test]
fn inet_addr_ipv6_loopback_with_port() {
use std::net::{IpAddr, Ipv6Addr};
let ia = InetAddr {
addr: Some(IpAddr::V6(Ipv6Addr::LOCALHOST)),
port: 8443,
};
assert_eq!(ia.port, 8443);
assert!(matches!(ia.addr, Some(IpAddr::V6(_))));
}
#[test]
fn socket_info_default() {
let s = SocketInfo::default();
assert_eq!(s.protocol, "");
assert!(s.tcp_state.is_none());
assert_eq!(s.local.port, 0);
assert_eq!(s.foreign.port, 0);
}
#[test]
fn open_file_full_name_no_append() {
let f = OpenFile {
name: "/tmp/test".to_string(),
..Default::default()
};
assert_eq!(f.full_name(), "/tmp/test");
}
#[test]
fn open_file_full_name_with_append() {
let f = OpenFile {
name: "/tmp/test".to_string(),
name_append: Some("(deleted)".to_string()),
..Default::default()
};
assert_eq!(f.full_name(), "/tmp/test (deleted)");
}
#[test]
fn open_file_full_name_empty_path_with_append() {
let f = OpenFile {
name: String::new(),
name_append: Some("(deleted)".to_string()),
..Default::default()
};
assert_eq!(f.full_name(), " (deleted)");
}
#[test]
fn open_file_size_or_offset_str() {
let f1 = OpenFile {
size: Some(4096),
..Default::default()
};
assert_eq!(f1.size_or_offset_str(), "4096");
let f2 = OpenFile {
offset: Some(0),
..Default::default()
};
assert_eq!(f2.size_or_offset_str(), "0t0");
let f3 = OpenFile::default();
assert_eq!(f3.size_or_offset_str(), "");
}
#[test]
fn open_file_size_prefers_size_over_offset() {
let f = OpenFile {
size: Some(100),
offset: Some(50),
..Default::default()
};
assert_eq!(f.size_or_offset_str(), "100");
}
#[test]
fn open_file_device_str() {
let f1 = OpenFile {
device: Some((1, 16)),
..Default::default()
};
assert_eq!(f1.device_str(), "1,16");
let f2 = OpenFile::default();
assert_eq!(f2.device_str(), "");
}
#[test]
fn open_file_device_str_prefers_rdev_for_special_files() {
let chr = OpenFile {
file_type: FileType::Chr,
device: Some((53, 10_917_010)),
rdev: Some((3, 2)),
..Default::default()
};
assert_eq!(chr.device_str(), "3,2");
let blk = OpenFile {
file_type: FileType::Blk,
device: Some((53, 10_917_010)),
rdev: Some((1, 4)),
..Default::default()
};
assert_eq!(blk.device_str(), "1,4");
let reg = OpenFile {
file_type: FileType::Reg,
device: Some((1, 13)),
rdev: Some((9, 9)),
..Default::default()
};
assert_eq!(reg.device_str(), "1,13");
let no_rdev = OpenFile {
file_type: FileType::Chr,
device: Some((1, 13)),
..Default::default()
};
assert_eq!(no_rdev.device_str(), "1,13");
}
#[test]
fn open_file_node_str_inode() {
let f = OpenFile {
inode: Some(12345),
..Default::default()
};
assert_eq!(f.node_str(), "12345");
}
#[test]
fn open_file_node_str_protocol() {
let f = OpenFile {
socket_info: Some(SocketInfo {
protocol: "TCP".to_string(),
..Default::default()
}),
..Default::default()
};
assert_eq!(f.node_str(), "TCP");
}
#[test]
fn open_file_node_str_empty() {
let f = OpenFile::default();
assert_eq!(f.node_str(), "");
}
#[test]
fn open_file_nlink_str_present() {
let f = OpenFile {
nlink: Some(0),
..Default::default()
};
assert_eq!(f.nlink_str(), "0");
let f = OpenFile {
nlink: Some(7),
..Default::default()
};
assert_eq!(f.nlink_str(), "7");
}
#[test]
fn open_file_nlink_str_absent_is_empty() {
let f = OpenFile::default();
assert_eq!(f.nlink_str(), "");
}
#[test]
fn process_username_for_root() {
let p = Process::new(1, 0, 1, 0, "launchd".to_string(), vec![]);
assert_eq!(p.username(), "root");
}
#[test]
fn process_username_unknown_uid() {
let p = Process::new(1, 0, 1, 99999, "test".to_string(), vec![]);
assert_eq!(p.username(), "99999");
}
#[test]
fn process_clone_preserves_pid_command_and_files() {
let files = vec![OpenFile {
fd: FdName::Number(3),
access: Access::Read,
file_type: FileType::Reg,
name: "/tmp/x".to_string(),
..Default::default()
}];
let p = Process::new(100, 1, 100, 0, "worker".to_string(), files);
let q = p.clone();
assert_eq!(q.pid, p.pid);
assert_eq!(q.command, p.command);
assert_eq!(q.files.len(), 1);
assert_eq!(q.files[0].name, "/tmp/x");
}
#[test]
fn process_clone_reuses_cached_username_when_populated() {
let p = Process::new(1, 0, 1, 0, "init".to_string(), vec![]);
let n = p.username().to_string();
let q = p.clone();
assert_eq!(q.username(), n.as_str());
}
#[test]
fn open_file_node_str_prefers_inode_over_socket_protocol() {
let f = OpenFile {
inode: Some(777),
socket_info: Some(SocketInfo {
protocol: "TCP".to_string(),
..Default::default()
}),
..Default::default()
};
assert_eq!(f.node_str(), "777");
}
#[test]
fn open_file_node_str_empty_protocol_skips_to_empty_without_inode() {
let f = OpenFile {
socket_info: Some(SocketInfo {
protocol: String::new(),
..Default::default()
}),
..Default::default()
};
assert_eq!(f.node_str(), "");
}
}