diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 42c5ba4cf..162b0b6d9 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -43,7 +43,7 @@ use uucore::display::{Quotable, print_all_env_vars}; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError}; use uucore::line_ending::LineEnding; #[cfg(unix)] -use uucore::signals::{ALL_SIGNALS, signal_by_name_or_value, signal_name_by_value}; +use uucore::signals::{signal_by_name_or_value, signal_name_by_value, signal_number_upper_bound}; use uucore::translate; use uucore::{format_usage, show_warning}; @@ -216,7 +216,7 @@ impl SignalRequest { f(sig, true)?; } if self.apply_all { - for sig_value in 1..ALL_SIGNALS.len() { + for sig_value in 1..=signal_number_upper_bound() { if self.signals.contains(&sig_value) { continue; } diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index ac40fa8cb..81186b5cb 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -13,7 +13,10 @@ use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::translate; -use uucore::signals::{ALL_SIGNALS, signal_by_name_or_value, signal_name_by_value}; +use uucore::signals::{ + signal_by_name_or_value, signal_list_name_by_value, signal_list_value_by_name_or_number, + signal_name_by_value, signal_number_upper_bound, +}; use uucore::{format_usage, show}; // When the -l option is selected, the program displays the type of signal related to a certain @@ -159,34 +162,44 @@ fn handle_obsolete(args: &mut Vec) -> Option { } fn table() { - for (idx, signal) in ALL_SIGNALS.iter().enumerate() { - println!("{idx: >#2} {signal}"); + for signal_value in 0..=signal_number_upper_bound() { + if let Some(signal_name) = signal_list_name_by_value(signal_value) { + println!("{signal_value: >#2} {signal_name}"); + } } } -fn print_signal(signal_name_or_value: &str) -> UResult<()> { - // Closure used to track the last 8 bits of the signal value - // when the -l option is passed only the lower 8 bits are important - // or the value is in range [128, 159] - // Example: kill -l 143 => TERM because 143 = 15 + 128 - // Example: kill -l 2304 => EXIT - let lower_8_bits = |x: usize| x & 0xff; - let option_num_parse = signal_name_or_value.parse::().ok(); +fn normalize_list_signal_value(signal_value: usize) -> Option { + // `kill -l` also accepts wait-status-like values and decodes the signal + // number from the low 8 bits. + let lower_8_bits = signal_value & 0xff; + if lower_8_bits <= signal_number_upper_bound() { + return Some(lower_8_bits); + } - for (value, &signal) in ALL_SIGNALS.iter().enumerate() { - if signal.eq_ignore_ascii_case(signal_name_or_value) - || format!("SIG{signal}").eq_ignore_ascii_case(signal_name_or_value) - { - println!("{value}"); - return Ok(()); - } else if signal_name_or_value == value.to_string() - || option_num_parse.is_some_and(|signal_value| lower_8_bits(signal_value) == value) - || option_num_parse.is_some_and(|signal_value| signal_value == value + OFFSET) - { - println!("{signal}"); + signal_value + .checked_sub(OFFSET) + .filter(|value| *value <= signal_number_upper_bound()) +} + +fn print_signal(signal_name_or_value: &str) -> UResult<()> { + if let Ok(signal_value) = signal_name_or_value.parse::() { + // GNU kill accepts plain signal numbers, values masked to the low 8 bits, + // and exit statuses that encode `128 + signal`. + if let Some(signal_value) = normalize_list_signal_value(signal_value) { + println!( + "{}", + signal_list_name_by_value(signal_value).unwrap_or_else(|| signal_value.to_string()) + ); return Ok(()); } } + + if let Some(signal_value) = signal_list_value_by_name_or_number(signal_name_or_value) { + println!("{signal_value}"); + return Ok(()); + } + Err(USimpleError::new( 1, translate!("kill-error-invalid-signal", "signal" => signal_name_or_value.quote()), @@ -194,8 +207,10 @@ fn print_signal(signal_name_or_value: &str) -> UResult<()> { } fn print_signals() { - for signal in ALL_SIGNALS { - println!("{signal}"); + for signal_value in 0..=signal_number_upper_bound() { + if let Some(signal_name) = signal_list_name_by_value(signal_value) { + println!("{signal_name}"); + } } } diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 319b36048..6b89704fc 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -12,6 +12,8 @@ #[cfg(unix)] use nix::errno::Errno; +#[cfg(any(target_os = "linux", target_os = "android"))] +use nix::libc; #[cfg(unix)] use nix::sys::signal::{ SaFlags, SigAction, SigHandler, SigHandler::SigDfl, SigHandler::SigIgn, SigSet, Signal, @@ -411,6 +413,63 @@ pub fn signal_name_by_value(signal_value: usize) -> Option<&'static str> { ALL_SIGNALS.get(signal_value).copied() } +#[cfg(any(target_os = "linux", target_os = "android"))] +fn realtime_signal_bounds() -> Option<(usize, usize)> { + let rtmin = libc::SIGRTMIN(); + let rtmax = libc::SIGRTMAX(); + + (0 < rtmin && rtmin <= rtmax).then_some((rtmin as usize, rtmax as usize)) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +fn realtime_signal_bounds() -> Option<(usize, usize)> { + None +} + +/// Returns the largest signal number that list-style interfaces should accept. +pub fn signal_number_upper_bound() -> usize { + let base = ALL_SIGNALS.len() - 1; + + realtime_signal_bounds().map_or(base, |(_, rtmax)| rtmax.max(base)) +} + +/// Returns the signal name for list-style interfaces. +pub fn signal_list_name_by_value(signal_value: usize) -> Option { + if let Some(signal_name) = signal_name_by_value(signal_value) { + return Some(signal_name.to_string()); + } + + realtime_signal_bounds().and_then(|(rtmin, rtmax)| { + if signal_value == rtmin { + Some("RTMIN".to_string()) + } else if signal_value == rtmax { + Some("RTMAX".to_string()) + } else { + None + } + }) +} + +/// Returns the signal value for list-style interfaces. +pub fn signal_list_value_by_name_or_number(spec: &str) -> Option { + let spec_upcase = spec.to_uppercase(); + + if let Ok(value) = spec_upcase.parse::() { + return (value <= signal_number_upper_bound()).then_some(value); + } + + if let Some(value) = signal_by_name_or_value(&spec_upcase) { + return Some(value); + } + + let signal_name = spec_upcase.trim_start_matches("SIG"); + realtime_signal_bounds().and_then(|(rtmin, rtmax)| match signal_name { + "RTMIN" => Some(rtmin), + "RTMAX" => Some(rtmax), + _ => None, + }) +} + /// Restores SIGPIPE to default behavior (process terminates on broken pipe). #[cfg(unix)] pub fn enable_pipe_errors() -> Result<(), Errno> { @@ -642,3 +701,56 @@ fn name() { assert_eq!(signal_name_by_value(value), Some(*signal)); } } + +#[test] +fn list_signal_names_match_static_signal_names() { + for (value, signal) in ALL_SIGNALS.iter().enumerate() { + assert_eq!(signal_list_name_by_value(value), Some(signal.to_string())); + } +} + +#[test] +fn list_signal_numbers_follow_upper_bound() { + assert_eq!( + signal_list_value_by_name_or_number(&signal_number_upper_bound().to_string()), + Some(signal_number_upper_bound()) + ); + assert_eq!( + signal_list_value_by_name_or_number(&(signal_number_upper_bound() + 1).to_string()), + None + ); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn linux_realtime_signal_upper_bound_includes_rtmax() { + let (_, rtmax) = realtime_signal_bounds().unwrap(); + assert!(signal_number_upper_bound() >= rtmax); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn linux_realtime_signal_names_are_listed() { + let (rtmin, rtmax) = realtime_signal_bounds().unwrap(); + + assert_eq!(signal_list_name_by_value(rtmin), Some("RTMIN".to_string())); + assert_eq!(signal_list_name_by_value(rtmax), Some("RTMAX".to_string())); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn linux_realtime_signal_names_resolve_to_runtime_values() { + let (rtmin, rtmax) = realtime_signal_bounds().unwrap(); + + assert_eq!(signal_list_value_by_name_or_number("RTMIN"), Some(rtmin)); + assert_eq!(signal_list_value_by_name_or_number("RTMAX"), Some(rtmax)); + assert_eq!(signal_list_value_by_name_or_number("SIGRTMIN"), Some(rtmin)); + assert_eq!(signal_list_value_by_name_or_number("SIGRTMAX"), Some(rtmax)); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn linux_unnamed_signal_numbers_are_valid_for_lists() { + assert_eq!(signal_list_value_by_name_or_number("32"), Some(32)); + assert_eq!(signal_list_value_by_name_or_number("33"), Some(33)); +} diff --git a/tests/by-util/test_kill.rs b/tests/by-util/test_kill.rs index aad1982d6..c666db371 100644 --- a/tests/by-util/test_kill.rs +++ b/tests/by-util/test_kill.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore IAMNOTASIGNAL +// spell-checker:ignore IAMNOTASIGNAL RTMAX RTMIN SIGRTMAX use regex::Regex; use std::os::unix::process::ExitStatusExt; use std::process::{Child, Command}; @@ -67,6 +67,16 @@ fn test_kill_list_all_signals() { .stdout_contains("EXIT"); } +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_kill_list_contains_realtime_signals() { + new_ucmd!() + .arg("-l") + .succeeds() + .stdout_contains("RTMIN") + .stdout_contains("RTMAX"); +} + #[test] fn test_kill_list_final_new_line() { let re = Regex::new("\\n$").unwrap(); @@ -85,6 +95,16 @@ fn test_kill_list_all_signals_as_table() { .stdout_contains("EXIT"); } +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_kill_table_contains_realtime_signals() { + new_ucmd!() + .arg("-t") + .succeeds() + .stdout_contains("RTMIN") + .stdout_contains("RTMAX"); +} + #[test] fn test_kill_table_starts_at_0() { new_ucmd!() @@ -118,6 +138,16 @@ fn test_kill_list_one_signal_from_number() { .stdout_only("KILL\n"); } +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_kill_list_rtmax_from_name() { + new_ucmd!() + .arg("-l") + .arg("RTMAX") + .succeeds() + .stdout_only(format!("{}\n", libc::SIGRTMAX())); +} + #[test] fn test_kill_list_one_signal_from_invalid_number() { new_ucmd!() @@ -385,6 +415,50 @@ fn test_kill_with_list_lower_bits_unrecognized() { new_ucmd!().arg("-l").arg("384").fails(); } +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_kill_with_list_unnamed_signal_numbers() { + new_ucmd!() + .arg("-l") + .arg("32") + .succeeds() + .stdout_only("32\n"); + new_ucmd!() + .arg("-l") + .arg("33") + .succeeds() + .stdout_only("33\n"); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_kill_with_list_all_signal_numbers_up_to_last_named_signal() { + let last_signal_name = new_ucmd!() + .arg("-l") + .succeeds() + .stdout_str() + .lines() + .last() + .unwrap() + .to_string(); + + let last_signal_number: usize = new_ucmd!() + .arg("-l") + .arg("--") + .arg(&last_signal_name) + .succeeds() + .stdout_str() + .trim() + .parse() + .unwrap(); + + let args = std::iter::once(String::from("--")) + .chain((0..=last_signal_number).map(|signal| signal.to_string())) + .collect::>(); + + new_ucmd!().arg("-l").args(&args).succeeds(); +} + #[test] fn test_kill_with_signal_and_table() { let target = Target::new();