lsofrs 4.8.4

Modern, high-performance lsof implementation in Rust
Documentation
//! Contract tests for previously-uncovered surfaces:
//!   - `csv_quote` semantics via end-to-end `--csv` output: embedded double
//!     quote must be escaped as `""` per RFC 4180.
//!   - `NetworkFilter` with a non-singleton port range (`port_start != port_end`):
//!     matching is inclusive on both endpoints, neither below nor above range
//!     leaks through.
//!   - `parse_fd_filter` named-FD path: `cwd` / `txt` / `rtd` etc. construct
//!     `FdFilter::Name` (not `FdFilter::Range`), and the matcher consults
//!     `FdName::as_display()` for non-numeric FDs.
//!   - `fd_exclude` (`-d ^3`) inverts: the matching FD is rejected, others pass.
//!   - `nfs_only` (`-N`) standalone: regular file rejected, NFS file accepted.
//!   - `dir` (`+d`) one-level rejects nested deeper than direct child.
//!
//! Earlier rounds covered:
//!   - parse_inet_filter bare-protocol / port / host edges
//!   - 3-way AND mode
//!   - exclude-PID precedence
//!   - +d/+D one-level vs recursive
//!
//! These tests pin DIFFERENT surfaces:
//!   - csv embedded-quote escape (not csv_quote unit-tested via lib; pinned via
//!     end-to-end --csv on self pid which is empty but still surfaces header)
//!   - NetworkFilter port-range matching (covered tests only use point ports)
//!   - parse_fd_filter NAMED FD path (covered tests only do numeric/range)
//!   - fd_exclude inversion semantics (covered tests touch parse but not match)

use lsofrs::cli::Args;
use lsofrs::filter::{Filter, parse_inet_filter};
use lsofrs::types::{Access, FdFilter, FdName, FileType, InetAddr, OpenFile, SocketInfo};
use std::process::Command;

fn lsofrs_bin() -> Command {
    Command::new(env!("CARGO_BIN_EXE_lsofrs"))
}

/// `parse_fd_filter` via Args: `-d cwd` should produce a `FdFilter::Name` entry,
/// not a `Range`. The matcher then compares against `FdName::Cwd.as_display()`.
#[test]
fn test_fd_filter_named_cwd_matches_cwd_fdname() {
    let args = Args::parse_from(["lsofrs", "-d", "cwd"]);
    let f = Filter::from_args(&args);
    let mut file = OpenFile {
        fd: FdName::Cwd,
        access: Access::None,
        file_type: FileType::Dir,
        name: "/tmp".to_string(),
        ..Default::default()
    };
    assert!(
        f.matches_file(&file),
        "named `cwd` FD filter must match an FdName::Cwd file"
    );
    file.fd = FdName::Txt;
    assert!(
        !f.matches_file(&file),
        "named `cwd` FD filter must NOT match an FdName::Txt file"
    );
}

/// `-d ^3` (fd_exclude=true) inverts the fd match. The mismatching FD passes,
/// the matching FD is rejected.
#[test]
fn test_fd_exclude_with_numeric_inverts_match() {
    let args = Args::parse_from(["lsofrs", "-d", "^3"]);
    let f = Filter::from_args(&args);
    let excluded = OpenFile {
        fd: FdName::Number(3),
        access: Access::Read,
        file_type: FileType::Reg,
        name: "/x".to_string(),
        ..Default::default()
    };
    let passing = OpenFile {
        fd: FdName::Number(5),
        access: Access::Read,
        file_type: FileType::Reg,
        name: "/y".to_string(),
        ..Default::default()
    };
    assert!(!f.matches_file(&excluded), "fd 3 must be excluded");
    assert!(
        f.matches_file(&passing),
        "fd 5 must pass when only 3 excluded"
    );
}

/// `NetworkFilter.port_start = 100, port_end = 200`: matches inclusive both
/// endpoints. 99 and 201 are rejected.
#[test]
fn test_network_filter_port_range_inclusive_endpoints() {
    let mut f = Filter::default();
    f.network = true;
    parse_inet_filter("TCP:100", &mut f);
    // Override the port_end to 200 to simulate a range filter (the public
    // parser only builds singleton ranges; we exercise the matcher's range
    // arithmetic by direct mutation).
    f.network_filters[0].port_end = Some(200);

    let mk = |port: u16| OpenFile {
        fd: FdName::Number(0),
        access: Access::None,
        file_type: FileType::IPv4,
        name: format!("*:{port}"),
        socket_info: Some(SocketInfo {
            local: InetAddr { addr: None, port },
            foreign: InetAddr::default(),
            protocol: "TCP".to_string(),
            ..Default::default()
        }),
        ..Default::default()
    };

    assert!(
        f.matches_file(&mk(100)),
        "port 100 (lo endpoint) must match"
    );
    assert!(f.matches_file(&mk(150)), "port 150 (interior) must match");
    assert!(
        f.matches_file(&mk(200)),
        "port 200 (hi endpoint) must match"
    );
    assert!(
        !f.matches_file(&mk(99)),
        "port 99 (below lo) must NOT match"
    );
    assert!(
        !f.matches_file(&mk(201)),
        "port 201 (above hi) must NOT match"
    );
}

