attodmi 0.3.0

Extremely simple code for partially decoding certain kinds of DMI entries from /sys/firmware/dmi/entries
Documentation
//! Partial and minimal parser for Linux sysfs DMI (SMBIOS) entries
//!
//! Most of this code was written based on dmidecode version 3.4 by
//! Alan Cox and Jean Delvare.
//!
//! ## Example
//!
//! ```no_run
//! use attodmi::{DmiRawData,DmiEntry,DmiError,DmiInstance,DmiSystemInformation};
//!
//! fn test()->Result<(),DmiError> {
//!     let raw = DmiRawData::from_file("/sys/firmware/dmi/entries/2-0/raw")?;
//!     let entry = DmiEntry::try_from_raw(&raw)?;
//!     println!("{:#?}",entry);
//!     let si = DmiSystemInformation::get_instance(0)?;
//!     if let Some(u) = si.uuid {
//!         println!("UUID: {}",u);
//!     }
//!     Ok(())
//!  }
//! ```
//!
//! All strings are ASCII strings.  Any bytes outside the 32 to 126
//! range are replaced by a dot character (ASCII 46), as done by
//! dmidecode.
use std::{
    error::Error,
    fmt::{
	Display,
	Formatter
    },
    fs::File,
    io::Read,
    path::Path,
};

/// The errors returned by this library
#[derive(Debug)]
pub enum DmiError {
    /// Too few elements in the value table of the DMI entry
    ShortTable,

    /// The data seems to be truncated
    ShortData,

    /// DMI entries of this type are not handled by the library
    UnhandledType(u8),

    /// An I/O error occurred while reading from a file
    IoError(std::io::Error),

    /// This error shouldn't happen but is included for consistency
    SliceError(std::array::TryFromSliceError)
}

impl Display for DmiError {
    fn fmt(&self,f:&mut Formatter<'_>)->std::result::Result<(),std::fmt::Error> {
	write!(f,"{:?}",self)
    }
}

impl From<std::io::Error> for DmiError {
    fn from(e:std::io::Error)->Self {
	Self::IoError(e)
    }
}

impl From<std::array::TryFromSliceError> for DmiError {
    fn from(e:std::array::TryFromSliceError)->Self {
	Self::SliceError(e)
    }
}

impl Error for DmiError { }

pub type Result<T> = std::result::Result<T,DmiError>;

/// A partially parsed sysfs DMI entry
#[derive(Debug)]
pub struct DmiRawData {
    pub dmi_type:u8,
    pub length:u8,
    pub handle:u16,
    pub data:Vec<u8>
}

/// DMI type 1
#[derive(Debug)]
pub struct DmiSystemInformation {
    pub manufacturer:String,
    pub product_name:String,
    pub version:String,
    pub serial_number:String,
    pub uuid:Option<DmiUuid>,
    pub sku:Option<String>,
    pub family:Option<String>
}

pub trait DmiInstance : Sized
{
    const TYPE_CODE : u8;

    fn get_instance(instance:usize)->Result<Self> {
	let raw = DmiRawData::from_sysfs(Self::TYPE_CODE,instance)?;
	Self::try_from_raw(&raw)
    }

    fn try_from_raw(raw:&DmiRawData)->Result<Self>;
}

/// A UUID as stored in a DMI table.  The bytes are in storage
/// order, which apparently does not match the usual display order.
#[derive(Debug)]
pub struct DmiUuid(pub [u8;16]);

/// Display a UUID using the byte order specified in SMBIOS versions
/// not older than 2.6.
impl Display for DmiUuid {
    fn fmt(&self,f:&mut Formatter<'_>)->std::result::Result<(),std::fmt::Error>
    {
	for j in [3,2,1,0,16,
		  5,4,16,
		  7,6,16,
		  8,9,16,
		  10,11,12,13,14,15] {
	    if j == 16 {
		write!(f,"-")?;
	    } else {
		write!(f,"{:02x}",self.0[j])?;
	    }
	}
	Ok(())
    }
}

/// DMI type 2
#[derive(Debug)]
pub struct DmiBaseBoardInformation {
    pub manufacturer:String,
    pub product_name:String,
    pub version:String,
    pub serial_number:String,
    pub asset_tag:Option<String>,
    pub location_in_chassis:Option<String>
}

/// DMI type 3
#[derive(Debug)]
pub struct DmiChassisInformation {
    pub manufacturer:String,
    pub version:String,
    pub serial_number:String,
    pub asset_tag:String,
}

/// An enumeration that holds all the known entry types
#[derive(Debug)]
pub enum DmiEntry {
    SI(DmiSystemInformation),
    BBI(DmiBaseBoardInformation),
    CI(DmiChassisInformation),
}

impl DmiRawData {
    /// Read DMI data from a sysfs file assuming standard mount
    /// points
    pub fn from_sysfs(dmi_type:u8,instance:usize)->Result<Self> {
	Self::from_file(format!("/sys/firmware/dmi/entries/{}-{}/raw",
				dmi_type,instance))
    }

