memlink-runtime 0.2.0

Dynamic module loading framework with circuit breaker, caching, pooling, health checks, versioning, and auto-discovery
Documentation
//! Module validation.

use std::collections::HashMap;
use std::sync::Arc;

use libloading::Library;

use crate::abi::{validate_abi_version, AbiVersionError, MEMLINK_ABI_VERSION};
use crate::exports::REQUIRED_EXPORTS;
use crate::instance::ModuleInstance;

#[derive(Debug, Clone)]
pub struct ValidationResult {
    pub valid: bool,
    pub warnings: Vec<String>,
    pub errors: Vec<String>,
    pub abi_version: Option<u32>,
    pub has_optional_exports: bool,
}

impl ValidationResult {
    pub fn valid() -> Self {
        ValidationResult {
            valid: true,
            warnings: vec![],
            errors: vec![],
            abi_version: None,
            has_optional_exports: false,
        }
    }

    pub fn invalid() -> Self {
        ValidationResult {
            valid: false,
            warnings: vec![],
            errors: vec![],
            abi_version: None,
            has_optional_exports: false,
        }
    }

    pub fn add_warning(&mut self, warning: impl Into<String>) {
        self.warnings.push(warning.into());
    }

    pub fn add_error(&mut self, error: impl Into<String>) {
        self.errors.push(error.into());
        self.valid = false;
    }
}

pub fn validate_module(_instance: &ModuleInstance) -> crate::Result<ValidationResult> {
    let mut result = ValidationResult::valid();
    result.abi_version = Some(MEMLINK_ABI_VERSION);
    Ok(result)
}

pub fn validate_exports(library: &Library) -> crate::Result<ValidationResult> {
    let mut result = ValidationResult::valid();
    let mut found_required = 0;
    let mut found_optional = 0;

    for &export_name in REQUIRED_EXPORTS {
        let symbol_name = format!("{}\0", export_name);
        let check: std::result::Result<libloading::Symbol<unsafe extern "C" fn()>, _> =
            unsafe { library.get(symbol_name.as_bytes()) };

        if check.is_ok() {
            found_required += 1;
        } else {
            result.add_error(format!("Missing required export: {}", export_name));
        }
    }

    for &export_name in crate::exports::OPTIONAL_EXPORTS {
        let symbol_name = format!("{}\0", export_name);
        let check: std::result::Result<libloading::Symbol<unsafe extern "C" fn()>, _> =
            unsafe { library.get(symbol_name.as_bytes()) };

        if check.is_ok() {
            found_optional += 1;
        }
    }

    result.has_optional_exports = found_optional > 0;

    if found_required == REQUIRED_EXPORTS.len() {
        result.add_warning(format!(
            "All {} required exports present, {} optional exports found",
            found_required, found_optional
        ));
    }

    Ok(result)
}

pub fn validate_abi(module_version: u32) -> ValidationResult {
    let mut result = ValidationResult::valid();
    result.abi_version = Some(module_version);

    match validate_abi_version(module_version) {
        Ok(()) => {
            if module_version < MEMLINK_ABI_VERSION {
                result.add_warning(format!(
                    "Module ABI version {} is older than current version {}. \
                     Compatibility is not guaranteed.",
                    module_version, MEMLINK_ABI_VERSION
                ));
            }
        }
        Err(AbiVersionError::TooOld { module, min_supported }) => {
            result.add_error(format!(
                "Module ABI version {} is too old (minimum supported: {})",
                module, min_supported
            ));
        }
        Err(AbiVersionError::TooNew { module, max_supported }) => {
            result.add_error(format!(
                "Module ABI version {} is too new (maximum supported: {})",
                module, max_supported
            ));
        }
    }

    result
}

#[derive(Debug, Clone)]
pub struct CachedValidation {
    pub result: ValidationResult,
    pub validated_at: u128,
}

impl CachedValidation {
    pub fn new(result: ValidationResult) -> Self {
        CachedValidation {
            result,
            validated_at: std::time::Instant::now().elapsed().as_nanos(),
        }
    }

    pub fn is_fresh(&self) -> bool {
        true
    }
}

#[derive(Debug, Default)]
pub struct ValidationCache {
    cache: HashMap<String, Arc<CachedValidation>>,
}

impl ValidationCache {
    pub fn new() -> Self {
        ValidationCache {
            cache: HashMap::new(),
        }
    }

    pub fn get(&self, key: &str) -> Option<&CachedValidation> {
        self.cache.get(key).map(|arc| arc.as_ref())
    }

    pub fn insert(&mut self, key: impl Into<String>, result: ValidationResult) {
        self.cache.insert(key.into(), Arc::new(CachedValidation::new(result)));
    }

    pub fn remove(&mut self, key: &str) {
        self.cache.remove(key);
    }

    pub fn clear(&mut self) {
        self.cache.clear();
    }

    pub fn len(&self) -> usize {
        self.cache.len()
    }

    pub fn is_empty(&self) -> bool {
        self.cache.is_empty()
    }
}