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")),
}
}