progit-plugin-sdk 0.3.0

Plugin SDK for ProGit — sandboxed LuaJIT runtime with capability-based security. LSL-1.0 (file-level copyleft, proprietary plugins allowed via the commercial bridge).
Documentation
// SPDX-License-Identifier: LSL-1.0
// Copyright (c) 2025 Markus Maiwald

//! WASM plugin runtime
//!
//! Provides a sandboxed WASM environment for high-performance plugins.

use crate::traits::*;
use anyhow::{Context, Result};
use std::path::Path;
use wasmtime::*;

/// WASM plugin runtime
pub struct WasmPlugin {
    engine: Engine,
    module: Module,
    metadata: PluginMetadata,
    context: Option<PluginContext>,
}

impl WasmPlugin {
    /// Load a WASM plugin from a file
    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
        let engine = Engine::default();
        let module = Module::from_file(&engine, path.as_ref())
            .context("Failed to load WASM module")?;
        
        // Extract metadata from WASM custom section
        let metadata = Self::extract_metadata(&module)?;
        
        Ok(Self {
            engine,
            module,
            metadata,
            context: None,
        })
    }
    
    /// Load a WASM plugin from bytes
    pub fn from_bytes(wasm_bytes: &[u8]) -> Result<Self> {
        let engine = Engine::default();
        let module = Module::new(&engine, wasm_bytes)
            .context("Failed to load WASM module from bytes")?;
        
        let metadata = Self::extract_metadata(&module)?;
        
        Ok(Self {
            engine,
            module,
            metadata,
            context: None,
        })
    }
    
    /// Extract plugin metadata from WASM custom section
    /// 
    /// Note: wasmtime doesn't expose custom_sections API directly.
    /// For now, we use a fallback default metadata.
    /// Real implementation would parse WASM bytes with wasmparser.
    fn extract_metadata(_module: &Module) -> Result<PluginMetadata> {
        // TODO: Parse WASM bytes with wasmparser to extract custom section
        // For MVP, return default metadata
        Ok(PluginMetadata {
            name: "wasm-plugin".to_string(),
            version: "0.1.0".to_string(),
            author: "unknown".to_string(),
            description: "WASM plugin".to_string(),
            hooks: vec![
                PluginHook::OnIssueCreated,
                PluginHook::OnIssueUpdated,
            ],
        })
    }
    
    /// Execute a WASM function with JSON data
    fn call_wasm_hook(&self, hook_name: &str, data: &serde_json::Value) -> Result<serde_json::Value> {
        let mut store = Store::new(&self.engine, ());
        let instance = Instance::new(&mut store, &self.module, &[])
            .context("Failed to instantiate WASM module")?;
        
        // Get memory export
        let memory = instance.get_memory(&mut store, "memory")
            .context("WASM module must export 'memory'")?;
        
        // Serialize data to JSON bytes
        let data_bytes = serde_json::to_vec(data)?;
        let data_len = data_bytes.len() as i32;
        
        // Allocate memory in WASM
        let alloc_fn = instance.get_typed_func::<i32, i32>(&mut store, "alloc")
            .context("WASM module must export 'alloc' function")?;
        let data_ptr = alloc_fn.call(&mut store, data_len)
            .context("Failed to allocate memory in WASM")?;
        
        // Write data to WASM memory
        memory.write(&mut store, data_ptr as usize, &data_bytes)
            .context("Failed to write data to WASM memory")?;
        
        // Call the hook function
        let hook_fn = instance.get_typed_func::<(i32, i32), i32>(&mut store, hook_name)
            .context(format!("WASM module must export '{}' function", hook_name))?;
        let result_ptr = hook_fn.call(&mut store, (data_ptr, data_len))
            .context(format!("Failed to call WASM hook '{}'", hook_name))?;
        
        // Read result length (first 4 bytes at result_ptr)
        let mut len_bytes = [0u8; 4];
        memory.read(&store, result_ptr as usize, &mut len_bytes)
            .context("Failed to read result length from WASM memory")?;
        let result_len = i32::from_le_bytes(len_bytes) as usize;
        
        // Read result data
        let mut result_bytes = vec![0u8; result_len];
        memory.read(&store, (result_ptr + 4) as usize, &mut result_bytes)
            .context("Failed to read result data from WASM memory")?;
        
        // Deserialize result
        let result: serde_json::Value = serde_json::from_slice(&result_bytes)
            .context("Failed to deserialize WASM result")?;
        
        // Free memory
        let free_fn = instance.get_typed_func::<i32, ()>(&mut store, "free")
            .context("WASM module must export 'free' function")?;
        free_fn.call(&mut store, data_ptr)
            .context("Failed to free data memory in WASM")?;
        free_fn.call(&mut store, result_ptr)
            .context("Failed to free result memory in WASM")?;
        
        Ok(result)
    }
}

impl Plugin for WasmPlugin {
    fn metadata(&self) -> &PluginMetadata {
        &self.metadata
    }
    
    fn init(&mut self, context: &PluginContext) -> PluginResult<()> {
        self.context = Some(context.clone());
        
        // Call init function if it exists
        let mut store = Store::new(&self.engine, ());
        if let Ok(instance) = Instance::new(&mut store, &self.module, &[]) {
            if let Ok(init_fn) = instance.get_typed_func::<(), ()>(&mut store, "init") {
                init_fn.call(&mut store, ())
                    .map_err(|e| PluginError::InitError(e.to_string()))?;
            }
        }
        
        Ok(())
    }
    
    fn execute_hook(&mut self, hook: &PluginHook, data: &serde_json::Value) -> PluginResult<serde_json::Value> {
        if !self.supports_hook(hook) {
            return Err(PluginError::UnsupportedHook(hook.clone()));
        }
        
        let hook_name = match hook {
            PluginHook::OnIssueCreated => "on_issue_created",
            PluginHook::OnIssueUpdated => "on_issue_updated",
            PluginHook::OnIssueDeleted => "on_issue_deleted",
            PluginHook::OnStatusChanged => "on_status_changed",
            PluginHook::OnSyncPush => "on_sync_push",
            PluginHook::OnSyncPull => "on_sync_pull",
            PluginHook::OnMergeRequestCreated => "on_merge_request_created",
            PluginHook::OnCommand(cmd) => cmd.as_str(),
        };
        
        self.call_wasm_hook(hook_name, data)
            .map_err(|e| PluginError::ExecutionError(e.to_string()))
    }
}

impl IssuePlugin for WasmPlugin {}
impl SyncPlugin for WasmPlugin {}

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

    #[test]
    fn test_wasm_plugin_metadata() {
        // This test would require a compiled WASM module
        // For now, just verify the struct can be created
        let engine = Engine::default();
        assert!(engine.config().wasm_simd());
    }
}