ndb_oui/
lib.rs

1use anyhow::Result;
2use rangemap::RangeInclusiveMap;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::io::Read;
6
7pub use mac_addr::MacAddr;
8
9pub const CSV_NAME: &str = "oui.csv";
10pub const BIN_NAME: &str = "oui.bin";
11
12/// Represents a single OUI entry
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct OuiEntry {
15    pub mac_prefix: String,
16    pub vendor: String,
17    pub vendor_detail: Option<String>,
18}
19
20/// Represents the OUI database
21pub struct OuiDb {
22    inner: HashMap<String, OuiEntry>,
23    inner_range: RangeInclusiveMap<u64, OuiEntry>,
24}
25
26impl OuiDb {
27    /// Create a new OUI database from a CSV reader
28    pub fn from_csv<R: Read>(reader: R) -> Result<Self, csv::Error> {
29        let mut rdr = csv::Reader::from_reader(reader);
30        let mut map = HashMap::new();
31        let mut range_map: RangeInclusiveMap<u64, OuiEntry> = RangeInclusiveMap::new();
32        for result in rdr.deserialize::<OuiEntry>() {
33            let entry = result?;
34            if let Some((prefix, bits)) = parse_mac_prefix_cidr(&entry.mac_prefix) {
35                let start = mac_to_u64(prefix) & (!0u64 << (48 - bits));
36                let end = start | ((1u64 << (48 - bits)) - 1);
37                range_map.insert(start..=end, entry.clone());
38            } else {
39                map.insert(entry.mac_prefix.clone(), entry);
40            }
41        }
42        Ok(Self {
43            inner: map,
44            inner_range: range_map,
45        })
46    }
47
48    /// Create a new OUI database from a vector of entries
49    pub fn from_entries(entries: Vec<OuiEntry>) -> Self {
50        let mut inner = HashMap::new();
51        let mut inner_range = RangeInclusiveMap::new();
52        for entry in entries {
53            if let Some((prefix, bits)) = parse_mac_prefix_cidr(&entry.mac_prefix) {
54                let start = mac_to_u64(prefix) & (!0u64 << (48 - bits));
55                let end = start | ((1u64 << (48 - bits)) - 1);
56                inner_range.insert(start..=end, entry.clone());
57            } else {
58                inner.insert(entry.mac_prefix.clone(), entry);
59            }
60        }
61        Self { inner, inner_range }
62    }
63
64    /// Create a new OUI database from a binary slice
65    fn from_slice(slice: &[u8]) -> Result<Self> {
66        let (entries, _): (Vec<OuiEntry>, _) =
67            bincode::serde::decode_from_slice(slice, bincode::config::standard())?;
68        Ok(Self::from_entries(entries))
69    }
70
71    /// Create a new OUI database from a bundled file
72    #[cfg(feature = "bundled")]
73    pub fn bundled() -> Self {
74        static BIN_DATA: &[u8] = include_bytes!("../data/oui.bin");
75        Self::from_slice(BIN_DATA).expect("Failed to load bundled oui.bin")
76    }
77
78    /// Get an OUI entry by its MAC prefix.
79    /// Use `lookup` or `lookup_mac` for more flexible lookups
80    pub fn get(&self, prefix: &str) -> Option<&OuiEntry> {
81        self.inner.get(prefix)
82    }
83
84    /// Get all OUI entries as an iterator
85    pub fn all(&self) -> impl Iterator<Item = (&String, &OuiEntry)> {
86        self.inner.iter()
87    }
88
89    /// Lookup from string MAC address (e.g., "ac:4a:56:12:34:56")
90    pub fn lookup(&self, mac_str: &str) -> Option<&OuiEntry> {
91        let mac = MacAddr::from_hex_format(mac_str);
92        self.lookup_mac(&mac)
93    }
94
95    /// Lookup from `MacAddr` instance
96    pub fn lookup_mac(&self, mac: &MacAddr) -> Option<&OuiEntry> {
97        let octets = mac.octets();
98
99        // Range (CIDR) match
100        let mac_u64 = mac_to_u64(octets);
101        if let Some(entry) = self.inner_range.get(&mac_u64) {
102            return Some(entry);
103        }
104
105        // Exact match
106        let key = format!("{:02X}:{:02X}:{:02X}", octets[0], octets[1], octets[2]);
107        self.inner.get(&key)
108    }
109
110    /// Get all entries as a vector
111    pub fn entries(&self) -> Vec<OuiEntry> {
112        self.inner.values().cloned().collect()
113    }
114}
115
116fn parse_mac_prefix_cidr(s: &str) -> Option<([u8; 6], u8)> {
117    let parts: Vec<&str> = s.split('/').collect();
118    if parts.len() != 2 {
119        return None;
120    }
121    let mac = MacAddr::from_hex_format(parts[0]);
122    let bits = parts[1].parse::<u8>().ok()?;
123    Some((mac.octets(), bits))
124}
125
126fn mac_to_u64(mac: [u8; 6]) -> u64 {
127    ((mac[0] as u64) << 40)
128        | ((mac[1] as u64) << 32)
129        | ((mac[2] as u64) << 24)
130        | ((mac[3] as u64) << 16)
131        | ((mac[4] as u64) << 8)
132        | (mac[5] as u64)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_lookup_known_mac_prefix() {
141        let db = OuiDb::bundled();
142        let entry = db.get("AC:4A:56").expect("Known prefix should exist");
143        assert_eq!(entry.vendor, "Cisco");
144        assert!(entry.vendor_detail.is_some());
145    }
146
147    #[test]
148    fn test_lookup_unknown_mac_prefix() {
149        let db = OuiDb::bundled();
150        assert!(db.get("FF:FF:FF").is_none());
151    }
152
153    #[test]
154    fn test_all_contains_entries() {
155        let db = OuiDb::bundled();
156        assert!(db.all().count() > 100);
157    }
158
159    #[test]
160    fn test_lookup_mac_exact() {
161        let db = OuiDb::bundled();
162        let mac = MacAddr::from_hex_format("ac:4a:56:12:34:56");
163        let entry = db.lookup_mac(&mac);
164        assert!(entry.is_some());
165    }
166
167    #[test]
168    fn test_lookup_mac_cidr() {
169        let db = OuiDb::bundled();
170        let mac = MacAddr::from_hex_format("fc:d2:b6:0a:00:00");
171        let entry = db.lookup_mac(&mac);
172        assert!(entry.is_some());
173    }
174
175    #[test]
176    fn test_lookup_mac_str() {
177        let db = OuiDb::bundled();
178        let entry = db.lookup("ac:4a:56:12:34:56");
179        assert!(entry.is_some());
180    }
181}