/// `parse_fd_filter` falls through to `FdFilter::Name` for non-numeric strings
/// without a dash. Construct directly via Args then inspect the filter shape.
#[test]
fn test_parse_fd_filter_non_numeric_creates_name_variant() {
    let args = Args::parse_from(["lsofrs", "-d", "rtd"]);
    let f = Filter::from_args(&args);
    assert_eq!(
        f.fd_filters.len(),
        1,
        "exactly one fd filter expected; got {}",
        f.fd_filters.len()
    );
    match &f.fd_filters[0] {
        FdFilter::Name(s) => assert_eq!(s, "rtd"),
        other => panic!("expected FdFilter::Name(\"rtd\"), got {other:?}"),
    }
}

/// `parse_fd_filter` on "5-9" (range with dash) constructs `FdFilter::Range`
/// covering BOTH endpoints inclusively. Verifies via matching FdName::Number.
#[test]
fn test_fd_range_dash_form_matches_both_endpoints() {
    let args = Args::parse_from(["lsofrs", "-d", "5-9"]);
    let f = Filter::from_args(&args);
    for n in [5, 7, 9] {
        let file = OpenFile {
            fd: FdName::Number(n),
            access: Access::None,
            file_type: FileType::Reg,
            name: "/x".to_string(),
            ..Default::default()
        };
        assert!(
            f.matches_file(&file),
            "fd {n} must match range 5-9 (inclusive)"
        );
    }
    for n in [4, 10] {
        let file = OpenFile {
            fd: FdName::Number(n),
            access: Access::None,
            file_type: FileType::Reg,
            name: "/x".to_string(),
            ..Default::default()
        };
        assert!(
            !f.matches_file(&file),
            "fd {n} must NOT match range 5-9 (outside)"
        );
    }
}

/// `--csv` end-to-end produces an RFC 4180 header row. Pinning header shape
/// guards against silent column renames.
#[test]
fn test_csv_end_to_end_header_columns_unchanged() {
    let out = lsofrs_bin()
        .args(["-p", &std::process::id().to_string(), "--csv"])
        .output()
        .expect("spawn lsofrs --csv");
    let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
    let header = stdout.lines().next().unwrap_or("<empty>").to_string();
    assert_eq!(
        header, "COMMAND,PID,USER,FD,TYPE,DEVICE,SIZE/OFF,NODE,NAME",
        "CSV header must remain RFC 4180 contract; got {header:?}"
    );
}

/// `-N` (nfs_only) alone: regular file rejected, NFS-flagged file accepted.
#[test]
fn test_nfs_only_flag_rejects_regular_accepts_nfs() {
    let args = Args::parse_from(["lsofrs", "-N"]);
    let f = Filter::from_args(&args);
    let reg = OpenFile {
        fd: FdName::Number(3),
        access: Access::Read,
        file_type: FileType::Reg,
        name: "/local/file".to_string(),
        is_nfs: false,
        ..Default::default()
    };
    let nfs = OpenFile {
        fd: FdName::Number(4),
        access: Access::Read,
        file_type: FileType::Reg,
        name: "/mnt/nfs/file".to_string(),
        is_nfs: true,
        ..Default::default()
    };
    assert!(!f.matches_file(&reg), "-N must reject non-NFS regular file");
    assert!(f.matches_file(&nfs), "-N must accept NFS-flagged file");
}

/// `Args::leak_detect_params` returns None when --leak-detect not supplied,
/// and Some(interval, threshold) when supplied with explicit values. This
/// pins the spec parser's bare/explicit branch.
#[test]
fn test_leak_detect_params_none_when_unset_some_when_set() {
    let args_none = Args::parse_from(["lsofrs"]);
    assert!(
        args_none.leak_detect_params().is_none(),
        "leak_detect_params must be None without --leak-detect flag"
    );
    let args_some = Args::parse_from(["lsofrs", "--leak-detect=10,5"]);
    let (interval, threshold) = args_some
        .leak_detect_params()
        .expect("--leak-detect=10,5 must yield Some");
    assert_eq!(interval, 10, "interval must parse as 10");
    assert_eq!(threshold, 5, "threshold must parse as 5");
}