use std::fmt;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct Signal {
number: i32,
}
impl Signal {
pub const INT: Self = Self { number: 2 };
pub const TERM: Self = Self { number: 15 };
pub const KILL: Self = Self { number: 9 };
#[must_use]
pub const fn from_number(number: i32) -> Self {
Self { number }
}
#[must_use]
pub const fn number(self) -> i32 {
self.number
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
let trimmed = name.trim();
if let Ok(number) = trimmed.parse::<i32>() {
return (number > 0).then_some(Self::from_number(number));
}
let upper = trimmed.to_ascii_uppercase();
let stripped = upper.strip_prefix("SIG").unwrap_or(&upper);
SIGNAL_TABLE
.iter()
.find(|entry| {
entry
.aliases
.iter()
.any(|alias| *alias == upper || *alias == stripped)
})
.map(|entry| Self::from_number(entry.number))
}
#[must_use]
pub fn name(self) -> Option<&'static str> {
SIGNAL_TABLE
.iter()
.find(|entry| entry.number == self.number)
.map(|entry| entry.canonical)
}
}
impl fmt::Display for Signal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.name() {
Some(name) => f.write_str(name),
None => write!(f, "signal({})", self.number),
}
}
}
struct SignalEntry {
canonical: &'static str,
aliases: &'static [&'static str],
number: i32,
}
static SIGNAL_TABLE: &[SignalEntry] = &[
SignalEntry {
canonical: "SIGHUP",
aliases: &["HUP", "HANGUP"],
number: 1,
},
SignalEntry {
canonical: "SIGINT",
aliases: &["INT", "INTERRUPT"],
number: 2,
},
SignalEntry {
canonical: "SIGQUIT",
aliases: &["QUIT"],
number: 3,
},
SignalEntry {
canonical: "SIGKILL",
aliases: &["KILL", "KILLED"],
number: 9,
},
SignalEntry {
canonical: "SIGUSR1",
aliases: &["USR1", "USER DEFINED SIGNAL 1"],
number: 10,
},
SignalEntry {
canonical: "SIGSEGV",
aliases: &["SEGV", "SEGMENTATION FAULT"],
number: 11,
},
SignalEntry {
canonical: "SIGUSR2",
aliases: &["USR2", "USER DEFINED SIGNAL 2"],
number: 12,
},
SignalEntry {
canonical: "SIGPIPE",
aliases: &["PIPE", "BROKEN PIPE"],
number: 13,
},
SignalEntry {
canonical: "SIGALRM",
aliases: &["ALRM", "ALARM CLOCK"],
number: 14,
},
SignalEntry {
canonical: "SIGTERM",
aliases: &["TERM", "TERMINATED"],
number: 15,
},
SignalEntry {
canonical: "SIGCONT",
aliases: &["CONT", "CONTINUED"],
number: 18,
},
SignalEntry {
canonical: "SIGSTOP",
aliases: &["STOP", "STOPPED", "STOPPED (SIGNAL)"],
number: 19,
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_name_accepts_short_names() {
assert_eq!(Signal::from_name("INT"), Some(Signal::INT));
assert_eq!(Signal::from_name("TERM"), Some(Signal::TERM));
assert_eq!(Signal::from_name("KILL"), Some(Signal::KILL));
}
#[test]
fn from_name_strips_sig_prefix() {
assert_eq!(Signal::from_name("SIGINT"), Some(Signal::INT));
assert_eq!(Signal::from_name("SIGTERM"), Some(Signal::TERM));
}
#[test]
fn from_name_is_case_insensitive() {
assert_eq!(Signal::from_name("sigint"), Some(Signal::INT));
assert_eq!(Signal::from_name("Hangup"), Some(Signal::from_number(1)));
}
#[test]
fn from_name_accepts_strsignal_descriptions() {
assert_eq!(Signal::from_name("Interrupt"), Some(Signal::INT));
assert_eq!(Signal::from_name("Terminated"), Some(Signal::TERM));
assert_eq!(
Signal::from_name("User defined signal 1"),
Some(Signal::from_number(10))
);
assert_eq!(
Signal::from_name("Stopped (signal)"),
Some(Signal::from_number(19))
);
}
#[test]
fn from_name_accepts_positive_numbers() {
assert_eq!(Signal::from_name("9"), Some(Signal::KILL));
assert_eq!(Signal::from_name(" 15 "), Some(Signal::TERM));
}
#[test]
fn from_name_rejects_zero_and_negative_numbers() {
assert_eq!(Signal::from_name("0"), None);
assert_eq!(Signal::from_name("-5"), None);
}
#[test]
fn from_name_rejects_unknown_names() {
assert_eq!(Signal::from_name("BOGUS"), None);
assert_eq!(Signal::from_name(""), None);
assert_eq!(Signal::from_name("SIG"), None);
}
#[test]
fn name_returns_canonical_form() {
assert_eq!(Signal::INT.name(), Some("SIGINT"));
assert_eq!(Signal::TERM.name(), Some("SIGTERM"));
assert_eq!(Signal::KILL.name(), Some("SIGKILL"));
assert_eq!(Signal::from_number(11).name(), Some("SIGSEGV"));
}
#[test]
fn name_returns_none_for_unknown_numbers() {
assert_eq!(Signal::from_number(255).name(), None);
}
#[test]
fn display_emits_canonical_name() {
assert_eq!(Signal::KILL.to_string(), "SIGKILL");
assert_eq!(Signal::TERM.to_string(), "SIGTERM");
assert_eq!(Signal::INT.to_string(), "SIGINT");
assert_eq!(Signal::from_number(11).to_string(), "SIGSEGV");
}
#[test]
fn display_falls_back_for_unknown_numbers() {
assert_eq!(Signal::from_number(255).to_string(), "signal(255)");
}
}