use std::{
collections::{HashMap, HashSet},
fs,
path::{Path, PathBuf},
time::SystemTime,
};
use crate::{
MacAddr,
error::{RLanLibError, Result},
oui::{
traits::Oui,
types::{OuiData, OuiDataUrl},
},
};
const DATA_URLS: [OuiDataUrl; 5] = [
OuiDataUrl {
basename: "oui.csv",
url: "https://standards-oui.ieee.org/oui/oui.csv",
},
OuiDataUrl {
basename: "mam.csv",
url: "https://standards-oui.ieee.org/oui28/mam.csv",
},
OuiDataUrl {
basename: "oui36.csv",
url: "https://standards-oui.ieee.org/oui36/oui36.csv",
},
OuiDataUrl {
basename: "cid.csv",
url: "https://standards-oui.ieee.org/cid/cid.csv",
},
OuiDataUrl {
basename: "iab.csv",
url: "https://standards-oui.ieee.org/iab/iab.csv",
},
];
fn clean_string(s: &str) -> String {
s.replace('\u{00A0}', " ").trim().to_string()
}
pub struct OuiDb {
data_dir: PathBuf,
csv_paths: Vec<PathBuf>,
data: HashMap<String, OuiData>,
}
impl OuiDb {
pub fn new(data_dir: &Path) -> Self {
let mut csv_paths = vec![];
for data_url in DATA_URLS {
csv_paths.push(data_dir.join(data_url.basename))
}
Self {
data_dir: data_dir.into(),
csv_paths,
data: HashMap::new(),
}
}
pub fn age(&self) -> Option<SystemTime> {
let mut time = None;
for data_url in DATA_URLS {
let file = self.data_dir.join(data_url.basename);
match fs::metadata(file) {
Ok(f) => {
if let Ok(t) = f.modified() {
if time.is_none() {
time = Some(t);
continue;
}
if let Some(current_t) = time.as_ref()
&& t < *current_t
{
time = Some(t)
}
} else {
return None;
}
}
Err(_e) => return None,
}
}
time
}
pub fn load_data(&mut self) -> Result<()> {
let mut used_ouis = HashSet::new();
for path in &self.csv_paths {
Self::load_csv(&mut self.data, path, &mut used_ouis)?;
}
Ok(())
}
pub fn update(&self) -> Result<()> {
for data_url in DATA_URLS {
let file_path = self.data_dir.join(data_url.basename);
let data = Self::request_oui_data(data_url.url)?;
std::fs::write(&file_path, data).map_err(|e| {
RLanLibError::Oui(format!(
"failed to write oui data: {}: {}",
file_path.display(),
e,
))
})?;
}
Ok(())
}
fn load_csv(
data: &mut HashMap<String, OuiData>,
path: &Path,
used_ouis: &mut HashSet<String>,
) -> Result<()> {
let mut rdr = csv::Reader::from_path(path).map_err(|e| {
RLanLibError::Oui(format!(
"failed to load csv data: {} : {}",
path.display(),
e
))
})?;
for result in rdr.records() {
let record = result.map_err(|e| {
RLanLibError::Oui(format!(
"failed to get csv record: {} : {}",
path.display(),
e
))
})?;
let oui = clean_string(record.get(1).ok_or_else(|| {
RLanLibError::Oui(format!(
"missing OUI field in record: {}",
path.display()
))
})?)
.to_ascii_uppercase();
let organization =
clean_string(record.get(2).ok_or_else(|| {
RLanLibError::Oui(format!(
"missing organization field in record: {}",
path.display()
))
})?);
if used_ouis.contains(&oui) {
log::debug!("Discarding duplicate OUI: {oui}: {organization}");
continue;
}
used_ouis.insert(oui.clone());
data.insert(oui, OuiData { organization });
}
Ok(())
}
fn request_oui_data(url: &str) -> Result<String> {
let data = ureq::get(url)
.header("User-Agent", "Mozilla/5.0 (compatible; r-lanscan)")
.call()
.map_err(|e| {
RLanLibError::Oui(format!(
"failed to request oui data from {url}: {}",
e
))
})?
.body_mut()
.read_to_string()
.map_err(|e| {
RLanLibError::Oui(format!(
"failed to read oui response body from {url}: {}",
e
))
})?;
Ok(data)
}
}
impl Oui for OuiDb {
fn lookup(&self, mac: MacAddr) -> Option<OuiData> {
let mut result: Option<OuiData> = None;
let mac_str =
mac.to_string().to_ascii_uppercase().replace([':', '-'], "");
if mac_str.len() >= 9 {
result = self.data.get(&mac_str[..9]).cloned();
}
if mac_str.len() >= 7 {
result = result.or_else(|| self.data.get(&mac_str[..7]).cloned());
}
if mac_str.len() >= 6 {
result = result.or_else(|| self.data.get(&mac_str[..6]).cloned());
}
result
}
}
#[cfg(test)]
#[path = "./db_tests.rs"]
mod tests;