hara-native 0.1.13

HAL-free native host runtime and package launcher for Hara
Documentation
use std::collections::{BTreeSet, HashSet};

use crate::kernel::Form;

pub(super) fn map<'a>(
    form: &'a Form,
    origin: &str,
    field: &str,
) -> Result<&'a [(Form, Form)], String> {
    match form {
        Form::Map(entries) => Ok(entries),
        _ => Err(malformed(origin, format!("{field} must be a map"))),
    }
}

pub(super) fn vector<'a>(form: &'a Form, origin: &str, field: &str) -> Result<&'a [Form], String> {
    match form {
        Form::Vector(values) => Ok(values),
        _ => Err(malformed(origin, format!("{field} must be a vector"))),
    }
}

pub(super) fn non_empty_string<'a>(
    form: &'a Form,
    origin: &str,
    field: &str,
) -> Result<&'a str, String> {
    match form {
        Form::String(value) if !value.is_empty() => Ok(value),
        _ => Err(malformed(
            origin,
            format!("{field} must be a non-empty string"),
        )),
    }
}

pub(super) fn named<'a>(form: &'a Form, origin: &str, field: &str) -> Result<&'a str, String> {
    match form {
        Form::Symbol(value) | Form::Keyword(value) | Form::String(value) if !value.is_empty() => {
            Ok(value)
        }
        _ => Err(malformed(origin, format!("{field} must be a named value"))),
    }
}

pub(super) fn keyword<'a>(form: &'a Form, origin: &str, field: &str) -> Result<&'a str, String> {
    match form {
        Form::Keyword(value) => Ok(value),
        _ => Err(malformed(origin, format!("{field} must be a keyword"))),
    }
}

pub(super) fn optional_string(
    entries: &[(Form, Form)],
    name: &str,
    origin: &str,
) -> Result<Option<String>, String> {
    optional(entries, name)
        .map(|form| non_empty_string(form, origin, name).map(str::to_owned))
        .transpose()
}

pub(super) fn optional_bool(
    entries: &[(Form, Form)],
    name: &str,
    origin: &str,
) -> Result<Option<bool>, String> {
    optional(entries, name)
        .map(|form| match form {
            Form::Bool(value) => Ok(*value),
            _ => Err(malformed(origin, format!("{name} must be boolean"))),
        })
        .transpose()
}

pub(super) fn keyword_set(
    form: &Form,
    origin: &str,
    field: &str,
) -> Result<BTreeSet<String>, String> {
    vector(form, origin, field)?
        .iter()
        .map(|form| keyword(form, origin, field).map(str::to_owned))
        .collect()
}

pub(super) fn named_set(
    form: &Form,
    origin: &str,
    field: &str,
) -> Result<BTreeSet<String>, String> {
    vector(form, origin, field)?
        .iter()
        .map(|form| named(form, origin, field).map(str::to_owned))
        .collect()
}

pub(super) fn key(form: &Form) -> Option<&str> {
    match form {
        Form::Keyword(value) | Form::Symbol(value) | Form::String(value) => Some(value),
        _ => None,
    }
}

pub(super) fn required<'a>(
    entries: &'a [(Form, Form)],
    name: &str,
    origin: &str,
) -> Result<&'a Form, String> {
    optional(entries, name)
        .ok_or_else(|| malformed(origin, format!("missing required field {name}")))
}

pub(super) fn optional<'a>(entries: &'a [(Form, Form)], name: &str) -> Option<&'a Form> {
    entries
        .iter()
        .find(|(candidate, _)| key(candidate) == Some(name))
        .map(|(_, value)| value)
}

pub(super) fn reject_unknown(
    entries: &[(Form, Form)],
    allowed: &[&str],
    origin: &str,
    scope: &str,
) -> Result<(), String> {
    let mut seen = HashSet::new();
    for (candidate, _) in entries {
        let Some(name) = key(candidate) else {
            return Err(malformed(origin, format!("{scope} keys must be named")));
        };
        if !allowed.contains(&name) {
            return Err(malformed(origin, format!("unknown {scope} field: {name}")));
        }
        if !seen.insert(name) {
            return Err(malformed(
                origin,
                format!("duplicate {scope} field: {name}"),
            ));
        }
    }
    Ok(())
}

pub(super) fn validate_module_path(value: &str, origin: &str) -> Result<(), String> {
    let unsafe_path = !value.ends_with(".wasm")
        || value.starts_with('/')
        || value.contains('\\')
        || value.contains(':')
        || value.bytes().any(|byte| byte == 0)
        || value
            .split('/')
            .any(|part| part.is_empty() || part == "." || part == "..");
    if unsafe_path {
        return Err(malformed(
            origin,
            "module must be a safe relative .wasm package path",
        ));
    }
    Ok(())
}

pub(super) fn valid_namespace(value: &str) -> bool {
    value.contains('.') && value.split('.').all(valid_component)
}

pub(super) fn valid_tag(value: &str) -> bool {
    value.split('.').all(valid_component)
}

pub(super) fn valid_binding_name(value: &str) -> bool {
    !value.is_empty()
        && value.chars().all(|ch| {
            ch.is_ascii_lowercase()
                || ch.is_ascii_digit()
                || matches!(ch, '-' | '?' | '!' | '*' | '+' | '<' | '>' | '=')
        })
}

fn valid_component(value: &str) -> bool {
    !value.is_empty()
        && value
            .chars()
            .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
}

pub(super) fn malformed(origin: &str, message: impl AsRef<str>) -> String {
    format!("wasm-interface/malformed {origin}: {}", message.as_ref())
}

pub(super) fn unsupported(origin: &str, message: impl AsRef<str>) -> String {
    format!(
        "wasm-interface/feature-unsupported {origin}: {}",
        message.as_ref()
    )
}