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#[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
20pub struct OuiDb {
22 inner: HashMap<String, OuiEntry>,
23 inner_range: RangeInclusiveMap<u64, OuiEntry>,
24}
25
26impl OuiDb {
27 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 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 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 #[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 pub fn get(&self, prefix: &str) -> Option<&OuiEntry> {
81 self.inner.get(prefix)
82 }
83
84 pub fn all(&self) -> impl Iterator<Item = (&String, &OuiEntry)> {
86 self.inner.iter()
87 }
88
89 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 pub fn lookup_mac(&self, mac: &MacAddr) -> Option<&OuiEntry> {
97 let octets = mac.octets();
98
99 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 let key = format!("{:02X}:{:02X}:{:02X}", octets[0], octets[1], octets[2]);
107 self.inner.get(&key)
108 }
109
110 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}