Functional Weave
Code in TypeScript

net.hostname-validate@1.0.0

impl/rust.rs

2,834 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

/// 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<String>| 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,
        }),
    }
}