use std::collections::BTreeMap;
use byteorder::{NetworkEndian, ReadBytesExt};
use eui48::MacAddress;
#[derive(Debug, Clone, Default)]
pub struct OuiDb {
db: BTreeMap<(u64, u64), OuiEntry>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct OuiEntry {
pub name_short: String,
pub name_long: Option<String>,
pub comment: Option<String>,
}
impl OuiEntry {
#[must_use]
pub fn short_long_desc(&self) -> (String, String) {
(
self.name_short.clone(),
self.name_long
.clone()
.unwrap_or_else(|| self.name_short.clone())
+ &self
.comment
.as_ref()
.map(|i| " ".to_owned() + i)
.unwrap_or_default(),
)
}
}
impl OuiDb {
pub fn new<'a, T: IntoIterator<Item = &'a str>>(input: T) -> Result<Self, String> {
let mut db = BTreeMap::default();
for line in input {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut parts = line.split('\t');
let prefix = match parts.next() {
None => continue,
Some(prefix) => Self::parse_mac_prefix(prefix)?,
};
let name_short = match parts.next() {
None => return Err("Could not parse name".to_owned()),
Some(i) => i.to_owned(),
};
let name_long = parts.next().map(ToOwned::to_owned);
let comment = parts.next().map(ToOwned::to_owned);
db.insert(
prefix,
OuiEntry {
name_short,
name_long,
comment,
},
);
}
Ok(OuiDb { db })
}
pub fn query(&self, mac: MacAddress) -> Result<Option<OuiEntry>, String> {
let query = Self::mac_to_u64(mac).map_err(|_| "Failed to parse mac_addr".to_owned())?;
let mut result = ((&u64::MIN, &u64::MAX), None);
for ((lo, hi), value) in &self.db {
if (&query >= lo && &query <= hi) && (hi - lo < result.0 .1 - result.0 .0) {
result = ((lo, hi), Some(value));
}
}
return Ok(result.1.cloned());
}
#[must_use]
pub fn mac_description(&self, mac: MacAddress) -> MacDescription {
if mac.is_local() {
MacDescription::Laa
} else if mac.is_broadcast() {
MacDescription::Broadcast
} else if mac.is_multicast() {
MacDescription::Multicast
} else {
match self.query(mac) {
Ok(Some(entry)) => MacDescription::Oui(entry),
_ => MacDescription::Unknown,
}
}
}
#[allow(clippy::from_str_radix_10)]
fn parse_mac_prefix(prefix: &str) -> Result<(u64, u64), String> {
let (oui, mask): (&str, u8) = match prefix.split_once('/') {
None => (prefix, 24),
Some((oui, mask)) => {
let mask = mask
.parse()
.map_err(|e| format!("Failed to parse mask len: {e} @ {prefix}"))?;
if !(8..=48).contains(&mask) {
return Err(format!("incorrect mask value: {mask} @ {prefix}"));
};
(oui, mask)
},
};
let oui = oui.replace([':', '-', '.'], "").to_uppercase();
let oui_int = u64::from_str_radix(&oui, 16)
.map_err(|e| format!("Failed to parse oui {oui} prefix: {e} @ {prefix}"))?;
let oui_start: u64 = if mask == 24 { oui_int << 24 } else { oui_int };
let oui_end: u64 = oui_start | 0xFFFF_FFFF_FFFF >> mask;
return Ok((oui_start, oui_end));
}
fn mac_to_u64(mac: MacAddress) -> Result<u64, String> {
let mac_bytes = mac.to_array();
let padded = [
0,
0,
mac_bytes[0],
mac_bytes[1],
mac_bytes[2],
mac_bytes[3],
mac_bytes[4],
mac_bytes[5],
];
let mut padded_mac = &padded[..8];
let mac_num = padded_mac
.read_u64::<NetworkEndian>()
.map_err(|e| format!("{e}"))?;
Ok(mac_num)
}
}
pub enum MacDescription {
Laa,
Broadcast,
Multicast,
Oui(OuiEntry),
Unknown,
}