lcpfs 2026.1.102

LCP File System - A ZFS-inspired copy-on-write filesystem for Rust
// Copyright 2025 LunaOS Contributors
// SPDX-License-Identifier: Apache-2.0

//! Host state and functions for WASM plugins.
//!
//! This module provides the host-side implementation of functions that
//! WASM plugins can call. These functions are sandboxed and permission-checked.

use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;

use super::error::PluginError;
use super::types::{Permission, PluginLimits, PluginManifest};

// ═══════════════════════════════════════════════════════════════════════════════
// HOST STATE
// ═══════════════════════════════════════════════════════════════════════════════

/// State maintained by the host during plugin execution.
#[derive(Default)]
pub struct HostState {
    /// Log messages emitted by the plugin
    pub logs: Vec<LogEntry>,
    /// Configuration values accessible to the plugin
    pub config: BTreeMap<String, String>,
    /// Paths the plugin is allowed to access
    pub allowed_paths: Vec<String>,
    /// Plugin manifest (for permission checking)
    pub manifest: PluginManifest,
    /// Resource limits
    pub limits: PluginLimits,
    /// Current memory usage in bytes
    pub memory_used: usize,
    /// Execution start time (microseconds since some epoch)
    pub execution_start_us: u64,
    /// File system access provider (callback)
    pub fs_provider: Option<FsProvider>,
}

impl HostState {
    /// Create new host state with manifest.
    pub fn new(manifest: PluginManifest) -> Self {
        Self {
            manifest,
            ..Default::default()
        }
    }

    /// Set configuration value.
    pub fn set_config(&mut self, key: &str, value: &str) {
        self.config.insert(key.into(), value.into());
    }

    /// Add allowed path pattern.
    pub fn add_allowed_path(&mut self, pattern: &str) {
        self.allowed_paths.push(pattern.into());
    }

    /// Check if a path is accessible.
    pub fn is_path_allowed(&self, path: &str) -> bool {
        // Check manifest permissions
        if self
            .manifest
            .has_permission(&Permission::PathAccess(path.into()))
        {
            return true;
        }

        // Check allowed paths
        for pattern in &self.allowed_paths {
            if pattern.ends_with('*') {
                let prefix = &pattern[..pattern.len() - 1];
                if path.starts_with(prefix) {
                    return true;
                }
            } else if pattern == path {
                return true;
            }
        }

        false
    }

    /// Log a message from the plugin.
    pub fn log(&mut self, level: LogLevel, message: String) {
        self.logs.push(LogEntry { level, message });
    }

    /// Get the last N log entries.
    pub fn recent_logs(&self, n: usize) -> &[LogEntry] {
        let start = self.logs.len().saturating_sub(n);
        &self.logs[start..]
    }

    /// Clear all logs.
    pub fn clear_logs(&mut self) {
        self.logs.clear();
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// LOG TYPES
// ═══════════════════════════════════════════════════════════════════════════════

/// Log levels for plugin messages.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum LogLevel {
    /// Trace level (most verbose)
    Trace = 0,
    /// Debug level
    Debug = 1,
    /// Info level
    Info = 2,
    /// Warning level
    Warn = 3,
    /// Error level
    Error = 4,
}

impl LogLevel {
    /// Convert from i32.
    pub fn from_i32(n: i32) -> Self {
        match n {
            0 => LogLevel::Trace,
            1 => LogLevel::Debug,
            2 => LogLevel::Info,
            3 => LogLevel::Warn,
            4 => LogLevel::Error,
            _ => LogLevel::Info,
        }
    }

    /// Get string representation.
    pub fn as_str(&self) -> &'static str {
        match self {
            LogLevel::Trace => "TRACE",
            LogLevel::Debug => "DEBUG",
            LogLevel::Info => "INFO",
            LogLevel::Warn => "WARN",
            LogLevel::Error => "ERROR",
        }
    }
}

/// A log entry from a plugin.
#[derive(Debug, Clone)]
pub struct LogEntry {
    /// Log level
    pub level: LogLevel,
    /// Log message
    pub message: String,
}

// ═══════════════════════════════════════════════════════════════════════════════
// FILESYSTEM PROVIDER
// ═══════════════════════════════════════════════════════════════════════════════

/// Trait for providing filesystem access to plugins.
pub trait FsAccess: Send + Sync {
    /// Read file contents.
    fn read_file(&self, path: &str) -> Result<Vec<u8>, PluginError>;

    /// Check if file exists.
    fn file_exists(&self, path: &str) -> bool;

    /// Get file size.
    fn file_size(&self, path: &str) -> Result<u64, PluginError>;
}

/// Boxed filesystem provider.
pub type FsProvider = alloc::boxed::Box<dyn FsAccess>;

// ═══════════════════════════════════════════════════════════════════════════════
// HOST FUNCTION IMPLEMENTATIONS
// ═══════════════════════════════════════════════════════════════════════════════

/// Host function implementations that can be called from WASM.
///
/// These are the actual implementations that get bound to WASM imports.
pub struct HostFunctions;

impl HostFunctions {
    /// host_log(level: i32, msg_ptr: i32, msg_len: i32)
    ///
    /// Log a message from the plugin.
    pub fn host_log(state: &mut HostState, level: i32, message: &str) {
        let log_level = LogLevel::from_i32(level);
        state.log(log_level, message.to_string());
    }

