Files
RedBear-OS/local/recipes/system/redbear-hwutils/source/src/lib.rs
T

314 lines
9.0 KiB
Rust

use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::sync::OnceLock;
const PCI_IDS_PATH: &str = "/usr/share/misc/pci.ids";
#[derive(Default)]
struct PciIdDatabase {
vendor_names: HashMap<u16, String>,
device_names: HashMap<(u16, u16), String>,
}
static PCI_ID_DATABASE: OnceLock<Option<PciIdDatabase>> = OnceLock::new();
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct PciLocation {
pub segment: u16,
pub bus: u8,
pub device: u8,
pub function: u8,
}
impl PciLocation {
pub fn scheme_path(&self) -> String {
format!(
"/scheme/pci/{:04x}--{:02x}--{:02x}.{}",
self.segment, self.bus, self.device, self.function
)
}
}
impl fmt::Display for PciLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:04x}:{:02x}:{:02x}.{}",
self.segment, self.bus, self.device, self.function
)
}
}
pub fn parse_pci_location(name: &str) -> Option<PciLocation> {
let (segment, rest) = name.split_once("--")?;
let (bus, rest) = rest.split_once("--")?;
let (device, function) = rest.split_once('.')?;
Some(PciLocation {
segment: u16::from_str_radix(segment, 16).ok()?,
bus: u8::from_str_radix(bus, 16).ok()?,
device: u8::from_str_radix(device, 16).ok()?,
function: function.parse().ok()?,
})
}
pub fn parse_args(
program: &str,
usage: &str,
args: impl IntoIterator<Item = String>,
) -> Result<(), String> {
let extras: Vec<String> = args.into_iter().skip(1).collect();
if extras.is_empty() {
return Ok(());
}
if extras.len() == 1 && matches!(extras[0].as_str(), "-h" | "--help") {
println!("{usage}");
return Err(String::new());
}
Err(format!(
"{program}: unsupported arguments: {}",
extras.join(" ")
))
}
pub fn describe_usb_device(manufacturer: Option<&str>, product: Option<&str>) -> String {
let mut parts = Vec::new();
if let Some(manufacturer) = manufacturer.filter(|value| !value.is_empty()) {
parts.push(manufacturer);
}
if let Some(product) = product.filter(|value| !value.is_empty()) {
parts.push(product);
}
if parts.is_empty() {
"USB device".to_string()
} else {
parts.join(" ")
}
}
fn load_pci_id_database() -> Option<PciIdDatabase> {
let text = fs::read_to_string(PCI_IDS_PATH).ok()?;
Some(parse_pci_id_database(&text))
}
fn parse_pci_id_database(text: &str) -> PciIdDatabase {
let mut database = PciIdDatabase::default();
let mut current_vendor = None;
for line in text.lines() {
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(rest) = line.strip_prefix("\t\t") {
let _ = rest;
continue;
}
if let Some(rest) = line.strip_prefix('\t') {
let Some(vendor_id) = current_vendor else {
continue;
};
let mut parts = rest.splitn(2, char::is_whitespace).filter(|part| !part.is_empty());
let Some(device_hex) = parts.next() else {
continue;
};
let Some(name) = parts.next() else {
continue;
};
let Ok(device_id) = u16::from_str_radix(device_hex, 16) else {
continue;
};
database
.device_names
.insert((vendor_id, device_id), name.trim().to_string());
continue;
}
let mut parts = line.splitn(2, char::is_whitespace).filter(|part| !part.is_empty());
let Some(vendor_hex) = parts.next() else {
continue;
};
let Some(name) = parts.next() else {
continue;
};
let Ok(vendor_id) = u16::from_str_radix(vendor_hex, 16) else {
continue;
};
current_vendor = Some(vendor_id);
database.vendor_names.insert(vendor_id, name.trim().to_string());
}
database
}
fn pci_id_database() -> Option<&'static PciIdDatabase> {
PCI_ID_DATABASE.get_or_init(load_pci_id_database).as_ref()
}
pub fn lookup_pci_vendor_name(vendor_id: u16) -> Option<String> {
pci_id_database()?.vendor_names.get(&vendor_id).cloned()
}
pub fn lookup_pci_device_name(vendor_id: u16, device_id: u16) -> Option<String> {
pci_id_database()?
.device_names
.get(&(vendor_id, device_id))
.cloned()
}
#[cfg(test)]
mod tests {
use super::{describe_usb_device, parse_args, parse_pci_id_database, parse_pci_location};
// --- parse_pci_location tests ---
#[test]
fn parse_pci_location_valid_input() {
let loc = parse_pci_location("0000--00--1f.2").unwrap();
assert_eq!(loc.segment, 0x0000);
assert_eq!(loc.bus, 0x00);
assert_eq!(loc.device, 0x1f);
assert_eq!(loc.function, 2);
}
#[test]
fn parse_pci_location_scheme_path_format() {
let loc = parse_pci_location("0003--01--0a.3").unwrap();
assert_eq!(loc.scheme_path(), "/scheme/pci/0003--01--0a.3");
}
#[test]
fn parse_pci_location_display_format() {
let loc = parse_pci_location("00ff--02--1c.0").unwrap();
assert_eq!(format!("{loc}"), "00ff:02:1c.0");
}
#[test]
fn parse_pci_location_missing_double_dash_returns_none() {
assert!(parse_pci_location("0000.00--1f.2").is_none());
}
#[test]
fn parse_pci_location_missing_dot_returns_none() {
assert!(parse_pci_location("0000--00--1f-2").is_none());
}
#[test]
fn parse_pci_location_non_hex_segment_returns_none() {
assert!(parse_pci_location("zzzz--00--1f.2").is_none());
}
#[test]
fn parse_pci_location_empty_string_returns_none() {
assert!(parse_pci_location("").is_none());
}
// --- describe_usb_device tests ---
#[test]
fn describe_usb_device_both_fields() {
assert_eq!(
describe_usb_device(Some("Logitech"), Some("USB Mouse")),
"Logitech USB Mouse"
);
}
#[test]
fn describe_usb_device_manufacturer_only() {
assert_eq!(describe_usb_device(Some("Logitech"), None), "Logitech");
}
#[test]
fn describe_usb_device_product_only() {
assert_eq!(describe_usb_device(None, Some("USB Mouse")), "USB Mouse");
}
#[test]
fn describe_usb_device_both_none() {
assert_eq!(describe_usb_device(None, None), "USB device");
}
#[test]
fn describe_usb_device_empty_manufacturer_filtered() {
assert_eq!(describe_usb_device(Some(""), Some("USB Mouse")), "USB Mouse");
}
#[test]
fn describe_usb_device_empty_product_filtered() {
assert_eq!(describe_usb_device(Some("Logitech"), Some("")), "Logitech");
}
// --- parse_args tests ---
#[test]
fn parse_args_empty_extras_returns_ok() {
assert!(parse_args("prog", "usage", vec!["prog".to_string()]).is_ok());
}
#[test]
fn parse_args_help_flag_returns_err_empty() {
let result = parse_args("prog", "usage text", vec!["prog".to_string(), "--help".to_string()]);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "");
}
#[test]
fn parse_args_h_flag_returns_err_empty() {
let result = parse_args("prog", "usage text", vec!["prog".to_string(), "-h".to_string()]);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "");
}
#[test]
fn parse_args_unknown_argument_returns_err_with_message() {
let result = parse_args(
"prog",
"usage text",
vec!["prog".to_string(), "bogus".to_string()],
);
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(msg.contains("unsupported arguments"), "expected 'unsupported arguments' in: {msg}");
}
// --- original pci_id_database tests ---
#[test]
fn parses_vendor_and_device_entries_from_pci_ids() {
let db = parse_pci_id_database(
"8086 Intel Corporation\n\t46A6 Alder Lake-P Integrated Graphics Controller\n1002 Advanced Micro Devices, Inc. [AMD/ATI]\n\t7480 Navi 32 [Radeon RX 7800 XT / 7700 XT]\n",
);
assert_eq!(
db.vendor_names.get(&0x8086).map(String::as_str),
Some("Intel Corporation")
);
assert_eq!(
db.device_names.get(&(0x8086, 0x46A6)).map(String::as_str),
Some("Alder Lake-P Integrated Graphics Controller")
);
assert_eq!(
db.device_names.get(&(0x1002, 0x7480)).map(String::as_str),
Some("Navi 32 [Radeon RX 7800 XT / 7700 XT]")
);
}
#[test]
fn ignores_subsystem_lines_and_comments() {
let db = parse_pci_id_database(
"# comment\n8086 Intel Corporation\n\t46A6 Alder Lake-P Integrated Graphics Controller\n\t\t17AA 3C6A Lenovo variant\n",
);
assert_eq!(db.vendor_names.len(), 1);
assert_eq!(db.device_names.len(), 1);
assert!(db.device_names.get(&(0x17AA, 0x3C6A)).is_none());
}
}