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

//! # WASM Storage Plugins
//!
//! A sandboxed WebAssembly plugin system for user-defined storage policies
//! and transformations.
//!
//! ## Overview
//!
//! WASM plugins allow users to extend LCPFS with custom logic written in
//! any language that compiles to WebAssembly. Plugins run in a sandbox
//! with controlled access to host functions.
//!
//! ## Use Cases
//!
//! - **Custom Compression**: Implement domain-specific compression algorithms
//! - **Data Validation**: Enforce schema or content rules before write
//! - **Encryption**: Custom key management and encryption schemes
//! - **Content Transformation**: Image resizing, format conversion on write
//! - **Auditing**: Log and track all data access
//! - **Access Control**: Fine-grained permission checks
//!
//! ## Architecture
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────────────┐
//! │                         LCPFS I/O Pipeline                         │
//! │   ┌─────────────┐    ┌─────────────┐    ┌─────────────┐           │
//! │   │  Validate   │───▶│  Transform  │───▶│  PreWrite   │───▶ Write │
//! │   └─────────────┘    └─────────────┘    └─────────────┘           │
//! │          ▲                  ▲                  ▲                   │
//! │          │                  │                  │                   │
//! │   ┌──────┴──────────────────┴──────────────────┴───────┐          │
//! │   │                   Plugin Manager                   │          │
//! │   │  ┌───────────┐  ┌───────────┐  ┌───────────┐      │          │
//! │   │  │ Plugin A  │  │ Plugin B  │  │ Plugin C  │      │          │
//! │   │  └───────────┘  └───────────┘  └───────────┘      │          │
//! │   └────────────────────────────────────────────────────┘          │
//! └─────────────────────────────────────────────────────────────────────┘
//! ```
//!
//! ## Plugin ABI
//!
//! Plugins must export these functions:
//!
//! ```c
//! // Initialize plugin, return 0 on success
//! int32_t plugin_init(void);
//!
//! // Process data, return output length or negative error
//! int32_t plugin_process(
//!     const uint8_t* ctx_ptr, uint32_t ctx_len,   // JSON context
//!     const uint8_t* data_ptr, uint32_t data_len, // Input data
//!     uint8_t* out_ptr, uint32_t out_cap          // Output buffer
//! );
//!
//! // Return manifest JSON, return length or negative error
//! int32_t plugin_manifest(uint8_t* out_ptr, uint32_t out_cap);
//!
//! // Optional: cleanup resources
//! void plugin_destroy(void);
//!
//! // Optional: memory allocation (for dynamic plugins)
//! int32_t alloc(int32_t size);
//! ```
//!
//! Plugins can import these host functions:
//!
//! ```c
//! // Log a message (level: 0=trace, 1=debug, 2=info, 3=warn, 4=error)
//! void host_log(int32_t level, const uint8_t* msg_ptr, uint32_t msg_len);
//!
//! // Read a file (if permitted), return bytes read or -1
//! int32_t host_read_file(
//!     const uint8_t* path_ptr, uint32_t path_len,
//!     uint8_t* out_ptr, uint32_t out_cap
//! );
//!
//! // Get config value, return length or -1 if not found
//! int32_t host_get_config(
//!     const uint8_t* key_ptr, uint32_t key_len,
//!     uint8_t* out_ptr, uint32_t out_cap
//! );
//!
//! // Check if file exists, return 1/0/-1
//! int32_t host_file_exists(const uint8_t* path_ptr, uint32_t path_len);
//!
//! // Get file size, return size or -1
//! int64_t host_file_size(const uint8_t* path_ptr, uint32_t path_len);
//! ```
//!
//! ## Usage
//!
//! ```rust,ignore
//! use lcpfs::wasm::{PluginManager, HookType, PipelineHooks};
//! use alloc::collections::BTreeMap;
//!
//! // Load a plugin
//! let wasm_bytes = include_bytes!("my_plugin.wasm");
//! PluginManager::load_plugin("my-plugin", wasm_bytes)?;
//!
//! // Attach to a dataset
//! let config = BTreeMap::new();
//! PluginManager::attach(
//!     "tank/data",
//!     "my-plugin",
//!     &[HookType::PreWrite, HookType::PostRead],
//!     &config,
//! )?;
//!
//! // In your I/O code, call pipeline hooks:
//! let data = b"content to write";
//! let processed = PipelineHooks::pre_write("tank/data", "/path/file.txt", data)?;
//! // write processed data...
//! ```
//!
//! ## Example Plugin (Rust)
//!
//! ```rust,ignore
//! #![no_std]
//!
//! #[no_mangle]
//! pub extern "C" fn plugin_init() -> i32 {
//!     0 // Success
//! }
//!
//! #[no_mangle]
//! pub extern "C" fn plugin_process(
//!     _ctx_ptr: *const u8, _ctx_len: u32,
//!     data_ptr: *const u8, data_len: u32,
//!     out_ptr: *mut u8, _out_cap: u32,
//! ) -> i32 {
//!     // Example: uppercase all text
//!     unsafe {
//!         let data = core::slice::from_raw_parts(data_ptr, data_len as usize);
//!         let out = core::slice::from_raw_parts_mut(out_ptr, data_len as usize);
//!         for (i, &b) in data.iter().enumerate() {
//!             out[i] = b.to_ascii_uppercase();
//!         }
//!     }
//!     data_len as i32
//! }
//!
//! #[no_mangle]
//! pub extern "C" fn plugin_manifest(out_ptr: *mut u8, out_cap: u32) -> i32 {
//!     let json = r#"{"name":"uppercase","version":"1.0","hooks":["Transform"]}"#;
//!     let bytes = json.as_bytes();
//!     if bytes.len() > out_cap as usize {
//!         return -1;
//!     }
//!     unsafe {
//!         core::ptr::copy_nonoverlapping(bytes.as_ptr(), out_ptr, bytes.len());
//!     }
//!     bytes.len() as i32
//! }
//! ```
//!
//! ## Security
//!
//! Plugins are sandboxed with:
//! - **Memory Limits**: Maximum WASM memory (default 16 MB)
//! - **Time Limits**: Maximum execution time (default 1 second)
//! - **Output Limits**: Maximum output size (default 64 MB)
//! - **Path Restrictions**: Only allowed paths are accessible
//! - **Permission System**: Explicit permissions for file/network access
//!
//! ## Feature Flag
//!
//! Enable the `wasm-plugins` feature to use this module:
//!
//! ```toml
//! [dependencies]
//! lcpfs = { version = "2026.1", features = ["wasm-plugins"] }
//! ```