    /// host_read_file(path_ptr: i32, path_len: i32, out_ptr: i32, out_cap: i32) -> i32
    ///
    /// Read file contents. Returns bytes read or negative error.
    pub fn host_read_file(
        state: &HostState,
        path: &str,
        out_buf: &mut [u8],
    ) -> Result<usize, PluginError> {
        // Check permission
        if !state.manifest.has_permission(&Permission::ReadContent) {
            return Err(PluginError::PermissionDenied("ReadContent".into()));
        }

        // Check path access
        if !state.is_path_allowed(path) {
            return Err(PluginError::PermissionDenied(alloc::format!(
                "path access: {}",
                path
            )));
        }

        // Read via provider
        let provider = state
            .fs_provider
            .as_ref()
            .ok_or_else(|| PluginError::IoError("no filesystem provider".into()))?;

        let data = provider.read_file(path)?;

        // Copy to output buffer
        let copy_len = data.len().min(out_buf.len());
        out_buf[..copy_len].copy_from_slice(&data[..copy_len]);

        Ok(copy_len)
    }

    /// host_get_config(key_ptr: i32, key_len: i32, out_ptr: i32, out_cap: i32) -> i32
    ///
    /// Get configuration value. Returns length or -1 if not found.
    pub fn host_get_config(state: &HostState, key: &str, out_buf: &mut [u8]) -> i32 {
        if let Some(value) = state.config.get(key) {
            let bytes = value.as_bytes();
            let copy_len = bytes.len().min(out_buf.len());
            out_buf[..copy_len].copy_from_slice(&bytes[..copy_len]);
            copy_len as i32
        } else {
            -1
        }
    }

    /// host_file_exists(path_ptr: i32, path_len: i32) -> i32
    ///
    /// Check if file exists. Returns 1 if exists, 0 if not, -1 on error.
    pub fn host_file_exists(state: &HostState, path: &str) -> i32 {
        if !state.is_path_allowed(path) {
            return -1;
        }

        match &state.fs_provider {
            Some(provider) => {
                if provider.file_exists(path) {
                    1
                } else {
                    0
                }
            }
            None => -1,
        }
    }

    /// host_file_size(path_ptr: i32, path_len: i32) -> i64
    ///
    /// Get file size. Returns size or -1 on error.
    pub fn host_file_size(state: &HostState, path: &str) -> i64 {
        if !state.is_path_allowed(path) {
            return -1;
        }

        match &state.fs_provider {
            Some(provider) => match provider.file_size(path) {
                Ok(size) => size as i64,
                Err(_) => -1,
            },
            None => -1,
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// TESTS
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_host_state_default() {
        let state = HostState::default();
        assert!(state.logs.is_empty());
        assert!(state.config.is_empty());
        assert!(state.allowed_paths.is_empty());
    }

    #[test]
    fn test_host_state_config() {
        let mut state = HostState::default();
        state.set_config("key1", "value1");
        state.set_config("key2", "value2");

        assert_eq!(state.config.get("key1"), Some(&"value1".to_string()));
        assert_eq!(state.config.get("key2"), Some(&"value2".to_string()));
        assert_eq!(state.config.get("key3"), None);
    }

    #[test]
    fn test_host_state_logging() {
        let mut state = HostState::default();

        HostFunctions::host_log(&mut state, 2, "info message");
        HostFunctions::host_log(&mut state, 4, "error message");

        assert_eq!(state.logs.len(), 2);
        assert_eq!(state.logs[0].level, LogLevel::Info);
        assert_eq!(state.logs[0].message, "info message");
        assert_eq!(state.logs[1].level, LogLevel::Error);
    }

    #[test]
    fn test_path_allowed() {
        let mut state = HostState::default();
        state.add_allowed_path("/data/*");
        state.add_allowed_path("/config/app.conf");

        assert!(state.is_path_allowed("/data/file.txt"));
        assert!(state.is_path_allowed("/data/subdir/file.txt"));
        assert!(state.is_path_allowed("/config/app.conf"));
        assert!(!state.is_path_allowed("/config/other.conf"));
        assert!(!state.is_path_allowed("/etc/passwd"));
    }

    #[test]
    fn test_log_level() {
        assert_eq!(LogLevel::from_i32(0), LogLevel::Trace);
        assert_eq!(LogLevel::from_i32(1), LogLevel::Debug);
        assert_eq!(LogLevel::from_i32(2), LogLevel::Info);
        assert_eq!(LogLevel::from_i32(3), LogLevel::Warn);
        assert_eq!(LogLevel::from_i32(4), LogLevel::Error);
        assert_eq!(LogLevel::from_i32(99), LogLevel::Info); // fallback
    }

    #[test]
    fn test_host_get_config() {
        let mut state = HostState::default();
        state.set_config("mykey", "myvalue");

        let mut buf = [0u8; 64];
        let len = HostFunctions::host_get_config(&state, "mykey", &mut buf);
        assert_eq!(len, 7);
        assert_eq!(&buf[..7], b"myvalue");

        let len = HostFunctions::host_get_config(&state, "missing", &mut buf);
        assert_eq!(len, -1);
    }

    #[test]
    fn test_recent_logs() {
        let mut state = HostState::default();
        for i in 0..10 {
            state.log(LogLevel::Info, alloc::format!("log {}", i));
        }

        let recent = state.recent_logs(3);
        assert_eq!(recent.len(), 3);
        assert_eq!(recent[0].message, "log 7");
        assert_eq!(recent[1].message, "log 8");
        assert_eq!(recent[2].message, "log 9");
    }
}