lspci/
db.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4use anyhow::{Context as _, Error, anyhow};
5use std::collections::HashMap;
6use std::str::Lines;
7
8#[derive(Debug)]
9struct Device<'a> {
10    name: &'a str,
11    device: u16,
12    #[allow(unused)]
13    subvendor: Option<u16>,
14    #[allow(unused)]
15    subdevice: Option<u16>,
16}
17
18#[derive(Debug)]
19struct Vendor<'a> {
20    name: &'a str,
21    devices: Vec<Device<'a>>,
22}
23
24#[derive(Debug)]
25struct SubClass<'a> {
26    name: &'a str,
27    subclass: u8,
28    prog_if: Option<u8>,
29}
30
31#[derive(Debug)]
32struct Class<'a> {
33    name: &'a str,
34    subclasses: Vec<SubClass<'a>>,
35}
36
37#[derive(Debug)]
38pub struct PciDb<'a> {
39    device_db: HashMap<u16, Vendor<'a>>,
40    class_db: HashMap<u8, Class<'a>>,
41}
42
43impl<'a> PciDb<'a> {
44    fn count_tabs(line: &str) -> usize {
45        let mut tabs = 0;
46        let mut chars = line.chars();
47        while chars.next() == Some('\t') {
48            tabs += 1;
49        }
50        tabs
51    }
52
53    // Parses from the input buffer's iterator until the final vendor id is seen.
54    fn parse_devices(iter: &mut Lines<'a>) -> Result<HashMap<u16, Vendor<'a>>, Error> {
55        // The flat text DB is represented as a tree, so we can cache vendor
56        // and device values to avoid excessive key lookups in the map.
57        let mut map: HashMap<u16, Vendor<'a>> = HashMap::new();
58        let mut cached_device: u16 = 0;
59        let mut cached_vendor: Option<&mut Vendor<'a>> = None;
60        for line in iter {
61            let trimmed = line.trim();
62            let len = trimmed.len();
63            // Newlines and comments
64            if trimmed.is_empty() || line.starts_with('#') {
65                continue;
66            }
67
68            // Indent level determines the type of node we are working with.
69            match PciDb::count_tabs(line) {
70                0 => {
71                    // vendor vendor_name
72                    // ex: 0014 Loongson Technology LLC
73                    let vendor = u16::from_str_radix(&trimmed[0..4], 16)?;
74                    let name = &trimmed[6..len];
75                    map.insert(vendor, Vendor { name, devices: Vec::new() });
76                    if vendor == 0xffff {
77                        break;
78                    }
79                    cached_vendor = map.get_mut(&vendor);
80                }
81                1 => {
82                    // device  device_name
83                    // ex: \t0014  Loongson Technology LLC
84                    cached_device = u16::from_str_radix(&trimmed[0..4], 16)?;
85                    let name = &trimmed[6..len];
86                    let v = cached_vendor.unwrap();
87                    v.devices.push(Device {
88                        name,
89                        device: cached_device,
90                        subvendor: None,
91                        subdevice: None,
92                    });
93                    cached_vendor = Some(v);
94                }
95                2 => {
96                    // subvendor subdevice  subsystem_name
97                    // ex: \t\t001c 0004  2 Channel CAN Bus SJC1000
98                    let subvendor = u16::from_str_radix(&trimmed[0..4], 16);
99                    let subdevice = u16::from_str_radix(&trimmed[5..9], 16);
100                    // Devices with subvendor/subdevice information have an
101                    // extra space, but those without do not.
102                    let name = &trimmed[10..len].trim();
103
104                    let v = cached_vendor.unwrap();
105                    v.devices.push(Device {
106                        name,
107                        device: cached_device,
108                        subvendor: subvendor.ok(),
109                        subdevice: subdevice.ok(),
110                    });
111                    cached_vendor = Some(v);
112                }
113                _ => return Err(anyhow!("Invalid line in db: \"{}\"", line)),
114            }
115        }
116
117        Ok(map)
118    }
119
120    // Parses the class information out of the iterator, parsing is in a similar format
121    // to the vendor:device information, but simpler.
122    fn parse_classes(iter: &mut Lines<'a>) -> Result<HashMap<u8, Class<'a>>, Error> {
123        let mut map: HashMap<u8, Class<'a>> = HashMap::new();
124        let mut cached_subclass: u8 = 0;
125        let mut cached_class: Option<&mut Class<'a>> = None;
126        for line in iter {
127            if line.is_empty() || line.starts_with('#') {
128                continue;
129            }
130
131            let len = line.len();
132            if line.starts_with('C') {
133                let class =
134                    u8::from_str_radix(&line[2..4], 16).context(format!("'{}'", &line[2..4]))?;
135                let name = &line[6..len];
136                map.insert(class, Class { name, subclasses: Vec::new() });
137                cached_class = map.get_mut(&class);
138            } else {
139                match PciDb::count_tabs(line) {
140                    1 => {
141                        cached_subclass = u8::from_str_radix(&line[1..3], 16)
142                            .context(format!("'{}'", &line[1..3]))?;
143                        let name = &line[5..len];
144                        let c = cached_class.unwrap();
145                        c.subclasses.push(SubClass {
146                            name,
147                            subclass: cached_subclass,
148                            prog_if: None,
149                        });
150                        cached_class = Some(c);
151                    }
152                    2 => {
153                        let prog_if = u8::from_str_radix(&line[2..4], 16)
154                            .context(format!("'{}'", &line[2..4]))?;
155                        let name = &line[6..len];
156                        let c = cached_class.unwrap();
157                        c.subclasses.push(SubClass {
158                            name,
159                            subclass: cached_subclass,
160                            prog_if: Some(prog_if),
161                        });
162                        cached_class = Some(c);
163                    }
164
165                    _ => return Err(anyhow!("Invalid line in db: \"{}\"", line)),
166                }
167            }
168        }
169        Ok(map)
170    }
171
172    pub fn new(id_buffer: &'a str) -> Result<Self, Error> {
173        let mut iter = id_buffer.lines();
174        let device_db = PciDb::parse_devices(&mut iter)?;
175        let class_db = PciDb::parse_classes(&mut iter)?;
176        Ok(PciDb { device_db, class_db })
177    }
178
179    pub fn find_device(&self, vendor: u16, device: u16) -> Option<String> {
180        if let Some(v_entry) = self.device_db.get(&vendor) {
181            let mut name = v_entry.name.to_string();
182            // The ID database sorts devices in a manner like:
183            // 1000  Broadcom / LSI
184            //        0001  53c810
185            //        000c  53c895
186            //               1000 1010  LSI8951U PCI to Ultra2 SCSI host adapter
187            //               1000 1020  LSI8952U PCI to Ultra2 SCSI host adapter
188            //               1de1 3906  DC-390U2B SCSI adapter
189            //               1de1 3907  DC-390U2W
190            for dev in &v_entry.devices {
191                if device == dev.device {
192                    name.push(' ');
193                    name.push_str(&dev.name);
194                    break;
195                }
196            }
197            return Some(name);
198        }
199        None
200    }
201
202    pub fn find_class(&self, class: u8, subclass: u8, prog_if: Option<u8>) -> Option<String> {
203        if let Some(c_entry) = self.class_db.get(&class) {
204            let mut name = c_entry.name.to_string();
205            for sub in &c_entry.subclasses {
206                if subclass == sub.subclass {
207                    if sub.prog_if == None {
208                        // We've found the basic subclass name, so replace the
209                        // base class name we found. A subclass without a program
210                        // interface is always the more general class name.
211                        name = sub.name.to_string();
212                    } else if prog_if.is_some() && prog_if == sub.prog_if {
213                        // If we find a matching program interface then append
214                        // it to the subclass name found above to get the more
215                        // specific name.
216                        name.push_str(&format!(" ({})", sub.name));
217                    }
218                }
219            }
220            // At the very least we have a base class name. We may also have a subclass name.
221            return Some(name);
222        }
223        None
224    }
225}
226
227#[cfg(test)]
228mod test {
229    use super::*;
230    use std::fs;
231    use std::sync::LazyLock;
232
233    static DB_BUF: LazyLock<String> = LazyLock::new(|| {
234        fs::read_to_string("/pkg/data/lspci/pci.ids").context("Couldn't open PCI IDs file").unwrap()
235    });
236    static DB: LazyLock<PciDb<'static>> =
237        LazyLock::new(|| PciDb::new(&DB_BUF).context("Couldnt parse database buffer").unwrap());
238
239    #[test]
240    fn vendor_without_devices() -> Result<(), Error> {
241        assert_eq!("SafeNet (wrong ID)", DB.find_device(0x0001, 0).unwrap());
242        // This vendor has no devices, does it still work if we specify one?
243        assert_eq!("SafeNet (wrong ID)", DB.find_device(0x0001, 0xFFFF).unwrap());
244        Ok(())
245    }
246
247    #[test]
248    fn general_device_and_subdevices() -> Result<(), Error> {
249        assert_eq!("PEAK-System Technik GmbH", DB.find_device(0x001c, 0).unwrap());
250        assert_eq!(
251            "PEAK-System Technik GmbH PCAN-PCI CAN-Bus controller",
252            DB.find_device(0x001c, 0x001).unwrap()
253        );
254        Ok(())
255    }
256
257    #[test]
258    fn device_not_found() -> Result<(), Error> {
259        // The device ID matches an Allied Telesis, Inc device, but the vendor is invalid.
260        assert_eq!(None, DB.find_device(0x0000, 0x8139));
261        // Vendor is LevelOne, but invalid device ID
262        assert_eq!("LevelOne", DB.find_device(0x018a, 0x1000).unwrap());
263        // Valid LevelOne FPC-0106TX
264        assert_eq!(
265            "LevelOne FPC-0106TX misprogrammed [RTL81xx]",
266            DB.find_device(0x018a, 0x0106).unwrap(),
267        );
268        Ok(())
269    }
270
271    struct ClassTest<'a> {
272        e: &'a str,
273        c: u8,
274        s: u8,
275        p: Option<u8>,
276    }
277
278    #[test]
279    fn class_lookup() -> Result<(), Error> {
280        let tests = vec![
281            ClassTest { e: "Bridge", c: 0x06, s: 0xFF, p: None },
282            ClassTest { e: "Bridge", c: 0x06, s: 0xFF, p: Some(0xFF) },
283            ClassTest { e: "Bridge", c: 0x06, s: 0x80, p: None },
284            ClassTest { e: "Host bridge", c: 0x06, s: 0x00, p: None },
285            ClassTest { e: "ISA bridge", c: 0x06, s: 0x01, p: None },
286            ClassTest { e: "EISA bridge", c: 0x06, s: 0x02, p: None },
287            ClassTest { e: "PCI bridge", c: 0x06, s: 0x04, p: None },
288            ClassTest { e: "PCI bridge (Normal decode)", c: 0x06, s: 0x04, p: Some(0x00) },
289            ClassTest { e: "PCI bridge (Subtractive decode)", c: 0x06, s: 0x04, p: Some(0x01) },
290        ];
291
292        assert_eq!(None, DB.find_class(0xEF, 0x00, None));
293        for test in &tests {
294            assert_eq!(test.e, DB.find_class(test.c, test.s, test.p).unwrap());
295        }
296        Ok(())
297    }
298}