use wasmtime::{Engine, Store};
use super::{
runtime::{
bindings::{self, exports::mercurio::plugin::guest, mercurio::plugin::types as wit},
bindings_0_1::{
self as bindings_old, exports::mercurio::plugin::guest as guest_old,
mercurio::plugin::types as old,
},
},
sandbox::State,
};
pub(crate) enum Pre {
V0_1(bindings_old::PluginPre<State>),
V0_2(bindings::PluginPre<State>),
}
pub(crate) enum Guest {
V0_1(guest_old::Guest),
V0_2(guest::Guest),
}
impl Pre {
pub(crate) fn engine(&self) -> &Engine {
match self {
Pre::V0_1(pre) => pre.engine(),
Pre::V0_2(pre) => pre.engine(),
}
}
pub(crate) async fn instantiate(&self, store: &mut Store<State>) -> wasmtime::Result<Guest> {
Ok(match self {
Pre::V0_1(pre) => Guest::V0_1(
pre.instantiate_async(store)
.await?
.mercurio_plugin_guest()
.clone(),
),
Pre::V0_2(pre) => Guest::V0_2(
pre.instantiate_async(store)
.await?
.mercurio_plugin_guest()
.clone(),
),
})
}
}
type Result<T> = wasmtime::Result<T>;
impl Guest {
pub(crate) async fn init(
&self,
store: &mut Store<State>,
config: &[(String, String)],
) -> Result<std::result::Result<wit::Registration, String>> {
Ok(match self {
Guest::V0_1(guest) => guest.call_init(store, config).await?.map(registration),
Guest::V0_2(guest) => guest.call_init(store, config).await?,
})
}
pub(crate) async fn authenticate(
&self,
store: &mut Store<State>,
client: &wit::ClientInfo,
username: &str,
password: &[u8],
) -> Result<wit::Decision> {
Ok(match self {
Guest::V0_1(guest) => decision(
guest
.call_authenticate(store, &client_info(client), username, password)
.await?,
),
Guest::V0_2(guest) => {
guest
.call_authenticate(store, client, username, password)
.await?
}
})
}
pub(crate) async fn auth_step(
&self,
store: &mut Store<State>,
client: &wit::ClientInfo,
method: &str,
state: Option<&[u8]>,
data: Option<&[u8]>,
) -> Result<wit::AuthResult> {
Ok(match self {
Guest::V0_1(guest) => auth_result(
guest
.call_auth_step(store, &client_info(client), method, state, data)
.await?,
),
Guest::V0_2(guest) => {
guest
.call_auth_step(store, client, method, state, data)
.await?
}
})
}
pub(crate) async fn authorize(
&self,
store: &mut Store<State>,
client: &wit::ClientInfo,
topic: &str,
access: wit::Access,
) -> Result<wit::Decision> {
Ok(match self {
Guest::V0_1(guest) => decision(
guest
.call_authorize(store, &client_info(client), topic, old_access(access))
.await?,
),
Guest::V0_2(guest) => guest.call_authorize(store, client, topic, access).await?,
})
}
pub(crate) async fn authorize_control(
&self,
store: &mut Store<State>,
client: &wit::ClientInfo,
topic: &str,
) -> Result<wit::Decision> {
Ok(match self {
Guest::V0_1(_) => wit::Decision::Defer,
Guest::V0_2(guest) => guest.call_authorize_control(store, client, topic).await?,
})
}
pub(crate) async fn on_message(
&self,
store: &mut Store<State>,
client: &wit::ClientInfo,
message: &wit::Message,
) -> Result<wit::MessageAction> {
Ok(match self {
Guest::V0_1(guest) => {
let message = old_message(message.clone());
match guest
.call_on_message(store, &client_info(client), &message)
.await?
{
old::MessageAction::Keep => wit::MessageAction::Keep,
old::MessageAction::Drop => wit::MessageAction::Drop,
old::MessageAction::Replace(changed) => {
wit::MessageAction::Replace(new_message(changed))
}
}
}
Guest::V0_2(guest) => guest.call_on_message(store, client, message).await?,
})
}
pub(crate) async fn on_event(
&self,
store: &mut Store<State>,
event: &wit::Event,
) -> Result<()> {
match self {
Guest::V0_1(guest) => guest.call_on_event(store, &old_event(event)).await,
Guest::V0_2(guest) => guest.call_on_event(store, event).await,
}
}
pub(crate) async fn on_tick(&self, store: &mut Store<State>) -> Result<()> {
match self {
Guest::V0_1(guest) => guest.call_on_tick(store).await,
Guest::V0_2(guest) => guest.call_on_tick(store).await,
}
}
pub(crate) async fn on_control(
&self,
store: &mut Store<State>,
client: &wit::ClientInfo,
path: &str,
payload: &[u8],
) -> Result<Option<Vec<u8>>> {
match self {
Guest::V0_1(guest) => {
guest
.call_on_control(store, &client_info(client), path, payload)
.await
}
Guest::V0_2(guest) => guest.call_on_control(store, client, path, payload).await,
}
}
}
fn client_info(client: &wit::ClientInfo) -> old::ClientInfo {
old::ClientInfo {
client_id: client.client_id.clone(),
username: client.username.clone(),
peer: client.peer.clone(),
}
}
fn old_access(access: wit::Access) -> old::Access {
match access {
wit::Access::Read => old::Access::Read,
wit::Access::Write => old::Access::Write,
wit::Access::Subscribe => old::Access::Subscribe,
}
}
fn decision(decision: old::Decision) -> wit::Decision {
match decision {
old::Decision::Allow => wit::Decision::Allow,
old::Decision::Deny => wit::Decision::Deny,
old::Decision::Defer => wit::Decision::Defer,
}
}
fn old_qos(qos: wit::Qos) -> old::Qos {
match qos {
wit::Qos::AtMostOnce => old::Qos::AtMostOnce,
wit::Qos::AtLeastOnce => old::Qos::AtLeastOnce,
wit::Qos::ExactlyOnce => old::Qos::ExactlyOnce,
}
}
fn new_qos(qos: old::Qos) -> wit::Qos {
match qos {
old::Qos::AtMostOnce => wit::Qos::AtMostOnce,
old::Qos::AtLeastOnce => wit::Qos::AtLeastOnce,
old::Qos::ExactlyOnce => wit::Qos::ExactlyOnce,
}
}
fn old_message(message: wit::Message) -> old::Message {
old::Message {
topic: message.topic,
payload: message.payload,
qos: old_qos(message.qos),
retain: message.retain,
payload_is_utf8: message.payload_is_utf8,
message_expiry: message.message_expiry,
content_type: message.content_type,
response_topic: message.response_topic,
correlation_data: message.correlation_data,
user_properties: message.user_properties,
}
}
fn new_message(message: old::Message) -> wit::Message {
wit::Message {
topic: message.topic,
payload: message.payload,
qos: new_qos(message.qos),
retain: message.retain,
payload_is_utf8: message.payload_is_utf8,
message_expiry: message.message_expiry,
content_type: message.content_type,
response_topic: message.response_topic,
correlation_data: message.correlation_data,
user_properties: message.user_properties,
}
}
fn old_event(event: &wit::Event) -> old::Event {
match event {
wit::Event::Connected(client) => old::Event::Connected(client_info(client)),
wit::Event::Disconnected(d) => old::Event::Disconnected(old::Disconnected {
client: client_info(&d.client),
reason: d.reason,
}),
wit::Event::Subscribed(s) => old::Event::Subscribed(old::Subscribed {
client: client_info(&s.client),
filter: s.filter.clone(),
qos: old_qos(s.qos),
}),
wit::Event::Unsubscribed(u) => old::Event::Unsubscribed(old::Unsubscribed {
client: client_info(&u.client),
filter: u.filter.clone(),
}),
wit::Event::SessionExpired(expired) => {
old::Event::SessionExpired(expired.client_id.clone())
}
}
}
fn registration(registration: old::Registration) -> wit::Registration {
let mut hooks = wit::Hooks::empty();
for (old_hook, hook) in [
(old::Hooks::AUTHENTICATE, wit::Hooks::AUTHENTICATE),
(old::Hooks::AUTHORIZE, wit::Hooks::AUTHORIZE),
(old::Hooks::ON_MESSAGE, wit::Hooks::ON_MESSAGE),
(old::Hooks::ON_EVENT, wit::Hooks::ON_EVENT),
(old::Hooks::ON_TICK, wit::Hooks::ON_TICK),
(old::Hooks::ON_CONTROL, wit::Hooks::ON_CONTROL),
] {
if registration.hooks.contains(old_hook) {
hooks |= hook;
}
}
wit::Registration {
hooks,
auth_methods: registration.auth_methods,
}
}
fn auth_result(result: old::AuthResult) -> wit::AuthResult {
match result {
old::AuthResult::Continue(challenge) => wit::AuthResult::Continue(wit::Challenge {
data: challenge.data,
state: challenge.state,
}),
old::AuthResult::Success(username) => wit::AuthResult::Success(wit::Success {
username,
data: None,
}),
old::AuthResult::Failed(code) => wit::AuthResult::Failed(wit::Failure {
reason: code,
reason_string: None,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_0_1_registrations_keep_their_hooks() {
let all = old::Hooks::all();
let converted = registration(old::Registration {
hooks: all,
auth_methods: vec!["M".into()],
});
assert_eq!(
converted.hooks | wit::Hooks::AUTHORIZE_CONTROL,
wit::Hooks::all()
);
assert!(!converted.hooks.contains(wit::Hooks::AUTHORIZE_CONTROL));
assert_eq!(converted.auth_methods, ["M"]);
let none = registration(old::Registration {
hooks: old::Hooks::empty(),
auth_methods: Vec::new(),
});
assert_eq!(none.hooks, wit::Hooks::empty());
}
#[test]
fn test_0_1_messages_survive_the_round_trip() {
let message = wit::Message {
topic: "t".into(),
payload: b"p".to_vec(),
qos: wit::Qos::ExactlyOnce,
retain: true,
payload_is_utf8: true,
message_expiry: Some(5),
content_type: Some("text/plain".into()),
response_topic: Some("r".into()),
correlation_data: Some(b"c".to_vec()),
user_properties: vec![("k".into(), "v".into())],
};
let back = new_message(old_message(message.clone()));
assert_eq!(format!("{back:?}"), format!("{message:?}"));
}
#[test]
fn test_0_1_authentication_failures_have_no_reason_string() {
let wit::AuthResult::Failed(failure) = auth_result(old::AuthResult::Failed(0x8c)) else {
panic!("expected a failure");
};
assert_eq!(failure.reason, 0x8c);
assert_eq!(failure.reason_string, None);
}
#[test]
fn test_0_1_expired_sessions_carry_the_client_id_alone() {
let expired = wit::Event::SessionExpired(wit::SessionExpired {
client_id: "c".into(),
mount_point: Some("tenant/".into()),
});
let old::Event::SessionExpired(client_id) = old_event(&expired) else {
panic!("expected a session-expired event");
};
assert_eq!(client_id, "c");
}
}