Skip to main content

linsight_plugin_sdk/
pciids.rs

1// SPDX-FileCopyrightText: 2026 VisorCraft LLC
2// SPDX-License-Identifier: GPL-3.0-only
3
4//! Parser for the kernel's `pci.ids` database (`/usr/share/hwdata/
5//! pci.ids` on Arch/Debian/Fedora/SUSE; `/usr/share/misc/pci.ids` on
6//! Debian as an alternative).
7//!
8//! Format:
9//! ```text
10//! vendor_hex  Vendor Name
11//! \tdevice_hex  Device Name
12//! \t\tsubvendor_hex subdevice_hex  Subsystem Name
13//! ```
14//! We only parse the vendor and device levels; subsystem lines are
15//! skipped. Lines starting with `#` and blank lines are ignored.
16
17use std::collections::HashMap;
18use std::path::Path;
19use std::sync::OnceLock;
20
21#[derive(Debug, Default)]
22pub struct PciIdDb {
23    vendors: HashMap<u16, String>,
24    devices: HashMap<(u16, u16), String>,
25}
26
27impl PciIdDb {
28    pub fn parse(text: &str) -> Self {
29        let mut db = Self::default();
30        let mut current_vendor: Option<u16> = None;
31        for line in text.lines() {
32            if line.starts_with('#') || line.trim().is_empty() {
33                continue;
34            }
35            if line.starts_with("\t\t") {
36                continue;
37            }
38            if let Some(rest) = line.strip_prefix('\t') {
39                let Some(vendor) = current_vendor else { continue };
40                let mut parts = rest.splitn(2, char::is_whitespace);
41                let Some(dev_hex) = parts.next() else { continue };
42                let name = parts.next().map(str::trim).unwrap_or("");
43                if let Ok(dev) = u16::from_str_radix(dev_hex, 16) {
44                    db.devices.insert((vendor, dev), name.to_owned());
45                }
46                continue;
47            }
48            let mut parts = line.splitn(2, char::is_whitespace);
49            let Some(vendor_hex) = parts.next() else { continue };
50            let name = parts.next().map(str::trim).unwrap_or("");
51            if let Ok(v) = u16::from_str_radix(vendor_hex, 16) {
52                current_vendor = Some(v);
53                db.vendors.insert(v, name.to_owned());
54            } else {
55                current_vendor = None;
56            }
57        }
58        db
59    }
60
61    pub fn lookup(&self, vendor: u16, device: u16) -> Option<String> {
62        self.devices.get(&(vendor, device)).cloned()
63    }
64
65    pub fn vendor_name(&self, vendor: u16) -> Option<String> {
66        self.vendors.get(&vendor).cloned()
67    }
68
69    /// Load from the canonical path, falling back to the Debian-alternate
70    /// path. Returns an empty DB if neither file exists.
71    pub fn load_default() -> Self {
72        for p in ["/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids"] {
73            if let Ok(text) = std::fs::read_to_string(Path::new(p)) {
74                return Self::parse(&text);
75            }
76        }
77        Self::default()
78    }
79
80    /// Process-wide cached default DB. First call parses; subsequent
81    /// calls return the cached reference. Plugins that don't need PCI
82    /// lookup never trigger the parse.
83    pub fn shared() -> &'static Self {
84        static CELL: OnceLock<PciIdDb> = OnceLock::new();
85        CELL.get_or_init(Self::load_default)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    const FIXTURE: &str = include_str!("../tests/fixtures/mini-pci.ids");
94
95    #[test]
96    fn parses_fixture_into_lookup_table() {
97        let db = PciIdDb::parse(FIXTURE);
98        assert_eq!(db.lookup(0x8086, 0xe223).as_deref(), Some("Battlemage [Arc B-series]"));
99        assert_eq!(db.lookup(0x8086, 0xb0a0).as_deref(), Some("Arrow Lake-S iGPU"));
100        assert_eq!(db.lookup(0x10de, 0x2782).as_deref(), Some("GeForce RTX 4070"));
101    }
102
103    #[test]
104    fn lookup_misses_return_none() {
105        let db = PciIdDb::parse(FIXTURE);
106        assert!(db.lookup(0x8086, 0x0000).is_none());
107        assert!(db.lookup(0xdead, 0xbeef).is_none());
108    }
109
110    #[test]
111    fn vendor_name_lookup() {
112        let db = PciIdDb::parse(FIXTURE);
113        assert_eq!(db.vendor_name(0x8086).as_deref(), Some("Intel Corporation"));
114        assert_eq!(db.vendor_name(0x10de).as_deref(), Some("NVIDIA Corporation"));
115    }
116
117    #[test]
118    fn parse_skips_subdevice_lines() {
119        let s = "8086  Intel\n\te223  Battlemage\n\t\t1234 5678  Some subsystem\n";
120        let db = PciIdDb::parse(s);
121        assert_eq!(db.lookup(0x8086, 0xe223).as_deref(), Some("Battlemage"));
122    }
123}