Functional Weave
Code in Rust

net.device-validate@1.0.0

impl/rust.rs

6,750 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
use super::net_cidr::{cidr_info, is_cidr};  ← from net.cidr ^1.0.0 · built alongside by fune
use super::net_hostname_validate::validate_hostname;  ← from net.hostname-validate ^1.0.0 · built alongside by fune
use super::net_ipv4::is_ipv4;  ← from net.ipv4 ^1.0.0 · built alongside by fune
use super::net_ipv6::{format_ipv6, is_ipv6, parse_ipv6};  ← from net.ipv6 ^1.0.0 · built alongside by fune
use super::net_mac_address::validate_mac_address;  ← from net.mac-address ^1.0.0 · built alongside by fune
use super::net_port::is_port;  ← from net.port ^1.0.0 · built alongside by fune

const KNOWN: [&str; 5] = ["name", "host", "ports", "mac", "subnet"];

/// A whole number, whichever way the JSON reader stored it (22 and 22.0 are the same port).
fn whole_number(value: &Value) -> Option<i64> {
    match value {
        Value::Int(i) => Some(*i),
        Value::Float(f) if f.is_finite() && f.fract() == 0.0 && f.abs() < 9.0e15 => Some(*f as i64),
        _ => None,
    }
}

/// An IPv6 block with its host bits cleared and its address in RFC 5952 form.
fn canonical_ipv6_block(text: &str) -> String {
    let (address, prefix_text) = text.split_once('/').unwrap_or((text, "128"));
    let prefix: i64 = prefix_text.parse().unwrap_or(128);
    let groups: Vec<i64> = parse_ipv6(address)
        .iter()
        .enumerate()
        .map(|(i, g)| {
            let bits = (prefix - 16 * i as i64).clamp(0, 16);
            if bits == 0 {
                0
            } else {
                g & ((0xffff_i64 << (16 - bits)) & 0xffff)
            }
        })
        .collect();
    format!("{}/{}", format_ipv6(&groups), prefix)
}

/// Check one inventory entry and say everything that is wrong with it at once,
/// so a person fixing a file does not play whack-a-mole one error per run.
/// Never panics: a bad entry is a result, not a crash.
pub fn validate_device(entry: &Value) -> DeviceValidation {
    let mut errors: Vec<String> = Vec::new();
    let empty: Vec<(String, Value)> = Vec::new();
    let pairs = match entry {
        Value::Obj(pairs) => pairs,
        _ => &empty,
    };
    let field = |key: &str| -> Option<&Value> {
        pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v).filter(|v| !v.is_null())
    };

    let mut name: Option<String> = None;
    match field("name") {
        None => errors.push("name is required".to_string()),
        Some(Value::Str(s)) => {
            let trimmed = s.trim_matches(|c: char| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0c' | '\x0b'));
            if trimmed.is_empty() {
                errors.push("name must not be empty".to_string());
            } else {
                name = Some(trimmed.to_string());
            }
        }
        Some(_) => errors.push("name must be text".to_string()),
    }

    let mut host: Option<String> = None;
    let mut host_kind: Option<String> = None;
    match field("host") {
        None => errors.push("host is required".to_string()),
        Some(Value::Str(s)) => {
            if is_ipv4(s) {
                host = Some(s.clone());
                host_kind = Some("ipv4".to_string());
            } else if is_ipv6(s) {
                host = Some(format_ipv6(&parse_ipv6(s)));
                host_kind = Some("ipv6".to_string());
            } else {
                let check = validate_hostname(s);
                if check.valid {
                    host = check.hostname;
                    host_kind = Some("hostname".to_string());
                } else {
                    errors.push(format!(
                        "host \"{}\" is not an IPv4 address, IPv6 address or host name: {}",
                        s,
                        check.reason.unwrap_or_default()
                    ));
                }
            }
        }
        Some(_) => errors.push("host must be text".to_string()),
    }

    let mut ports: Vec<i64> = Vec::new();
    match field("ports") {
        None => errors.push("ports is required".to_string()),
        Some(Value::Arr(items)) => {
            if items.is_empty() {
                errors.push("ports must not be empty".to_string());
            }
            for (i, item) in items.iter().enumerate() {
                match whole_number(item) {
                    None => errors.push(format!("ports[{}] must be a whole number", i)),
                    Some(port) if !is_port(port) => {
                        errors.push(format!("ports[{}] {} is not a port (1-65535)", i, port))
                    }
                    Some(port) if ports.contains(&port) => {
                        errors.push(format!("ports[{}] {} is listed twice", i, port))
                    }
                    Some(port) => ports.push(port),
                }
            }
        }
        Some(_) => errors.push("ports must be a list".to_string()),
    }

    let mut mac: Option<String> = None;
    match field("mac") {
        None => {}
        Some(Value::Str(s)) => {
            let check = validate_mac_address(s);
            if check.valid {
                mac = check.canonical;
            } else {
                errors.push(format!("mac \"{}\" is not valid: {}", s, check.reason.unwrap_or_default()));
            }
        }
        Some(_) => errors.push("mac must be text".to_string()),
    }

    let mut subnet: Option<String> = None;
    match field("subnet") {
        None => {}
        Some(Value::Str(s)) => {
            if !is_cidr(s) {
                errors.push(format!("subnet \"{}\" is not a CIDR block", s));
            } else if s.contains(':') {
                subnet = Some(canonical_ipv6_block(s));
            } else {
                subnet = Some(cidr_info(s).cidr);
            }
        }
        Some(_) => errors.push("subnet must be text".to_string()),
    }

    // Sorted, not in file order: JavaScript lists integer-like keys first, so
    // "file order" would differ between languages.
    let mut unknown: Vec<&String> = pairs.iter().map(|(k, _)| k).filter(|k| !KNOWN.contains(&k.as_str())).collect();
    unknown.sort();
    unknown.dedup();
    for key in unknown {
        errors.push(format!("unknown field \"{}\"", key));
    }

    DeviceValidation {
        valid: errors.is_empty(),
        errors,
        name,
        host,
        host_kind,
        ports,
        mac,
        subnet,
    }
}

fn opt(value: &Option<String>) -> Value {
    match value {
        Some(s) => Value::str(s),
        None => Value::Null,
    }
}

pub fn device_validation_to_value(v: &DeviceValidation) -> Value {
    Value::obj(vec![
        ("valid", Value::Bool(v.valid)),
        ("errors", Value::Arr(v.errors.iter().map(|e| Value::str(e)).collect())),
        ("name", opt(&v.name)),
        ("host", opt(&v.host)),
        ("hostKind", opt(&v.host_kind)),
        ("ports", Value::Arr(v.ports.iter().map(|p| Value::Int(*p)).collect())),
        ("mac", opt(&v.mac)),
        ("subnet", opt(&v.subnet)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    device_validation_to_value(&validate_device(&args[0]))
}