mod error;
mod host;
mod manager;
mod pipeline;
mod runtime;
mod types;

// Re-export public API
pub use error::PluginError;
pub use host::{FsAccess, HostState, LogEntry, LogLevel};
pub use manager::PluginManager;
pub use pipeline::{PipelineHooks, prepare_write, process_read};
pub use runtime::WasmPlugin;
pub use types::{
    HookType, ManifestError, Permission, PluginContext, PluginInfo, PluginLimits, PluginManifest,
};

// ═══════════════════════════════════════════════════════════════════════════════
// MODULE TESTS
// ═══════════════════════════════════════════════════════════════════════════════

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

    #[test]
    fn test_exports_accessible() {
        // Verify all exports are accessible
        let _ = HookType::PreWrite;
        let _ = Permission::ReadContent;
        let _ = PluginLimits::default();
        let _ = PluginContext::default();
        let _ = PluginManifest::default();
        let _ = LogLevel::Info;
    }

    #[test]
    fn test_hook_type_operations() {
        let hook = HookType::PreWrite;
        assert_eq!(hook.as_str(), "PreWrite");
        assert_eq!(HookType::from_str("PreWrite"), Some(HookType::PreWrite));
    }

    #[test]
    fn test_plugin_context_builder() {
        let ctx = PluginContext::new("/path/to/file", "tank/data", "write")
            .with_size(1024)
            .with_metadata("content-type", "text/plain");

        assert_eq!(ctx.path, "/path/to/file");
        assert_eq!(ctx.dataset, "tank/data");
        assert_eq!(ctx.operation, "write");
        assert_eq!(ctx.size, 1024);
        assert_eq!(ctx.metadata.get("content-type"), Some(&"text/plain".into()));
    }

    #[test]
    fn test_manifest_parsing() {
        let json = r#"{"name":"test","version":"1.0.0","hooks":["Transform"]}"#;
        let manifest = PluginManifest::from_json(json).unwrap();

        assert_eq!(manifest.name, "test");
        assert_eq!(manifest.version, "1.0.0");
        assert!(manifest.implements_hook(HookType::Transform));
    }

    #[test]
    fn test_pipeline_hooks_passthrough() {
        // When no plugins are attached, data passes through unchanged
        let data = b"hello world";

        let result = PipelineHooks::pre_write("no_plugins", "/path", data).unwrap();
        assert_eq!(result, data);

        let result = PipelineHooks::post_read("no_plugins", "/path", data).unwrap();
        assert_eq!(result, data);

        let result = PipelineHooks::transform("no_plugins", "/path", data).unwrap();
        assert_eq!(result, data);
    }

    #[test]
    fn test_plugin_manager_empty() {
        let plugins = PluginManager::list_plugins();
        // Don't assert empty - other tests may have loaded plugins
        let _ = plugins;

        assert!(!PluginManager::has_hooks("nonexistent", HookType::PreWrite));
    }
}