cpufetch-rs 0.0.5

A cross-platform Rust CLI and library for fetching detailed CPU information
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
//! CPUID parsing and abstraction layer.
//!
//! This module provides a safe interface for parsing CPUID information on `x86/x86_64`
//! processors. It abstracts the complexities of raw CPUID access and provides structured
//! data for CPU information gathering.
//!
//! The implementation uses the raw-cpuid crate for the actual CPUID instruction calls
//! but adds structure, error handling, and CPU-vendor specific logic.

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use raw_cpuid::CpuId;

use serde::{Deserialize, Serialize};
use std::fmt;

/// Maximum number of cache levels typically found in processors
const MAX_CACHE_LEVELS: usize = 4;

/// Error types specific to CPUID operations
#[derive(Debug, thiserror::Error)]
pub enum CpuidError {
    #[error("CPUID leaf {0} not supported")]
    UnsupportedLeaf(u32),
    #[error("CPUID leaf {0}, subleaf {1} not supported")]
    UnsupportedSubleaf(u32, u32),
    #[error("Cache level {0} information not available")]
    CacheInfoNotAvailable(u8),
    #[error("CPUID access failed: {0}")]
    AccessError(String),
    #[error("Unexpected CPUID result")]
    UnexpectedResult,
    #[error("Architecture not supported")]
    UnsupportedArchitecture,
}

/// Represents a CPU cache
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct CacheInfo {
    /// Cache level (1=L1, 2=L2, 3=L3, etc.)
    pub level: u8,
    /// Cache type (Data, Instruction, Unified)
    pub cache_type: CacheType,
    /// Cache size in KB
    pub size_kb: u32,
    /// Cache line size in bytes
    pub line_size: u16,
    /// Cache associativity
    pub associativity: u16,
    /// Number of sets
    pub sets: u32,
    /// Shared by how many cores
    pub shared_by: u16,
}

/// Types of CPU caches
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum CacheType {
    Data,
    Instruction,
    Unified,
    #[default]
    Unknown,
}

impl fmt::Display for CacheType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CacheType::Data => write!(f, "Data"),
            CacheType::Instruction => write!(f, "Instruction"),
            CacheType::Unified => write!(f, "Unified"),
            CacheType::Unknown => write!(f, "Unknown"),
        }
    }
}

/// Basic CPU information extracted from CPUID
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BasicInfo {
    /// CPU vendor identification string
    pub vendor_string: String,
    /// Brand/model string
    pub brand_string: String,
    /// Family ID
    pub family: u8,
    /// Model ID
    pub model: u8,
    /// Stepping ID
    pub stepping: u8,
    /// Extended family ID
    pub extended_family: u8,
    /// Extended model ID
    pub extended_model: u8,
    /// Processor type
    pub processor_type: u8,
    /// Base features supported
    pub base_features: u64,
    /// Extended features supported
    pub extended_features: u64,
}

/// Collection of cache information for all cache levels
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CacheTopology {
    /// Array of cache information for each detected cache
    /// Index 0 = L1 Instruction, 1 = L1 Data, 2 = L2, 3 = L3
    pub caches: [Option<CacheInfo>; MAX_CACHE_LEVELS],
}

/// Wrapper around raw-cpuid functionality providing higher-level abstractions
#[derive(Debug)]
pub struct CpuidWrapper {
    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
    cpuid: CpuId<raw_cpuid::CpuIdReaderNative>,
}

#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
impl Default for CpuidWrapper {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
impl Default for CpuidWrapper {
    fn default() -> Self {
        Self::new()
    }
}

impl CpuidWrapper {
    /// Create a new `CpuidWrapper` instance
    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
    #[must_use]
    pub fn new() -> Self {
        Self { cpuid: CpuId::new() }
    }

    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
    #[must_use]
    pub fn new() -> Self {
        Self {}
    }

    /// Get basic CPU information
    ///
    /// # Errors
    ///
    /// Returns `CpuidError` if CPUID access fails or the architecture is unsupported.
    pub fn get_basic_info(&self) -> Result<BasicInfo, CpuidError> {
        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
        {
            // Get vendor information
            let vendor = self
                .cpuid
                .get_vendor_info()
                .ok_or_else(|| CpuidError::AccessError("Failed to get vendor information".into()))?;

            // Get brand string if available
            let brand_string = self
                .cpuid
                .get_processor_brand_string()
                .map_or_else(|| "Unknown".to_string(), |brand| brand.as_str().trim().to_string());

            // Get basic feature information
            let feature_info = self.cpuid.get_feature_info().ok_or(CpuidError::UnsupportedLeaf(1))?;

            // Extract family, model, stepping details
            let family_id = feature_info.family_id();
            let model_id = feature_info.model_id();
            let stepping_id = feature_info.stepping_id();
            let extended_family_id = feature_info.extended_family_id();
            let extended_model_id = feature_info.extended_model_id();
            // processor_type() and raw edx()/ecx() removed in raw-cpuid 11.x
            let processor_type = 0u8;
            let base_features = 0u64;
            let extended_features = 0u64;

            Ok(BasicInfo {
                vendor_string: vendor.as_str().to_string(),
                brand_string,
                family: family_id,
                model: model_id,
                stepping: stepping_id,
                extended_family: extended_family_id,
                extended_model: extended_model_id,
                processor_type,
                base_features,
                extended_features,
            })
        }

        #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
        {
            Err(CpuidError::UnsupportedArchitecture)
        }
    }

    /// Get cache topology information
    ///
    /// # Errors
    ///
    /// Returns `CpuidError` if the architecture is unsupported.
    pub fn get_cache_topology(&self) -> Result<CacheTopology, CpuidError> {
        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
        {
            let mut topology = CacheTopology::default();
            let mut cache_found = false;

            // Try Intel/AMD deterministic cache parameters first (preferred method)
            if let Some(deterministic_cache) = self.cpuid.get_cache_parameters() {
                let cache_iter = deterministic_cache;
                let mut index = 0;

                // Iterate through all available cache levels
                for cache in cache_iter {
                    if index >= MAX_CACHE_LEVELS {
                        break;
                    }

                    // Map cache type
                    let cache_type = match cache.cache_type() {
                        raw_cpuid::CacheType::Data => CacheType::Data,
                        raw_cpuid::CacheType::Instruction => CacheType::Instruction,
                        raw_cpuid::CacheType::Unified => CacheType::Unified,
                        _ => CacheType::Unknown,
                    };

                    // Calculate cache size
                    let size_kb = cache.associativity()
                        * cache.physical_line_partitions()
                        * cache.coherency_line_size()
                        * cache.sets()
                        / 1024;

                    // Add to our topology at the appropriate index
                    let target_index = match (cache.level(), cache_type) {
                        (1, CacheType::Instruction) => 0,
                        (1, CacheType::Data) => 1,
                        (2, _) => 2,
                        (3, _) => 3,
                        _ => {
                            // For other levels, just use the index as is
                            // but ensure we don't exceed our array bounds
                            if index < MAX_CACHE_LEVELS {
                                index
                            } else {
                                continue;
                            }
                        },
                    };

                    // Cache fields are bounded by CPU hardware limits; truncation is intentional.
                    #[allow(clippy::cast_possible_truncation)]
                    let cache_entry = CacheInfo {
                        level: cache.level(),
                        cache_type,
                        size_kb: size_kb as u32,
                        line_size: cache.coherency_line_size() as u16,
                        associativity: cache.associativity() as u16,
                        sets: cache.sets() as u32,
                        shared_by: cache.max_cores_for_cache() as u16,
                    };
                    topology.caches[target_index] = Some(cache_entry);

                    cache_found = true;
                    index += 1;
                }

                if cache_found {
                    return Ok(topology);
                }
            }

            // Last resort: use legacy cache descriptors
            if self.cpuid.get_cache_info().is_some() {
                // We'll check for cache descriptors, but they're not well supported in newer CPUs
                // So this is primarily a fallback method
                // In raw-cpuid 11.5.0, the API for legacy cache info has changed
                cache_found = true; // Assume we found something even if we can't parse details
            }

            // Return whatever we found (might be empty if we didn't find any cache info)
            if !cache_found {
                // Try one more fallback - hardcoded defaults for known CPUs
                if let Ok(info) = self.get_basic_info() {
                    if info.vendor_string == "GenuineIntel" {
                        // Intel CPUs typically have at least L1 caches
                        topology.caches[0] = Some(CacheInfo {
                            level: 1,
                            cache_type: CacheType::Instruction,
                            size_kb: 32,      // Common L1 instruction cache size
                            line_size: 64,    // Common line size
                            associativity: 8, // Common associativity
                            sets: 0,
                            shared_by: 1,
                        });

                        topology.caches[1] = Some(CacheInfo {
                            level: 1,
                            cache_type: CacheType::Data,
                            size_kb: 32,      // Common L1 data cache size
                            line_size: 64,    // Common line size
                            associativity: 8, // Common associativity
                            sets: 0,
                            shared_by: 1,
                        });

                        // Note: this is only a fallback with reasonable defaults
                        // Real sizes should be detected by the methods above
                    } else if info.vendor_string == "AuthenticAMD" {
                        // AMD CPUs typically have at least L1 caches
                        topology.caches[0] = Some(CacheInfo {
                            level: 1,
                            cache_type: CacheType::Instruction,
                            size_kb: 64,      // Common L1 instruction cache size
                            line_size: 64,    // Common line size
                            associativity: 8, // Common associativity
                            sets: 0,
                            shared_by: 1,
                        });

                        topology.caches[1] = Some(CacheInfo {
                            level: 1,
                            cache_type: CacheType::Data,
                            size_kb: 32,      // Common L1 data cache size
                            line_size: 64,    // Common line size
                            associativity: 8, // Common associativity
                            sets: 0,
                            shared_by: 1,
                        });
                    }
                }
            }

            Ok(topology)
        }

        #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
        {
            Err(CpuidError::UnsupportedArchitecture)
        }
    }

