rcpufetch 0.0.5

[ALPHA] A rusty crossplatform, but simple CLI binutil for reading CPU information.
//! macOS CPU information module for rcpufetch.
//!
//! This module implements comprehensive CPU information gathering for macOS systems,
//! supporting both Intel and Apple Silicon architectures. It uses sysctl APIs and
//! system commands to collect model, vendor, core counts, cache sizes, frequency,
//! and CPU feature flags. All public items are documented following the standards
//! outlined in CONTRIBUTING.md and the linux.rs example.

use crate::cpu::ArchInfo;
use crate::report::CpuReport;
use std::process::Command;

/// (size_kb, count) as returned by sysctl-based cache-size lookups below.
type CacheSize = Option<(u32, u32)>;

/// Struct representing parsed macOS CPU information.
///
/// Contains comprehensive CPU information gathered from sysctl and system commands,
/// including model, vendor, architecture, core counts, cache sizes, frequency, and flags.
pub struct MacOSCpuInfo {
    /// CPU model name from sysctl
    model: String,
    /// Vendor (Intel, AMD, Apple, or Unknown)
    vendor: String,
    /// System architecture (e.g., x86_64, arm64)
    architecture: String,
    /// Physical core count
    physical_cores: u32,
    /// Logical core count (including hyperthreading)
    logical_cores: u32,
    /// Base frequency in MHz (if available)
    base_mhz: Option<f32>,
    /// L1 cache (size in KB, count)
    l1_size: Option<(u32, u32)>,
    /// L2 cache (size in KB, count)
    l2_size: Option<(u32, u32)>,
    /// L3 cache (size in KB, count)
    l3_size: Option<(u32, u32)>,
    /// CPU feature flags and capabilities
    flags: String,
    /// Architecture-derived vendor/microarchitecture/technology info (cpuid on Intel/AMD Macs)
    arch_info: ArchInfo,
}

impl MacOSCpuInfo {
    /// Gather all CPU information for macOS.
    ///
    /// Collects model, vendor, architecture, core counts, cache sizes, frequency,
    /// and CPU flags using sysctl and system commands. Handles both Intel and Apple Silicon.
    ///
    /// # Returns
    ///
    /// * `Ok(MacOSCpuInfo)` if all required information is gathered
    /// * `Err(String)` if a critical error occurs during information gathering
    pub fn new() -> Result<Self, String> {
        // Get CPU brand string
        let model = Self::get_sysctl_string("machdep.cpu.brand_string")?;

        // Get architecture using uname -m
        let architecture = Self::get_architecture()?;

        // Determine vendor from brand string
        let vendor = if model.to_lowercase().contains("intel") {
            "Intel".to_string()
        } else if model.to_lowercase().contains("amd") {
            "AMD".to_string()
        } else if model.to_lowercase().contains("apple") {
            "Apple".to_string()
        } else {
            "Unknown".to_string()
        };

        // Get core counts
        let physical_cores = Self::get_sysctl_u32("machdep.cpu.core_count")
            .unwrap_or_else(|_| Self::get_sysctl_u32("machdep.cpu.cores_per_package").unwrap_or(0));
        let logical_cores = Self::get_sysctl_u32("machdep.cpu.thread_count").unwrap_or_else(|_| {
            Self::get_sysctl_u32("machdep.cpu.logical_per_package").unwrap_or(physical_cores)
        });

        // Get base frequency (if available)
        let base_mhz = Self::get_sysctl_string("machdep.cpu.max_basic")
            .ok()
            .and_then(|s| s.parse::<f32>().ok());

        // Parse cache information - prefer detailed perflevel cache info for Apple Silicon
        let (l1_size, l2_size, l3_size) = Self::get_cache_info();

        // Get CPU flags
        let flags = Self::get_cpu_flags();

        // Architecture-derived info: cpuid on Intel/AMD Macs (the instruction works
        // identically under macOS since it's not a syscall); Apple Silicon already gets
        // a descriptive chip name straight from `machdep.cpu.brand_string`.
        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
        let arch_info = crate::cpu::x86::detect();
        #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
        let arch_info = ArchInfo::default();

        Ok(Self {
            model,
            vendor,
            architecture,
            physical_cores,
            logical_cores,
            base_mhz,
            l1_size,
            l2_size,
            l3_size,
            flags,
            arch_info,
        })
    }

