rcpufetch 0.0.5

[ALPHA] A rusty crossplatform, but simple CLI binutil for reading CPU information.
//! x86/x86_64 detection via the `cpuid` instruction (stdlib-only: `core::arch` intrinsics,
//! works unmodified on Linux/macOS/Windows/FreeBSD/OpenBSD since `cpuid` is a plain
//! userspace-executable instruction, not an OS syscall).

#[cfg(target_arch = "x86")]
use core::arch::x86::__cpuid_count;
#[cfg(target_arch = "x86_64")]
use core::arch::x86_64::__cpuid_count;

use super::{ArchInfo, tables::x86_uarch};

#[derive(Clone, Copy, Default)]
struct Regs {
    eax: u32,
    ebx: u32,
    ecx: u32,
    edx: u32,
}

fn cpuid(leaf: u32, subleaf: u32) -> Regs {
    let r = __cpuid_count(leaf, subleaf);
    Regs {
        eax: r.eax,
        ebx: r.ebx,
        ecx: r.ecx,
        edx: r.edx,
    }
}

fn max_leaf() -> u32 {
    cpuid(0, 0).eax
}

fn max_ext_leaf() -> u32 {
    cpuid(0x8000_0000, 0).eax
}

fn vendor_string() -> String {
    let r = cpuid(0, 0);
    let bytes: [u32; 3] = [r.ebx, r.edx, r.ecx];
    let mut s = String::with_capacity(12);
    for word in bytes {
        for shift in [0, 8, 16, 24] {
            s.push(((word >> shift) & 0xFF) as u8 as char);
        }
    }
    s
}

pub fn brand_string() -> Option<String> {
    if max_ext_leaf() < 0x8000_0004 {
        return None;
    }
    let mut bytes = Vec::with_capacity(48);
    for leaf in [0x8000_0002u32, 0x8000_0003, 0x8000_0004] {
        let r = cpuid(leaf, 0);
        for word in [r.eax, r.ebx, r.ecx, r.edx] {
            bytes.extend_from_slice(&word.to_le_bytes());
        }
    }
    let s = String::from_utf8_lossy(&bytes)
        .trim_matches(char::from(0))
        .trim()
        .to_string();
    if s.is_empty() { None } else { Some(s) }
}

struct Fms {
    ext_family: u32,
    family: u32,
    ext_model: u32,
    model: u32,
    stepping: u32,
}

fn family_model_stepping() -> Fms {
    let eax = cpuid(1, 0).eax;
    let stepping = eax & 0xF;
    let base_model = (eax >> 4) & 0xF;
    let base_family = (eax >> 8) & 0xF;
    let ext_model = (eax >> 16) & 0xF;
    let ext_family = (eax >> 20) & 0xFF;
    let family = if base_family == 0xF {
        base_family + ext_family
    } else {
        base_family
    };
    let model = if base_family == 0x6 || base_family == 0xF {
        (ext_model << 4) + base_model
    } else {
        base_model
    };
    Fms {
        ext_family,
        family,
        ext_model,
        model,
        stepping,
    }
}

fn lookup_uarch(table: &[x86_uarch::Entry], fms: &Fms) -> Option<(&'static str, Option<u32>)> {
    table.iter().find_map(|&(ef, f, em, m, s, name, proc)| {
        let ok = ef == fms.ext_family
            && f == fms.family
            && em.is_none_or(|v| v == fms.ext_model)
            && m.is_none_or(|v| v == fms.model)
            && s.is_none_or(|v| v == fms.stepping);
        ok.then_some((name, proc))
    })
}

fn features() -> Vec<String> {
    let mut flags = Vec::new();
    let l1 = cpuid(1, 0);
    if l1.edx & (1 << 25) != 0 {
        flags.push("sse".to_string());
    }
    if l1.edx & (1 << 26) != 0 {
        flags.push("sse2".to_string());
    }
    if l1.ecx & (1 << 0) != 0 {
        flags.push("sse3".to_string());
    }
    if l1.ecx & (1 << 9) != 0 {
        flags.push("ssse3".to_string());
    }
    if l1.ecx & (1 << 19) != 0 {
        flags.push("sse4_1".to_string());
    }
    if l1.ecx & (1 << 20) != 0 {
        flags.push("sse4_2".to_string());
    }
    if l1.ecx & (1 << 12) != 0 {
        flags.push("fma".to_string());
    }
    if l1.ecx & (1 << 25) != 0 {
        flags.push("aes".to_string());
    }
    if l1.ecx & (1 << 28) != 0 {
        flags.push("avx".to_string());
    }
    if max_leaf() >= 7 {
        let l7 = cpuid(7, 0);
        if l7.ebx & (1 << 5) != 0 {
            flags.push("avx2".to_string());
        }
        if l7.ebx & (1 << 16) != 0 {
            flags.push("avx512f".to_string());
        }
        if l7.ebx & (1 << 29) != 0 {
            flags.push("sha_ni".to_string());
        }
    }
    flags
}

pub fn detect() -> ArchInfo {
    let vendor_raw = vendor_string();
    let vendor = match vendor_raw.as_str() {
        "GenuineIntel" => "Intel",
        "AuthenticAMD" => "AMD",
        "HygonGenuine" => "Hygon",
        other => other,
    };

    let fms = family_model_stepping();
    let table: &[x86_uarch::Entry] = match vendor {
        "Intel" => x86_uarch::INTEL,
        "AMD" => x86_uarch::AMD,
        "Hygon" => x86_uarch::HYGON,
        _ => &[],
    };
    let (uarch, process_nm) = match lookup_uarch(table, &fms) {
        Some((name, proc)) => (Some(name.to_string()), proc),
        None => (None, None),
    };

    ArchInfo {
        vendor: Some(vendor.to_string()),
        uarch,
        soc: None,
        process_nm,
        features: features(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn zen3_matches_via_model_wildcard() {
        // AMD Zen 3 table entry is (10, 15, Some(3), None, None, "Zen 3", Some(7)) --
        // model/stepping are wildcards (None), only ext_family/family/ext_model must match.
        let fms = Fms {
            ext_family: 10,
            family: 15,
            ext_model: 3,
            model: 99,
            stepping: 5,
        };
        let (name, proc) = lookup_uarch(x86_uarch::AMD, &fms).expect("should match Zen 3");
        assert_eq!(name, "Zen 3");
        assert_eq!(proc, Some(7));
    }

    #[test]
    fn exact_model_match_required_when_table_specifies_one() {
        // i80486DX is (0, 4, Some(0), Some(0), None, ...) -- model must match exactly.
        let fms = Fms {
            ext_family: 0,
            family: 4,
            ext_model: 0,
            model: 0,
            stepping: 0,
        };
        assert_eq!(
            lookup_uarch(x86_uarch::INTEL, &fms).map(|(n, _)| n),
            Some("i80486DX")
        );

        let fms_wrong_model = Fms {
            ext_family: 0,
            family: 4,
            ext_model: 0,
            model: 99,
            stepping: 0,
        };
        assert_ne!(
            lookup_uarch(x86_uarch::INTEL, &fms_wrong_model).map(|(n, _)| n),
            Some("i80486DX")
        );
    }

    #[test]
    fn unknown_fms_returns_none() {
        let fms = Fms {
            ext_family: 255,
            family: 255,
            ext_model: 255,
            model: 255,
            stepping: 255,
        };
        assert!(lookup_uarch(x86_uarch::INTEL, &fms).is_none());
        assert!(lookup_uarch(x86_uarch::AMD, &fms).is_none());
    }
}