use std::collections::BTreeSet;
use std::io::BufRead;
use std::io::{self};
use super::FromBufRead;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct Status {
pub ppid: Option<u64>,
pub ruid: Option<u32>,
pub euid: Option<u32>,
pub suid: Option<u32>,
pub fsuid: Option<u32>,
pub rgid: Option<u32>,
pub egid: Option<u32>,
pub sgid: Option<u32>,
pub fsgid: Option<u32>,
pub groups: Option<Vec<u32>>,
pub umask: Option<u32>,
pub threads: Option<usize>,
pub sig_ign: Option<BTreeSet<usize>>,
pub sig_cgt: Option<BTreeSet<usize>>,
pub sig_blk: Option<BTreeSet<usize>>,
pub sig_pnd: Option<BTreeSet<usize>>,
pub shd_pnd: Option<BTreeSet<usize>>,
pub cpus_allowed: Option<BTreeSet<usize>>,
}
fn nibbles_to_signal_set(nibbles: impl Iterator<Item = u8>) -> BTreeSet<usize> {
let mut set = BTreeSet::new();
for (nibble_idx, nibble) in nibbles.enumerate() {
for bit in 0..4u8 {
if (nibble & (1 << bit)) != 0 {
set.insert(nibble_idx * 4 + bit as usize + 1);
}
}
}
set
}
pub fn signal_bitmask_to_set(val: u64) -> BTreeSet<usize> {
nibbles_to_signal_set((0..16).map(|i| ((val >> (i * 4)) & 0xf) as u8))
}
pub fn parse_signal_mask(s: &str) -> io::Result<BTreeSet<usize>> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"empty signal mask",
));
}
let nibbles: Vec<u8> = trimmed
.bytes()
.rev()
.map(|ch| match ch {
b'0'..=b'9' => Ok(ch - b'0'),
b'a'..=b'f' => Ok(ch - b'a' + 10),
b'A'..=b'F' => Ok(ch - b'A' + 10),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"invalid hex character '{}' in mask '{}'",
ch as char, trimmed
),
)),
})
.collect::<io::Result<_>>()?;
Ok(nibbles_to_signal_set(nibbles.into_iter()))
}
pub fn parse_cpuset_list(s: &str) -> io::Result<BTreeSet<usize>> {
let s = s.trim();
let mut set = BTreeSet::new();
if s.is_empty() {
return Ok(set);
}
for part in s.split(',') {
let part = part.trim();
if let Some((start, end)) = part.split_once('-') {
let start_trimmed = start.trim();
let start: usize = start_trimmed.parse().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid range start '{start_trimmed}': {e}"),
)
})?;
let end_trimmed = end.trim();
let end: usize = end_trimmed.parse().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid range end '{end_trimmed}': {e}"),
)
})?;
if start > end {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid range {start}-{end}: start > end"),
));
}
const MAX_CPUS: usize = 8192;
if end - start + 1 > MAX_CPUS {
let span = end - start + 1;
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"range {start}-{end} spans {span} values, exceeding limit of {MAX_CPUS}",
),
));
}
for cpu in start..=end {
set.insert(cpu);
}
} else {
let val: usize = part.parse().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid value '{part}': {e}"),
)
})?;
set.insert(val);
}
}
Ok(set)
}
fn parse_id_quad(key: &str, value: &str) -> io::Result<[u32; 4]> {
let parts: Vec<&str> = value.split_whitespace().collect();
if parts.len() != 4 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("{key}: expected 4 fields, got {}", parts.len()),
));
}
let mut ids = [0u32; 4];
for (i, part) in parts.iter().enumerate() {
ids[i] = part.parse().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("{key}: invalid value '{part}': {e}"),
)
})?;
}
Ok(ids)
}
impl FromBufRead for Status {
fn from_buf_read(reader: impl BufRead) -> io::Result<Self> {
let mut data = Status::default();
for line in reader.lines() {
let line = line?;
if let Some((key, value)) = line.split_once(':') {
let value = value.trim();
match key {
"PPid" => {
data.ppid = value
.parse()
.map_err(|e| eprintln!("warning: PPid: invalid value '{value}': {e}"))
.ok();
}
"Uid" => match parse_id_quad("Uid", value) {
Ok(ids) => {
data.ruid = Some(ids[0]);
data.euid = Some(ids[1]);
data.suid = Some(ids[2]);
data.fsuid = Some(ids[3]);
}
Err(e) => eprintln!("warning: {e}"),
},
"Gid" => match parse_id_quad("Gid", value) {
Ok(ids) => {
data.rgid = Some(ids[0]);
data.egid = Some(ids[1]);
data.sgid = Some(ids[2]);
data.fsgid = Some(ids[3]);
}
Err(e) => eprintln!("warning: {e}"),
},
"Groups" => {
data.groups = value
.split_whitespace()
.map(|s| s.parse::<u32>())
.collect::<Result<Vec<_>, _>>()
.map_err(|e| eprintln!("warning: Groups: invalid value: {e}"))
.ok();
}
"Umask" => {
data.umask = u32::from_str_radix(value, 8)
.map_err(|e| eprintln!("warning: Umask: invalid value '{value}': {e}"))
.ok();
}
"Threads" => {
data.threads = value
.parse()
.map_err(|e| {
eprintln!("warning: Threads: invalid value '{value}': {e}")
})
.ok();
}
"SigIgn" => {
data.sig_ign = parse_signal_mask(value)
.map_err(|e| eprintln!("warning: SigIgn: {e}"))
.ok();
}
"SigCgt" => {
data.sig_cgt = parse_signal_mask(value)
.map_err(|e| eprintln!("warning: SigCgt: {e}"))
.ok();
}
"SigBlk" => {
data.sig_blk = parse_signal_mask(value)
.map_err(|e| eprintln!("warning: SigBlk: {e}"))
.ok();
}
"SigPnd" => {
data.sig_pnd = parse_signal_mask(value)
.map_err(|e| eprintln!("warning: SigPnd: {e}"))
.ok();
}
"ShdPnd" => {
data.shd_pnd = parse_signal_mask(value)
.map_err(|e| eprintln!("warning: ShdPnd: {e}"))
.ok();
}
"Cpus_allowed_list" => {
data.cpus_allowed = parse_cpuset_list(value)
.map_err(|e| eprintln!("warning: Cpus_allowed_list: {e}"))
.ok();
}
_ => {}
}
}
}
Ok(data)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_empty_mask() {
let set = parse_signal_mask("0000000000000000").unwrap();
assert!(set.is_empty());
}
#[test]
fn parse_single_signal() {
let set = parse_signal_mask("0000000000000001").unwrap();
assert!(set.contains(&1));
assert!(!set.contains(&2));
}
#[test]
fn parse_multiple_signals() {
let set = parse_signal_mask("0000000000000003").unwrap();
assert!(set.contains(&1));
assert!(set.contains(&2));
assert!(!set.contains(&3));
}
#[test]
fn parse_error_on_empty() {
assert!(parse_signal_mask("").is_err());
}
#[test]
fn parse_error_on_invalid_hex() {
assert!(parse_signal_mask("xyz").is_err());
}
#[test]
fn parse_yields_correct_signals() {
let set = parse_signal_mask("5").unwrap();
assert_eq!(set.iter().copied().collect::<Vec<_>>(), vec![1, 3]);
}
#[test]
fn status_parses_uid_gid() {
let input = "Uid:\t1000\t1000\t1000\t1000\nGid:\t100\t100\t100\t100\n";
let status = Status::from_buf_read(input.as_bytes()).unwrap();
assert_eq!(status.ruid, Some(1000));
assert_eq!(status.euid, Some(1000));
assert_eq!(status.rgid, Some(100));
}
#[test]
fn status_parses_signal_masks() {
let input = "SigIgn:\t0000000000000004\nSigCgt:\t0000000000000002\n";
let status = Status::from_buf_read(input.as_bytes()).unwrap();
assert_eq!(
status
.sig_ign
.as_ref()
.unwrap()
.iter()
.copied()
.collect::<Vec<_>>(),
vec![3]
);
assert_eq!(
status
.sig_cgt
.as_ref()
.unwrap()
.iter()
.copied()
.collect::<Vec<_>>(),
vec![2]
);
}
}