fusabi_host/
engine.rs

1//! Fusabi engine wrapper with configuration and execution context.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use parking_lot::Mutex;
8
9use crate::capabilities::Capabilities;
10use crate::error::{Error, Result};
11use crate::limits::{LimitTracker, Limits};
12use crate::sandbox::{Sandbox, SandboxConfig};
13use crate::value::Value;
14
15/// Configuration for creating an Engine.
16#[derive(Debug, Clone)]
17pub struct EngineConfig {
18    /// Resource limits.
19    pub limits: Limits,
20    /// Capabilities granted to scripts.
21    pub capabilities: Capabilities,
22    /// Sandbox configuration.
23    pub sandbox: SandboxConfig,
24    /// Whether to enable debug mode.
25    pub debug: bool,
26    /// Custom metadata to attach to the engine.
27    pub metadata: HashMap<String, String>,
28}
29
30impl Default for EngineConfig {
31    fn default() -> Self {
32        Self {
33            limits: Limits::default(),
34            capabilities: Capabilities::safe_defaults(),
35            sandbox: SandboxConfig::default(),
36            debug: false,
37            metadata: HashMap::new(),
38        }
39    }
40}
41
42impl EngineConfig {
43    /// Create a new engine configuration.
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Set resource limits.
49    pub fn with_limits(mut self, limits: Limits) -> Self {
50        self.limits = limits;
51        self
52    }
53
54    /// Set capabilities.
55    pub fn with_capabilities(mut self, capabilities: Capabilities) -> Self {
56        self.capabilities = capabilities;
57        self
58    }
59
60    /// Set sandbox configuration.
61    pub fn with_sandbox(mut self, sandbox: SandboxConfig) -> Self {
62        self.sandbox = sandbox;
63        self
64    }
65
66    /// Enable debug mode.
67    pub fn with_debug(mut self, debug: bool) -> Self {
68        self.debug = debug;
69        self
70    }
71
72    /// Add metadata.
73    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
74        self.metadata.insert(key.into(), value.into());
75        self
76    }
77
78    /// Create a strict configuration for untrusted code.
79    pub fn strict() -> Self {
80        Self {
81            limits: Limits::strict(),
82            capabilities: Capabilities::none(),
83            sandbox: SandboxConfig::locked(),
84            debug: false,
85            metadata: HashMap::new(),
86        }
87    }
88
89    /// Create a permissive configuration for trusted code.
90    pub fn permissive() -> Self {
91        Self {
92            limits: Limits::unlimited(),
93            capabilities: Capabilities::all(),
94            sandbox: SandboxConfig::permissive(),
95            debug: false,
96            metadata: HashMap::new(),
97        }
98    }
99}
100
101/// Host function signature.
102pub type HostFn = Arc<dyn Fn(&[Value], &ExecutionContext) -> Result<Value> + Send + Sync>;
103
104/// Host function registry.
105#[derive(Default, Clone)]
106pub struct HostRegistry {
107    functions: HashMap<String, HostFn>,
108    modules: HashMap<String, HashMap<String, HostFn>>,
109}
110
111impl HostRegistry {
112    /// Create a new empty registry.
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    /// Register a global host function.
118    pub fn register<S, F>(&mut self, name: S, f: F)
119    where
120        S: Into<String>,
121        F: Fn(&[Value], &ExecutionContext) -> Result<Value> + Send + Sync + 'static,
122    {
123        self.functions.insert(name.into(), Arc::new(f));
124    }
125
126    /// Register a host function in a module namespace.
127    pub fn register_module<M, N, F>(&mut self, module: M, name: N, f: F)
128    where
129        M: Into<String>,
130        N: Into<String>,
131        F: Fn(&[Value], &ExecutionContext) -> Result<Value> + Send + Sync + 'static,
132    {
133        self.modules
134            .entry(module.into())
135            .or_default()
136            .insert(name.into(), Arc::new(f));
137    }
138
139    /// Look up a global function.
140    pub fn get(&self, name: &str) -> Option<&HostFn> {
141        self.functions.get(name)
142    }
143
144    /// Look up a module function.
145    pub fn get_module(&self, module: &str, name: &str) -> Option<&HostFn> {
146        self.modules.get(module).and_then(|m| m.get(name))
147    }
148
149    /// Get all registered function names.
150    pub fn function_names(&self) -> impl Iterator<Item = &String> {
151        self.functions.keys()
152    }
153
154    /// Get all registered module names.
155    pub fn module_names(&self) -> impl Iterator<Item = &String> {
156        self.modules.keys()
157    }
158
159    /// Merge another registry into this one.
160    pub fn merge(&mut self, other: HostRegistry) {
161        self.functions.extend(other.functions);
162        for (module, funcs) in other.modules {
163            self.modules.entry(module).or_default().extend(funcs);
164        }
165    }
166}
167
168impl std::fmt::Debug for HostRegistry {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        f.debug_struct("HostRegistry")
171            .field("functions", &self.functions.keys().collect::<Vec<_>>())
172            .field("modules", &self.modules.keys().collect::<Vec<_>>())
173            .finish()
174    }
175}
176
177/// Execution context passed to host functions.
178#[derive(Debug)]
179pub struct ExecutionContext {
180    /// Engine ID for tracking.
181    pub engine_id: u64,
182    /// Capabilities available to the script.
183    pub capabilities: Capabilities,
184    /// Current limit tracker.
185    limit_tracker: Mutex<LimitTracker>,
186    /// Sandbox instance.
187    sandbox: Sandbox,
188    /// Custom context data.
189    custom: Mutex<HashMap<String, Value>>,
190    /// Start time of current execution.
191    start_time: Instant,
192    /// Whether execution has been cancelled.
193    cancelled: std::sync::atomic::AtomicBool,
194}
195
196impl ExecutionContext {
197    /// Create a new execution context.
198    pub fn new(
199        engine_id: u64,
200        capabilities: Capabilities,
201        limits: Limits,
202        sandbox: Sandbox,
203    ) -> Self {
204        Self {
205            engine_id,
206            capabilities,
207            limit_tracker: Mutex::new(LimitTracker::new(limits)),
208            sandbox,
209            custom: Mutex::new(HashMap::new()),
210            start_time: Instant::now(),
211            cancelled: std::sync::atomic::AtomicBool::new(false),
212        }
213    }
214
215    /// Check if a capability is granted.
216    pub fn has_capability(&self, cap: crate::Capability) -> bool {
217        self.capabilities.has(cap)
218    }
219
220    /// Require a capability, returning an error if not granted.
221    pub fn require_capability(&self, cap: crate::Capability) -> Result<()> {
222        self.capabilities.require(cap)
223    }
224
225    /// Get the sandbox for permission checks.
226    pub fn sandbox(&self) -> &Sandbox {
227        &self.sandbox
228    }
229
230    /// Record instruction execution and check limits.
231    pub fn record_instructions(&self, count: u64) -> Result<()> {
232        self.limit_tracker.lock().record_instructions(count)?;
233        Ok(())
234    }
235
236    /// Record memory usage and check limits.
237    pub fn record_memory(&self, bytes: usize) -> Result<()> {
238        self.limit_tracker.lock().record_memory(bytes)?;
239        Ok(())
240    }
241
242    /// Record output and check limits.
243    pub fn record_output(&self, bytes: usize) -> Result<()> {
244        self.limit_tracker.lock().record_output(bytes)?;
245        Ok(())
246    }
247
248    /// Record filesystem operation and check limits.
249    pub fn record_fs_op(&self) -> Result<()> {
250        self.limit_tracker.lock().record_fs_op()?;
251        Ok(())
252    }
253
254    /// Record network operation and check limits.
255    pub fn record_net_op(&self) -> Result<()> {
256        self.limit_tracker.lock().record_net_op()?;
257        Ok(())
258    }
259
260    /// Check timeout and return error if exceeded.
261    pub fn check_timeout(&self) -> Result<()> {
262        self.limit_tracker.lock().check_timeout()?;
263        Ok(())
264    }
265
266    /// Get elapsed time since execution started.
267    pub fn elapsed(&self) -> Duration {
268        self.start_time.elapsed()
269    }
270
271    /// Check if execution has been cancelled.
272    pub fn is_cancelled(&self) -> bool {
273        self.cancelled.load(std::sync::atomic::Ordering::Relaxed)
274    }
275
276    /// Cancel execution.
277    pub fn cancel(&self) {
278        self.cancelled
279            .store(true, std::sync::atomic::Ordering::Relaxed);
280    }
281
282    /// Set custom context data.
283    pub fn set_custom(&self, key: impl Into<String>, value: Value) {
284        self.custom.lock().insert(key.into(), value);
285    }
286
287    /// Get custom context data.
288    pub fn get_custom(&self, key: &str) -> Option<Value> {
289        self.custom.lock().get(key).cloned()
290    }
291
292    /// Reset the context for a new execution.
293    pub fn reset(&self, limits: Limits) {
294        *self.limit_tracker.lock() = LimitTracker::new(limits);
295        self.cancelled
296            .store(false, std::sync::atomic::Ordering::Relaxed);
297        self.custom.lock().clear();
298    }
299}
300
301/// A Fusabi execution engine.
302///
303/// The engine provides a sandboxed environment for executing Fusabi scripts
304/// with configurable limits and capabilities.
305pub struct Engine {
306    id: u64,
307    config: EngineConfig,
308    registry: HostRegistry,
309    context: ExecutionContext,
310    /// Bytecode cache for compiled scripts.
311    bytecode_cache: Mutex<HashMap<String, Vec<u8>>>,
312}
313
314impl Engine {
315    /// Create a new engine with the given configuration.
316    pub fn new(config: EngineConfig) -> Result<Self> {
317        static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
318        let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
319
320        let sandbox = Sandbox::new(config.sandbox.clone())?;
321        let context = ExecutionContext::new(
322            id,
323            config.capabilities.clone(),
324            config.limits.clone(),
325            sandbox,
326        );
327
328        Ok(Self {
329            id,
330            config,
331            registry: HostRegistry::new(),
332            context,
333            bytecode_cache: Mutex::new(HashMap::new()),
334        })
335    }
336
337    /// Get the engine ID.
338    pub fn id(&self) -> u64 {
339        self.id
340    }
341
342    /// Get the engine configuration.
343    pub fn config(&self) -> &EngineConfig {
344        &self.config
345    }
346
347    /// Get mutable access to the host registry.
348    pub fn registry_mut(&mut self) -> &mut HostRegistry {
349        &mut self.registry
350    }
351
352    /// Get the host registry.
353    pub fn registry(&self) -> &HostRegistry {
354        &self.registry
355    }
356
357    /// Get the execution context.
358    pub fn context(&self) -> &ExecutionContext {
359        &self.context
360    }
361
362    /// Execute a source string and return the result.
363    pub fn execute(&self, source: &str) -> Result<Value> {
364        // Check for cancellation before starting (before reset clears it)
365        if self.context.is_cancelled() {
366            return Err(Error::Cancelled);
367        }
368
369        self.context.reset(self.config.limits.clone());
370
371        // Simulate compilation and execution
372        // In a real implementation, this would call the actual Fusabi VM
373        self.simulate_execution(source)
374    }
375
376    /// Execute compiled bytecode.
377    pub fn execute_bytecode(&self, bytecode: &[u8]) -> Result<Value> {
378        // Check for cancellation before starting (before reset clears it)
379        if self.context.is_cancelled() {
380            return Err(Error::Cancelled);
381        }
382
383        self.context.reset(self.config.limits.clone());
384
385        // Validate bytecode header
386        if bytecode.len() < 8 || &bytecode[0..4] != b"FZB\x00" {
387            return Err(Error::invalid_bytecode("invalid bytecode header"));
388        }
389
390        // Simulate bytecode execution
391        self.simulate_bytecode_execution(bytecode)
392    }
393
394    /// Cancel any ongoing execution.
395    pub fn cancel(&self) {
396        self.context.cancel();
397    }
398
399    /// Check if the engine is healthy.
400    pub fn is_healthy(&self) -> bool {
401        !self.context.is_cancelled()
402    }
403
404    // Internal simulation methods - would be replaced with actual VM calls
405
406    fn simulate_execution(&self, source: &str) -> Result<Value> {
407        // Check timeout periodically during "execution"
408        self.context.check_timeout()?;
409
410        // Record some instructions
411        self.context.record_instructions(source.len() as u64 * 10)?;
412
413        // Simple expression evaluation simulation
414        let trimmed = source.trim();
415
416        // Handle simple numeric expressions
417        if let Ok(n) = trimmed.parse::<i64>() {
418            return Ok(Value::Int(n));
419        }
420
421        if let Ok(f) = trimmed.parse::<f64>() {
422            return Ok(Value::Float(f));
423        }
424
425        // Handle simple string literals
426        if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() > 1 {
427            return Ok(Value::String(trimmed[1..trimmed.len() - 1].to_string()));
428        }
429
430        // Handle simple addition
431        if let Some(pos) = trimmed.find('+') {
432            let left = trimmed[..pos].trim();
433            let right = trimmed[pos + 1..].trim();
434            if let (Ok(l), Ok(r)) = (left.parse::<i64>(), right.parse::<i64>()) {
435                return Ok(Value::Int(l + r));
436            }
437        }
438
439        // Handle boolean literals
440        match trimmed {
441            "true" => return Ok(Value::Bool(true)),
442            "false" => return Ok(Value::Bool(false)),
443            "null" | "nil" => return Ok(Value::Null),
444            _ => {}
445        }
446
447        // Default: return null for unrecognized input
448        Ok(Value::Null)
449    }
450
451    fn simulate_bytecode_execution(&self, _bytecode: &[u8]) -> Result<Value> {
452        self.context.check_timeout()?;
453        self.context.record_instructions(100)?;
454        Ok(Value::Null)
455    }
456}
457
458impl std::fmt::Debug for Engine {
459    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460        f.debug_struct("Engine")
461            .field("id", &self.id)
462            .field("config", &self.config)
463            .field("registry", &self.registry)
464            .finish()
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    #[test]
473    fn test_engine_creation() {
474        let engine = Engine::new(EngineConfig::default()).unwrap();
475        assert!(engine.id() > 0);
476        assert!(engine.is_healthy());
477    }
478
479    #[test]
480    fn test_engine_execute_numbers() {
481        let engine = Engine::new(EngineConfig::default()).unwrap();
482
483        let result = engine.execute("42").unwrap();
484        assert_eq!(result, Value::Int(42));
485
486        let result = engine.execute("3.14").unwrap();
487        assert_eq!(result, Value::Float(3.14));
488    }
489
490    #[test]
491    fn test_engine_execute_addition() {
492        let engine = Engine::new(EngineConfig::default()).unwrap();
493
494        let result = engine.execute("1 + 2").unwrap();
495        assert_eq!(result, Value::Int(3));
496    }
497
498    #[test]
499    fn test_engine_execute_string() {
500        let engine = Engine::new(EngineConfig::default()).unwrap();
501
502        let result = engine.execute("\"hello\"").unwrap();
503        assert_eq!(result, Value::String("hello".into()));
504    }
505
506    #[test]
507    fn test_engine_execute_booleans() {
508        let engine = Engine::new(EngineConfig::default()).unwrap();
509
510        assert_eq!(engine.execute("true").unwrap(), Value::Bool(true));
511        assert_eq!(engine.execute("false").unwrap(), Value::Bool(false));
512        assert_eq!(engine.execute("null").unwrap(), Value::Null);
513    }
514
515    #[test]
516    fn test_engine_cancel() {
517        let engine = Engine::new(EngineConfig::default()).unwrap();
518        engine.cancel();
519
520        let result = engine.execute("42");
521        assert!(matches!(result, Err(Error::Cancelled)));
522    }
523
524    #[test]
525    fn test_host_registry() {
526        let mut registry = HostRegistry::new();
527
528        registry.register("test_fn", |args, _ctx| {
529            if args.is_empty() {
530                Ok(Value::Null)
531            } else {
532                Ok(args[0].clone())
533            }
534        });
535
536        registry.register_module("math", "add", |args, _ctx| {
537            let a = args.get(0).and_then(|v| v.as_int()).unwrap_or(0);
538            let b = args.get(1).and_then(|v| v.as_int()).unwrap_or(0);
539            Ok(Value::Int(a + b))
540        });
541
542        assert!(registry.get("test_fn").is_some());
543        assert!(registry.get_module("math", "add").is_some());
544        assert!(registry.get("nonexistent").is_none());
545    }
546
547    #[test]
548    fn test_execution_context_capabilities() {
549        use crate::Capability;
550
551        let caps = Capabilities::safe_defaults();
552        let sandbox = Sandbox::new(SandboxConfig::default()).unwrap();
553        let ctx = ExecutionContext::new(1, caps, Limits::default(), sandbox);
554
555        assert!(ctx.has_capability(Capability::TimeRead));
556        assert!(!ctx.has_capability(Capability::FsWrite));
557    }
558
559    #[test]
560    fn test_execution_context_custom_data() {
561        let sandbox = Sandbox::new(SandboxConfig::default()).unwrap();
562        let ctx = ExecutionContext::new(
563            1,
564            Capabilities::none(),
565            Limits::default(),
566            sandbox,
567        );
568
569        ctx.set_custom("key", Value::Int(42));
570        assert_eq!(ctx.get_custom("key"), Some(Value::Int(42)));
571        assert_eq!(ctx.get_custom("nonexistent"), None);
572    }
573
574    #[test]
575    fn test_engine_config_builder() {
576        let config = EngineConfig::new()
577            .with_limits(Limits::strict())
578            .with_capabilities(Capabilities::none())
579            .with_debug(true)
580            .with_metadata("name", "test-engine");
581
582        assert!(config.debug);
583        assert_eq!(config.metadata.get("name"), Some(&"test-engine".to_string()));
584    }
585}