Skip to content

MAC Address Generator — Rust source

Generate random EUI-48 MAC addresses with a chosen separator, optional OUI prefix, uppercase formatting, and a locally-administered flag. Runs entirely in your browser with crypto-grade randomness.

This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! mac-address-generator — random EUI-48 MAC address generator.
//!
//! Language: Rust (edition 2021, standard library only)
//! Source:   CosmoDev polyglot showcase port of the MAC Address Generator tool,
//!           ported from src/lib/mac-generator.ts (the canonical TypeScript
//!           implementation) and kept in lock-step with the Go twin at
//!           cli/mac-address-generator/mac-address-generator.go.
//! License:  display source — part of CosmoDev's polyglot tool pages.
//!
//! Design goals:
//!   - Pure + deterministic; never panics (public API returns `Result`, no panic).
//!   - Functionally equivalent to the TS reference: same inputs -> same outputs.
//!   - Self-contained: std only (no crates.io dependencies — no `rand` crate).
//!
//! API note: this port mirrors the TS canonical lib's public API, which is the
//! deterministic, RNG-injectable superset of the Go twin's. The Go twin draws
//! from crypto/rand directly (non-injectable), so its own suite asserts only
//! structural properties (regex shape, OUI prefix, U/L bit). Like the TS lib,
//! this port accepts an injected `rng` returning a `f64` in [0, 1), which lets
//! the showcase tests below assert exact MAC strings.
//!
//! stdlib RNG note: Rust's standard library ships no random-number generator
//! (the `rand` / `getrandom` crates do, but the brief forbids external deps).
//! The default RNG here therefore reads /dev/urandom via `std::fs` + `std::io`
//! (Unix; the production web/Go twins use the platform CSPRNG). Inject your own
//! `rng` for deterministic output — exactly as the TS tests do.

use std::fmt;

/// Error returned by [`generate`]. Never panics; the only failure mode is a
/// malformed OUI (not exactly 6 hex digits after stripping non-hex chars).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MacError {
    /// The OUI did not reduce to exactly 6 hex digits. Carries the raw input.
    BadOui(String),
}

impl fmt::Display for MacError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MacError::BadOui(raw) => write!(f, "OUI must be 6 hex digits, got {:?}", raw),
        }
    }
}

impl std::error::Error for MacError {}

/// Options mirror the TS `MacOptions`. Every field has a sensible default, so
/// callers can build it incrementally with `..Default::default()`.
///
/// Only `Default` is derived: the `rng` field holds a `Box<dyn Fn() -> f64>`,
/// which is neither `Clone` nor `Debug` (closures aren't). `generate` takes
/// `&Options`, so `Clone` is never needed.
#[derive(Default)]
pub struct Options {
    /// Joining chars. `None` -> default `:`. `Some("")` concatenates the octets.
    pub separator: Option<String>,
    /// Uppercase the hex letters in the result.
    pub uppercase: bool,
    /// Optional 6-hex-digit OUI prefix (separators tolerated, stripped first).
    pub oui: Option<String>,
    /// Force the U/L bit (0x02) of the first octet (locally-administered).
    pub locally_administered: bool,
    /// Random source returning a `f64` in [0, 1). `None` -> default /dev/urandom.
    pub rng: Option<Box<dyn Fn() -> f64>>,
}

/// Mirrors TS's `oui.replace(/[^0-9a-fA-F]/g, '')`: keep only hex digits.
fn strip_non_hex(s: &str) -> String {
    s.chars().filter(|c| c.is_ascii_hexdigit()).collect()
}

/// Build a random EUI-48 MAC address. The Rust twin of `generateMac()` in
/// src/lib/mac-generator.ts; agrees with the Go twin on every shared vector.
///
/// Pipeline mirrors the TS lib exactly:
///   1. if an OUI is supplied, strip non-hex chars and require exactly 6 hex
///      digits for the first 3 octets (else return `MacError::BadOui`);
///      otherwise draw 3 random octets (each `floor(rng() * 256)`).
///   2. always draw 3 more random octets from the same rng.
///   3. when `locally_administered` is set, force the U/L bit:
///      `octets[0] = (octets[0] & 0xFD) | 0x02`.
///   4. format the 6 octets as `%02x` pairs joined by the separator; uppercase
///      the whole string if requested.
pub fn generate(opts: &Options) -> Result<String, MacError> {
    let separator = opts.separator.as_deref().unwrap_or(":");
    let rng = opts.rng.as_deref();

    let mut octets: [u8; 6] = [0; 6];

    // Octets 0..3 — OUI (validated) or random.
    if let Some(oui) = &opts.oui {
        let clean = strip_non_hex(oui);
        if clean.len() != 6 {
            return Err(MacError::BadOui(oui.clone()));
        }
        for i in 0..3 {
            octets[i] = match u8::from_str_radix(&clean[i * 2..i * 2 + 2], 16) {
                Ok(v) => v,
                // Unreachable after strip_non_hex + len==6, but stay panic-free.
                Err(_) => return Err(MacError::BadOui(oui.clone())),
            };
        }
    } else {
        for i in 0..3 {
            octets[i] = random_octet(rng);
        }
    }

    // Octets 3..6 — always random.
    for i in 3..6 {
        octets[i] = random_octet(rng);
    }

    if opts.locally_administered {
        octets[0] = (octets[0] & 0xFD) | 0x02; // set U/L bit (0x02), clear multicast
    }

    let parts: Vec<String> = octets.iter().map(|b| format!("{:02x}", b)).collect();
    let mut mac = parts.join(separator);
    if opts.uppercase {
        mac = mac.to_uppercase();
    }
    Ok(mac)
}

