1use serde_json::Value;
2use wasmtime::component::{Access, Accessor, Linker};
3
4use crate::runtime::WasmHostState;
5
6impl 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 crate::bindings::camel::plugin::host::add_to_linker::<
71 WasmHostState,
72 wasmtime::component::HasSelf<WasmHostState>,
73 >(linker, |state| state)
74}
75
76async fn run_async_call(
81 registry: std::sync::Arc<std::sync::Mutex<camel_core::Registry>>,
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 = {
97 let guard = registry.lock().map_err(|e| {
98 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
99 "registry lock poisoned: {}",
100 e
101 ))
102 })?;
103 guard.get(&scheme).ok_or_else(|| {
104 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
105 "component not found for scheme: {}",
106 scheme
107 ))
108 })?
109 };
110
111 let endpoint = component
112 .create_endpoint(&uri, &camel_component_api::NoOpComponentContext)
113 .map_err(|e| {
114 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
115 "create_endpoint failed: {}",
116 e
117 ))
118 })?;
119
120 let rt: std::sync::Arc<dyn camel_component_api::RuntimeObservability> =
121 std::sync::Arc::new(camel_component_api::NoOpComponentContext);
122 let producer = endpoint
123 .create_producer(rt, &camel_api::ProducerContext::new())
124 .map_err(|e| {
125 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
126 "create_producer failed: {}",
127 e
128 ))
129 })?;
130
131 let exchange =
132 camel_api::Exchange::new(camel_api::Message::new(camel_api::Body::Text(payload)));
133
134 let result = producer.oneshot(exchange).await.map_err(|e| {
135 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
136 "endpoint call failed: {}",
137 e
138 ))
139 })?;
140
141 let body_str = match &result.output {
142 Some(msg) => match &msg.body {
143 camel_api::Body::Text(s) => s.clone(),
144 camel_api::Body::Json(v) => v.to_string(),
145 camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
146 camel_api::Body::Xml(s) => s.clone(),
147 camel_api::Body::Empty => String::new(),
148 camel_api::Body::Stream(_) => "<stream>".to_string(),
149 },
150 None => match &result.input.body {
151 camel_api::Body::Text(s) => s.clone(),
152 camel_api::Body::Json(v) => v.to_string(),
153 camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
154 camel_api::Body::Xml(s) => s.clone(),
155 camel_api::Body::Empty => String::new(),
156 camel_api::Body::Stream(_) => "<stream>".to_string(),
157 },
158 };
159
160 Ok(body_str)
161}
162
163async fn run_async_poll(
164 registry: std::sync::Arc<std::sync::Mutex<camel_core::Registry>>,
165 uri: String,
166 timeout_ms: u32,
167) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
168 let scheme = uri.split(':').next().unwrap_or("").to_string();
169 if scheme.is_empty() {
170 return Err(
171 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
172 "invalid URI (no scheme): {}",
173 uri
174 )),
175 );
176 }
177
178 let component = {
179 let guard = registry.lock().map_err(|e| {
180 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
181 "registry lock poisoned: {}",
182 e
183 ))
184 })?;
185 guard.get(&scheme).ok_or_else(|| {
186 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
187 "component not found for scheme: {}",
188 scheme
189 ))
190 })?
191 };
192
193 let endpoint = component
194 .create_endpoint(&uri, &camel_component_api::NoOpComponentContext)
195 .map_err(|e| {
196 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
197 "create_endpoint failed: {}",
198 e
199 ))
200 })?;
201
202 let mut poller = endpoint.polling_consumer().ok_or_else(|| {
203 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
204 "camel_poll requires a component that supports polling consumers (scheme: {})",
205 scheme
206 ))
207 })?;
208
209 let exchange = poller
210 .receive(std::time::Duration::from_millis(timeout_ms as u64))
211 .await
212 .map_err(|e| {
213 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
214 "poll failed: {}",
215 e
216 ))
217 })?;
218
219 let body_str = match exchange {
220 Some(ex) => {
221 let bytes = ex
222 .input
223 .body
224 .into_bytes(10 * 1024 * 1024)
225 .await
226 .map_err(|e| {
227 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
228 "body read failed: {}",
229 e
230 ))
231 })?;
232 String::from_utf8_lossy(&bytes).to_string()
233 }
234 None => {
235 return Err(
236 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
237 "no message received within {}ms timeout",
238 timeout_ms
239 )),
240 );
241 }
242 };
243
244 Ok(body_str)
245}
246
247macro_rules! impl_host_for_binding {
252 ($bindings_mod:ident) => {
253 impl crate::$bindings_mod::camel::plugin::host::Host for WasmHostState {}
256
257 impl crate::$bindings_mod::camel::plugin::host::HostWithStore<WasmHostState>
260 for wasmtime::component::HasSelf<WasmHostState>
261 {
262 async fn camel_call(
263 store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
264 uri: String,
265 payload: String,
266 ) -> Result<String, crate::$bindings_mod::camel::plugin::types::WasmError> {
267 WasmHostState::camel_call_impl(store, uri, payload)
268 .await
269 .map_err(map_plugin_wasm_error_to)
270 }
271
272 async fn camel_poll(
273 store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
274 uri: String,
275 timeout_ms: u32,
276 ) -> Result<String, crate::$bindings_mod::camel::plugin::types::WasmError> {
277 WasmHostState::camel_poll_impl(store, uri, timeout_ms)
278 .await
279 .map_err(map_plugin_wasm_error_to)
280 }
281
282 fn get_property(
283 mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
284 key: String,
285 ) -> Option<String> {
286 store.get().get_property_impl(key)
287 }
288
289 fn set_property(
290 mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
291 key: String,
292 value: String,
293 ) {
294 store.get().set_property_impl(key, value)
295 }
296
297 fn host_store(
298 mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
299 key: String,
300 value: String,
301 ) -> Result<(), crate::$bindings_mod::camel::plugin::types::WasmError> {
302 WasmHostState::host_store_impl(store.get(), key, value)
303 .map_err(map_plugin_wasm_error_to)
304 }
305
306 fn host_load(
307 mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
308 key: String,
309 ) -> Result<Option<String>, crate::$bindings_mod::camel::plugin::types::WasmError> {
310 WasmHostState::host_load_impl(store.get(), key).map_err(map_plugin_wasm_error_to)
311 }
312 }
313 };
314}
315
316fn map_plugin_wasm_error_to<E: FromWasmErrorVariant>(
321 e: crate::bindings::camel::plugin::types::WasmError,
322) -> E {
323 match e {
324 crate::bindings::camel::plugin::types::WasmError::ProcessorError(s) => {
325 E::from_processor_error(s)
326 }
327 crate::bindings::camel::plugin::types::WasmError::TypeConversion(s) => {
328 E::from_type_conversion(s)
329 }
330 crate::bindings::camel::plugin::types::WasmError::Io(s) => E::from_io(s),
331 crate::bindings::camel::plugin::types::WasmError::Timeout => E::from_timeout(),
332 }
333}
334
335trait FromWasmErrorVariant {
336 fn from_processor_error(s: String) -> Self;
337 fn from_type_conversion(s: String) -> Self;
338 fn from_io(s: String) -> Self;
339 fn from_timeout() -> Self;
340}
341
342impl FromWasmErrorVariant for crate::bindings::camel::plugin::types::WasmError {
343 fn from_processor_error(s: String) -> Self {
344 Self::ProcessorError(s)
345 }
346 fn from_type_conversion(s: String) -> Self {
347 Self::TypeConversion(s)
348 }
349 fn from_io(s: String) -> Self {
350 Self::Io(s)
351 }
352 fn from_timeout() -> Self {
353 Self::Timeout
354 }
355}
356
357impl FromWasmErrorVariant for crate::bean_bindings::camel::plugin::types::WasmError {
358 fn from_processor_error(s: String) -> Self {
359 Self::ProcessorError(s)
360 }
361 fn from_type_conversion(s: String) -> Self {
362 Self::TypeConversion(s)
363 }
364 fn from_io(s: String) -> Self {
365 Self::Io(s)
366 }
367 fn from_timeout() -> Self {
368 Self::Timeout
369 }
370}
371
372impl FromWasmErrorVariant for crate::security_policy_bindings::camel::plugin::types::WasmError {
373 fn from_processor_error(s: String) -> Self {
374 Self::ProcessorError(s)
375 }
376 fn from_type_conversion(s: String) -> Self {
377 Self::TypeConversion(s)
378 }
379 fn from_io(s: String) -> Self {
380 Self::Io(s)
381 }
382 fn from_timeout() -> Self {
383 Self::Timeout
384 }
385}
386
387pub fn add_bean_to_linker(linker: &mut Linker<WasmHostState>) -> Result<(), wasmtime::Error> {
388 crate::bean_bindings::camel::plugin::host::add_to_linker::<
389 WasmHostState,
390 wasmtime::component::HasSelf<WasmHostState>,
391 >(linker, |state| state)
392}
393
394impl_host_for_binding!(bean_bindings);
395
396pub fn add_security_policy_to_linker(
397 linker: &mut Linker<WasmHostState>,
398) -> Result<(), wasmtime::Error> {
399 crate::security_policy_bindings::camel::plugin::host::add_to_linker::<
400 WasmHostState,
401 wasmtime::component::HasSelf<WasmHostState>,
402 >(linker, |state| state)
403}
404
405impl_host_for_binding!(security_policy_bindings);
406
407struct DepthGuard<'a> {
414 depth: &'a std::sync::atomic::AtomicUsize,
415}
416
417impl<'a> DepthGuard<'a> {
418 fn new(depth: &'a std::sync::atomic::AtomicUsize) -> Option<Self> {
419 let prev = depth.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
420 if prev > 0 {
421 depth.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
422 return None;
423 }
424 Some(DepthGuard { depth })
425 }
426}
427
428impl Drop for DepthGuard<'_> {
429 fn drop(&mut self) {
430 self.depth.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
431 }
432}
433
434impl WasmHostState {
440 pub(crate) async fn camel_call_impl(
441 store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
442 uri: String,
443 payload: String,
444 ) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
445 let scheme = uri.split(':').next().unwrap_or("").to_string();
447 let can_call = store.with(|mut view| view.get().capabilities.can_call(&scheme));
448 if !can_call {
449 return Err(
450 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
451 "camel_call denied: scheme '{}' not in capability allowlist",
452 scheme
453 )),
454 );
455 }
456
457 let registry = store.with(|mut view| view.get().registry.clone());
460
461 let call_depth = store.with(|mut view| view.get().call_depth.clone());
469 let _depth_guard = match DepthGuard::new(call_depth.as_ref()) {
470 Some(g) => g,
471 None => {
472 return Err(
473 crate::bindings::camel::plugin::types::WasmError::ProcessorError(
474 "recursive wasm calls not supported".to_string(),
475 ),
476 );
477 }
478 };
479
480 run_async_call(registry, uri, payload).await
481 }
482
483 pub(crate) async fn camel_poll_impl(
484 store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
485 uri: String,
486 timeout_ms: u32,
487 ) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
488 let scheme = uri.split(':').next().unwrap_or("").to_string();
489 let can_call = store.with(|mut view| view.get().capabilities.can_call(&scheme));
490 if !can_call {
491 return Err(
492 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
493 "camel_poll denied: scheme '{}' not in capability allowlist",
494 scheme
495 )),
496 );
497 }
498
499 let registry = store.with(|mut view| view.get().registry.clone());
501 let call_depth = store.with(|mut view| view.get().call_depth.clone());
502 let _depth_guard = match DepthGuard::new(call_depth.as_ref()) {
503 Some(g) => g,
504 None => {
505 return Err(
506 crate::bindings::camel::plugin::types::WasmError::ProcessorError(
507 "recursive wasm calls not supported".to_string(),
508 ),
509 );
510 }
511 };
512
513 run_async_poll(registry, uri, timeout_ms).await
514 }
515
516 pub(crate) fn get_property_impl(&self, key: String) -> Option<String> {
517 self.properties.get(&key).map(|v| match v {
518 Value::String(s) => s.clone(),
519 other => other.to_string(),
520 })
521 }
522
523 pub(crate) fn set_property_impl(&mut self, key: String, value: String) {
524 let parsed = serde_json::from_str::<Value>(&value).unwrap_or(Value::String(value));
525 self.properties.insert(key, parsed);
526 }
527
528 pub(crate) fn host_store_impl(
529 state: &mut WasmHostState,
530 key: String,
531 value: String,
532 ) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
533 if !state.capabilities.host_kv {
534 return Err(
535 crate::bindings::camel::plugin::types::WasmError::ProcessorError(
536 "host_store denied: host_kv capability not granted".to_string(),
537 ),
538 );
539 }
540 state
541 .state_store
542 .store(&key, &value)
543 .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
544 }
545
546 pub(crate) fn host_load_impl(
547 state: &mut WasmHostState,
548 key: String,
549 ) -> Result<Option<String>, crate::bindings::camel::plugin::types::WasmError> {
550 if !state.capabilities.host_kv {
551 return Err(
552 crate::bindings::camel::plugin::types::WasmError::ProcessorError(
553 "host_load denied: host_kv capability not granted".to_string(),
554 ),
555 );
556 }
557 state
558 .state_store
559 .load(&key)
560 .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
561 }
562}
563
564#[allow(dead_code)]
575impl WasmHostState {
576 pub(crate) fn camel_call_gate(
581 &mut self,
582 uri: String,
583 ) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
584 let scheme = uri.split(':').next().unwrap_or("").to_string();
585 if !self.capabilities.can_call(&scheme) {
586 return Err(
587 crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
588 "camel_call denied: scheme '{}' not in capability allowlist",
589 scheme
590 )),
591 );
592 }
593 if self.call_depth.load(std::sync::atomic::Ordering::Relaxed) > 0 {
594 return Err(
595 crate::bindings::camel::plugin::types::WasmError::ProcessorError(
596 "recursive wasm calls not supported".to_string(),
597 ),
598 );
599 }
600 self.call_depth
601 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
602 Ok(())
603 }
604
605 pub(crate) fn release_call_depth(&self) {
606 self.call_depth
607 .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
608 }
609
610 pub(crate) fn host_store_gate(
611 &self,
612 key: String,
613 value: String,
614 ) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
615 if !self.capabilities.host_kv {
616 return Err(
617 crate::bindings::camel::plugin::types::WasmError::ProcessorError(
618 "host_store denied: host_kv capability not granted".to_string(),
619 ),
620 );
621 }
622 self.state_store
623 .store(&key, &value)
624 .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
625 }
626
627 pub(crate) fn host_load_gate(
628 &self,
629 key: String,
630 ) -> Result<Option<String>, crate::bindings::camel::plugin::types::WasmError> {
631 if !self.capabilities.host_kv {
632 return Err(
633 crate::bindings::camel::plugin::types::WasmError::ProcessorError(
634 "host_load denied: host_kv capability not granted".to_string(),
635 ),
636 );
637 }
638 self.state_store
639 .load(&key)
640 .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647 use camel_core::Registry;
648 use std::collections::HashMap;
649 use std::sync::Arc;
650
651 fn make_state(call_depth: usize) -> WasmHostState {
652 WasmHostState {
653 table: wasmtime::component::ResourceTable::new(),
654 wasi: wasmtime_wasi::WasiCtxBuilder::new()
655 .inherit_stderr()
656 .build(),
657 properties: HashMap::new(),
658 registry: Arc::new(std::sync::Mutex::new(Registry::new())),
659 call_depth: Arc::new(std::sync::atomic::AtomicUsize::new(call_depth)),
660 limits: wasmtime::StoreLimits::default(),
661 state_store: crate::state_store::StateStore::new(),
662 capabilities: crate::capabilities::WasmCapabilities::default(),
663 }
664 }
665
666 #[test]
667 fn test_recursion_guard_blocks_nested_calls() {
668 let state = make_state(1);
669 assert!(state.call_depth.load(std::sync::atomic::Ordering::Relaxed) > 0);
670 }
671
672 #[test]
673 fn test_recursion_guard_allows_initial_call() {
674 let state = make_state(0);
675 assert_eq!(
676 state.call_depth.load(std::sync::atomic::Ordering::Relaxed),
677 0
678 );
679 }
680
681 #[test]
682 fn test_get_property_string_value() {
683 let mut state = make_state(0);
684 state
685 .properties
686 .insert("key".to_string(), Value::String("value".to_string()));
687 assert_eq!(
688 state.get_property_impl("key".to_string()),
689 Some("value".to_string())
690 );
691 }
692
693 #[test]
694 fn test_get_property_missing_key() {
695 let state = make_state(0);
696 assert!(!state.properties.contains_key("missing"));
697 }
698
699 #[test]
700 fn test_set_property_json_value() {
701 let mut state = make_state(0);
702 state.set_property_impl("json_key".to_string(), "{\"nested\":true}".to_string());
703 assert!(state.properties.get("json_key").unwrap().is_object());
704 }
705
706 #[test]
707 fn test_uri_scheme_parsing() {
708 assert_eq!("direct".split(':').next().unwrap_or(""), "direct");
709 assert_eq!("log:info".split(':').next().unwrap_or(""), "log");
710 assert_eq!("noscheme".split(':').next().unwrap_or(""), "noscheme");
711 assert_eq!("".split(':').next().unwrap_or(""), "");
712 }
713
714 #[test]
715 fn test_camel_call_does_not_panic_inside_tokio_runtime() {
716 let mut state = make_state(0);
717 let result = state.camel_call_gate("noscheme".to_string());
718 assert!(
719 result.is_err(),
720 "should return error for empty scheme, not panic"
721 );
722 }
723
724 #[test]
732 fn test_camel_call_denied_without_capability() {
733 let mut state = make_state(0);
734 let result = state.camel_call_gate("log:info".to_string());
736 let err = result.unwrap_err();
737 let msg = format!("{:?}", err);
738 assert!(
739 msg.contains("denied"),
740 "expected 'denied' in error, got: {msg}"
741 );
742 }
743
744 #[test]
745 fn test_camel_poll_denied_without_capability() {
746 let state = make_state(0);
747 let can_call = state.capabilities.can_call("file:foo");
749 assert!(!can_call, "default capabilities must deny 'file' scheme");
750 }
751
752 #[test]
753 fn test_host_store_denied_without_capability() {
754 let state = make_state(0);
755 let result = state.host_store_gate("key".to_string(), "val".to_string());
756 let err = result.unwrap_err();
757 let msg = format!("{:?}", err);
758 assert!(
759 msg.contains("host_kv"),
760 "expected 'host_kv' in error, got: {msg}"
761 );
762 }
763
764 #[test]
765 fn test_host_load_denied_without_capability() {
766 let state = make_state(0);
767 let result = state.host_load_gate("key".to_string());
768 let err = result.unwrap_err();
769 let msg = format!("{:?}", err);
770 assert!(
771 msg.contains("host_kv"),
772 "expected 'host_kv' in error, got: {msg}"
773 );
774 }
775
776 #[test]
777 fn test_camel_call_allowed_with_capability_passes_scheme_check() {
778 let mut state = make_state(0);
782 state.capabilities = crate::capabilities::WasmCapabilities::from_scheme_list("noscheme");
783 let result = state.camel_call_gate("noscheme:foo".to_string());
785 assert!(
786 result.is_ok(),
787 "gate should pass when scheme is allowed; got: {result:?}"
788 );
789 state.release_call_depth();
790 }
791
792 #[test]
793 fn test_denied_capabilities_block_host_kv_via_field() {
794 let mut state = make_state(0);
797 state.capabilities = crate::capabilities::WasmCapabilities::denied();
798 assert!(!state.capabilities.host_kv);
799 assert!(
801 state
802 .host_store_gate("k".to_string(), "v".to_string())
803 .is_err()
804 );
805 assert!(state.host_load_gate("k".to_string()).is_err());
806 }
807
808 #[test]
824 fn test_depth_guard_increments_and_decrements() {
825 let state = make_state(0);
827 let depth = &state.call_depth;
828 assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
829 {
830 let _g = DepthGuard::new(depth).expect("first guard must succeed");
831 assert_eq!(
832 depth.load(std::sync::atomic::Ordering::SeqCst),
833 1,
834 "guard must increment to 1"
835 );
836 }
837 assert_eq!(
838 depth.load(std::sync::atomic::Ordering::SeqCst),
839 0,
840 "Drop must decrement back to 0"
841 );
842 }
843
844 #[test]
845 fn test_depth_guard_blocks_recursion() {
846 let state = make_state(0);
849 let depth = &state.call_depth;
850 let first = DepthGuard::new(depth).expect("first guard must succeed");
851 assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
852 let second = DepthGuard::new(depth);
853 assert!(second.is_none(), "nested guard must be rejected");
854 drop(second);
856 assert_eq!(
857 depth.load(std::sync::atomic::Ordering::SeqCst),
858 1,
859 "rejected guard must not leak an increment"
860 );
861 drop(first);
862 assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
863 }
864
865 #[test]
866 fn test_depth_guard_rolls_back_on_rejection() {
867 let state = make_state(0);
870 let depth = &state.call_depth;
871 let guard = DepthGuard::new(depth).expect("first guard");
872 assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
873 let _rejected = DepthGuard::new(depth);
876 assert_eq!(
877 depth.load(std::sync::atomic::Ordering::SeqCst),
878 1,
879 "after rejection, counter must be unchanged"
880 );
881 drop(guard);
882 assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
883 }
884
885 #[test]
886 fn test_depth_guard_decrements_on_explicit_drop() {
887 let state = make_state(0);
891 let depth = &state.call_depth;
892 let guard = DepthGuard::new(depth).expect("first guard");
893 assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
894 drop(guard);
895 assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
896 }
897}