use super::funejson::Value; 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 = [':', '-', '.'].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| match field { Some(s) => Value::str(s), None => Value::Null, }; let flag = |field: &Option| 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")), } }