    /// Get DMI data from the contents of a file such as
    /// `/sys/firmware/dmi/entries/3-0/raw`
    pub fn from_file<P:AsRef<Path>>(path:P)->Result<Self> {
	let fd = File::open(path)?;
	Self::from_reader(fd)
    }
    
    /// Call on the contents of a sysfs DMI entry
    pub fn from_reader<R:Read>(mut r:R)->Result<Self> {
	let mut data = Vec::new();
	let _ = r.read_to_end(&mut data)?;
	let dmi_type = data[0];
	let length = data[1];
	let handle = u16::from_le_bytes([data[2],data[3]]);
	Ok(Self {
	    dmi_type,
	    length,
	    handle,
	    data
	})
    }

    fn maybe_data(&self,id:usize)->Option<u8> {
	if id < self.length as usize {
	    Some(self.data[id])
	} else {
	    None
	}
    }

    fn data(&self,id:usize)->Result<u8> {
	self.maybe_data(id)
	    .ok_or(DmiError::ShortTable)
    }

    fn string(&self,id:usize)->Result<String> {
	let index = self.data(id)?;
	self.get_string(index)
    }

    fn maybe_string(&self,id:usize)->Result<Option<String>> {
	if let Some(index) = self.maybe_data(id) {
	    Ok(Some(self.get_string(index)?))
	} else {
	    Ok(None)
	}
    }

    fn uuid(&self,offset:usize)->Result<DmiUuid> {
	let d = &self.data;
	let n = d.len();
	if offset + 16 < n {
	    Ok(DmiUuid(d[offset .. offset + 16].try_into()?))
	} else {
	    Err(DmiError::ShortData)
	}
    }

    fn get_string(&self,mut index:u8)->Result<String> {
	let d = &self.data;
	let mut i = self.length as usize;
	let n = d.len();
	while index > 1 {
	    loop {
		if i >= n {
		    return Err(DmiError::ShortData);
		}
		if d[i] == 0 {
		    i += 1;
		    break;
		}
		i += 1;
	    }
	    index -= 1;
	}
	let mut out = String::new();
	loop {
	    if i + 1 >= n {
		break;
	    }
	    let c = d[i];
	    match c {
		0 => break,
		32 .. 127 => {
		    if let Some(c) = char::from_u32(c as u32) {
			out.push(c);
		    }
		},
		_ => out.push('.')
	    }
	    i += 1;
	}
	Ok(out)
    }
}

impl DmiInstance for DmiSystemInformation {
    const TYPE_CODE : u8 = 0x01;

    fn try_from_raw(raw:&DmiRawData)->Result<Self> {
	let manufacturer = raw.string(0x04)?;
	let product_name = raw.string(0x05)?;
	let version = raw.string(0x06)?;
	let serial_number = raw.string(0x07)?;
	let uuid =
	    if raw.length < 0x19 {
		None
	    } else {
		Some(raw.uuid(8)?)
	    };
	let sku = raw.maybe_string(0x19)?;
	let family = raw.maybe_string(0x1a)?;
	Ok(DmiSystemInformation {
	    manufacturer,
	    product_name,
	    version,
	    serial_number,
	    uuid,
	    sku,
	    family
	})
    }
}

impl DmiInstance for DmiBaseBoardInformation {
    const TYPE_CODE : u8 = 0x02;

    fn try_from_raw(raw:&DmiRawData)->Result<Self> {
	let manufacturer = raw.string(0x04)?;
	let product_name = raw.string(0x05)?;
	let version = raw.string(0x06)?;
	let serial_number = raw.string(0x07)?;
	let asset_tag = raw.maybe_string(0x08)?;
	let location_in_chassis = raw.maybe_string(0x0a)?;
	Ok(DmiBaseBoardInformation {
	    manufacturer,
	    product_name,
	    version,
	    serial_number,
	    asset_tag,
	    location_in_chassis
	})
    }
}

impl DmiInstance for DmiChassisInformation {
    const TYPE_CODE : u8 = 0x03;

    fn try_from_raw(raw:&DmiRawData)->Result<Self> {
	let manufacturer = raw.string(0x04)?;
	let version = raw.string(0x06)?;
	let serial_number = raw.string(0x07)?;
	let asset_tag = raw.string(0x08)?;
	Ok(DmiChassisInformation {
	    manufacturer,
	    version,
	    serial_number,
	    asset_tag
	})
    }
}

impl DmiEntry
{
    pub fn try_from_raw(raw:&DmiRawData)->Result<Self> {
	match raw.dmi_type {
	    1 => Ok(DmiEntry::SI(DmiSystemInformation::try_from_raw(raw)?)),
	    2 => Ok(DmiEntry::BBI(DmiBaseBoardInformation::try_from_raw(raw)?)),
	    3 => Ok(DmiEntry::CI(DmiChassisInformation::try_from_raw(raw)?)),
	    t => Err(DmiError::UnhandledType(t))
	}
    }
}