Skip to main content

camel_component_wasm/
host_functions.rs

1use serde_json::Value;
2use wasmtime::component::{Access, Accessor, Linker};
3
4use crate::runtime::WasmHostState;
5
6// Host marker trait must stay on WasmHostState (not HasSelf<WasmHostState>):
7// the add_to_linker bound `for<'a> D::Data<'a>: Host` resolves through the
8// `impl<T: Host> Host for &mut T` blanket impl, which requires the concrete
9// type inside &mut (_) to implement Host.
10// Sync `Host` trait impl: under the per-function import config the sync host
11// functions (get-property, set-property, host-store, host-load) live in
12// `HostWithStore` (taking an `Access`); `Host` remains an empty marker trait
13// required by the linker's where-clause.
14impl crate::bindings::camel::plugin::host::Host for WasmHostState {}
15
16impl crate::bindings::camel::plugin::host::HostWithStore<WasmHostState>
17    for wasmtime::component::HasSelf<WasmHostState>
18{
19    async fn camel_call(
20        store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
21        uri: String,
22        payload: String,
23    ) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
24        WasmHostState::camel_call_impl(store, uri, payload).await
25    }
26
27    async fn camel_poll(
28        store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
29        uri: String,
30        timeout_ms: u32,
31    ) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
32        WasmHostState::camel_poll_impl(store, uri, timeout_ms).await
33    }
34
35    fn get_property(
36        mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
37        key: String,
38    ) -> Option<String> {
39        store.get().get_property_impl(key)
40    }
41
42    fn set_property(
43        mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
44        key: String,
45        value: String,
46    ) {
47        store.get().set_property_impl(key, value)
48    }
49
50    fn host_store(
51        mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
52        key: String,
53        value: String,
54    ) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
55        WasmHostState::host_store_impl(store.get(), key, value)
56    }
57
58    fn host_load(
59        mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
60        key: String,
61    ) -> Result<Option<String>, crate::bindings::camel::plugin::types::WasmError> {
62        WasmHostState::host_load_impl(store.get(), key)
63    }
64}
65
66pub fn add_to_linker(linker: &mut Linker<WasmHostState>) -> Result<(), wasmtime::Error> {
67    // D = HasSelf<WasmHostState> uses wasmtime's built-in HasData impl
68    // (Data<'a> = &'a mut WasmHostState). The HostWithStore and Host trait
69    // impls target HasSelf<WasmHostState> and WasmHostState respectively.
70    crate::bindings::camel::plugin::host::add_to_linker::<
71        WasmHostState,
72        wasmtime::component::HasSelf<WasmHostState>,
73    >(linker, |state| state)
74}
75
76// Async helpers: the actual producer / poller logic. Pulled out of the trait
77// impls so they can also be invoked from the per-binding macro arms and from
78// tests (where we don't have an Accessor in scope).
79
80async fn run_async_call(
81    registry: std::sync::Arc<dyn camel_component_api::ComponentContext>,
82    uri: String,
83    payload: String,
84) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
85    use tower::ServiceExt;
86    let scheme = uri.split(':').next().unwrap_or("").to_string();
87    if scheme.is_empty() {
88        return Err(
89            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
90                "invalid URI (no scheme): {}",
91                uri
92            )),
93        );
94    }
95
96    let component = registry.resolve_component(&scheme).ok_or_else(|| {
97        crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
98            "component not found for scheme: {}",
99            scheme
100        ))
101    })?;
102
103    let endpoint = component
104        .create_endpoint(&uri, &camel_component_api::NoOpComponentContext)
105        .map_err(|e| {
106            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
107                "create_endpoint failed: {}",
108                e
109            ))
110        })?;
111
112    let rt: std::sync::Arc<dyn camel_component_api::RuntimeObservability> =
113        std::sync::Arc::new(camel_component_api::NoOpComponentContext);
114    let producer = endpoint
115        .create_producer(rt, &camel_api::ProducerContext::new())
116        .map_err(|e| {
117            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
118                "create_producer failed: {}",
119                e
120            ))
121        })?;
122
123    let exchange =
124        camel_api::Exchange::new(camel_api::Message::new(camel_api::Body::Text(payload)));
125
126    let result = producer.oneshot(exchange).await.map_err(|e| {
127        crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
128            "endpoint call failed: {}",
129            e
130        ))
131    })?;
132
133    let body_str = match &result.output {
134        Some(msg) => match &msg.body {
135            camel_api::Body::Text(s) => s.clone(),
136            camel_api::Body::Json(v) => v.to_string(),
137            camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
138            camel_api::Body::Xml(s) => s.clone(),
139            camel_api::Body::Empty => String::new(),
140            camel_api::Body::Stream(_) => "<stream>".to_string(),
141            _ => String::new(),
142        },
143        None => match &result.input.body {
144            camel_api::Body::Text(s) => s.clone(),
145            camel_api::Body::Json(v) => v.to_string(),
146            camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
147            camel_api::Body::Xml(s) => s.clone(),
148            camel_api::Body::Empty => String::new(),
149            camel_api::Body::Stream(_) => "<stream>".to_string(),
150            _ => String::new(),
151        },
152    };
153
154    Ok(body_str)
155}
156
157async fn run_async_poll(
158    registry: std::sync::Arc<dyn camel_component_api::ComponentContext>,
159    uri: String,
160    timeout_ms: u32,
161) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
162    let scheme = uri.split(':').next().unwrap_or("").to_string();
163    if scheme.is_empty() {
164        return Err(
165            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
166                "invalid URI (no scheme): {}",
167                uri
168            )),
169        );
170    }
171
172    let component = registry.resolve_component(&scheme).ok_or_else(|| {
173        crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
174            "component not found for scheme: {}",
175            scheme
176        ))
177    })?;
178
179    let endpoint = component
180        .create_endpoint(&uri, &camel_component_api::NoOpComponentContext)
181        .map_err(|e| {
182            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
183                "create_endpoint failed: {}",
184                e
185            ))
186        })?;
187
188    let mut poller = endpoint.polling_consumer().ok_or_else(|| {
189        crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
190            "camel_poll requires a component that supports polling consumers (scheme: {})",
191            scheme
192        ))
193    })?;
194
195    let exchange = poller
196        .receive(std::time::Duration::from_millis(timeout_ms as u64))
197        .await
198        .map_err(|e| {
199            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
200                "poll failed: {}",
201                e
202            ))
203        })?;
204
205    let body_str = match exchange {
206        Some(ex) => {
207            let bytes = ex
208                .input
209                .body
210                .into_bytes(10 * 1024 * 1024)
211                .await
212                .map_err(|e| {
213                    crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
214                        "body read failed: {}",
215                        e
216                    ))
217                })?;
218            String::from_utf8_lossy(&bytes).to_string()
219        }
220        None => {
221            return Err(
222                crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
223                    "no message received within {}ms timeout",
224                    timeout_ms
225                )),
226            );
227        }
228    };
229
230    Ok(body_str)
231}
232
233// Macro that implements the same `HostWithStore` trait for the bean and
234// security-policy bindings modules. Those bindings define an equivalent
235// `host` interface (separate types but identical shape), so the host impl is
236// duplicated against each module's generated `HostWithStore` trait.
237macro_rules! impl_host_for_binding {
238    ($bindings_mod:ident) => {
239        // Host marker stays on WasmHostState for the `impl<T: Host> Host for &mut T`
240        // blanket bound on D::Data<'a> inside add_to_linker.
241        impl crate::$bindings_mod::camel::plugin::host::Host for WasmHostState {}
242
243        // HostWithStore targets HasSelf<WasmHostState> (wasmtime's built-in HasData)
244        // instead of a hand-rolled HasData impl on WasmHostState.
245        impl crate::$bindings_mod::camel::plugin::host::HostWithStore<WasmHostState>
246            for wasmtime::component::HasSelf<WasmHostState>
247        {
248            async fn camel_call(
249                store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
250                uri: String,
251                payload: String,
252            ) -> Result<String, crate::$bindings_mod::camel::plugin::types::WasmError> {
253                WasmHostState::camel_call_impl(store, uri, payload)
254                    .await
255                    .map_err(map_plugin_wasm_error_to)
256            }
257
258            async fn camel_poll(
259                store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
260                uri: String,
261                timeout_ms: u32,
262            ) -> Result<String, crate::$bindings_mod::camel::plugin::types::WasmError> {
263                WasmHostState::camel_poll_impl(store, uri, timeout_ms)
264                    .await
265                    .map_err(map_plugin_wasm_error_to)
266            }
267
268            fn get_property(
269                mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
270                key: String,
271            ) -> Option<String> {
272                store.get().get_property_impl(key)
273            }
274
275            fn set_property(
276                mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
277                key: String,
278                value: String,
279            ) {
280                store.get().set_property_impl(key, value)
281            }
282
283            fn host_store(
284                mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
285                key: String,
286                value: String,
287            ) -> Result<(), crate::$bindings_mod::camel::plugin::types::WasmError> {
288                WasmHostState::host_store_impl(store.get(), key, value)
289                    .map_err(map_plugin_wasm_error_to)
290            }
291
292            fn host_load(
293                mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
294                key: String,
295            ) -> Result<Option<String>, crate::$bindings_mod::camel::plugin::types::WasmError> {
296                WasmHostState::host_load_impl(store.get(), key).map_err(map_plugin_wasm_error_to)
297            }
298        }
299    };
300}
301
302// Helper: re-map a canonical bindings::camel::plugin::types::WasmError to
303// the same shape produced by a different bindgen invocation (bean /
304// authorization-policy). All variants have identical structure, so this is
305// a straight field-for-field clone.
306fn map_plugin_wasm_error_to<E: FromWasmErrorVariant>(
307    e: crate::bindings::camel::plugin::types::WasmError,
308) -> E {
309    match e {
310        crate::bindings::camel::plugin::types::WasmError::ProcessorError(s) => {
311            E::from_processor_error(s)
312        }
313        crate::bindings::camel::plugin::types::WasmError::TypeConversion(s) => {
314            E::from_type_conversion(s)
315        }
316        crate::bindings::camel::plugin::types::WasmError::Io(s) => E::from_io(s),
317        crate::bindings::camel::plugin::types::WasmError::Timeout => E::from_timeout(),
318    }
319}
320
321trait FromWasmErrorVariant {
322    fn from_processor_error(s: String) -> Self;
323    fn from_type_conversion(s: String) -> Self;
324    fn from_io(s: String) -> Self;
325    fn from_timeout() -> Self;
326}
327
328impl FromWasmErrorVariant for crate::bindings::camel::plugin::types::WasmError {
329    fn from_processor_error(s: String) -> Self {
330        Self::ProcessorError(s)
331    }
332    fn from_type_conversion(s: String) -> Self {
333        Self::TypeConversion(s)
334    }
335    fn from_io(s: String) -> Self {
336        Self::Io(s)
337    }
338    fn from_timeout() -> Self {
339        Self::Timeout
340    }
341}
342
343impl FromWasmErrorVariant for crate::bean_bindings::camel::plugin::types::WasmError {
344    fn from_processor_error(s: String) -> Self {
345        Self::ProcessorError(s)
346    }
347    fn from_type_conversion(s: String) -> Self {
348        Self::TypeConversion(s)
349    }
350    fn from_io(s: String) -> Self {
351        Self::Io(s)
352    }
353    fn from_timeout() -> Self {
354        Self::Timeout
355    }
356}
357
358impl FromWasmErrorVariant for crate::security_policy_bindings::camel::plugin::types::WasmError {
359    fn from_processor_error(s: String) -> Self {
360        Self::ProcessorError(s)
361    }
362    fn from_type_conversion(s: String) -> Self {
363        Self::TypeConversion(s)
364    }
365    fn from_io(s: String) -> Self {
366        Self::Io(s)
367    }
368    fn from_timeout() -> Self {
369        Self::Timeout
370    }
371}
372
373pub fn add_bean_to_linker(linker: &mut Linker<WasmHostState>) -> Result<(), wasmtime::Error> {
374    crate::bean_bindings::camel::plugin::host::add_to_linker::<
375        WasmHostState,
376        wasmtime::component::HasSelf<WasmHostState>,
377    >(linker, |state| state)
378}
379
380impl_host_for_binding!(bean_bindings);
381
382pub fn add_security_policy_to_linker(
383    linker: &mut Linker<WasmHostState>,
384) -> Result<(), wasmtime::Error> {
385    crate::security_policy_bindings::camel::plugin::host::add_to_linker::<
386        WasmHostState,
387        wasmtime::component::HasSelf<WasmHostState>,
388    >(linker, |state| state)
389}
390
391impl_host_for_binding!(security_policy_bindings);
392
393// RAII recursion guard: increments `call_depth` on construction and
394// decrements on drop. The drop runs even if the future is cancelled,
395// which is the whole point: a manual inc/dec around an `.await` leaks
396// the increment forever if the future is dropped between the two sites.
397// Returns `None` from `new` if the depth was already > 0 (a nested
398// call is in flight) so the caller can return the recursion error.
399struct DepthGuard<'a> {
400    depth: &'a std::sync::atomic::AtomicUsize,
401}
402
403impl<'a> DepthGuard<'a> {
404    fn new(depth: &'a std::sync::atomic::AtomicUsize) -> Option<Self> {
405        let prev = depth.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
406        if prev > 0 {
407            depth.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
408            return None;
409        }
410        Some(DepthGuard { depth })
411    }
412}
413
414impl Drop for DepthGuard<'_> {
415    fn drop(&mut self) {
416        self.depth.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
417    }
418}
419
420// Inherent methods on `WasmHostState` that implement the host function
421// logic. The trait impls above are thin shims that call these. Splitting
422// the logic this way gives the test suite direct access to the same code
423// paths without needing an `Accessor<WasmHostState, HasSelf<WasmHostState>>` (which
424// can only be constructed inside a `run_concurrent` scope).
425impl WasmHostState {
426    pub(crate) async fn camel_call_impl(
427        store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
428        uri: String,
429        payload: String,
430    ) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
431        // Capability gate (H4): check scheme against per-world allowlist.
432        let scheme = uri.split(':').next().unwrap_or("").to_string();
433        let can_call = store.with(|mut view| view.get().capabilities.can_call(&scheme));
434        if !can_call {
435            return Err(
436                crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
437                    "camel_call denied: scheme '{}' not in capability allowlist",
438                    scheme
439                )),
440            );
441        }
442
443        // Snapshot the registry (cloned Arc — cheap) for the async work
444        // so we don't borrow `store` across the await.
445        let registry = store.with(|mut view| view.get().registry.clone());
446
447        // Recursion guard. The RAII `DepthGuard` decrements on drop, so
448        // even if the `run_async_call` future is cancelled mid-await the
449        // counter returns to 0 — otherwise every subsequent call would
450        // fail with "recursive wasm calls not supported".
451        //
452        // Clone the Arc<AtomicUsize> from store state — no unsafe needed.
453        // The guard borrows the Arc's inner AtomicUsize for the call scope.
454        let call_depth = store.with(|mut view| view.get().call_depth.clone());
455        let _depth_guard = match DepthGuard::new(call_depth.as_ref()) {
456            Some(g) => g,
457            None => {
458                return Err(
459                    crate::bindings::camel::plugin::types::WasmError::ProcessorError(
460                        "recursive wasm calls not supported".to_string(),
461                    ),
462                );
463            }
464        };
465
466        run_async_call(registry, uri, payload).await
467    }
468
469    pub(crate) async fn camel_poll_impl(
470        store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
471        uri: String,
472        timeout_ms: u32,
473    ) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
474        let scheme = uri.split(':').next().unwrap_or("").to_string();
475        let can_call = store.with(|mut view| view.get().capabilities.can_call(&scheme));
476        if !can_call {
477            return Err(
478                crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
479                    "camel_poll denied: scheme '{}' not in capability allowlist",
480                    scheme
481                )),
482            );
483        }
484
485        // Recursion guard (RAII — see camel_call_impl for rationale).
486        let registry = store.with(|mut view| view.get().registry.clone());
487        let call_depth = store.with(|mut view| view.get().call_depth.clone());
488        let _depth_guard = match DepthGuard::new(call_depth.as_ref()) {
489            Some(g) => g,
490            None => {
491                return Err(
492                    crate::bindings::camel::plugin::types::WasmError::ProcessorError(
493                        "recursive wasm calls not supported".to_string(),
494                    ),
495                );
496            }
497        };
498
499        run_async_poll(registry, uri, timeout_ms).await
500    }
501
502    pub(crate) fn get_property_impl(&self, key: String) -> Option<String> {
503        self.properties.get(&key).map(|v| match v {
504            Value::String(s) => s.clone(),
505            other => other.to_string(),
506        })
507    }
508
509    pub(crate) fn set_property_impl(&mut self, key: String, value: String) {
510        if key.len() > self.state_store.max_key_bytes() {
511            return;
512        }
513        if value.len() > self.state_store.max_value_bytes() {
514            return;
515        }
516        let parsed = serde_json::from_str::<Value>(&value).unwrap_or(Value::String(value));
517        self.properties.insert(key, parsed);
518    }
519
520    pub(crate) fn host_store_impl(
521        state: &mut WasmHostState,
522        key: String,
523        value: String,
524    ) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
525        if !state.capabilities.host_kv {
526            return Err(
527                crate::bindings::camel::plugin::types::WasmError::ProcessorError(
528                    "host_store denied: host_kv capability not granted".to_string(),
529                ),
530            );
531        }
532        state
533            .state_store
534            .store(&key, &value)
535            .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
536    }
537
538    pub(crate) fn host_load_impl(
539        state: &mut WasmHostState,
540        key: String,
541    ) -> Result<Option<String>, crate::bindings::camel::plugin::types::WasmError> {
542        if !state.capabilities.host_kv {
543            return Err(
544                crate::bindings::camel::plugin::types::WasmError::ProcessorError(
545                    "host_load denied: host_kv capability not granted".to_string(),
546                ),
547            );
548        }
549        state
550            .state_store
551            .load(&key)
552            .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
553    }
554}
555
556// Inherent (non-async) versions of the impl methods for the test suite.
557// The trait methods are async + store-bound, but the unit tests want to
558// exercise the gating logic synchronously against a plain `&mut
559// WasmHostState`. Both layers share the same recursion / capability /
560// host_kv gates.
561//
562// `dead_code` allow: the `*_gate` methods are referenced only from
563// `#[cfg(test)]` code in this same module, but the rustc dead-code lint
564// analyses test code separately and reports them as unused. The
565// #[allow(dead_code)] silences the false positive.
566#[allow(dead_code)]
567impl WasmHostState {
568    /// Same gating as `camel_call_impl` but synchronous and against a
569    /// plain `&mut self`. Returns the same error variant on gate failure.
570    /// Async work is NOT run — this is purely for unit-testing the gate
571    /// logic in isolation.
572    pub(crate) fn camel_call_gate(
573        &mut self,
574        uri: String,
575    ) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
576        let scheme = uri.split(':').next().unwrap_or("").to_string();
577        if !self.capabilities.can_call(&scheme) {
578            return Err(
579                crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
580                    "camel_call denied: scheme '{}' not in capability allowlist",
581                    scheme
582                )),
583            );
584        }
585        if self.call_depth.load(std::sync::atomic::Ordering::Relaxed) > 0 {
586            return Err(
587                crate::bindings::camel::plugin::types::WasmError::ProcessorError(
588                    "recursive wasm calls not supported".to_string(),
589                ),
590            );
591        }
592        self.call_depth
593            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
594        Ok(())
595    }
596
597    pub(crate) fn release_call_depth(&self) {
598        self.call_depth
599            .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
600    }
601
602    pub(crate) fn host_store_gate(
603        &self,
604        key: String,
605        value: String,
606    ) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
607        if !self.capabilities.host_kv {
608            return Err(
609                crate::bindings::camel::plugin::types::WasmError::ProcessorError(
610                    "host_store denied: host_kv capability not granted".to_string(),
611                ),
612            );
613        }
614        self.state_store
615            .store(&key, &value)
616            .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
617    }
618
619    pub(crate) fn host_load_gate(
620        &self,
621        key: String,
622    ) -> Result<Option<String>, crate::bindings::camel::plugin::types::WasmError> {
623        if !self.capabilities.host_kv {
624            return Err(
625                crate::bindings::camel::plugin::types::WasmError::ProcessorError(
626                    "host_load denied: host_kv capability not granted".to_string(),
627                ),
628            );
629        }
630        self.state_store
631            .load(&key)
632            .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639    use std::collections::HashMap;
640    use std::sync::Arc;
641
642    fn make_state(call_depth: usize) -> WasmHostState {
643        WasmHostState {
644            table: wasmtime::component::ResourceTable::new(),
645            wasi: wasmtime_wasi::WasiCtxBuilder::new().build(),
646            properties: HashMap::new(),
647            registry: Arc::new(camel_component_api::NoOpComponentContext),
648            call_depth: Arc::new(std::sync::atomic::AtomicUsize::new(call_depth)),
649            limits: wasmtime::StoreLimits::default(),
650            state_store: crate::state_store::StateStore::new(),
651            capabilities: crate::capabilities::WasmCapabilities::default(),
652        }
653    }
654
655    #[test]
656    fn test_recursion_guard_blocks_nested_calls() {
657        let state = make_state(1);
658        assert!(state.call_depth.load(std::sync::atomic::Ordering::Relaxed) > 0);
659    }
660
661    #[test]
662    fn test_recursion_guard_allows_initial_call() {
663        let state = make_state(0);
664        assert_eq!(
665            state.call_depth.load(std::sync::atomic::Ordering::Relaxed),
666            0
667        );
668    }
669
670    #[test]
671    fn test_get_property_string_value() {
672        let mut state = make_state(0);
673        state
674            .properties
675            .insert("key".to_string(), Value::String("value".to_string()));
676        assert_eq!(
677            state.get_property_impl("key".to_string()),
678            Some("value".to_string())
679        );
680    }
681
682    #[test]
683    fn test_get_property_missing_key() {
684        let state = make_state(0);
685        assert!(!state.properties.contains_key("missing"));
686    }
687
688    #[test]
689    fn test_set_property_json_value() {
690        let mut state = make_state(0);
691        state.set_property_impl("json_key".to_string(), "{\"nested\":true}".to_string());
692        assert!(state.properties.get("json_key").unwrap().is_object());
693    }
694
695    #[test]
696    fn test_uri_scheme_parsing() {
697        assert_eq!("direct".split(':').next().unwrap_or(""), "direct");
698        assert_eq!("log:info".split(':').next().unwrap_or(""), "log");
699        assert_eq!("noscheme".split(':').next().unwrap_or(""), "noscheme");
700        assert_eq!("".split(':').next().unwrap_or(""), "");
701    }
702
703    #[test]
704    fn test_camel_call_does_not_panic_inside_tokio_runtime() {
705        let mut state = make_state(0);
706        let result = state.camel_call_gate("noscheme".to_string());
707        assert!(
708            result.is_err(),
709            "should return error for empty scheme, not panic"
710        );
711    }
712
713    // Capability gating (H4 / H5)
714    // -----------------------------------------------------------------------
715    // Default capabilities are fail-closed: empty scheme allowlist, host_kv
716    // disabled. Tests below verify that the gates fire BEFORE any side effect
717    // (registry lookup, state store mutation) so a denied call cannot leak
718    // information or persist data.
719
720    #[test]
721    fn test_camel_call_denied_without_capability() {
722        let mut state = make_state(0);
723        // Default capabilities: empty allowlist
724        let result = state.camel_call_gate("log:info".to_string());
725        let err = result.unwrap_err();
726        let msg = format!("{:?}", err);
727        assert!(
728            msg.contains("denied"),
729            "expected 'denied' in error, got: {msg}"
730        );
731    }
732
733    #[test]
734    fn test_camel_poll_denied_without_capability() {
735        let state = make_state(0);
736        // The poll gate mirrors the call gate's scheme check.
737        let can_call = state.capabilities.can_call("file:foo");
738        assert!(!can_call, "default capabilities must deny 'file' scheme");
739    }
740
741    #[test]
742    fn test_host_store_denied_without_capability() {
743        let state = make_state(0);
744        let result = state.host_store_gate("key".to_string(), "val".to_string());
745        let err = result.unwrap_err();
746        let msg = format!("{:?}", err);
747        assert!(
748            msg.contains("host_kv"),
749            "expected 'host_kv' in error, got: {msg}"
750        );
751    }
752
753    #[test]
754    fn test_host_load_denied_without_capability() {
755        let state = make_state(0);
756        let result = state.host_load_gate("key".to_string());
757        let err = result.unwrap_err();
758        let msg = format!("{:?}", err);
759        assert!(
760            msg.contains("host_kv"),
761            "expected 'host_kv' in error, got: {msg}"
762        );
763    }
764
765    #[test]
766    fn test_camel_call_allowed_with_capability_passes_scheme_check() {
767        // Granting the scheme means the gate no longer denies — but with an
768        // empty registry, the next stage returns "component not found". This
769        // proves the gate runs BEFORE the registry lookup.
770        let mut state = make_state(0);
771        state.capabilities = crate::capabilities::WasmCapabilities::from_scheme_list("noscheme");
772        // The scheme check now passes; the gate increments call_depth.
773        let result = state.camel_call_gate("noscheme:foo".to_string());
774        assert!(
775            result.is_ok(),
776            "gate should pass when scheme is allowed; got: {result:?}"
777        );
778        state.release_call_depth();
779    }
780
781    #[test]
782    fn test_denied_capabilities_block_host_kv_via_field() {
783        // H5: policy worlds (denied caps) must have host_kv disabled — the
784        // gate is a single field check, not a per-key check.
785        let mut state = make_state(0);
786        state.capabilities = crate::capabilities::WasmCapabilities::denied();
787        assert!(!state.capabilities.host_kv);
788        // host_store and host_load both go through the same gate
789        assert!(
790            state
791                .host_store_gate("k".to_string(), "v".to_string())
792                .is_err()
793        );
794        assert!(state.host_load_gate("k".to_string()).is_err());
795    }
796
797    // ── DepthGuard: RAII recursion counter (C2, I2) ──────────────────────
798    //
799    // The guard increments `call_depth` on `new` and decrements on `Drop`.
800    // The three tests below cover the three behaviours the production code
801    // depends on: increment, drop-decrement, and recursion rejection.
802    //
803    // Per I2: the spec explicitly notes that "if testing through `Accessor`
804    // is too complex for unit tests, at minimum test the `DepthGuard`
805    // struct directly". The trait method signatures now use
806    // `&Accessor<WasmHostState, HasSelf<WasmHostState>>` (switched from
807    // hand-rolled HasData to wasmtime's built-in HasSelf). Accessor
808    // construction still requires `run_concurrent`, so DepthGuard unit
809    // tests remain the most practical verification path for the RAII
810    // recursion counter behaviour.
811
812    #[test]
813    fn test_depth_guard_increments_and_decrements() {
814        // new() must increment; Drop must decrement back to the original value.
815        let state = make_state(0);
816        let depth = &state.call_depth;
817        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
818        {
819            let _g = DepthGuard::new(depth).expect("first guard must succeed");
820            assert_eq!(
821                depth.load(std::sync::atomic::Ordering::SeqCst),
822                1,
823                "guard must increment to 1"
824            );
825        }
826        assert_eq!(
827            depth.load(std::sync::atomic::Ordering::SeqCst),
828            0,
829            "Drop must decrement back to 0"
830        );
831    }
832
833    #[test]
834    fn test_depth_guard_blocks_recursion() {
835        // A second guard while the first is alive must return None and must
836        // not leak an increment (the failed new() must roll back).
837        let state = make_state(0);
838        let depth = &state.call_depth;
839        let first = DepthGuard::new(depth).expect("first guard must succeed");
840        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
841        let second = DepthGuard::new(depth);
842        assert!(second.is_none(), "nested guard must be rejected");
843        // Drop the second (None) and verify the counter is still 1.
844        drop(second);
845        assert_eq!(
846            depth.load(std::sync::atomic::Ordering::SeqCst),
847            1,
848            "rejected guard must not leak an increment"
849        );
850        drop(first);
851        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
852    }
853
854    #[test]
855    fn test_depth_guard_rolls_back_on_rejection() {
856        // If new() is called when depth > 0, it must return None AND restore
857        // the counter (otherwise the increment would leak forever).
858        let state = make_state(0);
859        let depth = &state.call_depth;
860        let guard = DepthGuard::new(depth).expect("first guard");
861        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
862        // The second attempt increments-then-decrements; verify the transient
863        // bump does not affect the observed value after rejection.
864        let _rejected = DepthGuard::new(depth);
865        assert_eq!(
866            depth.load(std::sync::atomic::Ordering::SeqCst),
867            1,
868            "after rejection, counter must be unchanged"
869        );
870        drop(guard);
871        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
872    }
873
874    #[test]
875    fn test_depth_guard_decrements_on_explicit_drop() {
876        // Explicit drop() must decrement. This is the test the original
877        // manual inc/dec pattern was missing: a future that gets dropped
878        // between inc and dec would leak the counter.
879        let state = make_state(0);
880        let depth = &state.call_depth;
881        let guard = DepthGuard::new(depth).expect("first guard");
882        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
883        drop(guard);
884        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
885    }
886
887    // ── set_property_impl size limits (W2) ──────────────────────────────
888
889    #[test]
890    fn test_set_property_rejects_oversized_key() {
891        let mut state = make_state(0);
892        state.state_store = crate::state_store::StateStore::with_limits(256, 5, 65536);
893        state.set_property_impl("very_long_key_name".to_string(), "val".to_string());
894        assert!(
895            !state.properties.contains_key("very_long_key_name"),
896            "must not insert property when key exceeds max_key_bytes"
897        );
898    }
899
900    #[test]
901    fn test_set_property_rejects_oversized_value() {
902        let mut state = make_state(0);
903        state.state_store = crate::state_store::StateStore::with_limits(256, 1024, 5);
904        state.set_property_impl("k".to_string(), "this_value_is_way_too_long".to_string());
905        assert!(
906            !state.properties.contains_key("k"),
907            "must not insert property when value exceeds max_value_bytes"
908        );
909    }
910
911    #[test]
912    fn test_set_property_allows_within_bounds() {
913        let mut state = make_state(0);
914        state.state_store = crate::state_store::StateStore::with_limits(256, 1024, 65536);
915        state.set_property_impl("key".to_string(), "{\"x\":true}".to_string());
916        let val = state.properties.get("key");
917        assert!(
918            val.is_some(),
919            "must insert property when key and value are within limits"
920        );
921        if let Some(v) = val {
922            assert!(v.is_object(), "JSON value must be parsed as object");
923        }
924    }
925}