use libc;
pub fn parse_cpu_list(s: &str) -> Option<Vec<u32>> {
const MAX_CPU_RANGE_EXPANSION: u64 = 65_536;
let s = s.trim();
if s.is_empty() {
return None;
}
let mut out: Vec<u32> = Vec::new();
for token in s.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
if let Some((lo, hi)) = token.split_once('-') {
let lo: u32 = lo.parse().ok()?;
let hi: u32 = hi.parse().ok()?;
if hi < lo {
return None;
}
let span = (hi as u64) - (lo as u64) + 1;
if span > MAX_CPU_RANGE_EXPANSION {
return None;
}
for c in lo..=hi {
out.push(c);
}
} else {
out.push(token.parse::<u32>().ok()?);
}
}
out.sort_unstable();
out.dedup();
Some(out)
}
pub fn read_affinity(tid: i32) -> Option<Vec<u32>> {
let mut bits = AFFINITY_INITIAL_BITS;
loop {
let mut buffer = affinity_zeroed_buffer(bits);
let bytes = std::mem::size_of_val(buffer.as_slice());
let ret = unsafe {
libc::syscall(
libc::SYS_sched_getaffinity,
tid as libc::pid_t,
bytes,
buffer.as_mut_ptr(),
)
};
if ret >= 0 {
let written_bytes = ret as usize;
return extract_cpus_from_mask(&buffer, written_bytes);
}
let errno = std::io::Error::last_os_error().raw_os_error();
if errno != Some(libc::EINVAL) {
return None;
}
let Some(next) = affinity_next_bits(bits) else {
return None;
};
bits = next;
}
}
pub const AFFINITY_INITIAL_BITS: usize = 8192;
pub const AFFINITY_MAX_BITS: usize = 262144;
pub(crate) fn affinity_next_bits(current_bits: usize) -> Option<usize> {
let doubled = current_bits.checked_mul(2)?;
if doubled > AFFINITY_MAX_BITS {
None
} else {
Some(doubled)
}
}
fn affinity_zeroed_buffer(bits: usize) -> Vec<libc::c_ulong> {
let word_bits = libc::c_ulong::BITS as usize;
let words = bits.div_ceil(word_bits);
vec![0 as libc::c_ulong; words]
}
fn extract_cpus_from_mask(buffer: &[libc::c_ulong], written_bytes: usize) -> Option<Vec<u32>> {
let word_bytes = std::mem::size_of::<libc::c_ulong>();
let word_bits = libc::c_ulong::BITS as usize;
let written_words = written_bytes / word_bytes;
let mut cpus: Vec<u32> = Vec::new();
for (word_idx, &word) in buffer.iter().take(written_words).enumerate() {
if word == 0 {
continue;
}
for bit in 0..word_bits {
if word & (1 as libc::c_ulong) << bit != 0 {
let cpu = word_idx * word_bits + bit;
cpus.push(cpu as u32);
}
}
}
if cpus.is_empty() { None } else { Some(cpus) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_cpu_list_accepts_ranges_singletons_and_mixtures() {
assert_eq!(parse_cpu_list("0-3").unwrap(), vec![0, 1, 2, 3]);
assert_eq!(parse_cpu_list("5").unwrap(), vec![5]);
assert_eq!(parse_cpu_list("0,2,4").unwrap(), vec![0, 2, 4]);
assert_eq!(parse_cpu_list("0-2,4,6-7").unwrap(), vec![0, 1, 2, 4, 6, 7]);
}
#[test]
fn parse_cpu_list_rejects_malformed_input() {
assert!(parse_cpu_list("").is_none());
assert!(parse_cpu_list("5-3").is_none());
assert!(parse_cpu_list("abc").is_none());
assert!(parse_cpu_list("0-").is_none());
assert!(parse_cpu_list("-3").is_none());
}
#[test]
fn parse_cpu_list_dedups_and_sorts() {
assert_eq!(parse_cpu_list("3,0-2,1,2").unwrap(), vec![0, 1, 2, 3]);
}
#[test]
fn parse_cpu_list_rejects_huge_range() {
assert_eq!(parse_cpu_list("0-4294967295"), None);
assert_eq!(parse_cpu_list("0-65536"), None);
let at_cap = parse_cpu_list("0-65535").unwrap();
assert_eq!(at_cap.len(), 65_536);
let realistic = parse_cpu_list("0-8191").unwrap();
assert_eq!(realistic.len(), 8192);
}
#[test]
fn parse_cpu_list_single_element_range_lo_equals_hi() {
assert_eq!(parse_cpu_list("5-5").unwrap(), vec![5]);
assert_eq!(parse_cpu_list("0-0").unwrap(), vec![0]);
}
#[test]
fn parse_cpu_list_trailing_comma_accepted() {
assert_eq!(parse_cpu_list("0,1,").unwrap(), vec![0, 1]);
assert_eq!(parse_cpu_list(",0,1").unwrap(), vec![0, 1]);
}
#[test]
fn affinity_next_bits_doubles_until_ceiling() {
assert_eq!(AFFINITY_INITIAL_BITS, 8192);
assert_eq!(AFFINITY_MAX_BITS, 262144);
let mut cur = AFFINITY_INITIAL_BITS;
let expected = [16384usize, 32768, 65536, 131072, 262144];
for &want in &expected {
let next = affinity_next_bits(cur).expect("doubling must succeed below ceiling");
assert_eq!(next, want, "expected {want}, got {next}");
cur = next;
}
assert_eq!(
affinity_next_bits(AFFINITY_MAX_BITS),
None,
"at the ceiling, no further retry must be allowed",
);
}
#[test]
fn extract_cpus_from_mask_single_bit_in_first_word() {
let mut buf = vec![0 as libc::c_ulong; 4];
buf[0] = (1 as libc::c_ulong) << 5;
let bytes = std::mem::size_of_val(buf.as_slice());
let cpus = extract_cpus_from_mask(&buf, bytes).expect("non-empty mask");
assert_eq!(cpus, vec![5]);
}
#[test]
fn extract_cpus_from_mask_offset_bit_in_later_word() {
let word_bits = libc::c_ulong::BITS as usize;
let mut buf = vec![0 as libc::c_ulong; 4];
buf[2] = (1 as libc::c_ulong) << 3;
let bytes = std::mem::size_of_val(buf.as_slice());
let cpus = extract_cpus_from_mask(&buf, bytes).expect("non-empty mask");
let expected = (2 * word_bits + 3) as u32;
assert_eq!(cpus, vec![expected]);
}
#[test]
fn extract_cpus_from_mask_respects_written_bytes() {
let mut buf = vec![0 as libc::c_ulong; 4];
buf[0] = (1 as libc::c_ulong) << 7; buf[3] = 1 as libc::c_ulong; let one_word_bytes = std::mem::size_of::<libc::c_ulong>();
let cpus = extract_cpus_from_mask(&buf, one_word_bytes).expect("non-empty mask");
assert_eq!(cpus, vec![7]);
}
#[test]
fn extract_cpus_from_mask_empty_buffer_returns_none() {
let buf = vec![0 as libc::c_ulong; 4];
let bytes = std::mem::size_of_val(buf.as_slice());
assert_eq!(extract_cpus_from_mask(&buf, bytes), None);
}
#[test]
fn affinity_zeroed_buffer_rounds_up_and_is_zeroed() {
let word_bits = libc::c_ulong::BITS as usize;
let exact = affinity_zeroed_buffer(word_bits);
assert_eq!(exact.len(), 1);
let over = affinity_zeroed_buffer(word_bits + 1);
assert_eq!(over.len(), 2);
let init = affinity_zeroed_buffer(AFFINITY_INITIAL_BITS);
assert_eq!(init.len(), AFFINITY_INITIAL_BITS / word_bits);
assert!(init.iter().all(|&w| w == 0));
}
#[test]
fn read_affinity_for_self_returns_at_least_one_cpu() {
let pid = std::process::id() as i32;
let cpus = read_affinity(pid).expect("own affinity must resolve");
assert!(
!cpus.is_empty(),
"self affinity must carry at least one CPU"
);
let mut sorted = cpus.clone();
sorted.sort_unstable();
assert_eq!(cpus, sorted, "cpus must be returned sorted ascending");
}
}