    pub fn to_report(&self) -> CpuReport {
        let info = &self.arch_info;
        CpuReport {
            model: self.model.clone(),
            vendor: info.vendor.clone().unwrap_or_else(|| self.vendor.clone()),
            uarch: info.uarch.clone(),
            soc: None,
            technology_nm: info.process_nm,
            architecture: self.architecture.clone(),
            physical_cores: self.physical_cores,
            logical_cores: self.logical_cores,
            max_ghz: None,
            base_ghz: self.base_mhz.map(|mhz| mhz / 1000.0),
            // macOS's (size_kb, count) needs turning into report's (per_core, total) shape.
            l1i_kb: None,
            l1d_kb: self
                .l1_size
                .map(|(size, count)| (size, size * count.max(1))),
            l2_kb: self
                .l2_size
                .map(|(size, count)| (size, size * count.max(1))),
            l3_kb: self
                .l3_size
                .map(|(size, count)| (size, size * count.max(1))),
            flags: if !info.features.is_empty() {
                info.features.clone()
            } else {
                self.flags
                    .split(',')
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty())
                    .collect()
            },
        }
    }

    /// Helper function to get comprehensive cache information.
    ///
    /// Returns L1, L2, and L3 cache sizes and counts, using sysctl keys and
    /// performance level queries for Apple Silicon.
    ///
    /// # Returns
    ///
    /// Tuple of (L1, L2, L3) cache info as Option<(size_kb, count)>
    fn get_cache_info() -> (CacheSize, CacheSize, CacheSize) {
        // First try the traditional hw.cachesize approach
        let cache_sizes = Self::get_sysctl_string("hw.cachesize").unwrap_or_default();
        let cache_config = Self::get_sysctl_string("hw.cacheconfig").unwrap_or_default();

        let size_parts: Vec<&str> = cache_sizes.split_whitespace().collect();
        let config_parts: Vec<&str> = cache_config.split_whitespace().collect();

        let l1_size = if size_parts.len() >= 2 && config_parts.len() >= 2 {
            let size_bytes = size_parts[1].parse::<u32>().unwrap_or(0);
            let count = config_parts[1].parse::<u32>().unwrap_or(0);
            if size_bytes > 0 && count > 0 {
                Some((size_bytes / 1024, count)) // Convert bytes to KB
            } else {
                None
            }
        } else {
            None
        };

        let l2_size = if size_parts.len() >= 3 && config_parts.len() >= 3 {
            let size_bytes = size_parts[2].parse::<u32>().unwrap_or(0);
            let count = config_parts[2].parse::<u32>().unwrap_or(0);
            if size_bytes > 0 && count > 0 {
                Some((size_bytes / 1024, count)) // Convert bytes to KB
            } else {
                None
            }
        } else {
            None
        };

        let mut l3_size = if size_parts.len() >= 4 && config_parts.len() >= 4 {
            let size_bytes = size_parts[3].parse::<u32>().unwrap_or(0);
            let count = config_parts[3].parse::<u32>().unwrap_or(0);
            if size_bytes > 0 && count > 0 {
                Some((size_bytes / 1024, count)) // Convert bytes to KB
            } else {
                None
            }
        } else {
            None
        };

        // For Apple Silicon, if L3 is not available from hw.cachesize, check performance level caches
        if l3_size.is_none() {
            // Check if we have performance level cache information (Apple Silicon)
            let perf0_l2 = Self::get_sysctl_u32("hw.perflevel0.l2cachesize").ok();
            let perf1_l2 = Self::get_sysctl_u32("hw.perflevel1.l2cachesize").ok();

            if let (Some(p0_l2), Some(p1_l2)) = (perf0_l2, perf1_l2) {
                // If we have different performance levels with different L2 sizes,
                // report the larger one as "shared cache" equivalent
                if p0_l2 != p1_l2 {
                    let larger_cache = std::cmp::max(p0_l2, p1_l2);
                    l3_size = Some((larger_cache / 1024, 1)); // Convert bytes to KB, show as 1 unit
                }
            }
        }

        (l1_size, l2_size, l3_size)
    }

    /// Helper function to get a string value from sysctl.
    ///
    /// # Arguments
    ///
    /// * `key` - sysctl key to query
    ///
    /// # Returns
    ///
    /// * `Ok(String)` with the sysctl value
    /// * `Err(String)` with error context if sysctl fails
    fn get_sysctl_string(key: &str) -> Result<String, String> {
        let output = Command::new("sysctl")
            .arg("-n")
            .arg(key)
            .output()
            .map_err(|e| format!("Failed to execute sysctl: {}", e))?;

        if output.status.success() {
            Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
        } else {
            Err(format!("sysctl command failed for key: {}", key))
        }
    }

    /// Helper function to get a u32 value from sysctl.
    ///
    /// # Arguments
    ///
    /// * `key` - sysctl key to query
    ///
    /// # Returns
    ///
    /// * `Ok(u32)` with the parsed value
    /// * `Err(String)` with error context if sysctl fails or value is not a valid u32
    fn get_sysctl_u32(key: &str) -> Result<u32, String> {
        let value_str = Self::get_sysctl_string(key)?;
        value_str
            .parse::<u32>()
            .map_err(|e| format!("Failed to parse '{}' as u32: {}", value_str, e))
    }

    /// Get system architecture using uname -m.
    ///
    /// # Returns
    ///
    /// * `Ok(String)` with the architecture string
    /// * `Err(String)` if uname fails
    fn get_architecture() -> Result<String, String> {
        let output = Command::new("uname")
            .arg("-m")
            .output()
            .map_err(|e| format!("Failed to execute uname: {}", e))?;

        if output.status.success() {
            Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
        } else {
            Err("uname command failed".to_string())
        }
    }

    /// Get CPU flags from sysctl hw.optional.arm.* keys.
    ///
    /// Queries all available ARM CPU feature flags via sysctl and returns a comma-separated
    /// string of enabled features, similar to Linux /proc/cpuinfo flags. Only features with
    /// value '1' (enabled) are included in the output.
    ///
    /// # Returns
    ///
    /// Returns a comma-separated string of enabled CPU feature flags (e.g., "FEAT_AES,FEAT_SHA256,FEAT_CRC32")
    /// or an empty string if no flags are available or if not running on ARM architecture.
    fn get_cpu_flags() -> String {
        // Try to get a list of all hw.optional.arm.* sysctl keys
        let output = Command::new("sysctl").arg("hw.optional.arm.").output();

        match output {
            Ok(result) if result.status.success() => {
                let output_str = String::from_utf8_lossy(&result.stdout);
                let mut enabled_flags = Vec::new();

                for line in output_str.lines() {
                    if let Some((key, value)) = line.split_once(": ") {
                        // Parse the value - only include flags that are enabled (value = 1)
                        if value.trim() == "1" {
                            // Extract the flag name from the key (everything after "hw.optional.arm.")
                            if let Some(flag_name) = key.strip_prefix("hw.optional.arm.") {
                                enabled_flags.push(flag_name.to_string());
                            }
                        }
                    }
                }

                enabled_flags.join(",")
            }
            _ => String::new(), // Return empty string if sysctl fails (e.g., not ARM architecture)
        }
    }
}