use super::funejson::Value; /// Check a host name against the RFC 1123 / RFC 952 rules and normalise it: /// lower-case, one trailing dot (a fully qualified name) removed. /// /// Checks run in a fixed order and the first failure is the reason: empty, /// characters, total length, then each label in turn, then the top-level label. pub fn validate_hostname(text: &str) -> HostnameValidation { let name = text.strip_suffix('.').unwrap_or(text); let labels: Vec<&str> = if name.is_empty() { Vec::new() } else { name.split('.').collect() }; let count = labels.len() as i64; let fail = |reason: &str| HostnameValidation { valid: false, hostname: None, reason: Some(reason.to_string()), labels: count, }; if name.is_empty() { return fail("empty"); } // Characters first: once the text is known to be ASCII, every language // counts its length the same way. if !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.') { return fail("character other than a letter, digit, hyphen or dot"); } if name.len() > 253 { return fail("longer than 253 characters"); } for label in &labels { if label.is_empty() { return fail("empty label"); } if label.len() > 63 { return fail("label longer than 63 characters"); } if label.starts_with('-') || label.ends_with('-') { return fail("label starts or ends with a hyphen"); } } // RFC 3696 section 2: a top-level domain is never all-numeric, which keeps // 1.2.3.4 an address rather than a name. if labels[labels.len() - 1].bytes().all(|b| b.is_ascii_digit()) { return fail("top-level label is all numeric"); } HostnameValidation { valid: true, hostname: Some(name.to_ascii_lowercase()), reason: None, labels: count, } } /// Object keys are camelCase to match the shared vectors. pub fn hostname_validation_to_value(result: &HostnameValidation) -> Value { let text = |field: &Option| match field { Some(s) => Value::str(s), None => Value::Null, }; Value::obj(vec![ ("valid", Value::Bool(result.valid)), ("hostname", text(&result.hostname)), ("reason", text(&result.reason)), ("labels", Value::Int(result.labels)), ]) } pub fn fune_vector(args: &[Value]) -> Value { // A non-string is not a host name, as TypeScript and Python answer. match &args[0] { Value::Str(s) => hostname_validation_to_value(&validate_hostname(s)), _ => hostname_validation_to_value(&HostnameValidation { valid: false, hostname: None, reason: Some("not text".to_string()), labels: 0, }), } }