rsmanuf 2025.6.1

📇 A very simple Rust library to get the manufacturer of a specific MAC address
Documentation
use regex::Regex;
use std::{collections::BTreeMap, sync::LazyLock};

#[cfg(feature = "online")]
pub mod online;

struct Content {
    data: BTreeMap<String, String>,
    slash_28: BTreeMap<String, String>,
    slash_36: BTreeMap<String, String>,
}

static CONTENT: LazyLock<Content> = LazyLock::new(|| parse_content(include_str!("manuf.txt")));

pub fn lookup(mac: impl Into<String>) -> Result<String, String> {
    let new_mac = mac.into().to_ascii_uppercase().replace("-", ":");

    let regex = Regex::new(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$").unwrap();
    if regex.find(new_mac.as_str()).is_none() {
        return Err(String::from("Invalid MAC address"));
    }

    if let Some(res) = check_slash_28(&new_mac) {
        return Ok(res);
    }
    if let Some(res) = check_slash_36(&new_mac) {
        return Ok(res);
    }
    if let Some(manuf) = CONTENT.data.get(&new_mac) {
        return Ok(manuf.clone());
    }

    let prefix = &new_mac[0..8];
    let end = format!("{}:FF:FF:FF", prefix);
    for (prefix, manuf) in CONTENT.data.range(prefix.to_string()..=end).rev() {
        if new_mac.starts_with(prefix) {
            return Ok(manuf.clone());
        }
    }

    Ok(String::from("unknown"))
}

fn parse_content(source: &str) -> Content {
    let mut data = BTreeMap::<String, String>::new();
    let mut slash_28 = BTreeMap::<String, String>::new();
    let mut slash_36 = BTreeMap::<String, String>::new();

    for line in source.lines() {
        let current_line = line.replace("\t\t", "\t");
        let fields = current_line.split('\t').collect::<Vec<&str>>();

        if fields[0].starts_with("#") || line.is_empty() {
            continue;
        }

        let mac = fields[0].to_string();
        let manuf = fields[1].to_string();
        if mac.contains(":00/28") {
            slash_28.insert(mac.clone(), manuf.clone());
        } else if mac.contains(":00/36") {
            slash_36.insert(mac.clone(), manuf.clone());
        }
        data.insert(mac, manuf);
    }

    Content {
        data,
        slash_28,
        slash_36,
    }
}

fn check_slash_28(mac: &str) -> Option<String> {
    let new_mac = format!("{}0:00:00/28", &mac[..10]);
    CONTENT.slash_28.get(&new_mac).cloned()
}

fn check_slash_36(mac: &str) -> Option<String> {
    let new_mac = format!("{}0:00/36", &mac[..13]);
    CONTENT.slash_36.get(&new_mac).cloned()
}

#[deprecated(since = "2025.2.11", note = "please use `rsmanuf::lookup()` instead")]
#[derive(Debug, Clone)]
pub struct Index {}

#[allow(deprecated)]
impl Index {
    #[allow(clippy::new_without_default)]
    #[deprecated(since = "2025.2.11", note = "please use `rsmanuf::lookup()` instead")]
    pub fn new() -> Self {
        Index {}
    }

    #[deprecated(since = "2025.2.11", note = "please use `rsmanuf::lookup()` instead")]
    pub fn search(&self, mac: impl Into<String>) -> Result<String, String> {
        lookup(mac)
    }
}