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::{anyhow, Context as _, Error};
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 lazy_static::lazy_static;
231    use std::fs;
232
233    lazy_static! {
234        static ref DB_BUF: String = fs::read_to_string("/pkg/data/lspci/pci.ids")
235            .context("Couldn't open PCI IDs file")
236            .unwrap();
237        static ref DB: PciDb<'static> =
238            PciDb::new(&DB_BUF).context("Couldnt parse database buffer").unwrap();
239    }
240
241    #[test]
242    fn vendor_without_devices() -> Result<(), Error> {
243        assert_eq!("SafeNet (wrong ID)", DB.find_device(0x0001, 0).unwrap());
244        // This vendor has no devices, does it still work if we specify one?
245        assert_eq!("SafeNet (wrong ID)", DB.find_device(0x0001, 0xFFFF).unwrap());
246        Ok(())
247    }
248
249    #[test]
250    fn general_device_and_subdevices() -> Result<(), Error> {
251        assert_eq!("PEAK-System Technik GmbH", DB.find_device(0x001c, 0).unwrap());
252        assert_eq!(
253            "PEAK-System Technik GmbH PCAN-PCI CAN-Bus controller",
254            DB.find_device(0x001c, 0x001).unwrap()
255        );
256        Ok(())
257    }
258
259    #[test]
260    fn device_not_found() -> Result<(), Error> {
261        // The device ID matches an Allied Telesis, Inc device, but the vendor is invalid.
262        assert_eq!(None, DB.find_device(0x0000, 0x8139));
263        // Vendor is LevelOne, but invalid device ID
264        assert_eq!("LevelOne", DB.find_device(0x018a, 0x1000).unwrap());
265        // Valid LevelOne FPC-0106TX
266        assert_eq!(
267            "LevelOne FPC-0106TX misprogrammed [RTL81xx]",
268            DB.find_device(0x018a, 0x0106).unwrap(),
269        );
270        Ok(())
271    }
272
273    struct ClassTest<'a> {
274        e: &'a str,
275        c: u8,
276        s: u8,
277        p: Option<u8>,
278    }
279
280    #[test]
281    fn class_lookup() -> Result<(), Error> {
282        let tests = vec![
283            ClassTest { e: "Bridge", c: 0x06, s: 0xFF, p: None },
284            ClassTest { e: "Bridge", c: 0x06, s: 0xFF, p: Some(0xFF) },
285            ClassTest { e: "Bridge", c: 0x06, s: 0x80, p: None },
286            ClassTest { e: "Host bridge", c: 0x06, s: 0x00, p: None },
287            ClassTest { e: "ISA bridge", c: 0x06, s: 0x01, p: None },
288            ClassTest { e: "EISA bridge", c: 0x06, s: 0x02, p: None },
289            ClassTest { e: "PCI bridge", c: 0x06, s: 0x04, p: None },
290            ClassTest { e: "PCI bridge (Normal decode)", c: 0x06, s: 0x04, p: Some(0x00) },
291            ClassTest { e: "PCI bridge (Subtractive decode)", c: 0x06, s: 0x04, p: Some(0x01) },
292        ];
293
294        assert_eq!(None, DB.find_class(0xEF, 0x00, None));
295        for test in &tests {
296            assert_eq!(test.e, DB.find_class(test.c, test.s, test.p).unwrap());
297        }
298        Ok(())
299    }
300}