use std::collections::BTreeMap;
#[derive(Debug, Default, Clone)]
pub struct UsbIdDb {
vendors: BTreeMap<u16, String>,
products: BTreeMap<(u16, u16), String>,
}
impl UsbIdDb {
#[must_use]
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.is_empty() || line.starts_with('#') {
continue;
}
if let Some(rest) = line.strip_prefix('\t') {
if rest.starts_with('\t') {
continue;
}
if let (Some(vid), Some((pid, name))) = (current_vendor, parse_id_line(rest)) {
db.products.insert((vid, pid), name.to_owned());
}
continue;
}
if let Some((vid, name)) = parse_id_line(line) {
db.vendors.insert(vid, name.to_owned());
current_vendor = Some(vid);
} else {
current_vendor = None; }
}
db
}
#[must_use]
pub fn common() -> Self {
let mut db = Self::default();
for v in forensicnomicon_core::usb_vendors::COMMON_USB_VENDORS {
db.vendors.insert(v.vid, v.name.to_owned());
}
db
}
#[must_use]
pub fn vendor_name(&self, vid: u16) -> Option<&str> {
self.vendors.get(&vid).map(String::as_str)
}
#[must_use]
pub fn product_name(&self, vid: u16, pid: u16) -> Option<&str> {
self.products.get(&(vid, pid)).map(String::as_str)
}
#[must_use]
pub fn vendor_count(&self) -> usize {
self.vendors.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.vendors.is_empty()
}
}
fn parse_id_line(s: &str) -> Option<(u16, &str)> {
let id = u16::from_str_radix(s.get(0..4)?, 16).ok()?;
if s.get(4..6)? != " " {
return None;
}
let name = s.get(6..)?.trim_end();
(!name.is_empty()).then_some((id, name))
}
#[cfg(test)]
mod tests {
use super::*;
const FIXTURE: &str = "# comment line\n\
0781 SanDisk Corp.\n\
\t0001 SDDR-05a ImageMate CompactFlash Reader\n\
\t0002 SDDR-31 ImageMate II CompactFlash Reader\n\
0951 Kingston Technology\n\
\t1666 DataTraveler 100 G3/G4\n\
C 00 (Defined at Interface level)\n\
\t01 Audio\n";
#[test]
fn parses_vendor_name() {
let db = UsbIdDb::parse(FIXTURE);
assert_eq!(db.vendor_name(0x0781), Some("SanDisk Corp."));
}
#[test]
fn parses_product_name() {
let db = UsbIdDb::parse(FIXTURE);
assert_eq!(
db.product_name(0x0781, 0x0001),
Some("SDDR-05a ImageMate CompactFlash Reader")
);
}
#[test]
fn unknown_vendor_is_none() {
let db = UsbIdDb::parse(FIXTURE);
assert_eq!(db.vendor_name(0xFFFF), None);
}
#[test]
fn section_products_do_not_leak_to_previous_vendor() {
let db = UsbIdDb::parse(FIXTURE);
assert_eq!(db.product_name(0x0951, 0x0001), None);
assert_eq!(
db.product_name(0x0951, 0x1666),
Some("DataTraveler 100 G3/G4")
);
}
#[test]
fn comment_and_blank_lines_ignored() {
let db = UsbIdDb::parse(FIXTURE);
assert_eq!(db.vendor_count(), 2);
assert!(!db.is_empty());
}
#[test]
fn common_table_resolves_a_known_vendor() {
let db = UsbIdDb::common();
assert_eq!(db.vendor_name(0x0781), Some("SanDisk Corp."));
assert!(db.vendor_count() >= 20);
}
#[test]
fn interface_lines_are_skipped_not_read_as_products() {
let db = UsbIdDb::parse("0781 SanDisk Corp.\n\t5583 Ultra Fit\n\t\t00 Mass Storage\n");
assert_eq!(db.product_name(0x0781, 0x5583), Some("Ultra Fit"));
assert_eq!(db.product_name(0x0781, 0x0000), None);
}
#[test]
fn four_hex_chars_alone_do_not_make_an_id_line() {
let db = UsbIdDb::parse("0781 SanDisk Corp.\n0951\tKingston\n");
assert_eq!(db.vendor_count(), 0);
assert!(db.is_empty());
assert_eq!(db.vendor_name(0x0781), None);
}
#[test]
fn a_wider_separator_is_accepted_and_keeps_the_surplus_in_the_name() {
let db = UsbIdDb::parse("0abc Three Spaces\n");
assert_eq!(db.vendor_count(), 1);
assert_eq!(db.vendor_name(0x0abc), Some(" Three Spaces"));
}
}