use super::funejson::Value; use super::net_cidr::{cidr_info, is_cidr}; use super::net_hostname_validate::validate_hostname; use super::net_ipv4::is_ipv4; use super::net_ipv6::{format_ipv6, is_ipv6, parse_ipv6}; use super::net_mac_address::validate_mac_address; use super::net_port::is_port; 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 { 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 = 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 = 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 = 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 = None; let mut host_kind: Option = 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 = 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 = 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 = 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) -> 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])) }