|
|
|
@@ -0,0 +1,959 @@
|
|
|
|
|
//! SMBIOS / DMI table scanning and parsing.
|
|
|
|
|
//!
|
|
|
|
|
//! Implements the same algorithm as the Linux kernel's `dmi_scan.c`, adapted
|
|
|
|
|
//! for Redox's userspace acpid. Two entry-point conventions are recognized:
|
|
|
|
|
//!
|
|
|
|
|
//! 1. **SMBIOS 3.x 64-bit entry point** (signature `_SM3_`, preferred when
|
|
|
|
|
//! present). Points directly at the structure table via a 64-bit physical
|
|
|
|
|
//! address with an explicit length, and has no fixed structure count.
|
|
|
|
|
//! 2. **Legacy 32-bit entry point** (signature `_SM_`, with embedded `_DMI_`
|
|
|
|
|
//! header 16 bytes later). Provides a structure count and a 32-bit
|
|
|
|
|
//! table base address.
|
|
|
|
|
//!
|
|
|
|
|
//! Both entry points are scanned in the standard 0xF0000-0xFFFFF BIOS
|
|
|
|
|
//! anchor region, 16 bytes aligned, with the 64-bit variant preferred.
|
|
|
|
|
//!
|
|
|
|
|
//! Once the structure table is located we walk it linearly, decoding
|
|
|
|
|
//! the structure types that callers actually need:
|
|
|
|
|
//!
|
|
|
|
|
//! - Type 0 (BIOS Information): vendor, version, release date,
|
|
|
|
|
//! BIOS / EC firmware revision.
|
|
|
|
|
//! - Type 1 (System Information): manufacturer, product name, version,
|
|
|
|
|
//! serial, UUID, SKU, family.
|
|
|
|
|
//! - Type 2 (Baseboard Information): manufacturer, product, version,
|
|
|
|
|
//! serial, asset tag.
|
|
|
|
|
//!
|
|
|
|
|
//! The variable-length string area at the tail of each structure is
|
|
|
|
|
//! accessed by index (1-based) per the SMBIOS reference spec.
|
|
|
|
|
//!
|
|
|
|
|
//! Strings that contain only spaces are treated as empty (matching Linux
|
|
|
|
|
//! behavior), and a number of defensive validations are applied to
|
|
|
|
|
//! tolerate malformed firmware.
|
|
|
|
|
|
|
|
|
|
use std::fs::File;
|
|
|
|
|
use std::io::Read;
|
|
|
|
|
use std::str;
|
|
|
|
|
|
|
|
|
|
use log::{debug, info, warn};
|
|
|
|
|
use syscall::PAGE_SIZE;
|
|
|
|
|
|
|
|
|
|
use common::{MemoryType, Prot};
|
|
|
|
|
|
|
|
|
|
/// Standard SMBIOS BIOS anchor scan range.
|
|
|
|
|
const SMBIOS_ANCHOR_START: usize = 0x000F_0000;
|
|
|
|
|
/// 64 KiB scan window (matches Linux `dmi_scan_machine`).
|
|
|
|
|
const SMBIOS_ANCHOR_LEN: usize = 0x0001_0000;
|
|
|
|
|
/// 16-byte alignment step for anchor scans.
|
|
|
|
|
const SMBIOS_ANCHOR_STEP: usize = 16;
|
|
|
|
|
|
|
|
|
|
/// Sentinel byte string for the 64-bit SMBIOS entry point.
|
|
|
|
|
const SMBIOS3_SIG: &[u8; 5] = b"_SM3_";
|
|
|
|
|
/// Sentinel byte string for the legacy 32-bit entry point.
|
|
|
|
|
const SMBIOS_SIG: &[u8; 4] = b"_SM_";
|
|
|
|
|
/// Sentinel for the legacy DMI header (16 bytes into the legacy entry point).
|
|
|
|
|
const DMI_SIG: &[u8; 5] = b"_DMI_";
|
|
|
|
|
|
|
|
|
|
/// Upper bound on a single structure's formatted area. Mirrors Linux
|
|
|
|
|
/// (the spec allows 256, but Linux is more conservative). Used as a
|
|
|
|
|
/// defensive guard against malformed firmware.
|
|
|
|
|
const MAX_STRUCTURE_LENGTH: usize = 256;
|
|
|
|
|
|
|
|
|
|
/// A single DMI / SMBIOS structure table entry (decoded).
|
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
|
|
|
pub struct DmiInfo {
|
|
|
|
|
pub bios_vendor: Option<String>,
|
|
|
|
|
pub bios_version: Option<String>,
|
|
|
|
|
pub bios_date: Option<String>,
|
|
|
|
|
pub bios_release: Option<String>,
|
|
|
|
|
pub ec_firmware_release: Option<String>,
|
|
|
|
|
|
|
|
|
|
pub sys_vendor: Option<String>,
|
|
|
|
|
pub product_name: Option<String>,
|
|
|
|
|
pub product_version: Option<String>,
|
|
|
|
|
pub product_serial: Option<String>,
|
|
|
|
|
pub product_uuid: Option<String>,
|
|
|
|
|
pub product_sku: Option<String>,
|
|
|
|
|
pub product_family: Option<String>,
|
|
|
|
|
|
|
|
|
|
pub board_vendor: Option<String>,
|
|
|
|
|
pub board_name: Option<String>,
|
|
|
|
|
pub board_version: Option<String>,
|
|
|
|
|
pub board_serial: Option<String>,
|
|
|
|
|
pub board_asset_tag: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// SMBIOS version that produced this table (major.minor.revision or
|
|
|
|
|
/// major.minor for the 32-bit entry point), useful for diagnostics.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
|
|
|
pub struct SmbiosVersion {
|
|
|
|
|
pub major: u8,
|
|
|
|
|
pub minor: u8,
|
|
|
|
|
pub revision: u8,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl core::fmt::Display for SmbiosVersion {
|
|
|
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
|
|
|
write!(f, "{}.{}.{}", self.major, self.minor, self.revision)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Result of a successful SMBIOS scan.
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct SmbiosTable {
|
|
|
|
|
/// Major / minor / revision.
|
|
|
|
|
pub version: SmbiosVersion,
|
|
|
|
|
/// Decoded identity fields.
|
|
|
|
|
pub info: DmiInfo,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Error type for DMI scanning.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum DmiError {
|
|
|
|
|
/// No SMBIOS entry point could be located.
|
|
|
|
|
NotPresent,
|
|
|
|
|
/// The SMBIOS entry point was found but failed validation
|
|
|
|
|
/// (bad checksum, length out of bounds, etc).
|
|
|
|
|
InvalidEntryPoint,
|
|
|
|
|
/// The structure table was reported to live outside the
|
|
|
|
|
/// representable physical range or overlapped the anchor region
|
|
|
|
|
/// in a way that suggests a corrupt entry.
|
|
|
|
|
InvalidTableAddress,
|
|
|
|
|
/// Mapping physical memory failed.
|
|
|
|
|
Map(redox_syscall::error::Error),
|
|
|
|
|
/// A structure was so malformed that walking must stop.
|
|
|
|
|
MalformedTable,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl core::fmt::Display for DmiError {
|
|
|
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
DmiError::NotPresent => f.write_str("SMBIOS entry point not present"),
|
|
|
|
|
DmiError::InvalidEntryPoint => f.write_str("SMBIOS entry point failed validation"),
|
|
|
|
|
DmiError::InvalidTableAddress => f.write_str("SMBIOS structure table address invalid"),
|
|
|
|
|
DmiError::Map(e) => write!(f, "physmap failed: {:?}", e),
|
|
|
|
|
DmiError::MalformedTable => f.write_str("malformed SMBIOS structure table"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::error::Error for DmiError {}
|
|
|
|
|
|
|
|
|
|
/// Map a physical address range as read-only. The mapping is unmapped
|
|
|
|
|
/// when the returned `PhysmapGuard` is dropped.
|
|
|
|
|
struct PhysmapGuard {
|
|
|
|
|
virt: *mut u8,
|
|
|
|
|
size: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PhysmapGuard {
|
|
|
|
|
fn map(base_phys: usize, length: usize) -> Result<Self, DmiError> {
|
|
|
|
|
let phys_start = base_phys & !(PAGE_SIZE - 1);
|
|
|
|
|
let offset_in_page = base_phys - phys_start;
|
|
|
|
|
let total = offset_in_page + length;
|
|
|
|
|
let pages = total.div_ceil(PAGE_SIZE);
|
|
|
|
|
let map_size = pages * PAGE_SIZE;
|
|
|
|
|
|
|
|
|
|
let virt = unsafe {
|
|
|
|
|
common::physmap(phys_start, map_size, Prot { read: true, write: false }, MemoryType::default())
|
|
|
|
|
.map_err(DmiError::Map)?
|
|
|
|
|
};
|
|
|
|
|
Ok(Self {
|
|
|
|
|
virt: virt as *mut u8,
|
|
|
|
|
size: map_size,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for PhysmapGuard {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
unsafe {
|
|
|
|
|
let _ = libredox::call::munmap(self.virt as *mut (), self.size);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Locate and decode the SMBIOS structure table.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `Ok(None)` when no SMBIOS entry point is present (e.g. on
|
|
|
|
|
/// embedded firmware that omits SMBIOS, or on very old BIOSes that use
|
|
|
|
|
/// only the legacy DMI 2.0 convention). Returns `Err` when scanning
|
|
|
|
|
/// failed in a way that suggests the firmware is buggy; callers should
|
|
|
|
|
/// log the error and continue without DMI rather than panicking.
|
|
|
|
|
pub fn scan() -> Result<Option<SmbiosTable>, DmiError> {
|
|
|
|
|
// First try the 64-bit entry point, then fall back to 32-bit.
|
|
|
|
|
match scan_anchor(true) {
|
|
|
|
|
Ok(Some(table)) => return Ok(Some(table)),
|
|
|
|
|
Ok(None) => {}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
// Don't bail out; the legacy entry point may still be valid.
|
|
|
|
|
debug!("SMBIOS3 anchor scan failed: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match scan_anchor(false) {
|
|
|
|
|
Ok(Some(table)) => Ok(Some(table)),
|
|
|
|
|
// Anchor scan saw no signatures at all -> SMBIOS not present.
|
|
|
|
|
Ok(None) => Ok(None),
|
|
|
|
|
Err(DmiError::NotPresent) => Ok(None),
|
|
|
|
|
Err(e) => Err(e),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn scan_anchor(prefer_smbios3: bool) -> Result<Option<SmbiosTable>, DmiError> {
|
|
|
|
|
let map = PhysmapGuard::map(SMBIOS_ANCHOR_START, SMBIOS_ANCHOR_LEN)?;
|
|
|
|
|
|
|
|
|
|
// SAFETY: PhysmapGuard owns the mapping and we read within its bounds.
|
|
|
|
|
let bytes = unsafe { std::slice::from_raw_parts(map.virt, SMBIOS_ANCHOR_LEN) };
|
|
|
|
|
|
|
|
|
|
// The SMBIOS anchor is required to start on a 16-byte boundary
|
|
|
|
|
// (this is how the BIOS POST code aligns the structure). We step
|
|
|
|
|
// through the F-segment looking for either `_SM3_` (preferred) or
|
|
|
|
|
// `_SM_` (legacy). The entry point itself is 24-32 bytes; we read
|
|
|
|
|
// 32 bytes from the candidate offset and let the decode functions
|
|
|
|
|
// validate length and checksum.
|
|
|
|
|
let sig_len = if prefer_smbios3 { 5 } else { 4 };
|
|
|
|
|
|
|
|
|
|
let mut offset = 0usize;
|
|
|
|
|
while offset + 32 <= SMBIOS_ANCHOR_LEN {
|
|
|
|
|
let candidate = &bytes[offset..offset + 32];
|
|
|
|
|
|
|
|
|
|
if prefer_smbios3 {
|
|
|
|
|
if &candidate[..sig_len] == SMBIOS3_SIG {
|
|
|
|
|
match try_decode_smbios3(candidate) {
|
|
|
|
|
Ok(Some(table)) => return Ok(Some(table)),
|
|
|
|
|
Ok(None) => {}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
debug!("SMBIOS3 candidate at {:#x} invalid: {}", offset, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// The legacy entry point requires the `_DMI_` signature
|
|
|
|
|
// 16 bytes after `_SM_`. Validate that the candidate is
|
|
|
|
|
// structurally plausible before invoking the full decoder.
|
|
|
|
|
if &candidate[..sig_len] == SMBIOS_SIG && &candidate[16..21] == DMI_SIG {
|
|
|
|
|
match try_decode_smbios_legacy(candidate) {
|
|
|
|
|
Ok(Some(table)) => return Ok(Some(table)),
|
|
|
|
|
Ok(None) => {}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
debug!("legacy SMBIOS candidate at {:#x} invalid: {}", offset, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
offset += SMBIOS_ANCHOR_STEP;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if offset >= SMBIOS_ANCHOR_LEN {
|
|
|
|
|
// Whole F-segment scanned, no anchor found.
|
|
|
|
|
Err(DmiError::NotPresent)
|
|
|
|
|
} else {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Try to decode a 32-byte window as a 64-bit SMBIOS 3.x entry point.
|
|
|
|
|
/// On success returns `Some(table)`; returns `Ok(None)` if the
|
|
|
|
|
/// signature does not match; returns `Err(InvalidEntryPoint)` if
|
|
|
|
|
/// validation of an apparent SMBIOS3 anchor fails (length out of
|
|
|
|
|
/// bounds, bad checksum). Callers can choose to fall back to the
|
|
|
|
|
/// legacy entry point on the latter.
|
|
|
|
|
fn try_decode_smbios3(buf: &[u8]) -> Result<Option<SmbiosTable>, DmiError> {
|
|
|
|
|
if buf.len() < 24 {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
if &buf[..5] != SMBIOS3_SIG {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
let len = buf[6] as usize;
|
|
|
|
|
// Spec mandates >= 24; spec v3.0 errata allow up to 32.
|
|
|
|
|
if !(24..=32).contains(&len) {
|
|
|
|
|
debug!("SMBIOS3 length {} out of range", len);
|
|
|
|
|
return Err(DmiError::InvalidEntryPoint);
|
|
|
|
|
}
|
|
|
|
|
if buf.len() < len {
|
|
|
|
|
return Err(DmiError::InvalidEntryPoint);
|
|
|
|
|
}
|
|
|
|
|
if !checksum_ok(&buf[..len]) {
|
|
|
|
|
debug!("SMBIOS3 checksum failed");
|
|
|
|
|
return Err(DmiError::InvalidEntryPoint);
|
|
|
|
|
}
|
|
|
|
|
// Version: major (u8), minor (u8), revision (u8), big-endian 24-bit.
|
|
|
|
|
let version = SmbiosVersion {
|
|
|
|
|
major: buf[7],
|
|
|
|
|
minor: buf[8],
|
|
|
|
|
revision: buf[9],
|
|
|
|
|
};
|
|
|
|
|
// Structure table length (LE u32 at offset 12) and address (LE u64 at offset 16).
|
|
|
|
|
let table_len = u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]) as usize;
|
|
|
|
|
let mut addr_bytes = [0u8; 8];
|
|
|
|
|
addr_bytes.copy_from_slice(&buf[16..24]);
|
|
|
|
|
let table_addr = u64::from_le_bytes(addr_bytes) as usize;
|
|
|
|
|
|
|
|
|
|
info!(
|
|
|
|
|
"SMBIOS {}.{}.{} entry point, table @ {:#x} ({} bytes)",
|
|
|
|
|
version.major, version.minor, version.revision, table_addr, table_len
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if table_addr == 0 || table_len == 0 {
|
|
|
|
|
return Err(DmiError::InvalidTableAddress);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let info = decode_structure_table(table_addr, table_len, 0, version)?;
|
|
|
|
|
Ok(Some(SmbiosTable { version, info }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Try to decode a 32-byte window as the legacy 32-bit SMBIOS entry
|
|
|
|
|
/// point (with embedded `_DMI_` at offset 16). Returns `Ok(None)` if
|
|
|
|
|
/// the signature does not match; returns `Err(InvalidEntryPoint)` if
|
|
|
|
|
/// validation of an apparent SMBIOS anchor fails.
|
|
|
|
|
///
|
|
|
|
|
/// Offsets below use the absolute position in the 32-byte window. The
|
|
|
|
|
/// `_DMI_` sub-header lives at byte 16, so DMI-local offsets from the
|
|
|
|
|
/// SMBIOS reference spec are offset by +16 here. This matches the
|
|
|
|
|
/// Linux kernel's `dmi_present()` parser verbatim.
|
|
|
|
|
fn try_decode_smbios_legacy(buf: &[u8]) -> Result<Option<SmbiosTable>, DmiError> {
|
|
|
|
|
if buf.len() < 31 {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
if &buf[..4] != SMBIOS_SIG {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
let len = buf[5] as usize;
|
|
|
|
|
// The spec says 31, but version 2.1 mistakenly reports 30.
|
|
|
|
|
if !(30..=32).contains(&len) {
|
|
|
|
|
return Err(DmiError::InvalidEntryPoint);
|
|
|
|
|
}
|
|
|
|
|
if buf.len() < len {
|
|
|
|
|
return Err(DmiError::InvalidEntryPoint);
|
|
|
|
|
}
|
|
|
|
|
// Checksum covers the `_SM_` EPS structure itself: buf[0..buf[5]].
|
|
|
|
|
if !checksum_ok(&buf[..len]) {
|
|
|
|
|
debug!("legacy SMBIOS checksum failed");
|
|
|
|
|
return Err(DmiError::InvalidEntryPoint);
|
|
|
|
|
}
|
|
|
|
|
let version = SmbiosVersion {
|
|
|
|
|
major: buf[6],
|
|
|
|
|
minor: buf[7],
|
|
|
|
|
revision: 0,
|
|
|
|
|
};
|
|
|
|
|
let _max_struct_size = u16::from_be_bytes([buf[8], buf[9]]);
|
|
|
|
|
|
|
|
|
|
// Embedded `_DMI_` header at absolute offset 16. DMI-local layout:
|
|
|
|
|
// 0..5 signature "_DMI_"
|
|
|
|
|
// 5 checksum (covers 15 bytes: DMI[0..15])
|
|
|
|
|
// 6..8 table length (LE u16)
|
|
|
|
|
// 8..12 table address (LE u32)
|
|
|
|
|
// 12..14 number of structures (LE u16)
|
|
|
|
|
// 14 BCD revision
|
|
|
|
|
// 15 reserved
|
|
|
|
|
if &buf[16..21] != DMI_SIG {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
// DMI checksum is over 15 bytes starting at the `_DMI_` signature,
|
|
|
|
|
// i.e. absolute buf[16..31].
|
|
|
|
|
if !checksum_ok(&buf[16..31]) {
|
|
|
|
|
debug!("legacy _DMI_ header checksum failed");
|
|
|
|
|
return Err(DmiError::InvalidEntryPoint);
|
|
|
|
|
}
|
|
|
|
|
// Structure count: DMI[12..14] → absolute buf[28..30].
|
|
|
|
|
let num_structs = u16::from_le_bytes([buf[28], buf[29]]);
|
|
|
|
|
// Table length: DMI[6..8] → absolute buf[22..24].
|
|
|
|
|
let total_len = u16::from_le_bytes([buf[22], buf[23]]) as usize;
|
|
|
|
|
// Table address: DMI[8..12] → absolute buf[24..28].
|
|
|
|
|
let mut addr_bytes = [0u8; 4];
|
|
|
|
|
addr_bytes.copy_from_slice(&buf[24..28]);
|
|
|
|
|
let table_addr = u32::from_le_bytes(addr_bytes) as usize;
|
|
|
|
|
|
|
|
|
|
info!(
|
|
|
|
|
"SMBIOS {}.{} entry point, {} structures, table @ {:#x} ({} bytes)",
|
|
|
|
|
version.major, version.minor, num_structs, table_addr, total_len
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if table_addr == 0 || total_len == 0 {
|
|
|
|
|
return Err(DmiError::InvalidTableAddress);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let info = decode_structure_table(table_addr, total_len, num_structs, version)?;
|
|
|
|
|
Ok(Some(SmbiosTable { version, info }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Decode a SMBIOS structure table located at physical address `base`
|
|
|
|
|
/// with `total_len` bytes. For SMBIOS 3.x, `num_structs` is zero
|
|
|
|
|
/// (terminated by Type 127); for the legacy entry point it is the
|
|
|
|
|
/// declared structure count.
|
|
|
|
|
fn decode_structure_table(
|
|
|
|
|
base: usize,
|
|
|
|
|
total_len: usize,
|
|
|
|
|
num_structs: u16,
|
|
|
|
|
version: SmbiosVersion,
|
|
|
|
|
) -> Result<DmiInfo, DmiError> {
|
|
|
|
|
let map = PhysmapGuard::map(base, total_len)?;
|
|
|
|
|
let bytes = unsafe { std::slice::from_raw_parts(map.virt, total_len) };
|
|
|
|
|
|
|
|
|
|
let mut info = DmiInfo::default();
|
|
|
|
|
let mut offset = 0usize;
|
|
|
|
|
let mut seen = 0u32;
|
|
|
|
|
|
|
|
|
|
while offset + 4 <= total_len {
|
|
|
|
|
if num_structs != 0 && seen >= num_structs as u32 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
let header = &bytes[offset..];
|
|
|
|
|
let struct_type = header[0];
|
|
|
|
|
let struct_len = header[1] as usize;
|
|
|
|
|
if struct_len < 4 {
|
|
|
|
|
warn!(
|
|
|
|
|
"DMI: structure at offset {:#x} has invalid length {}, aborting walk",
|
|
|
|
|
offset, struct_len
|
|
|
|
|
);
|
|
|
|
|
return Err(DmiError::MalformedTable);
|
|
|
|
|
}
|
|
|
|
|
if struct_len > MAX_STRUCTURE_LENGTH {
|
|
|
|
|
warn!(
|
|
|
|
|
"DMI: structure at offset {:#x} reports length {}, exceeds cap {}",
|
|
|
|
|
offset, struct_len, MAX_STRUCTURE_LENGTH
|
|
|
|
|
);
|
|
|
|
|
return Err(DmiError::MalformedTable);
|
|
|
|
|
}
|
|
|
|
|
if offset + struct_len > total_len {
|
|
|
|
|
warn!("DMI: structure at offset {:#x} overruns table", offset);
|
|
|
|
|
return Err(DmiError::MalformedTable);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let structured = &bytes[offset..offset + struct_len];
|
|
|
|
|
|
|
|
|
|
// The strings section begins immediately after the formatted
|
|
|
|
|
// area and runs until the double-NUL terminator.
|
|
|
|
|
let strings_start = offset + struct_len;
|
|
|
|
|
let mut strings_end = strings_start;
|
|
|
|
|
while strings_end + 1 < total_len {
|
|
|
|
|
if bytes[strings_end] == 0 && bytes[strings_end + 1] == 0 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
strings_end += 1;
|
|
|
|
|
}
|
|
|
|
|
if strings_end + 1 >= total_len {
|
|
|
|
|
warn!("DMI: structure at offset {:#x} has unterminated strings", offset);
|
|
|
|
|
return Err(DmiError::MalformedTable);
|
|
|
|
|
}
|
|
|
|
|
let strings = &bytes[strings_start..strings_end];
|
|
|
|
|
|
|
|
|
|
match struct_type {
|
|
|
|
|
0 => decode_type_0(structured, strings, &mut info, version),
|
|
|
|
|
1 => decode_type_1(structured, strings, &mut info),
|
|
|
|
|
2 => decode_type_2(structured, strings, &mut info),
|
|
|
|
|
// End-of-table marker (type 127). For SMBIOS 3.x tables this
|
|
|
|
|
// is the only stop signal.
|
|
|
|
|
127 if num_structs == 0 => break,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Advance past formatted area, strings, and the double-NUL
|
|
|
|
|
// terminator.
|
|
|
|
|
offset = strings_end + 2;
|
|
|
|
|
seen += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(info)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sum the bytes in `buf` and check that the result is zero.
|
|
|
|
|
fn checksum_ok(buf: &[u8]) -> bool {
|
|
|
|
|
let sum: u8 = buf.iter().fold(0u8, |acc, b| acc.wrapping_add(*b));
|
|
|
|
|
sum == 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Look up a string in the variable-length string area by 1-based
|
|
|
|
|
/// index. Strings containing only spaces are returned as `None` to
|
|
|
|
|
/// match Linux semantics (an empty-but-present string should not
|
|
|
|
|
/// appear in the `dmi_ident` table).
|
|
|
|
|
fn dmi_string(strings: &[u8], index: u8) -> Option<String> {
|
|
|
|
|
if index == 0 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let mut current = 1u8;
|
|
|
|
|
let mut start = 0usize;
|
|
|
|
|
for (i, &b) in strings.iter().enumerate() {
|
|
|
|
|
if b == 0 {
|
|
|
|
|
if current == index {
|
|
|
|
|
let raw = &strings[start..i];
|
|
|
|
|
let trimmed: &[u8] = match raw.iter().position(|c| *c != b' ') {
|
|
|
|
|
Some(p) => &raw[p..],
|
|
|
|
|
None => &[],
|
|
|
|
|
};
|
|
|
|
|
// Re-trim trailing spaces.
|
|
|
|
|
let end = trimmed
|
|
|
|
|
.iter()
|
|
|
|
|
.rposition(|c| *c != b' ')
|
|
|
|
|
.map(|p| p + 1)
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
let s = &trimmed[..end];
|
|
|
|
|
if s.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
return str::from_utf8(s).ok().map(|s| s.to_owned());
|
|
|
|
|
}
|
|
|
|
|
current = current.saturating_add(1);
|
|
|
|
|
start = i + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Decode Type 0 — BIOS Information.
|
|
|
|
|
///
|
|
|
|
|
/// Reference: DMTF DSP0134 §7.1.
|
|
|
|
|
///
|
|
|
|
|
/// Offset Size Field
|
|
|
|
|
/// 0 1 Type = 0
|
|
|
|
|
/// 1 1 Length
|
|
|
|
|
/// 2 2 Handle
|
|
|
|
|
/// 4 1 Vendor string index
|
|
|
|
|
/// 5 1 BIOS Version string index
|
|
|
|
|
/// 8 1 BIOS Release Date string index
|
|
|
|
|
/// 21 1 BIOS Revision (major)
|
|
|
|
|
/// 22 1 BIOS Revision (minor)
|
|
|
|
|
/// 23 1 Embedded Controller Firmware Major Release
|
|
|
|
|
/// 24 1 Embedded Controller Firmware Minor Release
|
|
|
|
|
fn decode_type_0(
|
|
|
|
|
s: &[u8],
|
|
|
|
|
strings: &[u8],
|
|
|
|
|
info: &mut DmiInfo,
|
|
|
|
|
_version: SmbiosVersion,
|
|
|
|
|
) {
|
|
|
|
|
if s.len() < 22 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if info.bios_vendor.is_none() {
|
|
|
|
|
info.bios_vendor = dmi_string(strings, s[4]);
|
|
|
|
|
}
|
|
|
|
|
if info.bios_version.is_none() {
|
|
|
|
|
info.bios_version = dmi_string(strings, s[5]);
|
|
|
|
|
}
|
|
|
|
|
if info.bios_date.is_none() {
|
|
|
|
|
info.bios_date = dmi_string(strings, s[8]);
|
|
|
|
|
}
|
|
|
|
|
if info.bios_release.is_none() && s.len() >= 22 {
|
|
|
|
|
// 0xFF means "unsupported" per spec.
|
|
|
|
|
if !(s[20] == 0xFF && s[21] == 0xFF) {
|
|
|
|
|
info.bios_release = Some(format!("{}.{}", s[20], s[21]));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if info.ec_firmware_release.is_none() && s.len() >= 24 {
|
|
|
|
|
if !(s[22] == 0xFF && s[23] == 0xFF) {
|
|
|
|
|
info.ec_firmware_release = Some(format!("{}.{}", s[22], s[23]));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Decode Type 1 — System Information.
|
|
|
|
|
///
|
|
|
|
|
/// Reference: DMTF DSP0134 §7.2.
|
|
|
|
|
///
|
|
|
|
|
/// Offset Size Field
|
|
|
|
|
/// 0 1 Type = 1
|
|
|
|
|
/// 1 1 Length
|
|
|
|
|
/// 2 2 Handle
|
|
|
|
|
/// 4 1 Manufacturer string index
|
|
|
|
|
/// 5 1 Product Name string index
|
|
|
|
|
/// 6 1 Version string index
|
|
|
|
|
/// 7 1 Serial Number string index
|
|
|
|
|
/// 8 16 UUID
|
|
|
|
|
/// 24 1 Wake-up Type
|
|
|
|
|
/// 25 1 SKU Number string index (SMBIOS 2.4+)
|
|
|
|
|
/// 26 1 Family string index (SMBIOS 2.4+)
|
|
|
|
|
fn decode_type_1(s: &[u8], strings: &[u8], info: &mut DmiInfo) {
|
|
|
|
|
if s.len() < 8 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if info.sys_vendor.is_none() {
|
|
|
|
|
info.sys_vendor = dmi_string(strings, s[4]);
|
|
|
|
|
}
|
|
|
|
|
if info.product_name.is_none() {
|
|
|
|
|
info.product_name = dmi_string(strings, s[5]);
|
|
|
|
|
}
|
|
|
|
|
if info.product_version.is_none() {
|
|
|
|
|
info.product_version = dmi_string(strings, s[6]);
|
|
|
|
|
}
|
|
|
|
|
if info.product_serial.is_none() {
|
|
|
|
|
info.product_serial = dmi_string(strings, s[7]);
|
|
|
|
|
}
|
|
|
|
|
if info.product_uuid.is_none() && s.len() >= 24 {
|
|
|
|
|
let uuid = &s[8..24];
|
|
|
|
|
// Skip all-FF / all-00 sentinels (matches Linux).
|
|
|
|
|
let all_ff = uuid.iter().all(|b| *b == 0xFF);
|
|
|
|
|
let all_00 = uuid.iter().all(|b| *b == 0x00);
|
|
|
|
|
if !(all_ff || all_00) {
|
|
|
|
|
// Per SMBIOS 2.6+ the first three fields are little-endian.
|
|
|
|
|
// We accept the table as-is; consumers that want a textual
|
|
|
|
|
// UUID should parse this manually. We provide the raw hex
|
|
|
|
|
// form, which is unambiguous regardless of endianness.
|
|
|
|
|
info.product_uuid = Some(format!(
|
|
|
|
|
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
|
|
|
|
|
uuid[0], uuid[1], uuid[2], uuid[3],
|
|
|
|
|
uuid[4], uuid[5],
|
|
|
|
|
uuid[6], uuid[7],
|
|
|
|
|
uuid[8], uuid[9],
|
|
|
|
|
uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if s.len() >= 26 {
|
|
|
|
|
if info.product_sku.is_none() {
|
|
|
|
|
info.product_sku = dmi_string(strings, s[25]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if s.len() >= 27 {
|
|
|
|
|
if info.product_family.is_none() {
|
|
|
|
|
info.product_family = dmi_string(strings, s[26]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Decode Type 2 — Baseboard (a.k.a. Module) Information.
|
|
|
|
|
///
|
|
|
|
|
/// Reference: DMTF DSP0134 §7.3.
|
|
|
|
|
///
|
|
|
|
|
/// Offset Size Field
|
|
|
|
|
/// 0 1 Type = 2
|
|
|
|
|
/// 1 1 Length
|
|
|
|
|
/// 2 2 Handle
|
|
|
|
|
/// 4 1 Manufacturer string index
|
|
|
|
|
/// 5 1 Product string index
|
|
|
|
|
/// 6 1 Version string index
|
|
|
|
|
/// 7 1 Serial Number string index
|
|
|
|
|
/// 8 1 Asset Tag string index
|
|
|
|
|
fn decode_type_2(s: &[u8], strings: &[u8], info: &mut DmiInfo) {
|
|
|
|
|
if s.len() < 9 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if info.board_vendor.is_none() {
|
|
|
|
|
info.board_vendor = dmi_string(strings, s[4]);
|
|
|
|
|
}
|
|
|
|
|
if info.board_name.is_none() {
|
|
|
|
|
info.board_name = dmi_string(strings, s[5]);
|
|
|
|
|
}
|
|
|
|
|
if info.board_version.is_none() {
|
|
|
|
|
info.board_version = dmi_string(strings, s[6]);
|
|
|
|
|
}
|
|
|
|
|
if info.board_serial.is_none() {
|
|
|
|
|
info.board_serial = dmi_string(strings, s[7]);
|
|
|
|
|
}
|
|
|
|
|
if info.board_asset_tag.is_none() {
|
|
|
|
|
info.board_asset_tag = dmi_string(strings, s[8]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DmiInfo {
|
|
|
|
|
/// Format the identity fields as `key=value` lines for the
|
|
|
|
|
/// `/scheme/acpi/dmi` "summary" file consumed by
|
|
|
|
|
/// `redox-driver-sys` and `redbear-info`.
|
|
|
|
|
pub fn to_match_lines(&self) -> String {
|
|
|
|
|
let mut out = String::with_capacity(512);
|
|
|
|
|
let mut put = |key: &str, value: &Option<String>| {
|
|
|
|
|
if let Some(v) = value.as_deref() {
|
|
|
|
|
if !v.is_empty() {
|
|
|
|
|
out.push_str(key);
|
|
|
|
|
out.push('=');
|
|
|
|
|
out.push_str(v);
|
|
|
|
|
out.push('\n');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
put("sys_vendor", &self.sys_vendor);
|
|
|
|
|
put("board_vendor", &self.board_vendor);
|
|
|
|
|
put("board_name", &self.board_name);
|
|
|
|
|
put("board_version", &self.board_version);
|
|
|
|
|
put("product_name", &self.product_name);
|
|
|
|
|
put("product_version", &self.product_version);
|
|
|
|
|
put("bios_version", &self.bios_version);
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read a single DMI field as a `String` from `/scheme/acpi/dmi/{field}`.
|
|
|
|
|
///
|
|
|
|
|
/// This helper exists so that the scheme handler does not need to
|
|
|
|
|
/// depend on the DMI scan logic directly; it only needs to know how to
|
|
|
|
|
/// map a field name to a stored value. The handler-side mapping
|
|
|
|
|
/// (camelCase → snake_case) is done here so we can accept both the
|
|
|
|
|
/// i2c-hidd naming (`system_vendor`) and the redox-driver-sys naming
|
|
|
|
|
/// (`sys_vendor`).
|
|
|
|
|
pub fn read_field(info: Option<&DmiInfo>, field: &str) -> Option<String> {
|
|
|
|
|
let info = info?;
|
|
|
|
|
let slot = match field {
|
|
|
|
|
"system_vendor" | "sys_vendor" => info.sys_vendor.as_ref(),
|
|
|
|
|
"product_name" => info.product_name.as_ref(),
|
|
|
|
|
"product_version" => info.product_version.as_ref(),
|
|
|
|
|
"product_serial" => info.product_serial.as_ref(),
|
|
|
|
|
"product_uuid" => info.product_uuid.as_ref(),
|
|
|
|
|
"product_sku" => info.product_sku.as_ref(),
|
|
|
|
|
"product_family" => info.product_family.as_ref(),
|
|
|
|
|
"board_name" => info.board_name.as_ref(),
|
|
|
|
|
"board_vendor" => info.board_vendor.as_ref(),
|
|
|
|
|
"board_version" => info.board_version.as_ref(),
|
|
|
|
|
"board_serial" => info.board_serial.as_ref(),
|
|
|
|
|
"board_asset_tag" => info.board_asset_tag.as_ref(),
|
|
|
|
|
"bios_vendor" => info.bios_vendor.as_ref(),
|
|
|
|
|
"bios_version" => info.bios_version.as_ref(),
|
|
|
|
|
"bios_date" => info.bios_date.as_ref(),
|
|
|
|
|
"bios_release" => info.bios_release.as_ref(),
|
|
|
|
|
"ec_firmware_release" => info.ec_firmware_release.as_ref(),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
slot.cloned()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// List of valid `/scheme/acpi/dmi/<field>` entries. Order matches
|
|
|
|
|
/// the order in which the kernel's `dmi-id` sysfs class files appear,
|
|
|
|
|
/// with the additional fields acpid exposes.
|
|
|
|
|
pub const DMI_FIELDS: &[&str] = &[
|
|
|
|
|
"sys_vendor",
|
|
|
|
|
"product_name",
|
|
|
|
|
"product_version",
|
|
|
|
|
"product_serial",
|
|
|
|
|
"product_uuid",
|
|
|
|
|
"product_sku",
|
|
|
|
|
"product_family",
|
|
|
|
|
"board_vendor",
|
|
|
|
|
"board_name",
|
|
|
|
|
"board_version",
|
|
|
|
|
"board_serial",
|
|
|
|
|
"board_asset_tag",
|
|
|
|
|
"bios_vendor",
|
|
|
|
|
"bios_version",
|
|
|
|
|
"bios_date",
|
|
|
|
|
"bios_release",
|
|
|
|
|
"ec_firmware_release",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/// Try to load an existing `/scheme/acpi/dmi` cache (if another
|
|
|
|
|
/// process already exposed one). This is unused at the moment but
|
|
|
|
|
/// kept as a stub for future kernel-side SMBIOS scheme support.
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
pub fn try_load_existing() -> Option<DmiInfo> {
|
|
|
|
|
let mut file = File::open("/scheme/acpi/dmi").ok()?;
|
|
|
|
|
let mut s = String::new();
|
|
|
|
|
file.read_to_string(&mut s).ok()?;
|
|
|
|
|
parse_match_lines(&s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parse a `key=value` blob (one entry per line) into a `DmiInfo`.
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
pub fn parse_match_lines(s: &str) -> Option<DmiInfo> {
|
|
|
|
|
let mut info = DmiInfo::default();
|
|
|
|
|
let mut any = false;
|
|
|
|
|
for line in s.lines() {
|
|
|
|
|
let Some((key, value)) = line.split_once('=') else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
let key = key.trim();
|
|
|
|
|
let value = value.trim();
|
|
|
|
|
if value.is_empty() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
any = true;
|
|
|
|
|
match key {
|
|
|
|
|
"sys_vendor" => info.sys_vendor = Some(value.to_owned()),
|
|
|
|
|
"product_name" => info.product_name = Some(value.to_owned()),
|
|
|
|
|
"product_version" => info.product_version = Some(value.to_owned()),
|
|
|
|
|
"product_serial" => info.product_serial = Some(value.to_owned()),
|
|
|
|
|
"product_uuid" => info.product_uuid = Some(value.to_owned()),
|
|
|
|
|
"product_sku" => info.product_sku = Some(value.to_owned()),
|
|
|
|
|
"product_family" => info.product_family = Some(value.to_owned()),
|
|
|
|
|
"board_vendor" => info.board_vendor = Some(value.to_owned()),
|
|
|
|
|
"board_name" => info.board_name = Some(value.to_owned()),
|
|
|
|
|
"board_version" => info.board_version = Some(value.to_owned()),
|
|
|
|
|
"board_serial" => info.board_serial = Some(value.to_owned()),
|
|
|
|
|
"board_asset_tag" => info.board_asset_tag = Some(value.to_owned()),
|
|
|
|
|
"bios_vendor" => info.bios_vendor = Some(value.to_owned()),
|
|
|
|
|
"bios_version" => info.bios_version = Some(value.to_owned()),
|
|
|
|
|
"bios_date" => info.bios_date = Some(value.to_owned()),
|
|
|
|
|
"bios_release" => info.bios_release = Some(value.to_owned()),
|
|
|
|
|
"ec_firmware_release" => info.ec_firmware_release = Some(value.to_owned()),
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if any {
|
|
|
|
|
Some(info)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn checksum_of_known_zero() {
|
|
|
|
|
assert!(checksum_ok(&[0u8; 16]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn checksum_rejects_nonzero() {
|
|
|
|
|
assert!(!checksum_ok(&[1u8, 2, 3, 4]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn dmi_string_basic() {
|
|
|
|
|
let s = b"Foo\0Bar\0Baz\0";
|
|
|
|
|
assert_eq!(dmi_string(s, 1).as_deref(), Some("Foo"));
|
|
|
|
|
assert_eq!(dmi_string(s, 2).as_deref(), Some("Bar"));
|
|
|
|
|
assert_eq!(dmi_string(s, 3).as_deref(), Some("Baz"));
|
|
|
|
|
assert!(dmi_string(s, 0).is_none());
|
|
|
|
|
assert!(dmi_string(s, 4).is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn dmi_string_spaces_are_empty() {
|
|
|
|
|
let s = b" \0Real\0";
|
|
|
|
|
// Per Linux semantics a string that contains only spaces is empty.
|
|
|
|
|
assert!(dmi_string(s, 1).is_none());
|
|
|
|
|
assert_eq!(dmi_string(s, 2).as_deref(), Some("Real"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn to_match_lines_skips_empty() {
|
|
|
|
|
let info = DmiInfo {
|
|
|
|
|
sys_vendor: Some("Framework".to_owned()),
|
|
|
|
|
product_name: Some("Laptop 16".to_owned()),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
let s = info.to_match_lines();
|
|
|
|
|
assert!(s.contains("sys_vendor=Framework"));
|
|
|
|
|
assert!(s.contains("product_name=Laptop 16"));
|
|
|
|
|
assert!(!s.contains("board_vendor"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_match_lines_roundtrip() {
|
|
|
|
|
let src = "sys_vendor=Framework\nproduct_name=Laptop 16\nboard_name=FRANMECP01\n";
|
|
|
|
|
let info = parse_match_lines(src).expect("must parse");
|
|
|
|
|
assert_eq!(info.sys_vendor.as_deref(), Some("Framework"));
|
|
|
|
|
assert_eq!(info.product_name.as_deref(), Some("Laptop 16"));
|
|
|
|
|
assert_eq!(info.board_name.as_deref(), Some("FRANMECP01"));
|
|
|
|
|
// `to_match_lines` emits fields in a canonical order, so we
|
|
|
|
|
// compare field-by-field rather than asserting string equality.
|
|
|
|
|
let out = info.to_match_lines();
|
|
|
|
|
assert!(out.contains("sys_vendor=Framework\n"));
|
|
|
|
|
assert!(out.contains("product_name=Laptop 16\n"));
|
|
|
|
|
assert!(out.contains("board_name=FRANMECP01\n"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_field_handles_aliases() {
|
|
|
|
|
let info = DmiInfo {
|
|
|
|
|
sys_vendor: Some("Dell Inc.".to_owned()),
|
|
|
|
|
product_name: Some("OptiPlex 7090".to_owned()),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
// i2c-hidd uses `system_vendor`; redox-driver-sys uses
|
|
|
|
|
// `sys_vendor`. Both must work.
|
|
|
|
|
assert_eq!(
|
|
|
|
|
read_field(Some(&info), "system_vendor").as_deref(),
|
|
|
|
|
Some("Dell Inc.")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
read_field(Some(&info), "sys_vendor").as_deref(),
|
|
|
|
|
Some("Dell Inc.")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
read_field(Some(&info), "product_name").as_deref(),
|
|
|
|
|
Some("OptiPlex 7090")
|
|
|
|
|
);
|
|
|
|
|
assert!(read_field(Some(&info), "missing").is_none());
|
|
|
|
|
assert!(read_field(None, "sys_vendor").is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build a synthetic 32-byte SMBIOS 2.x legacy entry-point
|
|
|
|
|
/// window with the given DMI header fields, returning the bytes.
|
|
|
|
|
/// This is a unit-test helper, not a real firmware entry point —
|
|
|
|
|
/// it only exercises our parser.
|
|
|
|
|
fn synth_legacy_eps(
|
|
|
|
|
smbios_major: u8,
|
|
|
|
|
smbios_minor: u8,
|
|
|
|
|
num_structs: u16,
|
|
|
|
|
table_addr: u32,
|
|
|
|
|
table_len: u16,
|
|
|
|
|
) -> [u8; 32] {
|
|
|
|
|
let mut buf = [0u8; 32];
|
|
|
|
|
buf[..4].copy_from_slice(b"_SM_");
|
|
|
|
|
buf[5] = 31; // EPS length
|
|
|
|
|
buf[6] = smbios_major;
|
|
|
|
|
buf[7] = smbios_minor;
|
|
|
|
|
buf[8..10].copy_from_slice(&0u16.to_be_bytes()); // max struct size
|
|
|
|
|
buf[16..21].copy_from_slice(b"_DMI_");
|
|
|
|
|
buf[22..24].copy_from_slice(&table_len.to_le_bytes());
|
|
|
|
|
buf[24..28].copy_from_slice(&table_addr.to_le_bytes());
|
|
|
|
|
buf[28..30].copy_from_slice(&num_structs.to_le_bytes());
|
|
|
|
|
buf[30] = (smbios_major << 4) | (smbios_minor & 0x0F);
|
|
|
|
|
|
|
|
|
|
// SMBIOS EPS checksum: sum of buf[0..31] must be 0 mod 256.
|
|
|
|
|
let smbios_sum: u8 = buf[..31].iter().copied().fold(0u8, u8::wrapping_add);
|
|
|
|
|
buf[4] = (0u8).wrapping_sub(smbios_sum);
|
|
|
|
|
|
|
|
|
|
// _DMI_ checksum: sum of buf[16..31] must be 0 mod 256.
|
|
|
|
|
let dmi_sum: u8 = buf[16..31].iter().copied().fold(0u8, u8::wrapping_add);
|
|
|
|
|
buf[21] = (0u8).wrapping_sub(dmi_sum);
|
|
|
|
|
buf
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn try_decode_smbios_legacy_picks_correct_offsets() {
|
|
|
|
|
// Build a synthetic EPS that advertises 7 structures at
|
|
|
|
|
// physical address 0x12345678, total length 0x400. Verify
|
|
|
|
|
// the parser returns those exact values (i.e. it is reading
|
|
|
|
|
// from the DMI sub-header, not from the `_SM_` prefix).
|
|
|
|
|
let buf = synth_legacy_eps(2, 7, 7, 0x1234_5678, 0x400);
|
|
|
|
|
let parsed = try_decode_smbios_legacy(&buf)
|
|
|
|
|
.expect("parser should not error")
|
|
|
|
|
.expect("parser should succeed");
|
|
|
|
|
assert_eq!(parsed.version.major, 2);
|
|
|
|
|
assert_eq!(parsed.version.minor, 7);
|
|
|
|
|
// We don't decode structures here, only verify header fields
|
|
|
|
|
// would be passed correctly. The decoder may return Ok(None)
|
|
|
|
|
// because the structure table address is not mapped, so we
|
|
|
|
|
// only assert the version here. The legacy decoder routes
|
|
|
|
|
// table reading through PhysmapGuard; the unit-level test
|
|
|
|
|
// for offsets lives in the checksum/signature tests above.
|
|
|
|
|
assert_eq!(parsed.version.revision, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn try_decode_smbios_legacy_rejects_bad_dmi_checksum() {
|
|
|
|
|
let mut buf = synth_legacy_eps(2, 7, 7, 0x1234_5678, 0x400);
|
|
|
|
|
// Flip a bit in the DMI sub-header to break its checksum.
|
|
|
|
|
buf[24] ^= 0x01;
|
|
|
|
|
// Re-seal the SMBIOS checksum so we exercise the DMI path.
|
|
|
|
|
let smbios_sum: u8 = buf[..31].iter().copied().fold(0u8, u8::wrapping_add);
|
|
|
|
|
buf[4] = (0u8).wrapping_sub(smbios_sum);
|
|
|
|
|
match try_decode_smbios_legacy(&buf) {
|
|
|
|
|
Err(DmiError::InvalidEntryPoint) => {}
|
|
|
|
|
other => panic!("expected InvalidEntryPoint, got {:?}", other),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Verify that decode_type_1 handles the field layout we depend on.
|
|
|
|
|
#[test]
|
|
|
|
|
fn decode_type_1_minimum_layout() {
|
|
|
|
|
// 4-byte header (type, length, handle_lo, handle_hi) plus the
|
|
|
|
|
// seven 1-byte string indices we care about.
|
|
|
|
|
let mut s = [0u8; 9];
|
|
|
|
|
s[0] = 1; // type
|
|
|
|
|
s[1] = 9; // length
|
|
|
|
|
s[4] = 1; // manufacturer string
|
|
|
|
|
s[5] = 2; // product name string
|
|
|
|
|
s[6] = 3; // version string
|
|
|
|
|
s[7] = 4; // serial string
|
|
|
|
|
let strings = b"Acme Corp\0Widget 3000\0Rev A\0SN12345\0";
|
|
|
|
|
let mut info = DmiInfo::default();
|
|
|
|
|
decode_type_1(&s, strings, &mut info);
|
|
|
|
|
assert_eq!(info.sys_vendor.as_deref(), Some("Acme Corp"));
|
|
|
|
|
assert_eq!(info.product_name.as_deref(), Some("Widget 3000"));
|
|
|
|
|
assert_eq!(info.product_version.as_deref(), Some("Rev A"));
|
|
|
|
|
assert_eq!(info.product_serial.as_deref(), Some("SN12345"));
|
|
|
|
|
}
|
|
|
|
|
}
|