/// Convenience wrapper using default options (colon separator, lowercase).
pub fn generate_default() -> Result<String, MacError> {
    generate(&Options::default())
}

/// Draw one octet via the injected rng, or fall back to the default CSPRNG.
/// `floor(rng() * 256)` mirrors `Math.floor(rng() * 256)` in the TS lib.
fn random_octet(rng: Option<&dyn Fn() -> f64>) -> u8 {
    match rng {
        Some(f) => (f() * 256.0).floor() as u8,
        None => default_random_octet(),
    }
}

/// Default CSPRNG octet from /dev/urandom (Unix). stdlib-only: no `rand` crate.
/// On platforms where /dev/urandom is unavailable this returns 0 — the public
/// API stays panic-free regardless of platform (production web/Go twins always
/// have a platform CSPRNG; inject `rng` in `Options` to bypass this path).
fn default_random_octet() -> u8 {
    use std::io::Read;
    let mut buf = [0u8; 1];
    if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
        if f.read_exact(&mut buf).is_ok() {
            return buf[0];
        }
    }
    0
}

// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn deterministic_zero_rng() {
        // rng=0 -> floor(0*256)=0 for every octet.
        let opts = Options { rng: Some(Box::new(|| 0.0)), ..Default::default() };
        assert_eq!(generate(&opts).unwrap(), "00:00:00:00:00:00");
    }

    #[test]
    fn uppercase_and_separators() {
        // floor(0.7 * 256) == 179 == 0xb3.
        let up = Options { rng: Some(Box::new(|| 0.7)), uppercase: true, ..Default::default() };
        assert_eq!(generate(&up).unwrap(), "B3:B3:B3:B3:B3:B3");
        // Separators: '.', and '' (concatenate).
        let dot = Options {
            rng: Some(Box::new(|| 0.0)),
            separator: Some(".".into()),
            ..Default::default()
        };
        assert_eq!(generate(&dot).unwrap(), "00.00.00.00.00.00");
        let cat = Options {
            rng: Some(Box::new(|| 0.0)),
            separator: Some(String::new()),
            ..Default::default()
        };
        assert_eq!(generate(&cat).unwrap(), "000000000000");
    }

    #[test]
    fn oui_prefix() {
        let opts = Options {
            rng: Some(Box::new(|| 0.0)),
            oui: Some("aabbcc".into()),
            ..Default::default()
        };
        assert_eq!(generate(&opts).unwrap(), "aa:bb:cc:00:00:00");
        // Separators embedded in the OUI are stripped first.
        let opts2 = Options {
            rng: Some(Box::new(|| 0.0)),
            oui: Some("aa-bb-cc".into()),
            ..Default::default()
        };
        assert_eq!(generate(&opts2).unwrap(), "aa:bb:cc:00:00:00");
    }

    #[test]
    fn locally_administered_bit() {
        // 0x00 with U/L bit set -> 0x02.
        let opts = Options {
            rng: Some(Box::new(|| 0.0)),
            locally_administered: true,
            ..Default::default()
        };
        assert_eq!(generate(&opts).unwrap(), "02:00:00:00:00:00");
        // floor(0.5 * 256) == 128 == 0x80; U/L set -> (0x80 & 0xFD) | 0x02 = 0x82.
        let opts2 = Options {
            rng: Some(Box::new(|| 0.5)),
            locally_administered: true,
            ..Default::default()
        };
        assert_eq!(generate(&opts2).unwrap(), "82:80:80:80:80:80");
    }

    #[test]
    fn malformed_oui_is_an_error() {
        let short = Options {
            rng: Some(Box::new(|| 0.0)),
            oui: Some("abc".into()),
            ..Default::default()
        };
        assert_eq!(generate(&short), Err(MacError::BadOui("abc".into())));
        let long = Options {
            rng: Some(Box::new(|| 0.0)),
            oui: Some("aabbccdd".into()),
            ..Default::default()
        };
        assert!(generate(&long).is_err());
    }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →