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<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
233macro_rules! impl_host_for_binding {
238 ($bindings_mod:ident) => {
239 impl crate::$bindings_mod::camel::plugin::host::Host for WasmHostState {}
242
243 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
302fn 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
393struct 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
420impl 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 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 let registry = store.with(|mut view| view.get().registry.clone());
446
447 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 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#[allow(dead_code)]
567impl WasmHostState {
568 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 #[test]
721 fn test_camel_call_denied_without_capability() {
722 let mut state = make_state(0);
723 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 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 let mut state = make_state(0);
771 state.capabilities = crate::capabilities::WasmCapabilities::from_scheme_list("noscheme");
772 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 let mut state = make_state(0);
786 state.capabilities = crate::capabilities::WasmCapabilities::denied();
787 assert!(!state.capabilities.host_kv);
788 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 #[test]
813 fn test_depth_guard_increments_and_decrements() {
814 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 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(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 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 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 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 #[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}