use bytes::Bytes;
#[cfg(feature = "tier-2-hook-host-v2")]
pub mod capability_linker;
#[cfg(feature = "tier-2-hook-host-v2")]
pub mod wasmtime_host;
#[derive(Debug, Default)]
pub struct ExtraBytesBuilder {
inner: bytes::BytesMut,
}
impl ExtraBytesBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn append(&mut self, bytes: &[u8]) {
self.inner.extend_from_slice(bytes);
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn freeze(self) -> Bytes {
self.inner.freeze()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum CapToken {
StateRead,
StateWrite,
EmitExtraBytes,
FuelConsumed,
}
#[cfg(any(feature = "tier-2-hook-host-v2", feature = "tier-2-observer-host-v2"))]
impl crate::wasm_runtime_common::sealed_impl::Sealed for CapToken {}
#[cfg(any(feature = "tier-2-hook-host-v2", feature = "tier-2-observer-host-v2"))]
impl crate::wasm_runtime_common::HookCapTokenSealed for CapToken {}
#[derive(Debug)]
pub struct HookContext<'b> {
pub capabilities: &'b [CapToken],
pub extra: &'b mut ExtraBytesBuilder,
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum HookError {
#[error("hook budget exhausted")]
BudgetExceeded,
#[error("capability denied: {0:?}")]
CapabilityDenied(CapToken),
#[error("hook trap: {0}")]
Trapped(&'static str),
#[error("post-hook policy re-validation failed")]
PolicyReValidationFailed,
}
pub trait HookHost {
fn invoke(&self, ctx: &mut HookContext<'_>) -> Result<(), HookError>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopHookHost;
impl NoopHookHost {
pub fn new() -> Self {
Self
}
}
impl HookHost for NoopHookHost {
fn invoke(&self, _ctx: &mut HookContext<'_>) -> Result<(), HookError> {
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn extra_bytes_builder_append_and_freeze() {
let mut b = ExtraBytesBuilder::new();
assert!(b.is_empty());
b.append(b"hello ");
b.append(b"world");
assert_eq!(b.len(), 11);
let frozen = b.freeze();
assert_eq!(&frozen[..], b"hello world");
}
#[test]
fn noop_host_passes_through_without_mutation() {
let host = NoopHookHost::new();
let mut extra = ExtraBytesBuilder::new();
extra.append(b"prefix");
let caps = [CapToken::EmitExtraBytes];
let mut ctx = HookContext {
capabilities: &caps,
extra: &mut extra,
};
assert!(host.invoke(&mut ctx).is_ok());
assert_eq!(extra.len(), 6);
}
#[test]
fn cap_token_set_round_trips() {
let tokens = [
CapToken::StateRead,
CapToken::StateWrite,
CapToken::EmitExtraBytes,
CapToken::FuelConsumed,
];
for t in tokens {
assert_eq!(t, t);
assert!(!format!("{t:?}").is_empty());
}
}
#[test]
fn hook_error_display_does_not_panic() {
let errors = [
HookError::BudgetExceeded,
HookError::CapabilityDenied(CapToken::StateRead),
HookError::Trapped("unexpected"),
HookError::PolicyReValidationFailed,
];
for e in errors {
assert!(!format!("{e}").is_empty());
}
}
}