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 →