    /// Detect whether the CPU is running inside a hypervisor.
    ///
    /// Checks CPUID leaf 0x1 ECX bit 31 (hypervisor present bit).  If set,
    /// attempts to identify the hypervisor from leaf 0x40000000.
    /// Returns `None` on bare metal or non-x86 platforms.
    #[must_use]
    pub fn detect_hypervisor(&self) -> Option<String> {
        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
        {
            let feature_info = self.cpuid.get_feature_info()?;
            if !feature_info.has_hypervisor() {
                return None;
            }
            // Try to name the hypervisor from leaf 0x40000000
            if let Some(hv_info) = self.cpuid.get_hypervisor_info() {
                let name = match hv_info.identify() {
                    raw_cpuid::Hypervisor::Xen => "Xen",
                    raw_cpuid::Hypervisor::VMware => "VMware",
                    raw_cpuid::Hypervisor::HyperV => "Hyper-V",
                    raw_cpuid::Hypervisor::KVM => "KVM",
                    raw_cpuid::Hypervisor::Bhyve => "bhyve",
                    raw_cpuid::Hypervisor::QNX => "QNX",
                    raw_cpuid::Hypervisor::ACRN => "ACRN",
                    _ => "Unknown",
                };
                return Some(name.to_string());
            }
            Some("Unknown".to_string())
        }

        #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
        None
    }

    /// Check if a specific CPUID feature is supported.
    /// Raw bit-level access was removed in raw-cpuid 11.x; always returns false.
    #[must_use]
    pub fn has_feature(&self, _feature: u32, _register: CpuidRegister) -> bool {
        false
    }

    /// Check if a specific extended CPUID feature is supported.
    /// Raw bit-level access was removed in raw-cpuid 11.x; always returns false.
    #[must_use]
    pub fn has_extended_feature(&self, _feature: u32, _register: CpuidRegister) -> bool {
        false
    }
}

/// CPUID registers for feature bits
#[derive(Debug, Clone, Copy)]
pub enum CpuidRegister {
    EAX,
    EBX,
    ECX,
    EDX,
}

#[cfg(test)]
mod tests {
    #[cfg(any(
        all(target_arch = "x86", not(target_env = "sgx"), target_feature = "sse"),
        all(target_arch = "x86_64", not(target_env = "sgx"))
    ))]
    use super::*;

    #[test]
    #[cfg(any(
        all(target_arch = "x86", not(target_env = "sgx"), target_feature = "sse"),
        all(target_arch = "x86_64", not(target_env = "sgx"))
    ))]
    fn test_basic_info() {
        let wrapper = CpuidWrapper::new();
        let info = wrapper.get_basic_info().expect("Failed to get basic CPU info");

        // Basic sanity checks that should pass on any x86/x86_64 CPU
        assert!(!info.vendor_string.is_empty());
        assert!(!info.brand_string.is_empty());

        // Vendor and brand must be present on any x86/x86_64 CPU
        assert!(info.family > 0, "Family ID should be non-zero on real hardware");
    }

    #[test]
    #[cfg(any(
        all(target_arch = "x86", not(target_env = "sgx"), target_feature = "sse"),
        all(target_arch = "x86_64", not(target_env = "sgx"))
    ))]
    fn test_cache_topology() {
        let wrapper = CpuidWrapper::new();
        let topology = wrapper.get_cache_topology().expect("Failed to get cache topology");

        // Most CPUs should have at least one cache
        let has_at_least_one_cache = topology.caches.iter().any(Option::is_some);
        assert!(has_at_least_one_cache, "No caches detected on this CPU");
    }
}