use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::error::PluginError;
use super::types::{Permission, PluginLimits, PluginManifest};
#[derive(Default)]
pub struct HostState {
pub logs: Vec<LogEntry>,
pub config: BTreeMap<String, String>,
pub allowed_paths: Vec<String>,
pub manifest: PluginManifest,
pub limits: PluginLimits,
pub memory_used: usize,
pub execution_start_us: u64,
pub fs_provider: Option<FsProvider>,
}
impl HostState {
pub fn new(manifest: PluginManifest) -> Self {
Self {
manifest,
..Default::default()
}
}
pub fn set_config(&mut self, key: &str, value: &str) {
self.config.insert(key.into(), value.into());
}
pub fn add_allowed_path(&mut self, pattern: &str) {
self.allowed_paths.push(pattern.into());
}
pub fn is_path_allowed(&self, path: &str) -> bool {
if self
.manifest
.has_permission(&Permission::PathAccess(path.into()))
{
return true;
}
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
}
pub fn log(&mut self, level: LogLevel, message: String) {
self.logs.push(LogEntry { level, message });
}
pub fn recent_logs(&self, n: usize) -> &[LogEntry] {
let start = self.logs.len().saturating_sub(n);
&self.logs[start..]
}
pub fn clear_logs(&mut self) {
self.logs.clear();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum LogLevel {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
}
impl LogLevel {
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,
}
}
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
}
}
}
#[derive(Debug, Clone)]
pub struct LogEntry {
pub level: LogLevel,
pub message: String,
}
pub trait FsAccess: Send + Sync {
fn read_file(&self, path: &str) -> Result<Vec<u8>, PluginError>;
fn file_exists(&self, path: &str) -> bool;
fn file_size(&self, path: &str) -> Result<u64, PluginError>;
}
pub type FsProvider = alloc::boxed::Box<dyn FsAccess>;
pub struct HostFunctions;
impl HostFunctions {
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());
}
pub fn host_read_file(
state: &HostState,
path: &str,
out_buf: &mut [u8],
) -> Result<usize, PluginError> {
if !state.manifest.has_permission(&Permission::ReadContent) {
return Err(PluginError::PermissionDenied("ReadContent".into()));
}
if !state.is_path_allowed(path) {
return Err(PluginError::PermissionDenied(alloc::format!(
"path access: {}",
path
)));
}
let provider = state
.fs_provider
.as_ref()
.ok_or_else(|| PluginError::IoError("no filesystem provider".into()))?;
let data = provider.read_file(path)?;
let copy_len = data.len().min(out_buf.len());
out_buf[..copy_len].copy_from_slice(&data[..copy_len]);
Ok(copy_len)
}
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
}
}
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,
}
}
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,
}
}
}
#[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); }
#[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");
}
}