use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::RwLock;
use wasmtime::{Engine, Instance, Linker, Module, Store, TypedFunc, Memory};
use super::config::PluginRuntimeConfig;
use super::host_functions::HostFunctionRegistry;
use super::host_imports::{register_crypto_imports, register_kv_imports, KvBackend, StoreCtx};
use super::sandbox::{PluginSandbox, SecurityPolicy, ResourceLimits};
use super::{
AuthRequest, AuthResult, HookType, PluginMetadata, PreQueryResult,
QueryContext, RouteResult,
};
#[derive(Debug, Clone)]
pub enum PluginError {
LoadError(String),
InstantiationError(String),
ExecutionError(String),
Timeout(String),
MemoryExceeded(String),
SecurityViolation(String),
InvalidManifest(String),
HookNotFound(String),
RuntimeError(String),
}
impl std::fmt::Display for PluginError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PluginError::LoadError(msg) => write!(f, "Load error: {}", msg),
PluginError::InstantiationError(msg) => write!(f, "Instantiation error: {}", msg),
PluginError::ExecutionError(msg) => write!(f, "Execution error: {}", msg),
PluginError::Timeout(msg) => write!(f, "Timeout: {}", msg),
PluginError::MemoryExceeded(msg) => write!(f, "Memory exceeded: {}", msg),
PluginError::SecurityViolation(msg) => write!(f, "Security violation: {}", msg),
PluginError::InvalidManifest(msg) => write!(f, "Invalid manifest: {}", msg),
PluginError::HookNotFound(msg) => write!(f, "Hook not found: {}", msg),
PluginError::RuntimeError(msg) => write!(f, "Runtime error: {}", msg),
}
}
}
impl std::error::Error for PluginError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PluginState {
Loading,
Running,
Paused,
Error(String),
Unloading,
}
pub struct LoadedPlugin {
pub metadata: PluginMetadata,
pub state: PluginState,
pub path: PathBuf,
module: Module,
sandbox: PluginSandbox,
instance_data: RwLock<PluginInstanceData>,
loaded_at: Instant,
last_invoked: RwLock<Option<Instant>>,
invocation_count: std::sync::atomic::AtomicU64,
}
struct PluginInstanceData {
memory_used: usize,
fuel_consumed: u64,
state: HashMap<String, Vec<u8>>,
}
impl LoadedPlugin {
pub fn new(
metadata: PluginMetadata,
path: PathBuf,
module: Module,
sandbox: PluginSandbox,
) -> Self {
Self {
metadata,
state: PluginState::Running,
path,
module,
sandbox,
instance_data: RwLock::new(PluginInstanceData {
memory_used: 0,
fuel_consumed: 0,
state: HashMap::new(),
}),
loaded_at: Instant::now(),
last_invoked: RwLock::new(None),
invocation_count: std::sync::atomic::AtomicU64::new(0),
}
}
pub(crate) fn module(&self) -> &Module {
&self.module
}
pub fn memory_used(&self) -> usize {
self.instance_data.read().memory_used
}
pub fn invocation_count(&self) -> u64 {
self.invocation_count.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn uptime(&self) -> Duration {
self.loaded_at.elapsed()
}
pub fn last_invoked(&self) -> Option<Instant> {
*self.last_invoked.read()
}
pub fn record_invocation(&self) {
self.invocation_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
*self.last_invoked.write() = Some(Instant::now());
}
}
pub struct WasmPluginRuntime {
config: PluginRuntimeConfig,
engine: Engine,
host_functions: Arc<HostFunctionRegistry>,
kv: KvBackend,
module_cache: RwLock<HashMap<PathBuf, Module>>,
default_policy: SecurityPolicy,
created_at: Instant,
}
impl WasmPluginRuntime {
pub fn new(config: &PluginRuntimeConfig) -> Result<Self, PluginError> {
let host_functions = Arc::new(HostFunctionRegistry::new());
let mut engine_config = wasmtime::Config::new();
if config.fuel_metering {
engine_config.consume_fuel(true);
}
engine_config.epoch_interruption(true);
let engine = Engine::new(&engine_config).map_err(|e| {
PluginError::RuntimeError(format!("wasmtime engine init: {}", e))
})?;
let default_policy = SecurityPolicy {
allowed_hosts: vec!["localhost".to_string()],
allowed_paths: vec![config.plugin_dir.clone()],
max_memory: config.memory_limit,
max_execution_time: config.timeout,
allow_network: false,
allow_filesystem: false,
};
Ok(Self {
config: config.clone(),
engine,
host_functions,
kv: KvBackend::new(),
module_cache: RwLock::new(HashMap::new()),
default_policy,
created_at: Instant::now(),
})
}
pub fn kv(&self) -> &KvBackend {
&self.kv
}
pub(crate) fn engine(&self) -> &Engine {
&self.engine
}
pub fn config(&self) -> &PluginRuntimeConfig {
&self.config
}
pub fn instantiate(
&self,
manifest: &super::loader::PluginManifest,
wasm_bytes: &[u8],
) -> Result<LoadedPlugin, PluginError> {
if wasm_bytes.len() < 8 {
return Err(PluginError::LoadError("WASM module too small".to_string()));
}
if &wasm_bytes[0..4] != b"\x00asm" {
return Err(PluginError::LoadError("Invalid WASM magic number".to_string()));
}
let metadata = PluginMetadata {
name: manifest.name.clone(),
version: manifest.version.clone(),
description: manifest.description.clone(),
author: manifest.author.clone(),
hooks: manifest.hooks.clone(),
permissions: manifest.permissions.clone(),
min_memory: manifest.min_memory,
max_memory: manifest.max_memory.min(self.config.memory_limit),
};
let resource_limits = ResourceLimits {
max_memory: metadata.max_memory,
max_execution_time: self.config.timeout,
max_fuel: if self.config.fuel_metering {
Some(self.config.fuel_limit)
} else {
None
},
max_table_elements: 10000,
max_instances: 1,
};
let sandbox = PluginSandbox::new(
self.default_policy.clone(),
resource_limits,
manifest.permissions.clone(),
);
let module = Module::from_binary(&self.engine, wasm_bytes).map_err(|e| {
PluginError::InstantiationError(format!("wasmtime compile: {}", e))
})?;
{
let mut cache = self.module_cache.write();
cache.insert(manifest.path.clone(), module.clone());
}
Ok(LoadedPlugin::new(
metadata,
manifest.path.clone(),
module,
sandbox,
))
}
pub fn call_hook(
&self,
plugin: &LoadedPlugin,
hook: HookType,
args: &[u8],
) -> Result<Vec<u8>, PluginError> {
if !plugin.metadata.hooks.contains(&hook) {
return Err(PluginError::HookNotFound(format!(
"Plugin {} does not support hook {:?}",
plugin.metadata.name, hook
)));
}
if plugin.state != PluginState::Running {
return Err(PluginError::ExecutionError(format!(
"Plugin {} is not running (state: {:?})",
plugin.metadata.name, plugin.state
)));
}
plugin.record_invocation();
let store_ctx = StoreCtx {
plugin_name: plugin.metadata.name.clone(),
kv: self.kv.clone(),
};
let mut store: Store<StoreCtx> = Store::new(&self.engine, store_ctx);
if self.config.fuel_metering {
store.set_fuel(self.config.fuel_limit).map_err(|e| {
PluginError::RuntimeError(format!("set_fuel: {}", e))
})?;
}
store.set_epoch_deadline(u64::MAX);
let mut linker: Linker<StoreCtx> = Linker::new(&self.engine);
register_kv_imports(&mut linker)?;
register_crypto_imports(&mut linker)?;
let instance = linker
.instantiate(&mut store, &plugin.module)
.map_err(|e| {
PluginError::InstantiationError(format!(
"instantiate {}: {}",
plugin.metadata.name, e
))
})?;
let memory = instance.get_memory(&mut store, "memory").ok_or_else(|| {
PluginError::ExecutionError(format!(
"plugin {} does not export `memory`",
plugin.metadata.name
))
})?;
let alloc = get_typed::<_, i32, i32>(&instance, &mut store, "alloc")?;
let dealloc = get_typed::<_, (i32, i32), ()>(&instance, &mut store, "dealloc")?;
let in_len = args.len() as i32;
let in_ptr = alloc.call(&mut store, in_len).map_err(|e| {
PluginError::ExecutionError(format!("alloc({}): {}", in_len, e))
})?;
if in_len > 0 {
write_memory(&memory, &mut store, in_ptr, args)?;
}
let export_name = hook.export_name();
let result_bytes = match get_typed::<_, (i32, i32), i64>(&instance, &mut store, export_name) {
Ok(hook_fn) => {
let packed = hook_fn.call(&mut store, (in_ptr, in_len)).map_err(|e| {
PluginError::ExecutionError(format!(
"hook {} call: {}",
export_name, e
))
})?;
let out_ptr = (packed >> 32) as i32;
let out_len = (packed & 0xFFFF_FFFF) as i32;
if out_len > 0 {
let bytes = read_memory(&memory, &store, out_ptr, out_len)?;
let _ = dealloc.call(&mut store, (out_ptr, out_len));
bytes
} else {
Vec::new()
}
}
Err(_) => {
let observer = get_typed::<_, (i32, i32), ()>(
&instance,
&mut store,
export_name,
)?;
observer.call(&mut store, (in_ptr, in_len)).map_err(|e| {
PluginError::ExecutionError(format!(
"observer hook {} call: {}",
export_name, e
))
})?;
Vec::new()
}
};
let _ = dealloc.call(&mut store, (in_ptr, in_len));
if self.config.fuel_metering {
if let Ok(remaining) = store.get_fuel() {
let consumed = self.config.fuel_limit.saturating_sub(remaining);
plugin.instance_data.write().fuel_consumed = consumed;
}
}
plugin.instance_data.write().memory_used =
(memory.data_size(&store)) as usize;
Ok(result_bytes)
}
pub fn call_pre_query(
&self,
plugin: &LoadedPlugin,
ctx: &QueryContext,
) -> Result<PreQueryResult, PluginError> {
let args = serde_json::to_vec(ctx).map_err(|e| {
PluginError::ExecutionError(format!("Failed to serialize context: {}", e))
})?;
let result = self.call_hook(plugin, HookType::PreQuery, &args)?;
if result.is_empty() {
return Ok(PreQueryResult::Continue);
}
serde_json::from_slice(&result).map_err(|e| {
PluginError::ExecutionError(format!("Failed to deserialize result: {}", e))
})
}
pub fn call_authenticate(
&self,
plugin: &LoadedPlugin,
request: &AuthRequest,
) -> Result<AuthResult, PluginError> {
let args = serde_json::to_vec(request).map_err(|e| {
PluginError::ExecutionError(format!("Failed to serialize request: {}", e))
})?;
let result = self.call_hook(plugin, HookType::Authenticate, &args)?;
if result.is_empty() {
return Ok(AuthResult::Defer);
}
serde_json::from_slice(&result).map_err(|e| {
PluginError::ExecutionError(format!("Failed to deserialize result: {}", e))
})
}
pub fn call_route(
&self,
plugin: &LoadedPlugin,
ctx: &QueryContext,
) -> Result<RouteResult, PluginError> {
let args = serde_json::to_vec(ctx).map_err(|e| {
PluginError::ExecutionError(format!("Failed to serialize context: {}", e))
})?;
let result = self.call_hook(plugin, HookType::Route, &args)?;
if result.is_empty() {
return Ok(RouteResult::Default);
}
serde_json::from_slice(&result).map_err(|e| {
PluginError::ExecutionError(format!("Failed to deserialize result: {}", e))
})
}
pub fn stats(&self) -> RuntimeStats {
RuntimeStats {
uptime: self.created_at.elapsed(),
cached_modules: self.module_cache.read().len(),
fuel_metering_enabled: self.config.fuel_metering,
memory_limit: self.config.memory_limit,
timeout: self.config.timeout,
}
}
}
#[derive(Debug, Clone)]
pub struct RuntimeStats {
pub uptime: Duration,
pub cached_modules: usize,
pub fuel_metering_enabled: bool,
pub memory_limit: usize,
pub timeout: Duration,
}
impl serde::Serialize for QueryContext {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("QueryContext", 5)?;
state.serialize_field("query", &self.query)?;
state.serialize_field("normalized", &self.normalized)?;
state.serialize_field("tables", &self.tables)?;
state.serialize_field("is_read_only", &self.is_read_only)?;
state.serialize_field("hook_context", &self.hook_context)?;
state.end()
}
}
fn get_typed<T, P, R>(
instance: &Instance,
store: &mut Store<T>,
name: &str,
) -> Result<TypedFunc<P, R>, PluginError>
where
P: wasmtime::WasmParams,
R: wasmtime::WasmResults,
{
instance
.get_typed_func::<P, R>(store, name)
.map_err(|e| PluginError::ExecutionError(format!("export `{}`: {}", name, e)))
}
fn write_memory<T>(
memory: &Memory,
store: &mut Store<T>,
ptr: i32,
bytes: &[u8],
) -> Result<(), PluginError> {
memory.write(store, ptr as usize, bytes).map_err(|e| {
PluginError::ExecutionError(format!("memory.write @ {}: {}", ptr, e))
})
}
fn read_memory<T>(
memory: &Memory,
store: &Store<T>,
ptr: i32,
len: i32,
) -> Result<Vec<u8>, PluginError> {
if len <= 0 {
return Ok(Vec::new());
}
let mut out = vec![0u8; len as usize];
memory.read(store, ptr as usize, &mut out).map_err(|e| {
PluginError::ExecutionError(format!("memory.read @ {}+{}: {}", ptr, len, e))
})?;
Ok(out)
}
impl serde::Serialize for AuthRequest {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("AuthRequest", 5)?;
state.serialize_field("headers", &self.headers)?;
state.serialize_field("username", &self.username)?;
state.serialize_field("password", &self.password)?;
state.serialize_field("client_ip", &self.client_ip)?;
state.serialize_field("database", &self.database)?;
state.end()
}
}
impl<'de> serde::Deserialize<'de> for PreQueryResult {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct Helper {
action: String,
#[serde(default)]
value: Option<String>,
#[serde(default)]
data: Option<Vec<u8>>,
}
let helper = Helper::deserialize(deserializer)?;
match helper.action.as_str() {
"continue" => Ok(PreQueryResult::Continue),
"rewrite" => Ok(PreQueryResult::Rewrite(
helper.value.unwrap_or_default(),
)),
"block" => Ok(PreQueryResult::Block(
helper.value.unwrap_or_default(),
)),
"cached" => Ok(PreQueryResult::Cached(
helper.data.unwrap_or_default(),
)),
_ => Ok(PreQueryResult::Continue),
}
}
}
impl<'de> serde::Deserialize<'de> for AuthResult {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct Helper {
action: String,
#[serde(default)]
identity: Option<IdentityHelper>,
#[serde(default)]
message: Option<String>,
}
#[derive(serde::Deserialize)]
struct IdentityHelper {
user_id: String,
username: String,
#[serde(default)]
roles: Vec<String>,
#[serde(default)]
tenant_id: Option<String>,
}
let helper = Helper::deserialize(deserializer)?;
match helper.action.as_str() {
"success" => {
let id = helper.identity.unwrap_or(IdentityHelper {
user_id: String::new(),
username: String::new(),
roles: Vec::new(),
tenant_id: None,
});
Ok(AuthResult::Success(super::Identity {
user_id: id.user_id,
username: id.username,
roles: id.roles,
tenant_id: id.tenant_id,
claims: std::collections::HashMap::new(),
}))
}
"denied" => Ok(AuthResult::Denied(
helper.message.unwrap_or_default(),
)),
"defer" => Ok(AuthResult::Defer),
_ => Ok(AuthResult::Defer),
}
}
}
impl<'de> serde::Deserialize<'de> for RouteResult {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct Helper {
action: String,
#[serde(default)]
target: Option<String>,
#[serde(default)]
reason: Option<String>,
}
let helper = Helper::deserialize(deserializer)?;
match helper.action.as_str() {
"default" => Ok(RouteResult::Default),
"node" => Ok(RouteResult::Node(helper.target.unwrap_or_default())),
"primary" => Ok(RouteResult::Primary),
"standby" => Ok(RouteResult::Standby),
"branch" => Ok(RouteResult::Branch(helper.target.unwrap_or_default())),
"block" => Ok(RouteResult::Block(
helper.reason.unwrap_or_else(|| "blocked by plugin".to_string()),
)),
_ => Ok(RouteResult::Default),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_test_module(engine: &Engine) -> Module {
const PAYLOAD: &[u8] = b"hello-from-wasm";
let payload_hex: String = PAYLOAD
.iter()
.map(|b| format!("\\{:02x}", b))
.collect();
let wat = format!(
r#"
(module
(memory (export "memory") 1)
;; Trivial alloc: always returns offset 4096 (test inputs
;; are tiny so non-overlapping reuse is fine here). Real
;; plugins ship a real allocator; the runtime only cares
;; that `alloc` returns a writable address.
(func (export "alloc") (param $size i32) (result i32)
(i32.const 4096))
(func (export "dealloc") (param $ptr i32) (param $size i32)
(drop (local.get $ptr))
(drop (local.get $size)))
;; Result-returning hook: writes PAYLOAD at offset 1024 and
;; returns (1024 << 32) | PAYLOAD.len.
(func (export "pre_query")
(param $in_ptr i32) (param $in_len i32) (result i64)
(i64.or
(i64.shl (i64.const 1024) (i64.const 32))
(i64.const {payload_len})))
;; Observer hook: takes args, returns nothing.
(func (export "post_query")
(param $in_ptr i32) (param $in_len i32)
(drop (local.get $in_ptr)))
(data (i32.const 1024) "{payload}")
)
"#,
payload = payload_hex,
payload_len = PAYLOAD.len(),
);
let bytes = wat::parse_str(&wat).expect("wat parses");
Module::from_binary(engine, &bytes).expect("module compiles")
}
#[test]
fn test_plugin_error_display() {
let err = PluginError::LoadError("test".to_string());
assert!(err.to_string().contains("Load error"));
let err = PluginError::Timeout("plugin-a".to_string());
assert!(err.to_string().contains("Timeout"));
}
#[test]
fn test_plugin_state() {
assert_eq!(PluginState::Running, PluginState::Running);
assert_ne!(PluginState::Running, PluginState::Paused);
}
#[test]
fn test_runtime_creation() {
let config = PluginRuntimeConfig::default();
let runtime = WasmPluginRuntime::new(&config);
assert!(runtime.is_ok());
}
#[test]
fn test_runtime_stats() {
let config = PluginRuntimeConfig::default();
let runtime = WasmPluginRuntime::new(&config).unwrap();
let stats = runtime.stats();
assert_eq!(stats.cached_modules, 0);
assert!(stats.fuel_metering_enabled);
}
#[test]
fn test_loaded_plugin_invocation_count() {
let engine = Engine::default();
let module = build_test_module(&engine);
let metadata = PluginMetadata::default();
let sandbox = PluginSandbox::default();
let plugin = LoadedPlugin::new(
metadata,
PathBuf::from("/test/plugin.wasm"),
module,
sandbox,
);
assert_eq!(plugin.invocation_count(), 0);
plugin.record_invocation();
assert_eq!(plugin.invocation_count(), 1);
plugin.record_invocation();
assert_eq!(plugin.invocation_count(), 2);
}
#[test]
fn test_call_hook_roundtrips_real_wasm() {
let mut config = PluginRuntimeConfig::default();
config.fuel_metering = false;
let runtime = WasmPluginRuntime::new(&config).unwrap();
let module = build_test_module(runtime.engine());
let mut metadata = PluginMetadata::default();
metadata.name = "test-roundtrip".to_string();
metadata.hooks = vec![HookType::PreQuery, HookType::PostQuery];
let plugin = LoadedPlugin::new(
metadata,
PathBuf::from("/test/roundtrip.wasm"),
module,
PluginSandbox::default(),
);
let bytes = runtime
.call_hook(&plugin, HookType::PreQuery, b"ignored input")
.expect("pre_query call");
assert_eq!(bytes, b"hello-from-wasm");
assert_eq!(plugin.invocation_count(), 1);
let out = runtime
.call_hook(&plugin, HookType::PostQuery, b"some bytes")
.expect("post_query call");
assert!(out.is_empty());
assert_eq!(plugin.invocation_count(), 2);
}
#[test]
fn test_call_hook_rejects_undeclared_hook() {
let runtime = WasmPluginRuntime::new(&PluginRuntimeConfig::default()).unwrap();
let module = build_test_module(runtime.engine());
let mut metadata = PluginMetadata::default();
metadata.hooks = vec![]; let plugin = LoadedPlugin::new(
metadata,
PathBuf::from("/test/empty.wasm"),
module,
PluginSandbox::default(),
);
let err = runtime
.call_hook(&plugin, HookType::PreQuery, &[])
.unwrap_err();
assert!(matches!(err, PluginError::HookNotFound(_)));
}
#[test]
fn test_call_hook_missing_export_returns_error() {
let runtime = WasmPluginRuntime::new(&PluginRuntimeConfig::default()).unwrap();
let module = build_test_module(runtime.engine());
let mut metadata = PluginMetadata::default();
metadata.hooks = vec![HookType::Authenticate];
let plugin = LoadedPlugin::new(
metadata,
PathBuf::from("/test/missing.wasm"),
module,
PluginSandbox::default(),
);
let err = runtime
.call_hook(&plugin, HookType::Authenticate, &[])
.unwrap_err();
assert!(matches!(err, PluginError::ExecutionError(_)));
}
fn build_kv_test_module(engine: &Engine) -> Module {
let wat = r#"
(module
(import "env" "kv_set"
(func $kv_set (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 100) "key")
(data (i32.const 200) "value")
(func (export "alloc") (param i32) (result i32) (i32.const 4096))
(func (export "dealloc") (param i32 i32))
;; pre_query: kv_set("key", "value"); return 0 (no payload).
(func (export "pre_query")
(param $in_ptr i32) (param $in_len i32) (result i64)
(drop (call $kv_set
(i32.const 100) (i32.const 3)
(i32.const 200) (i32.const 5)))
(i64.const 0))
)
"#;
let bytes = wat::parse_str(wat).expect("kv-wat parses");
Module::from_binary(engine, &bytes).expect("kv module compiles")
}
#[test]
fn test_host_kv_import_persists_value() {
let mut config = PluginRuntimeConfig::default();
config.fuel_metering = false;
let runtime = WasmPluginRuntime::new(&config).unwrap();
let module = build_kv_test_module(runtime.engine());
let mut metadata = PluginMetadata::default();
metadata.name = "kv-test-plugin".to_string();
metadata.hooks = vec![HookType::PreQuery];
let plugin = LoadedPlugin::new(
metadata,
PathBuf::from("/test/kv.wasm"),
module,
PluginSandbox::default(),
);
assert_eq!(runtime.kv().get("kv-test-plugin", b"key"), None);
let _ = runtime
.call_hook(&plugin, HookType::PreQuery, &[])
.expect("pre_query call");
assert_eq!(
runtime.kv().get("kv-test-plugin", b"key"),
Some(b"value".to_vec())
);
assert_eq!(runtime.kv().get("other-plugin", b"key"), None);
}
fn build_sha256_test_module(engine: &Engine) -> Module {
let wat = r#"
(module
(import "env" "sha256_hex"
(func $sha256_hex (param i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 100) "abc")
(func (export "alloc") (param i32) (result i32) (i32.const 4096))
(func (export "dealloc") (param i32 i32))
(func (export "pre_query")
(param $in_ptr i32) (param $in_len i32) (result i64)
(drop (call $sha256_hex
(i32.const 100) (i32.const 3)
(i32.const 200)))
(i64.or
(i64.shl (i64.const 200) (i64.const 32))
(i64.const 64)))
)
"#;
let bytes = wat::parse_str(wat).expect("sha256-wat parses");
Module::from_binary(engine, &bytes).expect("sha256 module compiles")
}
#[test]
fn test_route_result_deserialises_block_with_reason() {
let json = r#"{"action":"block","reason":"cross-region read forbidden"}"#;
let r: RouteResult = serde_json::from_str(json).expect("block deserialises");
match r {
RouteResult::Block(reason) => {
assert_eq!(reason, "cross-region read forbidden");
}
other => panic!("expected Block, got {:?}", other),
}
}
#[test]
fn test_route_result_block_defaults_reason_when_missing() {
let json = r#"{"action":"block"}"#;
let r: RouteResult = serde_json::from_str(json).expect("block deserialises");
match r {
RouteResult::Block(reason) => {
assert!(!reason.is_empty(), "default reason should not be empty");
}
other => panic!("expected Block, got {:?}", other),
}
}
#[test]
fn test_host_sha256_import_matches_rfc_6234_vector() {
const SHA256_OF_ABC: &[u8; 64] =
b"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
let mut config = PluginRuntimeConfig::default();
config.fuel_metering = false;
let runtime = WasmPluginRuntime::new(&config).unwrap();
let module = build_sha256_test_module(runtime.engine());
let mut metadata = PluginMetadata::default();
metadata.name = "sha256-test-plugin".to_string();
metadata.hooks = vec![HookType::PreQuery];
let plugin = LoadedPlugin::new(
metadata,
PathBuf::from("/test/sha256.wasm"),
module,
PluginSandbox::default(),
);
let out = runtime
.call_hook(&plugin, HookType::PreQuery, &[])
.expect("pre_query call");
assert_eq!(out.len(), 64);
assert_eq!(&out[..], SHA256_OF_ABC);
}
}