use serde_json::Value;
use wasmtime::component::{Access, Accessor, Linker};
use crate::runtime::WasmHostState;
impl crate::bindings::camel::plugin::host::Host for WasmHostState {}
impl crate::bindings::camel::plugin::host::HostWithStore<WasmHostState>
for wasmtime::component::HasSelf<WasmHostState>
{
async fn camel_call(
store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
uri: String,
payload: String,
) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
WasmHostState::camel_call_impl(store, uri, payload).await
}
async fn camel_poll(
store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
uri: String,
timeout_ms: u32,
) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
WasmHostState::camel_poll_impl(store, uri, timeout_ms).await
}
fn get_property(
mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
key: String,
) -> Option<String> {
store.get().get_property_impl(key)
}
fn set_property(
mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
key: String,
value: String,
) {
store.get().set_property_impl(key, value)
}
fn host_store(
mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
key: String,
value: String,
) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
WasmHostState::host_store_impl(store.get(), key, value)
}
fn host_load(
mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
key: String,
) -> Result<Option<String>, crate::bindings::camel::plugin::types::WasmError> {
WasmHostState::host_load_impl(store.get(), key)
}
}
pub fn add_to_linker(linker: &mut Linker<WasmHostState>) -> Result<(), wasmtime::Error> {
crate::bindings::camel::plugin::host::add_to_linker::<
WasmHostState,
wasmtime::component::HasSelf<WasmHostState>,
>(linker, |state| state)
}
async fn run_async_call(
registry: std::sync::Arc<std::sync::Mutex<camel_core::Registry>>,
uri: String,
payload: String,
) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
use tower::ServiceExt;
let scheme = uri.split(':').next().unwrap_or("").to_string();
if scheme.is_empty() {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"invalid URI (no scheme): {}",
uri
)),
);
}
let component = {
let guard = registry.lock().map_err(|e| {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"registry lock poisoned: {}",
e
))
})?;
guard.get(&scheme).ok_or_else(|| {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"component not found for scheme: {}",
scheme
))
})?
};
let endpoint = component
.create_endpoint(&uri, &camel_component_api::NoOpComponentContext)
.map_err(|e| {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"create_endpoint failed: {}",
e
))
})?;
let rt: std::sync::Arc<dyn camel_component_api::RuntimeObservability> =
std::sync::Arc::new(camel_component_api::NoOpComponentContext);
let producer = endpoint
.create_producer(rt, &camel_api::ProducerContext::new())
.map_err(|e| {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"create_producer failed: {}",
e
))
})?;
let exchange =
camel_api::Exchange::new(camel_api::Message::new(camel_api::Body::Text(payload)));
let result = producer.oneshot(exchange).await.map_err(|e| {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"endpoint call failed: {}",
e
))
})?;
let body_str = match &result.output {
Some(msg) => match &msg.body {
camel_api::Body::Text(s) => s.clone(),
camel_api::Body::Json(v) => v.to_string(),
camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
camel_api::Body::Xml(s) => s.clone(),
camel_api::Body::Empty => String::new(),
camel_api::Body::Stream(_) => "<stream>".to_string(),
},
None => match &result.input.body {
camel_api::Body::Text(s) => s.clone(),
camel_api::Body::Json(v) => v.to_string(),
camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
camel_api::Body::Xml(s) => s.clone(),
camel_api::Body::Empty => String::new(),
camel_api::Body::Stream(_) => "<stream>".to_string(),
},
};
Ok(body_str)
}
async fn run_async_poll(
registry: std::sync::Arc<std::sync::Mutex<camel_core::Registry>>,
uri: String,
timeout_ms: u32,
) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
let scheme = uri.split(':').next().unwrap_or("").to_string();
if scheme.is_empty() {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"invalid URI (no scheme): {}",
uri
)),
);
}
let component = {
let guard = registry.lock().map_err(|e| {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"registry lock poisoned: {}",
e
))
})?;
guard.get(&scheme).ok_or_else(|| {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"component not found for scheme: {}",
scheme
))
})?
};
let endpoint = component
.create_endpoint(&uri, &camel_component_api::NoOpComponentContext)
.map_err(|e| {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"create_endpoint failed: {}",
e
))
})?;
let mut poller = endpoint.polling_consumer().ok_or_else(|| {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"camel_poll requires a component that supports polling consumers (scheme: {})",
scheme
))
})?;
let exchange = poller
.receive(std::time::Duration::from_millis(timeout_ms as u64))
.await
.map_err(|e| {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"poll failed: {}",
e
))
})?;
let body_str = match exchange {
Some(ex) => {
let bytes = ex
.input
.body
.into_bytes(10 * 1024 * 1024)
.await
.map_err(|e| {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"body read failed: {}",
e
))
})?;
String::from_utf8_lossy(&bytes).to_string()
}
None => {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"no message received within {}ms timeout",
timeout_ms
)),
);
}
};
Ok(body_str)
}
macro_rules! impl_host_for_binding {
($bindings_mod:ident) => {
impl crate::$bindings_mod::camel::plugin::host::Host for WasmHostState {}
impl crate::$bindings_mod::camel::plugin::host::HostWithStore<WasmHostState>
for wasmtime::component::HasSelf<WasmHostState>
{
async fn camel_call(
store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
uri: String,
payload: String,
) -> Result<String, crate::$bindings_mod::camel::plugin::types::WasmError> {
WasmHostState::camel_call_impl(store, uri, payload)
.await
.map_err(map_plugin_wasm_error_to)
}
async fn camel_poll(
store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
uri: String,
timeout_ms: u32,
) -> Result<String, crate::$bindings_mod::camel::plugin::types::WasmError> {
WasmHostState::camel_poll_impl(store, uri, timeout_ms)
.await
.map_err(map_plugin_wasm_error_to)
}
fn get_property(
mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
key: String,
) -> Option<String> {
store.get().get_property_impl(key)
}
fn set_property(
mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
key: String,
value: String,
) {
store.get().set_property_impl(key, value)
}
fn host_store(
mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
key: String,
value: String,
) -> Result<(), crate::$bindings_mod::camel::plugin::types::WasmError> {
WasmHostState::host_store_impl(store.get(), key, value)
.map_err(map_plugin_wasm_error_to)
}
fn host_load(
mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
key: String,
) -> Result<Option<String>, crate::$bindings_mod::camel::plugin::types::WasmError> {
WasmHostState::host_load_impl(store.get(), key).map_err(map_plugin_wasm_error_to)
}
}
};
}
fn map_plugin_wasm_error_to<E: FromWasmErrorVariant>(
e: crate::bindings::camel::plugin::types::WasmError,
) -> E {
match e {
crate::bindings::camel::plugin::types::WasmError::ProcessorError(s) => {
E::from_processor_error(s)
}
crate::bindings::camel::plugin::types::WasmError::TypeConversion(s) => {
E::from_type_conversion(s)
}
crate::bindings::camel::plugin::types::WasmError::Io(s) => E::from_io(s),
crate::bindings::camel::plugin::types::WasmError::Timeout => E::from_timeout(),
}
}
trait FromWasmErrorVariant {
fn from_processor_error(s: String) -> Self;
fn from_type_conversion(s: String) -> Self;
fn from_io(s: String) -> Self;
fn from_timeout() -> Self;
}
impl FromWasmErrorVariant for crate::bindings::camel::plugin::types::WasmError {
fn from_processor_error(s: String) -> Self {
Self::ProcessorError(s)
}
fn from_type_conversion(s: String) -> Self {
Self::TypeConversion(s)
}
fn from_io(s: String) -> Self {
Self::Io(s)
}
fn from_timeout() -> Self {
Self::Timeout
}
}
impl FromWasmErrorVariant for crate::bean_bindings::camel::plugin::types::WasmError {
fn from_processor_error(s: String) -> Self {
Self::ProcessorError(s)
}
fn from_type_conversion(s: String) -> Self {
Self::TypeConversion(s)
}
fn from_io(s: String) -> Self {
Self::Io(s)
}
fn from_timeout() -> Self {
Self::Timeout
}
}
impl FromWasmErrorVariant for crate::security_policy_bindings::camel::plugin::types::WasmError {
fn from_processor_error(s: String) -> Self {
Self::ProcessorError(s)
}
fn from_type_conversion(s: String) -> Self {
Self::TypeConversion(s)
}
fn from_io(s: String) -> Self {
Self::Io(s)
}
fn from_timeout() -> Self {
Self::Timeout
}
}
pub fn add_bean_to_linker(linker: &mut Linker<WasmHostState>) -> Result<(), wasmtime::Error> {
crate::bean_bindings::camel::plugin::host::add_to_linker::<
WasmHostState,
wasmtime::component::HasSelf<WasmHostState>,
>(linker, |state| state)
}
impl_host_for_binding!(bean_bindings);
pub fn add_security_policy_to_linker(
linker: &mut Linker<WasmHostState>,
) -> Result<(), wasmtime::Error> {
crate::security_policy_bindings::camel::plugin::host::add_to_linker::<
WasmHostState,
wasmtime::component::HasSelf<WasmHostState>,
>(linker, |state| state)
}
impl_host_for_binding!(security_policy_bindings);
struct DepthGuard<'a> {
depth: &'a std::sync::atomic::AtomicUsize,
}
impl<'a> DepthGuard<'a> {
fn new(depth: &'a std::sync::atomic::AtomicUsize) -> Option<Self> {
let prev = depth.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if prev > 0 {
depth.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
return None;
}
Some(DepthGuard { depth })
}
}
impl Drop for DepthGuard<'_> {
fn drop(&mut self) {
self.depth.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
}
}
impl WasmHostState {
pub(crate) async fn camel_call_impl(
store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
uri: String,
payload: String,
) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
let scheme = uri.split(':').next().unwrap_or("").to_string();
let can_call = store.with(|mut view| view.get().capabilities.can_call(&scheme));
if !can_call {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"camel_call denied: scheme '{}' not in capability allowlist",
scheme
)),
);
}
let registry = store.with(|mut view| view.get().registry.clone());
let call_depth = store.with(|mut view| view.get().call_depth.clone());
let _depth_guard = match DepthGuard::new(call_depth.as_ref()) {
Some(g) => g,
None => {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(
"recursive wasm calls not supported".to_string(),
),
);
}
};
run_async_call(registry, uri, payload).await
}
pub(crate) async fn camel_poll_impl(
store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
uri: String,
timeout_ms: u32,
) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
let scheme = uri.split(':').next().unwrap_or("").to_string();
let can_call = store.with(|mut view| view.get().capabilities.can_call(&scheme));
if !can_call {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"camel_poll denied: scheme '{}' not in capability allowlist",
scheme
)),
);
}
let registry = store.with(|mut view| view.get().registry.clone());
let call_depth = store.with(|mut view| view.get().call_depth.clone());
let _depth_guard = match DepthGuard::new(call_depth.as_ref()) {
Some(g) => g,
None => {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(
"recursive wasm calls not supported".to_string(),
),
);
}
};
run_async_poll(registry, uri, timeout_ms).await
}
pub(crate) fn get_property_impl(&self, key: String) -> Option<String> {
self.properties.get(&key).map(|v| match v {
Value::String(s) => s.clone(),
other => other.to_string(),
})
}
pub(crate) fn set_property_impl(&mut self, key: String, value: String) {
let parsed = serde_json::from_str::<Value>(&value).unwrap_or(Value::String(value));
self.properties.insert(key, parsed);
}
pub(crate) fn host_store_impl(
state: &mut WasmHostState,
key: String,
value: String,
) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
if !state.capabilities.host_kv {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(
"host_store denied: host_kv capability not granted".to_string(),
),
);
}
state
.state_store
.store(&key, &value)
.map_err(crate::bindings::camel::plugin::types::WasmError::Io)
}
pub(crate) fn host_load_impl(
state: &mut WasmHostState,
key: String,
) -> Result<Option<String>, crate::bindings::camel::plugin::types::WasmError> {
if !state.capabilities.host_kv {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(
"host_load denied: host_kv capability not granted".to_string(),
),
);
}
state
.state_store
.load(&key)
.map_err(crate::bindings::camel::plugin::types::WasmError::Io)
}
}
#[allow(dead_code)]
impl WasmHostState {
pub(crate) fn camel_call_gate(
&mut self,
uri: String,
) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
let scheme = uri.split(':').next().unwrap_or("").to_string();
if !self.capabilities.can_call(&scheme) {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
"camel_call denied: scheme '{}' not in capability allowlist",
scheme
)),
);
}
if self.call_depth.load(std::sync::atomic::Ordering::Relaxed) > 0 {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(
"recursive wasm calls not supported".to_string(),
),
);
}
self.call_depth
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
pub(crate) fn release_call_depth(&self) {
self.call_depth
.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn host_store_gate(
&self,
key: String,
value: String,
) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
if !self.capabilities.host_kv {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(
"host_store denied: host_kv capability not granted".to_string(),
),
);
}
self.state_store
.store(&key, &value)
.map_err(crate::bindings::camel::plugin::types::WasmError::Io)
}
pub(crate) fn host_load_gate(
&self,
key: String,
) -> Result<Option<String>, crate::bindings::camel::plugin::types::WasmError> {
if !self.capabilities.host_kv {
return Err(
crate::bindings::camel::plugin::types::WasmError::ProcessorError(
"host_load denied: host_kv capability not granted".to_string(),
),
);
}
self.state_store
.load(&key)
.map_err(crate::bindings::camel::plugin::types::WasmError::Io)
}
}
#[cfg(test)]
mod tests {
use super::*;
use camel_core::Registry;
use std::collections::HashMap;
use std::sync::Arc;
fn make_state(call_depth: usize) -> WasmHostState {
WasmHostState {
table: wasmtime::component::ResourceTable::new(),
wasi: wasmtime_wasi::WasiCtxBuilder::new()
.inherit_stderr()
.build(),
properties: HashMap::new(),
registry: Arc::new(std::sync::Mutex::new(Registry::new())),
call_depth: Arc::new(std::sync::atomic::AtomicUsize::new(call_depth)),
limits: wasmtime::StoreLimits::default(),
state_store: crate::state_store::StateStore::new(),
capabilities: crate::capabilities::WasmCapabilities::default(),
}
}
#[test]
fn test_recursion_guard_blocks_nested_calls() {
let state = make_state(1);
assert!(state.call_depth.load(std::sync::atomic::Ordering::Relaxed) > 0);
}
#[test]
fn test_recursion_guard_allows_initial_call() {
let state = make_state(0);
assert_eq!(
state.call_depth.load(std::sync::atomic::Ordering::Relaxed),
0
);
}
#[test]
fn test_get_property_string_value() {
let mut state = make_state(0);
state
.properties
.insert("key".to_string(), Value::String("value".to_string()));
assert_eq!(
state.get_property_impl("key".to_string()),
Some("value".to_string())
);
}
#[test]
fn test_get_property_missing_key() {
let state = make_state(0);
assert!(!state.properties.contains_key("missing"));
}
#[test]
fn test_set_property_json_value() {
let mut state = make_state(0);
state.set_property_impl("json_key".to_string(), "{\"nested\":true}".to_string());
assert!(state.properties.get("json_key").unwrap().is_object());
}
#[test]
fn test_uri_scheme_parsing() {
assert_eq!("direct".split(':').next().unwrap_or(""), "direct");
assert_eq!("log:info".split(':').next().unwrap_or(""), "log");
assert_eq!("noscheme".split(':').next().unwrap_or(""), "noscheme");
assert_eq!("".split(':').next().unwrap_or(""), "");
}
#[test]
fn test_camel_call_does_not_panic_inside_tokio_runtime() {
let mut state = make_state(0);
let result = state.camel_call_gate("noscheme".to_string());
assert!(
result.is_err(),
"should return error for empty scheme, not panic"
);
}
#[test]
fn test_camel_call_denied_without_capability() {
let mut state = make_state(0);
let result = state.camel_call_gate("log:info".to_string());
let err = result.unwrap_err();
let msg = format!("{:?}", err);
assert!(
msg.contains("denied"),
"expected 'denied' in error, got: {msg}"
);
}
#[test]
fn test_camel_poll_denied_without_capability() {
let state = make_state(0);
let can_call = state.capabilities.can_call("file:foo");
assert!(!can_call, "default capabilities must deny 'file' scheme");
}
#[test]
fn test_host_store_denied_without_capability() {
let state = make_state(0);
let result = state.host_store_gate("key".to_string(), "val".to_string());
let err = result.unwrap_err();
let msg = format!("{:?}", err);
assert!(
msg.contains("host_kv"),
"expected 'host_kv' in error, got: {msg}"
);
}
#[test]
fn test_host_load_denied_without_capability() {
let state = make_state(0);
let result = state.host_load_gate("key".to_string());
let err = result.unwrap_err();
let msg = format!("{:?}", err);
assert!(
msg.contains("host_kv"),
"expected 'host_kv' in error, got: {msg}"
);
}
#[test]
fn test_camel_call_allowed_with_capability_passes_scheme_check() {
let mut state = make_state(0);
state.capabilities = crate::capabilities::WasmCapabilities::from_scheme_list("noscheme");
let result = state.camel_call_gate("noscheme:foo".to_string());
assert!(
result.is_ok(),
"gate should pass when scheme is allowed; got: {result:?}"
);
state.release_call_depth();
}
#[test]
fn test_denied_capabilities_block_host_kv_via_field() {
let mut state = make_state(0);
state.capabilities = crate::capabilities::WasmCapabilities::denied();
assert!(!state.capabilities.host_kv);
assert!(
state
.host_store_gate("k".to_string(), "v".to_string())
.is_err()
);
assert!(state.host_load_gate("k".to_string()).is_err());
}
#[test]
fn test_depth_guard_increments_and_decrements() {
let state = make_state(0);
let depth = &state.call_depth;
assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
{
let _g = DepthGuard::new(depth).expect("first guard must succeed");
assert_eq!(
depth.load(std::sync::atomic::Ordering::SeqCst),
1,
"guard must increment to 1"
);
}
assert_eq!(
depth.load(std::sync::atomic::Ordering::SeqCst),
0,
"Drop must decrement back to 0"
);
}
#[test]
fn test_depth_guard_blocks_recursion() {
let state = make_state(0);
let depth = &state.call_depth;
let first = DepthGuard::new(depth).expect("first guard must succeed");
assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
let second = DepthGuard::new(depth);
assert!(second.is_none(), "nested guard must be rejected");
drop(second);
assert_eq!(
depth.load(std::sync::atomic::Ordering::SeqCst),
1,
"rejected guard must not leak an increment"
);
drop(first);
assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
}
#[test]
fn test_depth_guard_rolls_back_on_rejection() {
let state = make_state(0);
let depth = &state.call_depth;
let guard = DepthGuard::new(depth).expect("first guard");
assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
let _rejected = DepthGuard::new(depth);
assert_eq!(
depth.load(std::sync::atomic::Ordering::SeqCst),
1,
"after rejection, counter must be unchanged"
);
drop(guard);
assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
}
#[test]
fn test_depth_guard_decrements_on_explicit_drop() {
let state = make_state(0);
let depth = &state.call_depth;
let guard = DepthGuard::new(depth).expect("first guard");
assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
drop(guard);
assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
}
}