Functional Weave
Code in Rust

net.mac-address@1.0.0

impl/rust.rs

3,235 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

fn invalid(reason: &str) -> MacValidation {
    MacValidation {
        valid: false,
        canonical: None,
        reason: Some(reason.to_string()),
        multicast: None,
        locally_administered: None,
        oui: None,
    }
}

/// Check a MAC (EUI-48) address written in any of the four common forms and
/// normalise it to lower-case colon form.
///
/// Exactly four shapes are accepted: aa:bb:cc:dd:ee:ff, aa-bb-cc-dd-ee-ff,
/// aabb.ccdd.eeff (Cisco) and aabbccddeeff. Nothing is trimmed and single-digit
/// octets are refused, because "a:b:c:d:e:f" is more often a typo than a MAC.
pub fn validate_mac_address(text: &str) -> MacValidation {
    if text.is_empty() {
        return invalid("empty");
    }
    let seps: Vec<char> = [':', '-', '.'].iter().copied().filter(|s| text.contains(*s)).collect();
    if seps.len() > 1 {
        return invalid("mixed separators");
    }

    // Count characters, not bytes, so a non-ASCII digit fails as "not a hex
    // digit" here just as it does in TypeScript and Python.
    let parts: Vec<&str> = match seps.first() {
        None => vec![text],
        Some(sep) => text.split(*sep).collect(),
    };
    let chars = |s: &str| s.chars().count();
    let shape_ok = match seps.first() {
        None => chars(text) == 12,
        Some('.') => parts.len() == 3 && parts.iter().all(|p| chars(p) == 4),
        Some(_) => parts.len() == 6 && parts.iter().all(|p| chars(p) == 2),
    };
    if !shape_ok {
        return invalid("not a recognised MAC address format");
    }

    let hex: String = parts.concat();
    if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
        return invalid("not a hex digit");
    }

    let lower = hex.to_ascii_lowercase();
    let octets: Vec<&str> = (0..6).map(|i| &lower[i * 2..i * 2 + 2]).collect();
    let first = u8::from_str_radix(octets[0], 16).unwrap();
    MacValidation {
        valid: true,
        canonical: Some(octets.join(":")),
        reason: None,
        // The I/G bit is the least significant bit of the first octet, the U/L bit the next.
        multicast: Some(first & 1 == 1),
        locally_administered: Some(first & 2 == 2),
        oui: Some(octets[..3].join("-").to_ascii_uppercase()),
    }
}

/// Object keys are camelCase to match the shared vectors.
pub fn mac_validation_to_value(result: &MacValidation) -> Value {
    let text = |field: &Option<String>| match field {
        Some(s) => Value::str(s),
        None => Value::Null,
    };
    let flag = |field: &Option<bool>| match field {
        Some(b) => Value::Bool(*b),
        None => Value::Null,
    };
    Value::obj(vec![
        ("valid", Value::Bool(result.valid)),
        ("canonical", text(&result.canonical)),
        ("reason", text(&result.reason)),
        ("multicast", flag(&result.multicast)),
        ("locallyAdministered", flag(&result.locally_administered)),
        ("oui", text(&result.oui)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    // A non-string is not a MAC address, as TypeScript and Python answer.
    match &args[0] {
        Value::Str(s) => mac_validation_to_value(&validate_mac_address(s)),
        _ => mac_validation_to_value(&invalid("not text")),
    }
}