procutils-common 0.2.0

Shared utilities for procutils tools (utmp, signal, procmatch, fmt, uid)
Documentation
//! Canonical signal table and signal-name parsing shared by `kill`,
//! `pkill`, `skill` and friends.

use rustix::process::Signal;

/// Canonical (number, name) table of standard Linux signals.
///
/// Signal names are stored without the `SIG` prefix, matching the format
/// expected by procps-ng's `kill -l` and `kill -L` output. This is the
/// list of standard signals (1–31); real-time signals are omitted for
/// now and can be added later if needed.
pub const SIGNALS: &[(i32, &str)] = &[
    (1, "HUP"),
    (2, "INT"),
    (3, "QUIT"),
    (4, "ILL"),
    (5, "TRAP"),
    (6, "ABRT"),
    (7, "BUS"),
    (8, "FPE"),
    (9, "KILL"),
    (10, "USR1"),
    (11, "SEGV"),
    (12, "USR2"),
    (13, "PIPE"),
    (14, "ALRM"),
    (15, "TERM"),
    (16, "STKFLT"),
    (17, "CHLD"),
    (18, "CONT"),
    (19, "STOP"),
    (20, "TSTP"),
    (21, "TTIN"),
    (22, "TTOU"),
    (23, "URG"),
    (24, "XCPU"),
    (25, "XFSZ"),
    (26, "VTALRM"),
    (27, "PROF"),
    (28, "WINCH"),
    (29, "IO"),
    (30, "PWR"),
    (31, "SYS"),
];

/// Parse a signal specification — either a name (with or without `SIG`
/// prefix) or a numeric value — into a `rustix::process::Signal`.
pub fn parse_signal(name: &str) -> Option<Signal> {
    if let Ok(n) = name.parse::<i32>() {
        return Signal::from_named_raw(n);
    }

    let name = name
        .strip_prefix("SIG")
        .unwrap_or(name)
        .to_ascii_uppercase();

    match name.as_str() {
        "HUP" => Some(Signal::HUP),
        "INT" => Some(Signal::INT),
        "QUIT" => Some(Signal::QUIT),
        "ILL" => Some(Signal::ILL),
        "TRAP" => Some(Signal::TRAP),
        "ABRT" | "IOT" => Some(Signal::ABORT),
        "BUS" => Some(Signal::BUS),
        "FPE" => Some(Signal::FPE),
        "KILL" => Some(Signal::KILL),
        "USR1" => Some(Signal::USR1),
        "SEGV" => Some(Signal::SEGV),
        "USR2" => Some(Signal::USR2),
        "PIPE" => Some(Signal::PIPE),
        "ALRM" => Some(Signal::ALARM),
        "TERM" => Some(Signal::TERM),
        "STKFLT" => Some(Signal::STKFLT),
        "CHLD" | "CLD" => Some(Signal::CHILD),
        "CONT" => Some(Signal::CONT),
        "STOP" => Some(Signal::STOP),
        "TSTP" => Some(Signal::TSTP),
        "TTIN" => Some(Signal::TTIN),
        "TTOU" => Some(Signal::TTOU),
        "URG" => Some(Signal::URG),
        "XCPU" => Some(Signal::XCPU),
        "XFSZ" => Some(Signal::XFSZ),
        "VTALRM" => Some(Signal::VTALARM),
        "PROF" => Some(Signal::PROF),
        "WINCH" => Some(Signal::WINCH),
        "IO" | "POLL" => Some(Signal::IO),
        "PWR" => Some(Signal::POWER),
        "SYS" => Some(Signal::SYS),
        _ => None,
    }
}

/// Look up the canonical name (no `SIG` prefix) for a signal number.
pub fn name_for_number(n: i32) -> Option<&'static str> {
    SIGNALS
        .iter()
        .find(|(num, _)| *num == n)
        .map(|(_, name)| *name)
}

/// Look up the signal number for a name. Accepts the name with or
/// without the `SIG` prefix; case-sensitive after stripping the prefix.
pub fn number_for_name(name: &str) -> Option<i32> {
    let name = name
        .strip_prefix("SIG")
        .unwrap_or(name)
        .to_ascii_uppercase();
    SIGNALS
        .iter()
        .find(|(_, n)| *n == name)
        .map(|(num, _)| *num)
}

/// First user-accessible real-time signal number. On Linux this is
/// 34 — the kernel reserves 32 and 33 for the threading library.
pub fn rt_min() -> i32 {
    libc::SIGRTMIN()
}

/// Last real-time signal number, typically 64 on Linux.
pub fn rt_max() -> i32 {
    libc::SIGRTMAX()
}

/// Resolve `name` (with or without `SIG` prefix) to a real-time
/// signal number, if it parses as `RTMIN`, `RTMIN+N`, `RTMAX`, or
/// `RTMAX-N`. Returns `None` for non-RT names. The offset must keep
/// the resulting number within `[rt_min(), rt_max()]`.
pub fn parse_rt_name(name: &str) -> Option<i32> {
    let s = name
        .strip_prefix("SIG")
        .unwrap_or(name)
        .to_ascii_uppercase();
    let min = rt_min();
    let max = rt_max();
    let in_range = |n: i32| -> Option<i32> {
        if (min..=max).contains(&n) {
            Some(n)
        } else {
            None
        }
    };

    if s == "RTMIN" {
        return Some(min);
    }
    if s == "RTMAX" {
        return Some(max);
    }
    if let Some(rest) = s.strip_prefix("RTMIN+") {
        let off: i32 = rest.parse().ok()?;
        if off < 0 {
            return None;
        }
        return in_range(min + off);
    }
    if let Some(rest) = s.strip_prefix("RTMAX-") {
        let off: i32 = rest.parse().ok()?;
        if off < 0 {
            return None;
        }
        return in_range(max - off);
    }
    None
}

/// Format a real-time signal number as `RTMIN` (for the minimum) or
/// `RTMIN+N` (for higher RT signals). Returns `None` for non-RT
/// numbers.
pub fn rt_name_for_number(n: i32) -> Option<String> {
    let min = rt_min();
    let max = rt_max();
    if !(min..=max).contains(&n) {
        return None;
    }
    if n == min {
        Some("RTMIN".into())
    } else {
        Some(format!("RTMIN+{}", n - min))
    }
}

/// Iterator over `(number, name)` pairs for the standard signals
/// (1–31) followed by the running kernel's real-time signals
/// (`rt_min()..=rt_max()`).
pub fn all_signals() -> impl Iterator<Item = (i32, String)> {
    let standard = SIGNALS.iter().map(|(n, name)| (*n, (*name).to_string()));
    let rt = (rt_min()..=rt_max())
        .filter_map(|n| rt_name_for_number(n).map(|s| (n, s)));
    standard.chain(rt)
}

/// Resolve any signal spec — standard name (with or without `SIG`),
/// RT name (`RTMIN`, `RTMIN+N`, `RTMAX`, `RTMAX-N`), or numeric form
/// — to a raw signum suitable for `kill(2)`. Numeric `0` (the null
/// signal) is rejected here because `Signal` cannot represent zero;
/// callers that accept `kill -0` (e.g. the `kill` binary) must
/// special-case it.
pub fn parse_signum_any(s: &str) -> Option<i32> {
    if let Ok(n) = s.parse::<i32>() {
        if (1..=31).contains(&n) || (rt_min()..=rt_max()).contains(&n) {
            return Some(n);
        }
        return None;
    }
    if let Some(sig) = parse_signal(s) {
        return Some(sig.as_raw());
    }
    parse_rt_name(s)
}

/// Inverse of [`parse_signum_any`]: format any standard or RT signum
/// as a name. Returns `None` if `n` is outside both ranges.
pub fn name_for_number_any(n: i32) -> Option<String> {
    if let Some(name) = name_for_number(n) {
        return Some(name.to_string());
    }
    rt_name_for_number(n)
}