use std::collections::HashMap;
use std::thread;
use std::time::Duration;
use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
use crate::sources::helpers::{extract_delta_bytes_i64, run_command};
const IOREG_PATH: &str = "/usr/sbin/ioreg";
const SNAPSHOT_DELAY: Duration = Duration::from_millis(80);
const NUM_SNAPSHOTS: usize = 4;
static IOREGISTRY_INFO: SourceInfo = SourceInfo {
name: "ioregistry",
description: "Mines macOS IORegistry for fluctuating hardware counters and extracts LSBs of their deltas",
physics: "Mines the macOS IORegistry for all fluctuating hardware counters \u{2014} GPU \
utilization, NVMe SMART counters, memory controller stats, Neural Engine \
buffer allocations, DART IOMMU activity, Mach port counts, and display \
vsync counters. Each counter is driven by independent hardware subsystems. \
The LSBs of their deltas capture silicon-level activity across the entire SoC.",
category: SourceCategory::System,
platform: Platform::MacOS,
requirements: &[Requirement::IOKit],
entropy_rate_estimate: 2.0,
composite: false,
is_fast: false,
};
pub struct IORegistryEntropySource;
fn snapshot_ioreg() -> Option<HashMap<String, i64>> {
let stdout = run_command(IOREG_PATH, &["-l", "-w0"])?;
let mut map = HashMap::new();
for line in stdout.lines() {
let trimmed = line.trim();
let trimmed = trimmed
.trim_start_matches('|')
.trim_start_matches('+')
.trim();
extract_quoted_key_numbers(trimmed, &mut map);
}
Some(map)
}
fn extract_quoted_key_numbers(s: &str, map: &mut HashMap<String, i64>) {
let bytes = s.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
if bytes[i] != b'"' {
i += 1;
continue;
}
let key_start = i + 1;
let mut key_end = key_start;
while key_end < len && bytes[key_end] != b'"' {
key_end += 1;
}
if key_end >= len {
break;
}
let key = &s[key_start..key_end];
let mut j = key_end + 1;
while j < len && bytes[j] == b' ' {
j += 1;
}
if j >= len || bytes[j] != b'=' {
i = key_end + 1;
continue;
}
j += 1;
while j < len && bytes[j] == b' ' {
j += 1;
}
let num_start = j;
if j < len && bytes[j] == b'-' {
j += 1;
}
while j < len && bytes[j].is_ascii_digit() {
j += 1;
}
if j > num_start
&& (j >= len || !bytes[j].is_ascii_alphanumeric())
&& let Ok(v) = s[num_start..j].parse::<i64>()
{
map.insert(key.to_string(), v);
}
i = j.max(key_end + 1);
}
}
impl EntropySource for IORegistryEntropySource {
fn info(&self) -> &SourceInfo {
&IOREGISTRY_INFO
}
fn is_available(&self) -> bool {
cfg!(target_os = "macos") && std::path::Path::new(IOREG_PATH).exists()
}
fn collect(&self, n_samples: usize) -> Vec<u8> {
let mut snapshots: Vec<HashMap<String, i64>> = Vec::with_capacity(NUM_SNAPSHOTS);
for i in 0..NUM_SNAPSHOTS {
if i > 0 {
thread::sleep(SNAPSHOT_DELAY);
}
match snapshot_ioreg() {
Some(snap) => snapshots.push(snap),
None => return Vec::new(),
}
}
if snapshots.len() < 2 {
return Vec::new();
}
let common_keys: Vec<String> = {
let first = &snapshots[0];
first
.keys()
.filter(|k| snapshots.iter().all(|snap| snap.contains_key(*k)))
.cloned()
.collect()
};
let mut all_deltas: Vec<i64> = Vec::new();
for key in &common_keys {
for pair in snapshots.windows(2) {
let v1 = pair[0][key];
let v2 = pair[1][key];
let delta = v2.wrapping_sub(v1);
if delta != 0 {
all_deltas.push(delta);
}
}
}
extract_delta_bytes_i64(&all_deltas, n_samples)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sources::helpers::extract_lsbs_i64 as extract_lsbs;
#[test]
fn ioregistry_info() {
let src = IORegistryEntropySource;
assert_eq!(src.name(), "ioregistry");
assert_eq!(src.info().category, SourceCategory::System);
assert!((src.info().entropy_rate_estimate - 2.0).abs() < f64::EPSILON);
}
#[test]
fn extract_lsbs_basic() {
let deltas = vec![1i64, 2, 3, 4, 5, 6, 7, 8];
let bytes = extract_lsbs(&deltas);
assert_eq!(bytes.len(), 1);
assert_eq!(bytes[0], 0xAA);
}
#[test]
#[cfg(target_os = "macos")]
#[ignore] fn ioregistry_collects_bytes() {
let src = IORegistryEntropySource;
if src.is_available() {
let data = src.collect(64);
assert!(!data.is_empty());
assert!(data.len() <= 64);
}
}
}