use std::collections::HashMap;
use std::path::Path;
use std::sync::OnceLock;
#[derive(Debug, Default)]
pub struct PciIdDb {
vendors: HashMap<u16, String>,
devices: HashMap<(u16, u16), String>,
}
impl PciIdDb {
pub fn parse(text: &str) -> Self {
let mut db = Self::default();
let mut current_vendor: Option<u16> = None;
for line in text.lines() {
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
if line.starts_with("\t\t") {
continue;
}
if let Some(rest) = line.strip_prefix('\t') {
let Some(vendor) = current_vendor else { continue };
let mut parts = rest.splitn(2, char::is_whitespace);
let Some(dev_hex) = parts.next() else { continue };
let name = parts.next().map(str::trim).unwrap_or("");
if let Ok(dev) = u16::from_str_radix(dev_hex, 16) {
db.devices.insert((vendor, dev), name.to_owned());
}
continue;
}
let mut parts = line.splitn(2, char::is_whitespace);
let Some(vendor_hex) = parts.next() else { continue };
let name = parts.next().map(str::trim).unwrap_or("");
if let Ok(v) = u16::from_str_radix(vendor_hex, 16) {
current_vendor = Some(v);
db.vendors.insert(v, name.to_owned());
} else {
current_vendor = None;
}
}
db
}
pub fn lookup(&self, vendor: u16, device: u16) -> Option<String> {
self.devices.get(&(vendor, device)).cloned()
}
pub fn vendor_name(&self, vendor: u16) -> Option<String> {
self.vendors.get(&vendor).cloned()
}
pub fn load_default() -> Self {
for p in ["/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids"] {
if let Ok(text) = std::fs::read_to_string(Path::new(p)) {
return Self::parse(&text);
}
}
Self::default()
}
pub fn shared() -> &'static Self {
static CELL: OnceLock<PciIdDb> = OnceLock::new();
CELL.get_or_init(Self::load_default)
}
}
#[cfg(test)]
mod tests {
use super::*;
const FIXTURE: &str = include_str!("../tests/fixtures/mini-pci.ids");
#[test]
fn parses_fixture_into_lookup_table() {
let db = PciIdDb::parse(FIXTURE);
assert_eq!(db.lookup(0x8086, 0xe223).as_deref(), Some("Battlemage [Arc B-series]"));
assert_eq!(db.lookup(0x8086, 0xb0a0).as_deref(), Some("Arrow Lake-S iGPU"));
assert_eq!(db.lookup(0x10de, 0x2782).as_deref(), Some("GeForce RTX 4070"));
}
#[test]
fn lookup_misses_return_none() {
let db = PciIdDb::parse(FIXTURE);
assert!(db.lookup(0x8086, 0x0000).is_none());
assert!(db.lookup(0xdead, 0xbeef).is_none());
}
#[test]
fn vendor_name_lookup() {
let db = PciIdDb::parse(FIXTURE);
assert_eq!(db.vendor_name(0x8086).as_deref(), Some("Intel Corporation"));
assert_eq!(db.vendor_name(0x10de).as_deref(), Some("NVIDIA Corporation"));
}
#[test]
fn parse_skips_subdevice_lines() {
let s = "8086 Intel\n\te223 Battlemage\n\t\t1234 5678 Some subsystem\n";
let db = PciIdDb::parse(s);
assert_eq!(db.lookup(0x8086, 0xe223).as_deref(), Some("Battlemage"));
}
}