diff --git a/README.md b/README.md index cd3b70a..2579b34 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,12 @@ sudo dmitui - [x] System (type 1) - [x] Baseboard (type 2) - [x] Chassis (type 3) (Partially) +- [x] Processor (type 4) +- [x] Cache (type 7) +- [x] System Slots (type 9) (Partially) - [x] Firmware Language Information (type 13) - [x] Physical Memory Array (type 16) +- [x] Memory Device (type 17) - [x] Portable Battery (type 22) ## ⚖️ License diff --git a/src/dmi.rs b/src/dmi.rs index 9cd5df0..87835b5 100644 --- a/src/dmi.rs +++ b/src/dmi.rs @@ -1,8 +1,11 @@ mod baseboard; mod battery; +mod cache; mod chassis; mod firmware; mod memory; +mod processor; +mod slot; mod system; use std::{ @@ -15,39 +18,46 @@ use anyhow::{Result, bail}; use crate::dmi::baseboard::Baseboard; use crate::dmi::battery::Battery; +use crate::dmi::cache::Cache; use crate::dmi::chassis::Chassis; use crate::dmi::firmware::Firmware; -use crate::dmi::memory::{Memory, PhysicalMemoryArray}; +use crate::dmi::memory::{Memory, MemoryDevice, PhysicalMemoryArray}; +use crate::dmi::processor::{Processor, Processors}; +use crate::dmi::slot::{Slot, Slots}; use crate::dmi::system::System; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ Frame, layout::{Alignment, Constraint, Direction, Layout}, - style::{Color, Style, Stylize}, + style::{Style, Stylize}, text::{Line, Span}, widgets::{Block, BorderType, Borders, Padding}, }; #[derive(Debug)] pub struct DMI { - firmware: Firmware, - system: System, - baseboard: Baseboard, - chassis: Chassis, - memory: Memory, + firmware: Option, + system: Option, + baseboard: Option, + chassis: Option, + processors: Option, + memory: Option, + slots: Option, battery: Option, pub focused_section: FocusedSection, } #[non_exhaustive] -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum FocusedSection { Firmware, System, Baseboard, Chassis, + Processor, Memory, + Slots, Battery, } @@ -65,8 +75,12 @@ impl From<[u8; 4]> for Header { 1 => StructureType::System, 2 => StructureType::Baseboard, 3 => StructureType::Chassis, + 4 => StructureType::Processor, + 7 => StructureType::Cache, + 9 => StructureType::SystemSlots, 13 => StructureType::FirmwareLanguage, 16 => StructureType::PhysicalMemoryArray, + 17 => StructureType::MemoryDevice, 22 => StructureType::Battery, 127 => StructureType::End, _ => StructureType::Other, @@ -75,7 +89,7 @@ impl From<[u8; 4]> for Header { Self { structure_type, length: value[1], - handle: u16::from_be_bytes([value[2], value[3]]), + handle: u16::from_le_bytes([value[2], value[3]]), } } } @@ -87,8 +101,12 @@ pub enum StructureType { System = 1, Baseboard = 2, Chassis = 3, + Processor = 4, + Cache = 7, + SystemSlots = 9, FirmwareLanguage = 13, PhysicalMemoryArray = 16, + MemoryDevice = 17, Battery = 22, End = 127, Other = 255, @@ -102,20 +120,21 @@ impl DMI { let mut system: Option = None; let mut baseboard: Option = None; let mut chassis: Option = None; - let mut memory: Option = None; + let mut processor_list: Vec = Vec::new(); + let mut caches: Vec = Vec::new(); + let mut physical_memory_array: Option = None; + let mut memory_devices: Vec = Vec::new(); + let mut slot_list: Vec = Vec::new(); let mut battery: Option = None; let dmi_file_path = Path::new("/sys/firmware/dmi/tables/DMI"); match dmi_file_path.try_exists() { Ok(true) => {} - Ok(false) | Err(_) => { - eprintln!("No SMBIOS found"); - std::process::exit(1); - } + Ok(false) | Err(_) => bail!("No SMBIOS found"), } - let mem_file = File::open("/sys/firmware/dmi/tables/DMI")?; + let mem_file = File::open(dmi_file_path)?; let mut file = BufReader::new(mem_file); loop { @@ -135,28 +154,26 @@ impl DMI { let mut data = vec![0; header.length.saturating_sub(4) as usize]; file.read_exact(&mut data)?; - // Read Strings + // Read strings. The string-set ends with an extra NUL after the + // last string's terminator, so for a structure with no strings the + // formatted area is followed by two NUL bytes. let mut text: Vec = Vec::new(); - - let mut previous_read_zero: bool = false; - let mut previous_read_string: bool = false; + let mut saw_leading_zero = false; loop { let mut string_buf = Vec::new(); - if let Ok(number_of_bytes_read) = file.read_until(0, &mut string_buf) { - if number_of_bytes_read == 1 { - if previous_read_zero { + match file.read_until(0, &mut string_buf)? { + 0 => break, + 1 => { + // Empty entry (just the terminator byte). + if !text.is_empty() || saw_leading_zero { break; - } else { - if previous_read_string { - break; - } - previous_read_zero = true; } - } else { + saw_leading_zero = true; + } + _ => { string_buf.pop(); text.push(String::from_utf8_lossy(&string_buf).to_string()); - previous_read_string = true; } } } @@ -174,6 +191,15 @@ impl DMI { StructureType::Chassis => { chassis = Some(Chassis::from((data, text))); } + StructureType::Processor => { + processor_list.push(Processor::from((data, text))); + } + StructureType::Cache => { + caches.push(Cache::parse(header.handle, data)); + } + StructureType::SystemSlots => { + slot_list.push(Slot::from((data, text))); + } StructureType::FirmwareLanguage => { let language_infos = firmware::LanguageInfos::from((data, text)); @@ -182,9 +208,10 @@ impl DMI { } } StructureType::PhysicalMemoryArray => { - memory = Some(Memory { - physical_memory_array: PhysicalMemoryArray::from(data.as_slice()), - }); + physical_memory_array = Some(PhysicalMemoryArray::from(data.as_slice())); + } + StructureType::MemoryDevice => { + memory_devices.push(MemoryDevice::from((data, text))); } StructureType::Battery => { battery = Some(Battery::from((data, text))); @@ -193,102 +220,116 @@ impl DMI { } } + let memory = physical_memory_array.map(|pma| Memory::new(pma, memory_devices)); + let processors = Processors::new(processor_list, caches); + let slots = Slots::new(slot_list); + + let focused_section = [ + (FocusedSection::Firmware, firmware.is_some()), + (FocusedSection::System, system.is_some()), + (FocusedSection::Baseboard, baseboard.is_some()), + (FocusedSection::Chassis, chassis.is_some()), + (FocusedSection::Processor, processors.is_some()), + (FocusedSection::Memory, memory.is_some()), + (FocusedSection::Slots, slots.is_some()), + (FocusedSection::Battery, battery.is_some()), + ] + .into_iter() + .find_map(|(s, present)| present.then_some(s)) + .ok_or_else(|| anyhow::anyhow!("No supported DMI structures found"))?; + Ok(Self { - firmware: firmware.unwrap(), - system: system.unwrap(), - baseboard: baseboard.unwrap(), - chassis: chassis.unwrap(), - memory: memory.unwrap(), + firmware, + system, + baseboard, + chassis, + processors, + memory, + slots, battery, - focused_section: FocusedSection::Firmware, + focused_section, }) } - pub fn handle_key_events(&mut self, key_event: KeyEvent) { - match key_event.code { - KeyCode::Tab => match self.focused_section { - FocusedSection::Firmware => self.focused_section = FocusedSection::System, - FocusedSection::System => self.focused_section = FocusedSection::Baseboard, - FocusedSection::Baseboard => self.focused_section = FocusedSection::Chassis, - FocusedSection::Chassis => self.focused_section = FocusedSection::Memory, - FocusedSection::Memory => self.focused_section = FocusedSection::Battery, - FocusedSection::Battery => self.focused_section = FocusedSection::Firmware, - }, - KeyCode::BackTab => match self.focused_section { - FocusedSection::Firmware => self.focused_section = FocusedSection::Battery, - FocusedSection::System => self.focused_section = FocusedSection::Firmware, - FocusedSection::Baseboard => self.focused_section = FocusedSection::System, - FocusedSection::Chassis => self.focused_section = FocusedSection::Baseboard, - FocusedSection::Memory => self.focused_section = FocusedSection::Chassis, - FocusedSection::Battery => self.focused_section = FocusedSection::Memory, - }, - _ => {} + fn available_sections(&self) -> Vec { + let mut sections = Vec::with_capacity(8); + if self.firmware.is_some() { + sections.push(FocusedSection::Firmware); + } + if self.system.is_some() { + sections.push(FocusedSection::System); + } + if self.baseboard.is_some() { + sections.push(FocusedSection::Baseboard); } + if self.chassis.is_some() { + sections.push(FocusedSection::Chassis); + } + if self.processors.is_some() { + sections.push(FocusedSection::Processor); + } + if self.memory.is_some() { + sections.push(FocusedSection::Memory); + } + if self.slots.is_some() { + sections.push(FocusedSection::Slots); + } + if self.battery.is_some() { + sections.push(FocusedSection::Battery); + } + sections } - fn title_span(&self, header_section: FocusedSection) -> Span<'_> { - let is_focused = self.focused_section == header_section; - match header_section { - FocusedSection::Firmware => { - if is_focused { - Span::styled( - " Firmware ", - Style::default().bg(Color::Yellow).fg(Color::Black).bold(), - ) - } else { - Span::from(" Firmware ").fg(Color::DarkGray) - } - } - FocusedSection::System => { - if is_focused { - Span::styled( - " System ", - Style::default().bg(Color::Yellow).fg(Color::Black).bold(), - ) - } else { - Span::from(" System ").fg(Color::DarkGray) - } + pub fn handle_key_events(&mut self, key_event: KeyEvent) { + let sections = self.available_sections(); + let Some(idx) = sections.iter().position(|s| *s == self.focused_section) else { + return; + }; + + match key_event.code { + KeyCode::Tab => { + self.focused_section = sections[(idx + 1) % sections.len()]; } - FocusedSection::Baseboard => { - if is_focused { - Span::styled( - " Baseboard ", - Style::default().bg(Color::Yellow).fg(Color::Black).bold(), - ) - } else { - Span::from(" Baseboard ").fg(Color::DarkGray) - } + KeyCode::BackTab => { + self.focused_section = sections[(idx + sections.len() - 1) % sections.len()]; } - FocusedSection::Chassis => { - if is_focused { - Span::styled( - " Chassis ", - Style::default().bg(Color::Yellow).fg(Color::Black).bold(), - ) - } else { - Span::from(" Chassis ").fg(Color::DarkGray) + _ => match self.focused_section { + FocusedSection::Memory => { + if let Some(memory) = &mut self.memory { + memory.handle_key_events(key_event); + } } - } - FocusedSection::Memory => { - if is_focused { - Span::styled( - " Memory ", - Style::default().bg(Color::Yellow).fg(Color::Black).bold(), - ) - } else { - Span::from(" Memory ").fg(Color::DarkGray) + FocusedSection::Processor => { + if let Some(processors) = &mut self.processors { + processors.handle_key_events(key_event); + } } - } - FocusedSection::Battery => { - if is_focused { - Span::styled( - " Battery ", - Style::default().bg(Color::Yellow).fg(Color::Black).bold(), - ) - } else { - Span::from(" Battery ").fg(Color::DarkGray) + FocusedSection::Slots => { + if let Some(slots) = &mut self.slots { + slots.handle_key_events(key_event); + } } - } + _ => {} + }, + } + } + + fn title_span(&self, header_section: FocusedSection) -> Span<'_> { + let label = match header_section { + FocusedSection::Firmware => " Firmware ", + FocusedSection::System => " System ", + FocusedSection::Baseboard => " Baseboard ", + FocusedSection::Chassis => " Chassis ", + FocusedSection::Processor => " Processor ", + FocusedSection::Memory => " Memory ", + FocusedSection::Slots => " Slots ", + FocusedSection::Battery => " Battery ", + }; + + if self.focused_section == header_section { + Span::styled(label, Style::new().bold().reversed()) + } else { + Span::from(label).dim() } } @@ -303,45 +344,79 @@ impl DMI { (chunks[0], chunks[1]) }; + let title_spans: Vec> = self + .available_sections() + .into_iter() + .map(|s| self.title_span(s)) + .collect(); + frame.render_widget( Block::default() - .title(Line::from(vec![ - self.title_span(FocusedSection::Firmware), - self.title_span(FocusedSection::System), - self.title_span(FocusedSection::Baseboard), - self.title_span(FocusedSection::Chassis), - self.title_span(FocusedSection::Memory), - self.title_span(FocusedSection::Battery), - ])) + .title(Line::from(title_spans)) .title_alignment(Alignment::Left) .padding(Padding::top(1)) .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .style(Style::default()) - .border_style(Style::default().fg(Color::Yellow)), + .border_type(BorderType::Rounded), section_block, ); // Help banner - let message = Line::from("⇆ : Navigation").centered().cyan(); + let inner_nav = match self.focused_section { + FocusedSection::Memory => self + .memory + .as_ref() + .is_some_and(|m| !m.memory_devices.is_empty()), + FocusedSection::Processor => self + .processors + .as_ref() + .is_some_and(Processors::has_multiple), + FocusedSection::Slots => self.slots.as_ref().is_some_and(Slots::has_multiple), + _ => false, + }; + let help_text = if inner_nav { + "⇆ : Sections ↑↓ : Cycle" + } else { + "⇆ : Navigation" + }; + let message = Line::from(help_text).centered().dim(); frame.render_widget(message, help_block); match self.focused_section { FocusedSection::Firmware => { - self.firmware.render(frame, section_block); + if let Some(firmware) = &self.firmware { + firmware.render(frame, section_block); + } } FocusedSection::System => { - self.system.render(frame, section_block); + if let Some(system) = &self.system { + system.render(frame, section_block); + } } FocusedSection::Baseboard => { - self.baseboard.render(frame, section_block); + if let Some(baseboard) = &self.baseboard { + baseboard.render(frame, section_block); + } } FocusedSection::Chassis => { - self.chassis.render(frame, section_block); + if let Some(chassis) = &self.chassis { + chassis.render(frame, section_block); + } + } + FocusedSection::Processor => { + if let Some(processors) = &mut self.processors { + processors.render(frame, section_block); + } } FocusedSection::Memory => { - self.memory.render(frame, section_block); + if let Some(memory) = &mut self.memory { + memory.render(frame, section_block); + } + } + FocusedSection::Slots => { + if let Some(slots) = &mut self.slots { + slots.render(frame, section_block); + } } FocusedSection::Battery => { if let Some(battery) = &self.battery { diff --git a/src/dmi/cache.rs b/src/dmi/cache.rs new file mode 100644 index 0000000..e310df8 --- /dev/null +++ b/src/dmi/cache.rs @@ -0,0 +1,117 @@ +// SMBIOS Type 7 (Cache Information). Spec reference: DSP0134 §7.8. + +#[derive(Debug)] +pub struct Cache { + pub handle: u16, + installed_size: CacheSize, + cache_type: CacheType, +} + +impl Cache { + pub fn parse(handle: u16, data: Vec) -> Self { + let installed_size_field = u16::from_le_bytes(data[5..7].try_into().unwrap()); + let installed_size_2 = if data.len() >= 23 { + Some(u32::from_le_bytes(data[19..23].try_into().unwrap())) + } else { + None + }; + let installed_size = CacheSize::from_fields(installed_size_field, installed_size_2); + + let cache_type = data + .get(13) + .copied() + .map_or(CacheType::Unknown, CacheType::from); + + Self { + handle, + installed_size, + cache_type, + } + } + + pub fn summary(&self) -> String { + if matches!(self.installed_size, CacheSize::NotInstalled) { + return "Not installed".to_string(); + } + format!("{}, {}", self.installed_size, self.cache_type) + } +} + +#[derive(Debug)] +enum CacheSize { + NotInstalled, + Unknown, + Kilobytes(u64), +} + +impl CacheSize { + fn from_fields(size_field: u16, size2_field: Option) -> Self { + match size_field { + 0 => CacheSize::NotInstalled, + 0xFFFF => match size2_field { + Some(0) => CacheSize::NotInstalled, + Some(v) => decode_size_2(v), + None => CacheSize::Unknown, + }, + v => decode_size(v), + } + } +} + +// 16-bit size field: bit 15 = granularity (0 → 1 KB, 1 → 64 KB), bits 0..14 = count. +fn decode_size(v: u16) -> CacheSize { + let count = (v & 0x7FFF) as u64; + let granularity_kb: u64 = if v & 0x8000 == 0 { 1 } else { 64 }; + CacheSize::Kilobytes(count * granularity_kb) +} + +// 32-bit size field: bit 31 = granularity (0 → 1 KB, 1 → 64 KB), bits 0..30 = count. +fn decode_size_2(v: u32) -> CacheSize { + let count = (v & 0x7FFF_FFFF) as u64; + let granularity_kb: u64 = if v & 0x8000_0000 == 0 { 1 } else { 64 }; + CacheSize::Kilobytes(count * granularity_kb) +} + +impl std::fmt::Display for CacheSize { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CacheSize::NotInstalled => write!(f, "Not installed"), + CacheSize::Unknown => write!(f, "Unknown"), + CacheSize::Kilobytes(kb) => { + if *kb >= 1024 && kb.is_multiple_of(1024) { + write!(f, "{} MB", kb / 1024) + } else if *kb >= 1024 { + write!(f, "{:.1} MB", *kb as f64 / 1024.0) + } else { + write!(f, "{kb} KB") + } + } + } + } +} + +#[derive(Debug, strum::Display)] +enum CacheType { + #[strum(to_string = "Other")] + Other, + #[strum(to_string = "Unknown")] + Unknown, + #[strum(to_string = "Instruction")] + Instruction, + #[strum(to_string = "Data")] + Data, + #[strum(to_string = "Unified")] + Unified, +} + +impl From for CacheType { + fn from(value: u8) -> Self { + match value { + 3 => Self::Instruction, + 4 => Self::Data, + 5 => Self::Unified, + 1 => Self::Other, + _ => Self::Unknown, + } + } +} diff --git a/src/dmi/memory.rs b/src/dmi/memory.rs index bd85e13..45d58ea 100644 --- a/src/dmi/memory.rs +++ b/src/dmi/memory.rs @@ -1,21 +1,147 @@ +use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ Frame, - layout::{Constraint, Margin, Rect}, - style::Stylize, - widgets::{Block, Cell, Padding, Row, Table}, + layout::{Constraint, Direction, Layout, Margin, Rect}, + style::{Style, Stylize}, + text::{Line, Span}, + widgets::{Block, BorderType, Borders, Cell, List, ListItem, ListState, Padding, Row, Table}, }; #[derive(Debug)] pub struct Memory { pub physical_memory_array: PhysicalMemoryArray, + pub memory_devices: Vec, + selected_device: usize, } impl Memory { + pub fn new( + physical_memory_array: PhysicalMemoryArray, + memory_devices: Vec, + ) -> Self { + Self { + physical_memory_array, + memory_devices, + selected_device: 0, + } + } + + fn device_layout(&self) -> DeviceLayout { + let mut has_soldered = false; + let mut has_socketed = false; + for d in &self.memory_devices { + match d.form_factor.kind() { + FormFactorKind::Soldered => has_soldered = true, + FormFactorKind::Socketed => has_socketed = true, + FormFactorKind::Unknown => {} + } + } + match (has_soldered, has_socketed) { + (true, false) => DeviceLayout::Soldered, + (false, true) => DeviceLayout::Socketed, + _ => DeviceLayout::Mixed, + } + } + + pub fn handle_key_events(&mut self, key_event: KeyEvent) { + if self.memory_devices.is_empty() { + return; + } + match key_event.code { + KeyCode::Down | KeyCode::Char('j') => { + self.selected_device = (self.selected_device + 1) % self.memory_devices.len(); + } + KeyCode::Up | KeyCode::Char('k') => { + self.selected_device = (self.selected_device + self.memory_devices.len() - 1) + % self.memory_devices.len(); + } + _ => {} + } + } + pub fn render(&mut self, frame: &mut Frame, block: Rect) { - self.physical_memory_array.render(frame, block); + if self.memory_devices.is_empty() { + self.physical_memory_array.render(frame, block); + return; + } + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Fill(1)]) + .split(block.inner(Margin::new(4, 2))); + + let count_label = match self.device_layout() { + DeviceLayout::Soldered => "Chips: ", + DeviceLayout::Socketed => "Slots: ", + DeviceLayout::Mixed => "Devices: ", + }; + + let summary = Line::from(vec![ + Span::from("Total Capacity: ").bold(), + Span::from(self.physical_memory_array.max_capacity.clone()), + Span::from(" "), + Span::from(count_label).bold(), + Span::from(self.physical_memory_array.number_memory_devices.to_string()), + Span::from(" "), + Span::from("ECC: ").bold(), + Span::from(self.physical_memory_array.error_correction.to_string()), + ]); + frame.render_widget(summary, chunks[0]); + + let max_label = self + .memory_devices + .iter() + .map(|d| d.device_locator.chars().count()) + .max() + .unwrap_or(0) as u16; + // 4 = 2 borders + 2 horizontal padding + let list_width = max_label.saturating_add(4).max(14); + + let body = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(list_width), Constraint::Fill(1)]) + .split(chunks[1]); + + let items: Vec> = self + .memory_devices + .iter() + .map(|d| ListItem::new(d.device_locator.clone())) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .padding(Padding::new(1, 1, 1, 0)), + ) + .highlight_style(Style::new().bold().reversed()) + .highlight_symbol(""); + + let mut state = ListState::default(); + state.select(Some(self.selected_device)); + frame.render_stateful_widget(list, body[0], &mut state); + + if let Some(device) = self.memory_devices.get(self.selected_device) { + device.render(frame, body[1]); + } } } +#[derive(Debug, Clone, Copy)] +enum DeviceLayout { + Soldered, + Socketed, + Mixed, +} + +#[derive(Debug, Clone, Copy)] +enum FormFactorKind { + Soldered, + Socketed, + Unknown, +} + #[derive(Debug)] pub struct PhysicalMemoryArray { location: Location, @@ -30,21 +156,22 @@ impl From<&[u8]> for PhysicalMemoryArray { fn from(data: &[u8]) -> Self { let max_capacity = { let value = u32::from_le_bytes(data[3..7].try_into().unwrap()); + // Per SMBIOS spec, 0x80000000 in the DWORD field means the actual + // value is in the Extended Maximum Capacity QWORD (in bytes). + let kb: u64 = if value == 0x80000000 && data.len() >= 19 { + u64::from_le_bytes(data[11..19].try_into().unwrap()) / 1024 + } else { + value as u64 + }; - if value == 0x80008000 { - format!("{}T", u64::from_le_bytes(data[11..19].try_into().unwrap())) + if kb <= 1024 { + format!("{kb}K") + } else if kb <= 1024 * 1024 { + format!("{}M", kb / 1024) + } else if kb <= 1024 * 1024 * 1024 { + format!("{}G", kb / 1024 / 1024) } else { - match value { - value if value <= 1024 => { - format!("{value}K") - } - value if value <= 1024 * 1024 => { - format!("{}M", value / 1024) - } - _ => { - format!("{}G", value / 1024 / 1024) - } - } + format!("{}T", kb / 1024 / 1024 / 1024) } }; let error_information_handle = { @@ -229,3 +356,446 @@ impl From for ErrorCorrection { } } } + +fn string_ref(idx: u8, text: &[String]) -> String { + if idx == 0 { + return "Not Specified".to_string(); + } + text.get((idx - 1) as usize) + .cloned() + .unwrap_or_else(|| "Not Specified".to_string()) +} + +#[derive(Debug)] +pub struct MemoryDevice { + device_locator: String, + bank_locator: String, + size: MemorySize, + form_factor: FormFactor, + memory_type: MemoryType, + memory_technology: MemoryTechnology, + speed: Option, + configured_speed: Option, + rank: Option, + configured_voltage_mv: Option, + manufacturer: String, + serial_number: String, + asset_tag: String, + part_number: String, +} + +impl From<(Vec, Vec)> for MemoryDevice { + fn from((data, text): (Vec, Vec)) -> Self { + let size_field = u16::from_le_bytes(data[8..10].try_into().unwrap()); + let extended_size = if data.len() >= 28 { + Some(u32::from_le_bytes(data[24..28].try_into().unwrap())) + } else { + None + }; + let size = MemorySize::from_fields(size_field, extended_size); + + let form_factor = FormFactor::from(data[10]); + let memory_type = MemoryType::from(data[14]); + + let speed = { + let v = u16::from_le_bytes(data[17..19].try_into().unwrap()); + if v == 0 { None } else { Some(v) } + }; + + let rank = data.get(23).copied().map(|b| b & 0x0F).filter(|r| *r != 0); + + let configured_speed = data + .get(28..30) + .and_then(|s| s.try_into().ok()) + .map(u16::from_le_bytes) + .filter(|v| *v != 0); + + let configured_voltage_mv = data + .get(34..36) + .and_then(|s| s.try_into().ok()) + .map(u16::from_le_bytes) + .filter(|v| *v != 0); + + let memory_technology = data + .get(36) + .copied() + .map(MemoryTechnology::from) + .unwrap_or(MemoryTechnology::Unknown); + + let manufacturer = data + .get(19) + .copied() + .map_or_else(|| "Not Specified".to_string(), |b| string_ref(b, &text)); + let serial_number = data + .get(20) + .copied() + .map_or_else(|| "Not Specified".to_string(), |b| string_ref(b, &text)); + let asset_tag = data + .get(21) + .copied() + .map_or_else(|| "Not Specified".to_string(), |b| string_ref(b, &text)); + let part_number = data + .get(22) + .copied() + .map_or_else(|| "Not Specified".to_string(), |b| string_ref(b, &text)); + + Self { + device_locator: string_ref(data[12], &text), + bank_locator: string_ref(data[13], &text), + size, + form_factor, + memory_type, + memory_technology, + speed, + configured_speed, + rank, + configured_voltage_mv, + manufacturer, + serial_number, + asset_tag, + part_number, + } + } +} + +impl MemoryDevice { + fn render(&self, frame: &mut Frame, block: Rect) { + let speed_text = match self.speed { + Some(v) => format!("{v} MT/s"), + None => "Unknown".to_string(), + }; + let configured_speed_text = match self.configured_speed { + Some(v) => format!("{v} MT/s"), + None => "Unknown".to_string(), + }; + let rank_text = match self.rank { + Some(v) => v.to_string(), + None => "Unknown".to_string(), + }; + let voltage_text = match self.configured_voltage_mv { + Some(mv) => format_voltage(mv), + None => "Unknown".to_string(), + }; + + let rows = vec![ + Row::new(vec![ + Cell::from("Size").bold(), + Cell::from(self.size.to_string()), + ]), + Row::new(vec![ + Cell::from("Type").bold(), + Cell::from(self.memory_type.to_string()), + ]), + Row::new(vec![ + Cell::from("Technology").bold(), + Cell::from(self.memory_technology.to_string()), + ]), + Row::new(vec![ + Cell::from("Form Factor").bold(), + Cell::from(self.form_factor.to_string()), + ]), + Row::new(vec![Cell::from("Rank").bold(), Cell::from(rank_text)]), + Row::new(vec![Cell::from("Speed").bold(), Cell::from(speed_text)]), + Row::new(vec![ + Cell::from("Configured Speed").bold(), + Cell::from(configured_speed_text), + ]), + Row::new(vec![Cell::from("Voltage").bold(), Cell::from(voltage_text)]), + Row::new(vec![ + Cell::from("Bank Locator").bold(), + Cell::from(self.bank_locator.clone()), + ]), + Row::new(vec![ + Cell::from("Manufacturer").bold(), + Cell::from(self.manufacturer.clone()), + ]), + Row::new(vec![ + Cell::from("Part Number").bold(), + Cell::from(self.part_number.clone()), + ]), + Row::new(vec![ + Cell::from("Serial Number").bold(), + Cell::from(self.serial_number.clone()), + ]), + Row::new(vec![ + Cell::from("Asset Tag").bold(), + Cell::from(self.asset_tag.clone()), + ]), + ]; + + let widths = [Constraint::Length(18), Constraint::Fill(1)]; + let table = Table::new(rows, widths).block(Block::new().padding(Padding::uniform(2))); + frame.render_widget(table, block.inner(Margin::new(2, 0))); + } +} + +fn format_voltage(mv: u16) -> String { + let s = format!("{:.3}", mv as f64 / 1000.0); + let trimmed = s.trim_end_matches('0').trim_end_matches('.'); + format!("{trimmed} V") +} + +#[derive(Debug)] +enum MemorySize { + Empty, + Unknown, + Megabytes(u64), +} + +impl MemorySize { + fn from_fields(size_field: u16, extended_size: Option) -> Self { + match size_field { + 0 => MemorySize::Empty, + 0xFFFF => MemorySize::Unknown, + 0x7FFF => match extended_size { + Some(es) => MemorySize::Megabytes((es & 0x7FFF_FFFF) as u64), + None => MemorySize::Unknown, + }, + v if v & 0x8000 == 0 => MemorySize::Megabytes((v & 0x7FFF) as u64), + v => { + // KB granularity + let kb = (v & 0x7FFF) as u64; + MemorySize::Megabytes(kb / 1024) + } + } + } +} + +impl std::fmt::Display for MemorySize { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MemorySize::Empty => write!(f, "Empty"), + MemorySize::Unknown => write!(f, "Unknown"), + MemorySize::Megabytes(mb) => { + if *mb >= 1024 && mb.is_multiple_of(1024) { + write!(f, "{} GB", mb / 1024) + } else if *mb >= 1024 { + write!(f, "{:.1} GB", *mb as f64 / 1024.0) + } else { + write!(f, "{mb} MB") + } + } + } + } +} + +#[derive(Debug, strum::Display)] +enum FormFactor { + #[strum(to_string = "Other")] + Other, + #[strum(to_string = "Unknown")] + Unknown, + #[strum(to_string = "SIMM")] + Simm, + #[strum(to_string = "SIP")] + Sip, + #[strum(to_string = "Chip")] + Chip, + #[strum(to_string = "DIP")] + Dip, + #[strum(to_string = "ZIP")] + Zip, + #[strum(to_string = "Proprietary Card")] + ProprietaryCard, + #[strum(to_string = "DIMM")] + Dimm, + #[strum(to_string = "TSOP")] + Tsop, + #[strum(to_string = "Row of chips")] + RowOfChips, + #[strum(to_string = "RIMM")] + Rimm, + #[strum(to_string = "SODIMM")] + Sodimm, + #[strum(to_string = "SRIMM")] + Srimm, + #[strum(to_string = "FB-DIMM")] + FbDimm, + #[strum(to_string = "Die")] + Die, +} + +impl From for FormFactor { + fn from(value: u8) -> Self { + match value { + 1 => Self::Other, + 3 => Self::Simm, + 4 => Self::Sip, + 5 => Self::Chip, + 6 => Self::Dip, + 7 => Self::Zip, + 8 => Self::ProprietaryCard, + 9 => Self::Dimm, + 10 => Self::Tsop, + 11 => Self::RowOfChips, + 12 => Self::Rimm, + 13 => Self::Sodimm, + 14 => Self::Srimm, + 15 => Self::FbDimm, + 16 => Self::Die, + _ => Self::Unknown, + } + } +} + +impl FormFactor { + fn kind(&self) -> FormFactorKind { + match self { + Self::Chip | Self::RowOfChips | Self::Die => FormFactorKind::Soldered, + Self::Simm + | Self::Sip + | Self::Dip + | Self::Zip + | Self::ProprietaryCard + | Self::Dimm + | Self::Tsop + | Self::Rimm + | Self::Sodimm + | Self::Srimm + | Self::FbDimm => FormFactorKind::Socketed, + Self::Other | Self::Unknown => FormFactorKind::Unknown, + } + } +} + +#[derive(Debug, strum::Display)] +enum MemoryType { + #[strum(to_string = "Other")] + Other, + #[strum(to_string = "Unknown")] + Unknown, + #[strum(to_string = "DRAM")] + Dram, + #[strum(to_string = "EDRAM")] + Edram, + #[strum(to_string = "VRAM")] + Vram, + #[strum(to_string = "SRAM")] + Sram, + #[strum(to_string = "RAM")] + Ram, + #[strum(to_string = "ROM")] + Rom, + #[strum(to_string = "FLASH")] + Flash, + #[strum(to_string = "EEPROM")] + Eeprom, + #[strum(to_string = "FEPROM")] + Feprom, + #[strum(to_string = "EPROM")] + Eprom, + #[strum(to_string = "CDRAM")] + Cdram, + #[strum(to_string = "3DRAM")] + Dram3D, + #[strum(to_string = "SDRAM")] + Sdram, + #[strum(to_string = "SGRAM")] + Sgram, + #[strum(to_string = "RDRAM")] + Rdram, + #[strum(to_string = "DDR")] + Ddr, + #[strum(to_string = "DDR2")] + Ddr2, + #[strum(to_string = "DDR2 FB-DIMM")] + Ddr2FbDimm, + #[strum(to_string = "DDR3")] + Ddr3, + #[strum(to_string = "FBD2")] + Fbd2, + #[strum(to_string = "DDR4")] + Ddr4, + #[strum(to_string = "LPDDR")] + LpDdr, + #[strum(to_string = "LPDDR2")] + LpDdr2, + #[strum(to_string = "LPDDR3")] + LpDdr3, + #[strum(to_string = "LPDDR4")] + LpDdr4, + #[strum(to_string = "Logical non-volatile device")] + LogicalNonVolatile, + #[strum(to_string = "HBM")] + Hbm, + #[strum(to_string = "HBM2")] + Hbm2, + #[strum(to_string = "DDR5")] + Ddr5, + #[strum(to_string = "LPDDR5")] + LpDdr5, + #[strum(to_string = "HBM3")] + Hbm3, +} + +#[derive(Debug, strum::Display)] +enum MemoryTechnology { + #[strum(to_string = "Other")] + Other, + #[strum(to_string = "Unknown")] + Unknown, + #[strum(to_string = "DRAM")] + Dram, + #[strum(to_string = "NVDIMM-N")] + NvdimmN, + #[strum(to_string = "NVDIMM-F")] + NvdimmF, + #[strum(to_string = "NVDIMM-P")] + NvdimmP, + #[strum(to_string = "Intel Optane persistent memory")] + IntelOptane, +} + +impl From for MemoryTechnology { + fn from(value: u8) -> Self { + match value { + 1 => Self::Other, + 3 => Self::Dram, + 4 => Self::NvdimmN, + 5 => Self::NvdimmF, + 6 => Self::NvdimmP, + 7 => Self::IntelOptane, + _ => Self::Unknown, + } + } +} + +impl From for MemoryType { + fn from(value: u8) -> Self { + match value { + 1 => Self::Other, + 3 => Self::Dram, + 4 => Self::Edram, + 5 => Self::Vram, + 6 => Self::Sram, + 7 => Self::Ram, + 8 => Self::Rom, + 9 => Self::Flash, + 10 => Self::Eeprom, + 11 => Self::Feprom, + 12 => Self::Eprom, + 13 => Self::Cdram, + 14 => Self::Dram3D, + 15 => Self::Sdram, + 16 => Self::Sgram, + 17 => Self::Rdram, + 18 => Self::Ddr, + 19 => Self::Ddr2, + 20 => Self::Ddr2FbDimm, + 24 => Self::Ddr3, + 25 => Self::Fbd2, + 26 => Self::Ddr4, + 27 => Self::LpDdr, + 28 => Self::LpDdr2, + 29 => Self::LpDdr3, + 30 => Self::LpDdr4, + 31 => Self::LogicalNonVolatile, + 32 => Self::Hbm, + 33 => Self::Hbm2, + 34 => Self::Ddr5, + 35 => Self::LpDdr5, + 36 => Self::Hbm3, + _ => Self::Unknown, + } + } +} diff --git a/src/dmi/processor.rs b/src/dmi/processor.rs new file mode 100644 index 0000000..2b0a05b --- /dev/null +++ b/src/dmi/processor.rs @@ -0,0 +1,764 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Margin, Rect}, + style::{Style, Stylize}, + widgets::{Block, BorderType, Borders, Cell, List, ListItem, ListState, Padding, Row, Table}, +}; + +use crate::dmi::cache::Cache; + +fn string_ref(idx: u8, text: &[String]) -> String { + if idx == 0 { + return "Not Specified".to_string(); + } + text.get((idx - 1) as usize) + .cloned() + .unwrap_or_else(|| "Not Specified".to_string()) +} + +#[derive(Debug)] +pub struct Processors { + list: Vec, + caches: Vec, + selected: usize, +} + +impl Processors { + pub fn new(list: Vec, caches: Vec) -> Option { + if list.is_empty() { + None + } else { + Some(Self { + list, + caches, + selected: 0, + }) + } + } + + pub fn has_multiple(&self) -> bool { + self.list.len() >= 2 + } + + pub fn handle_key_events(&mut self, key_event: KeyEvent) { + if !self.has_multiple() { + return; + } + match key_event.code { + KeyCode::Down | KeyCode::Char('j') => { + self.selected = (self.selected + 1) % self.list.len(); + } + KeyCode::Up | KeyCode::Char('k') => { + self.selected = (self.selected + self.list.len() - 1) % self.list.len(); + } + _ => {} + } + } + + pub fn render(&mut self, frame: &mut Frame, block: Rect) { + if !self.has_multiple() { + self.list[0].render(frame, block, &self.caches); + return; + } + + let max_label = self + .list + .iter() + .map(|p| p.socket_designation.chars().count()) + .max() + .unwrap_or(0) as u16; + // 4 = 2 borders + 2 horizontal padding + let list_width = max_label.saturating_add(4).max(14); + + let body = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(list_width), Constraint::Fill(1)]) + .split(block.inner(Margin::new(4, 1))); + + let items: Vec> = self + .list + .iter() + .map(|p| ListItem::new(p.socket_designation.clone())) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .padding(Padding::new(1, 1, 1, 0)), + ) + .highlight_style(Style::new().bold().reversed()) + .highlight_symbol(""); + + let mut state = ListState::default(); + state.select(Some(self.selected)); + frame.render_stateful_widget(list, body[0], &mut state); + + if let Some(processor) = self.list.get(self.selected) { + processor.render(frame, body[1], &self.caches); + } + } +} + +#[derive(Debug)] +pub struct Processor { + socket_designation: String, + processor_type: ProcessorType, + family: u16, + manufacturer: String, + version: String, + voltage: VoltageInfo, + max_speed: Option, + current_speed: Option, + status: ProcessorStatus, + upgrade: u8, + l1_cache: Option, + l2_cache: Option, + l3_cache: Option, + core_count: Option, + core_enabled: Option, + thread_count: Option, + serial_number: String, + asset_tag: String, + part_number: String, +} + +impl From<(Vec, Vec)> for Processor { + fn from((data, text): (Vec, Vec)) -> Self { + let family = { + let f1 = data[2]; + if f1 == 0xFE && data.len() >= 38 { + u16::from_le_bytes(data[36..38].try_into().unwrap()) + } else { + f1 as u16 + } + }; + + let max_speed = { + let v = u16::from_le_bytes(data[16..18].try_into().unwrap()); + (v != 0).then_some(v) + }; + let current_speed = { + let v = u16::from_le_bytes(data[18..20].try_into().unwrap()); + (v != 0).then_some(v) + }; + + let core_count = read_count(data.get(31).copied(), data.get(38..40)); + let core_enabled = read_count(data.get(32).copied(), data.get(40..42)); + let thread_count = read_count(data.get(33).copied(), data.get(42..44)); + + let serial_number = data + .get(28) + .copied() + .map_or_else(|| "Not Specified".to_string(), |b| string_ref(b, &text)); + let asset_tag = data + .get(29) + .copied() + .map_or_else(|| "Not Specified".to_string(), |b| string_ref(b, &text)); + let part_number = data + .get(30) + .copied() + .map_or_else(|| "Not Specified".to_string(), |b| string_ref(b, &text)); + + let l1_cache = cache_handle(&data, 22); + let l2_cache = cache_handle(&data, 24); + let l3_cache = cache_handle(&data, 26); + + Self { + socket_designation: string_ref(data[0], &text), + processor_type: ProcessorType::from(data[1]), + family, + manufacturer: string_ref(data[3], &text), + version: string_ref(data[12], &text), + voltage: VoltageInfo::from(data[13]), + max_speed, + current_speed, + status: ProcessorStatus::from(data[20]), + upgrade: data[21], + l1_cache, + l2_cache, + l3_cache, + core_count, + core_enabled, + thread_count, + serial_number, + asset_tag, + part_number, + } + } +} + +// Read a u16 cache handle at the given offset in the structure's data slice. +// Returns None if the structure is too short or the handle is 0xFFFF +// ("the device does not have any cache of this level"). +fn cache_handle(data: &[u8], offset: usize) -> Option { + let slice = data.get(offset..offset + 2)?; + let handle = u16::from_le_bytes(slice.try_into().ok()?); + (handle != 0xFFFF).then_some(handle) +} + +fn read_count(legacy: Option, extended: Option<&[u8]>) -> Option { + match legacy? { + 0 => None, + 0xFF => extended + .and_then(|s| s.try_into().ok()) + .map(u16::from_le_bytes), + v => Some(v as u16), + } +} + +impl Processor { + fn render(&self, frame: &mut Frame, block: Rect, caches: &[Cache]) { + let speed_cell = |v: Option| match v { + Some(s) => format!("{s} MHz"), + None => "Unknown".to_string(), + }; + let count_cell = |v: Option| match v { + Some(c) => c.to_string(), + None => "Unknown".to_string(), + }; + let cache_row = |label: &'static str, handle: Option| { + let summary = handle + .and_then(|h| caches.iter().find(|c| c.handle == h)) + .map(Cache::summary) + .unwrap_or_else(|| "Not present".to_string()); + Row::new(vec![Cell::from(label).bold(), Cell::from(summary)]) + }; + + let rows = vec![ + Row::new(vec![ + Cell::from("Socket").bold(), + Cell::from(self.socket_designation.clone()), + ]), + Row::new(vec![ + Cell::from("Type").bold(), + Cell::from(self.processor_type.to_string()), + ]), + Row::new(vec![ + Cell::from("Manufacturer").bold(), + Cell::from(self.manufacturer.clone()), + ]), + Row::new(vec![ + Cell::from("Version").bold(), + Cell::from(self.version.clone()), + ]), + Row::new(vec![ + Cell::from("Family").bold(), + Cell::from(family_name(self.family, &self.manufacturer)), + ]), + Row::new(vec![ + Cell::from("Max Speed").bold(), + Cell::from(speed_cell(self.max_speed)), + ]), + Row::new(vec![ + Cell::from("Current Speed").bold(), + Cell::from(speed_cell(self.current_speed)), + ]), + Row::new(vec![ + Cell::from("Cores").bold(), + Cell::from(count_cell(self.core_count)), + ]), + Row::new(vec![ + Cell::from("Cores Enabled").bold(), + Cell::from(count_cell(self.core_enabled)), + ]), + Row::new(vec![ + Cell::from("Threads").bold(), + Cell::from(count_cell(self.thread_count)), + ]), + Row::new(vec![ + Cell::from("Voltage").bold(), + Cell::from(self.voltage.to_string()), + ]), + Row::new(vec![ + Cell::from("Status").bold(), + Cell::from(self.status.to_string()), + ]), + Row::new(vec![ + Cell::from("Upgrade").bold(), + Cell::from(upgrade_name(self.upgrade)), + ]), + cache_row("L1 Cache", self.l1_cache), + cache_row("L2 Cache", self.l2_cache), + cache_row("L3 Cache", self.l3_cache), + Row::new(vec![ + Cell::from("Part Number").bold(), + Cell::from(self.part_number.clone()), + ]), + Row::new(vec![ + Cell::from("Serial Number").bold(), + Cell::from(self.serial_number.clone()), + ]), + Row::new(vec![ + Cell::from("Asset Tag").bold(), + Cell::from(self.asset_tag.clone()), + ]), + ]; + + let widths = [Constraint::Length(18), Constraint::Fill(1)]; + let table = Table::new(rows, widths).block(Block::new().padding(Padding::uniform(2))); + frame.render_widget(table, block.inner(Margin::new(2, 0))); + } +} + +#[derive(Debug, strum::Display)] +enum ProcessorType { + #[strum(to_string = "Other")] + Other, + #[strum(to_string = "Unknown")] + Unknown, + #[strum(to_string = "Central Processor")] + Central, + #[strum(to_string = "Math Processor")] + Math, + #[strum(to_string = "DSP Processor")] + Dsp, + #[strum(to_string = "Video Processor")] + Video, +} + +impl From for ProcessorType { + fn from(value: u8) -> Self { + match value { + 3 => Self::Central, + 4 => Self::Math, + 5 => Self::Dsp, + 6 => Self::Video, + 2 => Self::Unknown, + _ => Self::Other, + } + } +} + +#[derive(Debug)] +struct ProcessorStatus { + populated: bool, + cpu_status: u8, +} + +impl From for ProcessorStatus { + fn from(value: u8) -> Self { + Self { + populated: (value & 0b0100_0000) != 0, + cpu_status: value & 0b0000_0111, + } + } +} + +impl std::fmt::Display for ProcessorStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if !self.populated { + return write!(f, "Unpopulated"); + } + let cpu = match self.cpu_status { + 0 => "Unknown", + 1 => "Enabled", + 2 => "Disabled by user (BIOS Setup)", + 3 => "Disabled by BIOS (POST Error)", + 4 => "Idle, waiting to be enabled", + 7 => "Other", + _ => "Reserved", + }; + write!(f, "Populated, {cpu}") + } +} + +#[derive(Debug)] +enum VoltageInfo { + Current(u8), // tenths of a volt + Capabilities { v5: bool, v33: bool, v29: bool }, +} + +impl From for VoltageInfo { + fn from(value: u8) -> Self { + if value & 0b1000_0000 != 0 { + Self::Current(value & 0b0111_1111) + } else { + Self::Capabilities { + v5: value & 0b0000_0001 != 0, + v33: value & 0b0000_0010 != 0, + v29: value & 0b0000_0100 != 0, + } + } + } +} + +impl std::fmt::Display for VoltageInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Current(tenths) => { + let volts = *tenths as f64 / 10.0; + write!(f, "{volts:.1} V") + } + Self::Capabilities { v5, v33, v29 } => { + let mut parts: Vec<&str> = Vec::new(); + if *v5 { + parts.push("5 V"); + } + if *v33 { + parts.push("3.3 V"); + } + if *v29 { + parts.push("2.9 V"); + } + if parts.is_empty() { + write!(f, "Unknown") + } else { + write!(f, "Capable of {}", parts.join(", ")) + } + } + } + } +} + +// Family table transcribed from dmidecode 3.7+ (dmi_processor_family in dmidecode.c). +// Spec reference: SMBIOS DSP0134 §7.5.2. +const FAMILY_NAMES: &[(u16, &str)] = &[ + (0x01, "Other"), + (0x02, "Unknown"), + (0x03, "8086"), + (0x04, "80286"), + (0x05, "80386"), + (0x06, "80486"), + (0x07, "8087"), + (0x08, "80287"), + (0x09, "80387"), + (0x0A, "80487"), + (0x0B, "Pentium"), + (0x0C, "Pentium Pro"), + (0x0D, "Pentium II"), + (0x0E, "Pentium MMX"), + (0x0F, "Celeron"), + (0x10, "Pentium II Xeon"), + (0x11, "Pentium III"), + (0x12, "M1"), + (0x13, "M2"), + (0x14, "Celeron M"), + (0x15, "Pentium 4 HT"), + (0x16, "Intel"), + (0x18, "Duron"), + (0x19, "K5"), + (0x1A, "K6"), + (0x1B, "K6-2"), + (0x1C, "K6-3"), + (0x1D, "Athlon"), + (0x1E, "AMD29000"), + (0x1F, "K6-2+"), + (0x20, "Power PC"), + (0x21, "Power PC 601"), + (0x22, "Power PC 603"), + (0x23, "Power PC 603+"), + (0x24, "Power PC 604"), + (0x25, "Power PC 620"), + (0x26, "Power PC x704"), + (0x27, "Power PC 750"), + (0x28, "Core Duo"), + (0x29, "Core Duo Mobile"), + (0x2A, "Core Solo Mobile"), + (0x2B, "Atom"), + (0x2C, "Core M"), + (0x2D, "Core m3"), + (0x2E, "Core m5"), + (0x2F, "Core m7"), + (0x30, "Alpha"), + (0x31, "Alpha 21064"), + (0x32, "Alpha 21066"), + (0x33, "Alpha 21164"), + (0x34, "Alpha 21164PC"), + (0x35, "Alpha 21164a"), + (0x36, "Alpha 21264"), + (0x37, "Alpha 21364"), + (0x38, "Turion II Ultra Dual-Core Mobile M"), + (0x39, "Turion II Dual-Core Mobile M"), + (0x3A, "Athlon II Dual-Core M"), + (0x3B, "Opteron 6100"), + (0x3C, "Opteron 4100"), + (0x3D, "Opteron 6200"), + (0x3E, "Opteron 4200"), + (0x3F, "FX"), + (0x40, "MIPS"), + (0x41, "MIPS R4000"), + (0x42, "MIPS R4200"), + (0x43, "MIPS R4400"), + (0x44, "MIPS R4600"), + (0x45, "MIPS R10000"), + (0x46, "C-Series"), + (0x47, "E-Series"), + (0x48, "A-Series"), + (0x49, "G-Series"), + (0x4A, "Z-Series"), + (0x4B, "R-Series"), + (0x4C, "Opteron 4300"), + (0x4D, "Opteron 6300"), + (0x4E, "Opteron 3300"), + (0x4F, "FirePro"), + (0x50, "SPARC"), + (0x51, "SuperSPARC"), + (0x52, "MicroSPARC II"), + (0x53, "MicroSPARC IIep"), + (0x54, "UltraSPARC"), + (0x55, "UltraSPARC II"), + (0x56, "UltraSPARC IIi"), + (0x57, "UltraSPARC III"), + (0x58, "UltraSPARC IIIi"), + (0x60, "68040"), + (0x61, "68xxx"), + (0x62, "68000"), + (0x63, "68010"), + (0x64, "68020"), + (0x65, "68030"), + (0x66, "Athlon X4"), + (0x67, "Opteron X1000"), + (0x68, "Opteron X2000"), + (0x69, "Opteron A-Series"), + (0x6A, "Opteron X3000"), + (0x6B, "Zen"), + (0x70, "Hobbit"), + (0x78, "Crusoe TM5000"), + (0x79, "Crusoe TM3000"), + (0x7A, "Efficeon TM8000"), + (0x80, "Weitek"), + (0x82, "Itanium"), + (0x83, "Athlon 64"), + (0x84, "Opteron"), + (0x85, "Sempron"), + (0x86, "Turion 64"), + (0x87, "Dual-Core Opteron"), + (0x88, "Athlon 64 X2"), + (0x89, "Turion 64 X2"), + (0x8A, "Quad-Core Opteron"), + (0x8B, "Third-Generation Opteron"), + (0x8C, "Phenom FX"), + (0x8D, "Phenom X4"), + (0x8E, "Phenom X2"), + (0x8F, "Athlon X2"), + (0x90, "PA-RISC"), + (0x91, "PA-RISC 8500"), + (0x92, "PA-RISC 8000"), + (0x93, "PA-RISC 7300LC"), + (0x94, "PA-RISC 7200"), + (0x95, "PA-RISC 7100LC"), + (0x96, "PA-RISC 7100"), + (0xA0, "V30"), + (0xA1, "Quad-Core Xeon 3200"), + (0xA2, "Dual-Core Xeon 3000"), + (0xA3, "Quad-Core Xeon 5300"), + (0xA4, "Dual-Core Xeon 5100"), + (0xA5, "Dual-Core Xeon 5000"), + (0xA6, "Dual-Core Xeon LV"), + (0xA7, "Dual-Core Xeon ULV"), + (0xA8, "Dual-Core Xeon 7100"), + (0xA9, "Quad-Core Xeon 5400"), + (0xAA, "Quad-Core Xeon"), + (0xAB, "Dual-Core Xeon 5200"), + (0xAC, "Dual-Core Xeon 7200"), + (0xAD, "Quad-Core Xeon 7300"), + (0xAE, "Quad-Core Xeon 7400"), + (0xAF, "Multi-Core Xeon 7400"), + (0xB0, "Pentium III Xeon"), + (0xB1, "Pentium III Speedstep"), + (0xB2, "Pentium 4"), + (0xB3, "Xeon"), + (0xB4, "AS400"), + (0xB5, "Xeon MP"), + (0xB6, "Athlon XP"), + (0xB7, "Athlon MP"), + (0xB8, "Itanium 2"), + (0xB9, "Pentium M"), + (0xBA, "Celeron D"), + (0xBB, "Pentium D"), + (0xBC, "Pentium EE"), + (0xBD, "Core Solo"), + // 0xBE handled as special case in family_name + (0xBF, "Core 2 Duo"), + (0xC0, "Core 2 Solo"), + (0xC1, "Core 2 Extreme"), + (0xC2, "Core 2 Quad"), + (0xC3, "Core 2 Extreme Mobile"), + (0xC4, "Core 2 Duo Mobile"), + (0xC5, "Core 2 Solo Mobile"), + (0xC6, "Core i7"), + (0xC7, "Dual-Core Celeron"), + (0xC8, "IBM390"), + (0xC9, "G4"), + (0xCA, "G5"), + (0xCB, "ESA/390 G6"), + (0xCC, "z/Architecture"), + (0xCD, "Core i5"), + (0xCE, "Core i3"), + (0xCF, "Core i9"), + (0xD2, "C7-M"), + (0xD3, "C7-D"), + (0xD4, "C7"), + (0xD5, "Eden"), + (0xD6, "Multi-Core Xeon"), + (0xD7, "Dual-Core Xeon 3xxx"), + (0xD8, "Quad-Core Xeon 3xxx"), + (0xD9, "Nano"), + (0xDA, "Dual-Core Xeon 5xxx"), + (0xDB, "Quad-Core Xeon 5xxx"), + (0xDD, "Dual-Core Xeon 7xxx"), + (0xDE, "Quad-Core Xeon 7xxx"), + (0xDF, "Multi-Core Xeon 7xxx"), + (0xE0, "Multi-Core Xeon 3400"), + (0xE4, "Opteron 3000"), + (0xE5, "Sempron II"), + (0xE6, "Embedded Opteron Quad-Core"), + (0xE7, "Phenom Triple-Core"), + (0xE8, "Turion Ultra Dual-Core Mobile"), + (0xE9, "Turion Dual-Core Mobile"), + (0xEA, "Athlon Dual-Core"), + (0xEB, "Sempron SI"), + (0xEC, "Phenom II"), + (0xED, "Athlon II"), + (0xEE, "Six-Core Opteron"), + (0xEF, "Sempron M"), + (0xFA, "i860"), + (0xFB, "i960"), + (0x100, "ARMv7"), + (0x101, "ARMv8"), + (0x102, "ARMv9"), + (0x103, "ARM"), + (0x104, "SH-3"), + (0x105, "SH-4"), + (0x118, "ARM"), + (0x119, "StrongARM"), + (0x12C, "6x86"), + (0x12D, "MediaGX"), + (0x12E, "MII"), + (0x140, "WinChip"), + (0x15E, "DSP"), + (0x1F4, "Video Processor"), + (0x200, "RV32"), + (0x201, "RV64"), + (0x202, "RV128"), + (0x258, "LoongArch"), + (0x259, "Loongson 1"), + (0x25A, "Loongson 2"), + (0x25B, "Loongson 3"), + (0x25C, "Loongson 2K"), + (0x25D, "Loongson 3A"), + (0x25E, "Loongson 3B"), + (0x25F, "Loongson 3C"), + (0x260, "Loongson 3D"), + (0x261, "Loongson 3E"), + (0x262, "Dual-Core Loongson 2K 2xxx"), + (0x26C, "Quad-Core Loongson 3A 5xxx"), + (0x26D, "Multi-Core Loongson 3A 5xxx"), + (0x26E, "Quad-Core Loongson 3B 5xxx"), + (0x26F, "Multi-Core Loongson 3B 5xxx"), + (0x270, "Multi-Core Loongson 3C 5xxx"), + (0x271, "Multi-Core Loongson 3D 5xxx"), +]; + +fn family_name(family: u16, manufacturer: &str) -> String { + // 0xBE is ambiguous: Intel Core 2 vs AMD K7. Decode using manufacturer. + if family == 0xBE { + if manufacturer.contains("Intel") { + return "Core 2".to_string(); + } + if manufacturer.contains("AMD") { + return "K7".to_string(); + } + return "Core 2 or K7".to_string(); + } + + FAMILY_NAMES + .iter() + .find_map(|(id, name)| (*id == family).then_some((*name).to_string())) + .unwrap_or_else(|| format!("Family {family:#x}")) +} + +// Upgrade array transcribed from dmidecode 3.7+ (dmi_processor_upgrade in dmidecode.c). +// Spec reference: SMBIOS DSP0134 §7.5.5. Indexed by code - 0x01. +const UPGRADE_NAMES: &[&str] = &[ + "Other", // 0x01 + "Unknown", // 0x02 + "Daughter Board", // 0x03 + "ZIF Socket", // 0x04 + "Replaceable Piggy Back", // 0x05 + "None", // 0x06 + "LIF Socket", // 0x07 + "Slot 1", // 0x08 + "Slot 2", // 0x09 + "370-pin Socket", // 0x0A + "Slot A", // 0x0B + "Slot M", // 0x0C + "Socket 423", // 0x0D + "Socket A (Socket 462)", // 0x0E + "Socket 478", // 0x0F + "Socket 754", // 0x10 + "Socket 940", // 0x11 + "Socket 939", // 0x12 + "Socket mPGA604", // 0x13 + "Socket LGA771", // 0x14 + "Socket LGA775", // 0x15 + "Socket S1", // 0x16 + "Socket AM2", // 0x17 + "Socket F (1207)", // 0x18 + "Socket LGA1366", // 0x19 + "Socket G34", // 0x1A + "Socket AM3", // 0x1B + "Socket C32", // 0x1C + "Socket LGA1156", // 0x1D + "Socket LGA1567", // 0x1E + "Socket PGA988A", // 0x1F + "Socket BGA1288", // 0x20 + "Socket rPGA988B", // 0x21 + "Socket BGA1023", // 0x22 + "Socket BGA1224", // 0x23 + "Socket BGA1155", // 0x24 + "Socket LGA1356", // 0x25 + "Socket LGA2011", // 0x26 + "Socket FS1", // 0x27 + "Socket FS2", // 0x28 + "Socket FM1", // 0x29 + "Socket FM2", // 0x2A + "Socket LGA2011-3", // 0x2B + "Socket LGA1356-3", // 0x2C + "Socket LGA1150", // 0x2D + "Socket BGA1168", // 0x2E + "Socket BGA1234", // 0x2F + "Socket BGA1364", // 0x30 + "Socket AM4", // 0x31 + "Socket LGA1151", // 0x32 + "Socket BGA1356", // 0x33 + "Socket BGA1440", // 0x34 + "Socket BGA1515", // 0x35 + "Socket LGA3647-1", // 0x36 + "Socket SP3", // 0x37 + "Socket SP3r2", // 0x38 + "Socket LGA2066", // 0x39 + "Socket BGA1392", // 0x3A + "Socket BGA1510", // 0x3B + "Socket BGA1528", // 0x3C + "Socket LGA4189", // 0x3D + "Socket LGA1200", // 0x3E + "Socket LGA4677", // 0x3F + "Socket LGA1700", // 0x40 + "Socket BGA1744", // 0x41 + "Socket BGA1781", // 0x42 + "Socket BGA1211", // 0x43 + "Socket BGA2422", // 0x44 + "Socket LGA1211", // 0x45 + "Socket LGA2422", // 0x46 + "Socket LGA5773", // 0x47 + "Socket BGA5773", // 0x48 + "Socket AM5", // 0x49 + "Socket SP5", // 0x4A + "Socket SP6", // 0x4B + "Socket BGA883", // 0x4C + "Socket BGA1190", // 0x4D + "Socket BGA4129", // 0x4E + "Socket LGA4710", // 0x4F + "Socket LGA7529", // 0x50 +]; + +fn upgrade_name(upgrade: u8) -> String { + if !(0x01..=0x50).contains(&upgrade) { + return format!("Upgrade {upgrade:#x}"); + } + UPGRADE_NAMES[(upgrade - 1) as usize].to_string() +} diff --git a/src/dmi/slot.rs b/src/dmi/slot.rs new file mode 100644 index 0000000..1ae5e1d --- /dev/null +++ b/src/dmi/slot.rs @@ -0,0 +1,368 @@ +// SMBIOS Type 9 (System Slots). Spec reference: DSP0134 §7.10. + +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Margin, Rect}, + style::{Style, Stylize}, + widgets::{Block, BorderType, Borders, Cell, List, ListItem, ListState, Padding, Row, Table}, +}; + +fn string_ref(idx: u8, text: &[String]) -> String { + if idx == 0 { + return "Not Specified".to_string(); + } + text.get((idx - 1) as usize) + .cloned() + .unwrap_or_else(|| "Not Specified".to_string()) +} + +#[derive(Debug)] +pub struct Slots { + list: Vec, + selected: usize, +} + +impl Slots { + pub fn new(list: Vec) -> Option { + if list.is_empty() { + None + } else { + Some(Self { list, selected: 0 }) + } + } + + pub fn has_multiple(&self) -> bool { + self.list.len() >= 2 + } + + pub fn handle_key_events(&mut self, key_event: KeyEvent) { + if !self.has_multiple() { + return; + } + match key_event.code { + KeyCode::Down | KeyCode::Char('j') => { + self.selected = (self.selected + 1) % self.list.len(); + } + KeyCode::Up | KeyCode::Char('k') => { + self.selected = (self.selected + self.list.len() - 1) % self.list.len(); + } + _ => {} + } + } + + pub fn render(&mut self, frame: &mut Frame, block: Rect) { + if !self.has_multiple() { + self.list[0].render(frame, block); + return; + } + + let max_label = self + .list + .iter() + .map(|s| s.designation.chars().count()) + .max() + .unwrap_or(0) as u16; + // 4 = 2 borders + 2 horizontal padding + let list_width = max_label.saturating_add(4).max(14); + + let body = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(list_width), Constraint::Fill(1)]) + .split(block.inner(Margin::new(4, 2))); + + let items: Vec> = self + .list + .iter() + .map(|s| ListItem::new(s.designation.clone())) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .padding(Padding::new(1, 1, 1, 0)), + ) + .highlight_style(Style::new().bold().reversed()) + .highlight_symbol(""); + + let mut state = ListState::default(); + state.select(Some(self.selected)); + frame.render_stateful_widget(list, body[0], &mut state); + + if let Some(slot) = self.list.get(self.selected) { + slot.render(frame, body[1]); + } + } +} + +#[derive(Debug)] +pub struct Slot { + designation: String, + slot_type: u8, + bus_width: u8, + current_usage: u8, + length: u8, + id: u16, + bdf: Option, +} + +#[derive(Debug)] +struct BusDeviceFunction { + segment: u16, + bus: u8, + device: u8, + function: u8, +} + +impl std::fmt::Display for BusDeviceFunction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:04x}:{:02x}:{:02x}.{}", + self.segment, self.bus, self.device, self.function + ) + } +} + +impl From<(Vec, Vec)> for Slot { + fn from((data, text): (Vec, Vec)) -> Self { + let id = u16::from_le_bytes(data[5..7].try_into().unwrap()); + + // Segment/Bus/Device-Function only present in SMBIOS 2.6+ + let bdf = if data.len() >= 13 { + let segment = u16::from_le_bytes(data[9..11].try_into().unwrap()); + let bus = data[11]; + let devfunc = data[12]; + // Unset values are 0xFFFF/0xFF — skip if all unset. + if segment == 0xFFFF && bus == 0xFF && devfunc == 0xFF { + None + } else { + Some(BusDeviceFunction { + segment, + bus, + device: devfunc >> 3, + function: devfunc & 0x07, + }) + } + } else { + None + }; + + Self { + designation: string_ref(data[0], &text), + slot_type: data[1], + bus_width: data[2], + current_usage: data[3], + length: data[4], + id, + bdf, + } + } +} + +impl Slot { + fn render(&self, frame: &mut Frame, block: Rect) { + let mut rows = vec![ + Row::new(vec![ + Cell::from("Designation").bold(), + Cell::from(self.designation.clone()), + ]), + Row::new(vec![ + Cell::from("Type").bold(), + Cell::from(slot_type_name(self.slot_type)), + ]), + Row::new(vec![ + Cell::from("Bus Width").bold(), + Cell::from(slot_bus_width_name(self.bus_width)), + ]), + Row::new(vec![ + Cell::from("Current Usage").bold(), + Cell::from(slot_current_usage_name(self.current_usage)), + ]), + Row::new(vec![ + Cell::from("Length").bold(), + Cell::from(slot_length_name(self.length)), + ]), + Row::new(vec![ + Cell::from("ID").bold(), + Cell::from(self.id.to_string()), + ]), + ]; + if let Some(bdf) = &self.bdf { + rows.push(Row::new(vec![ + Cell::from("Bus:Device.Function").bold(), + Cell::from(bdf.to_string()), + ])); + } + + let widths = [Constraint::Length(22), Constraint::Fill(1)]; + let table = Table::new(rows, widths).block(Block::new().padding(Padding::uniform(2))); + frame.render_widget(table, block.inner(Margin::new(2, 0))); + } +} + +// Slot Type table transcribed from dmidecode 3.7+ (dmi_slot_type in dmidecode.c). +// Spec reference: SMBIOS DSP0134 §7.10.1. +const SLOT_TYPE_LOW: &[&str] = &[ + "Other", // 0x01 + "Unknown", // 0x02 + "ISA", // 0x03 + "MCA", // 0x04 + "EISA", // 0x05 + "PCI", // 0x06 + "PC Card (PCMCIA)", // 0x07 + "VLB", // 0x08 + "Proprietary", // 0x09 + "Processor Card", // 0x0A + "Proprietary Memory Card", // 0x0B + "I/O Riser Card", // 0x0C + "NuBus", // 0x0D + "PCI-66", // 0x0E + "AGP", // 0x0F + "AGP 2x", // 0x10 + "AGP 4x", // 0x11 + "PCI-X", // 0x12 + "AGP 8x", // 0x13 + "M.2 Socket 1-DP", // 0x14 + "M.2 Socket 1-SD", // 0x15 + "M.2 Socket 2", // 0x16 + "M.2 Socket 3", // 0x17 + "MXM Type I", // 0x18 + "MXM Type II", // 0x19 + "MXM Type III", // 0x1A + "MXM Type III-HE", // 0x1B + "MXM Type IV", // 0x1C + "MXM 3.0 Type A", // 0x1D + "MXM 3.0 Type B", // 0x1E + "PCI Express 2 SFF-8639 (U.2)", // 0x1F + "PCI Express 3 SFF-8639 (U.2)", // 0x20 + "PCI Express Mini 52-pin with bottom-side keep-outs", // 0x21 + "PCI Express Mini 52-pin without bottom-side keep-outs", // 0x22 + "PCI Express Mini 76-pin", // 0x23 + "PCI Express 4 SFF-8639 (U.2)", // 0x24 + "PCI Express 5 SFF-8639 (U.2)", // 0x25 + "OCP NIC 3.0 Small Form Factor (SFF)", // 0x26 + "OCP NIC 3.0 Large Form Factor (LFF)", // 0x27 + "OCP NIC Prior to 3.0", // 0x28 +]; + +// Mirrors dmidecode's spelling, including "FLexbus". +const SLOT_TYPE_CXL: &str = "CXL FLexbus 1.0"; + +const SLOT_TYPE_HIGH: &[&str] = &[ + "PC-98/C20", // 0xA0 + "PC-98/C24", // 0xA1 + "PC-98/E", // 0xA2 + "PC-98/Local Bus", // 0xA3 + "PC-98/Card", // 0xA4 + "PCI Express", // 0xA5 + "PCI Express x1", // 0xA6 + "PCI Express x2", // 0xA7 + "PCI Express x4", // 0xA8 + "PCI Express x8", // 0xA9 + "PCI Express x16", // 0xAA + "PCI Express 2", // 0xAB + "PCI Express 2 x1", // 0xAC + "PCI Express 2 x2", // 0xAD + "PCI Express 2 x4", // 0xAE + "PCI Express 2 x8", // 0xAF + "PCI Express 2 x16", // 0xB0 + "PCI Express 3", // 0xB1 + "PCI Express 3 x1", // 0xB2 + "PCI Express 3 x2", // 0xB3 + "PCI Express 3 x4", // 0xB4 + "PCI Express 3 x8", // 0xB5 + "PCI Express 3 x16", // 0xB6 + "", // 0xB7 — out of spec gap in dmidecode + "PCI Express 4", // 0xB8 + "PCI Express 4 x1", // 0xB9 + "PCI Express 4 x2", // 0xBA + "PCI Express 4 x4", // 0xBB + "PCI Express 4 x8", // 0xBC + "PCI Express 4 x16", // 0xBD + "PCI Express 5", // 0xBE + "PCI Express 5 x1", // 0xBF + "PCI Express 5 x2", // 0xC0 + "PCI Express 5 x4", // 0xC1 + "PCI Express 5 x8", // 0xC2 + "PCI Express 5 x16", // 0xC3 + "PCI Express 6+", // 0xC4 + "EDSFF E1", // 0xC5 + "EDSFF E3", // 0xC6 +]; + +fn slot_type_name(code: u8) -> String { + if (0x01..=0x28).contains(&code) { + return SLOT_TYPE_LOW[(code - 0x01) as usize].to_string(); + } + if code == 0x30 { + return SLOT_TYPE_CXL.to_string(); + } + if (0xA0..=0xC6).contains(&code) { + let s = SLOT_TYPE_HIGH[(code - 0xA0) as usize]; + if !s.is_empty() { + return s.to_string(); + } + } + format!("Slot type {code:#x}") +} + +// Spec reference: SMBIOS DSP0134 §7.10.2. +const SLOT_BUS_WIDTH: &[&str] = &[ + "Other", // 0x01 + "Unknown", // 0x02 + "8 bit", // 0x03 + "16 bit", // 0x04 + "32 bit", // 0x05 + "64 bit", // 0x06 + "128 bit", // 0x07 + "1x or x1", // 0x08 + "2x or x2", // 0x09 + "4x or x4", // 0x0A + "8x or x8", // 0x0B + "12x or x12", // 0x0C + "16x or x16", // 0x0D + "32x or x32", // 0x0E +]; + +fn slot_bus_width_name(code: u8) -> String { + if (0x01..=0x0E).contains(&code) { + return SLOT_BUS_WIDTH[(code - 0x01) as usize].to_string(); + } + format!("Bus width {code:#x}") +} + +// Spec reference: SMBIOS DSP0134 §7.10.3. +const SLOT_CURRENT_USAGE: &[&str] = &[ + "Other", // 0x01 + "Unknown", // 0x02 + "Available", // 0x03 + "In Use", // 0x04 + "Unavailable", // 0x05 +]; + +fn slot_current_usage_name(code: u8) -> String { + if (0x01..=0x05).contains(&code) { + return SLOT_CURRENT_USAGE[(code - 0x01) as usize].to_string(); + } + format!("Usage {code:#x}") +} + +// Spec reference: SMBIOS DSP0134 §7.10.4. +const SLOT_LENGTH: &[&str] = &[ + "Other", // 0x01 + "Unknown", // 0x02 + "Short", // 0x03 + "Long", // 0x04 + "2.5\" drive form factor", // 0x05 + "3.5\" drive form factor", // 0x06 +]; + +fn slot_length_name(code: u8) -> String { + if (0x01..=0x06).contains(&code) { + return SLOT_LENGTH[(code - 0x01) as usize].to_string(); + } + format!("Length {code:#x}") +}