use crate::{error::VmError, vm::Context, Error};
use std::fmt;
use wasmtime::Store;
pub(crate) mod lock_world {
wasmtime::component::bindgen!({
world: "lock-script",
});
}
pub(crate) mod unlock_world {
wasmtime::component::bindgen!({
world: "unlock-script",
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScriptKind {
Module,
Component,
}
const MODULE_VERSION_BYTE: u8 = 0x01;
const COMPONENT_VERSION_BYTE: u8 = 0x0d;
const WASM_MAGIC: [u8; 4] = [0x00, b'a', b's', b'm'];
impl ScriptKind {
#[must_use]
pub fn detect(bytes: &[u8]) -> Option<Self> {
if bytes.len() >= 5 && bytes[..4] == WASM_MAGIC {
return match bytes[4] {
MODULE_VERSION_BYTE => Some(Self::Module),
COMPONENT_VERSION_BYTE => Some(Self::Component),
_ => None,
};
}
Self::detect_text(bytes)
}
fn detect_text(bytes: &[u8]) -> Option<Self> {
let text = std::str::from_utf8(bytes).ok()?;
let text = text.strip_prefix('\u{feff}').unwrap_or(text);
let mut rest = text.trim_start();
loop {
if rest.is_empty() {
return None;
}
if rest.starts_with(";;") {
let idx = rest.find('\n')?;
rest = rest[idx + 1..].trim_start();
continue;
}
if rest.starts_with("(;") {
rest = skip_block_comment(rest)?.trim_start();
continue;
}
break;
}
let token = rest.strip_prefix('(').unwrap_or(rest);
if starts_token(token, "component") {
return Some(Self::Component);
}
if starts_token(token, "module") {
return Some(Self::Module);
}
None
}
}
fn skip_block_comment(rest: &str) -> Option<&str> {
let bytes = rest.as_bytes();
let mut depth: usize = 0;
let mut idx: usize = 0;
while idx + 1 < bytes.len() {
if bytes[idx] == b'(' && bytes[idx + 1] == b';' {
depth += 1;
idx += 2;
continue;
}
if bytes[idx] == b';' && bytes[idx + 1] == b')' {
depth -= 1;
idx += 2;
if depth == 0 {
return rest.get(idx..);
}
continue;
}
idx += 1;
}
None
}
fn starts_token(text: &str, token: &str) -> bool {
match text.strip_prefix(token) {
Some("") => true,
Some(after) => !after
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'),
None => false,
}
}
impl fmt::Display for ScriptKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Module => "module",
Self::Component => "component",
})
}
}
pub struct ComponentInstance {
instance: wasmtime::component::Instance,
pub store: Store<Context>,
}
impl ComponentInstance {
pub(crate) const fn new(
instance: wasmtime::component::Instance,
store: Store<Context>,
) -> Self {
Self { instance, store }
}
pub fn run(&mut self, fname: &str) -> Result<bool, Error> {
match fname {
"for_great_justice" | "for-great-justice" => {
let world = unlock_world::UnlockScript::new(&mut self.store, &self.instance)
.map_err(|e| VmError::ExecutionError {
function: fname.to_string(),
message: format!("failed to bind export '{fname}': {e}"),
})?;
let result = world.call_for_great_justice(&mut self.store).map_err(|e| {
VmError::ExecutionError {
function: fname.to_string(),
message: format!("function '{fname}' execution failed: {e}"),
}
})?;
Ok(result != 0)
}
"move_every_zig" | "move-every-zig" => {
let world =
lock_world::LockScript::new(&mut self.store, &self.instance).map_err(|e| {
VmError::ExecutionError {
function: fname.to_string(),
message: format!("failed to bind export '{fname}': {e}"),
}
})?;
let result = world.call_move_every_zig(&mut self.store).map_err(|e| {
VmError::ExecutionError {
function: fname.to_string(),
message: format!("function '{fname}' execution failed: {e}"),
}
})?;
Ok(result != 0)
}
_ => Err(VmError::ExecutionError {
function: fname.to_string(),
message: format!(
"'{fname}' is not a script export; expected 'for_great_justice' or \
'move_every_zig' (kebab spellings accepted)"
),
}
.into()),
}
}
#[must_use]
pub fn log(&self) -> Vec<u8> {
self.store.data().log.clone()
}
}
fn as_bool(value: wasmtime::Val) -> bool {
value.i32().is_some_and(|raw| raw != 0)
}
impl unlock_world::cryptid::wacc::host::Host for Context {
fn push(&mut self, key: String) -> bool {
as_bool(Self::push(self, &key))
}
fn push_value(&mut self, data: Vec<u8>) -> bool {
as_bool(Self::push_value(self, data))
}
fn branch(&mut self, key: String) -> String {
Self::branch(self, &key)
}
fn check_eq(&mut self, key: String) -> bool {
as_bool(Self::check_eq(self, &key))
}
fn check_preimage(&mut self, key: String) -> bool {
as_bool(Self::check_preimage(self, &key))
}
fn check_preimage_value(&mut self, hash: Vec<u8>, key: String) -> bool {
as_bool(Self::check_preimage_value(self, &hash, &key))
}
fn check_signature(&mut self, key: String, msg: String) -> bool {
as_bool(Self::check_signature(self, &key, &msg))
}
fn log(&mut self, text: String) -> bool {
as_bool(Self::log(self, &text))
}
}
impl lock_world::cryptid::wacc::host::Host for Context {
fn push(&mut self, key: String) -> bool {
as_bool(Self::push(self, &key))
}
fn push_value(&mut self, data: Vec<u8>) -> bool {
as_bool(Self::push_value(self, data))
}
fn branch(&mut self, key: String) -> String {
Self::branch(self, &key)
}
fn check_eq(&mut self, key: String) -> bool {
as_bool(Self::check_eq(self, &key))
}
fn check_preimage(&mut self, key: String) -> bool {
as_bool(Self::check_preimage(self, &key))
}
fn check_preimage_value(&mut self, hash: Vec<u8>, key: String) -> bool {
as_bool(Self::check_preimage_value(self, &hash, &key))
}
fn check_signature(&mut self, key: String, msg: String) -> bool {
as_bool(Self::check_signature(self, &key, &msg))
}
fn log(&mut self, text: String) -> bool {
as_bool(Self::log(self, &text))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_accepts_binary_kinds() {
let module = [0x00, 0x61, 0x73, 0x6d, 0x01];
assert_eq!(ScriptKind::detect(&module), Some(ScriptKind::Module));
let component = [0x00, 0x61, 0x73, 0x6d, 0x0d];
assert_eq!(ScriptKind::detect(&component), Some(ScriptKind::Component));
}
#[test]
fn detect_rejects_unknown_binary_versions_and_garbage() {
let unknown_version = [0x00, 0x61, 0x73, 0x6d, 0x02];
assert_eq!(ScriptKind::detect(&unknown_version), None);
assert_eq!(ScriptKind::detect(b"not wasm bytes"), None);
assert_eq!(ScriptKind::detect(&[]), None);
assert_eq!(ScriptKind::detect(&[0x00, 0x61]), None);
}
#[test]
fn detect_accepts_text_kinds() {
assert_eq!(ScriptKind::detect(b"(module)"), Some(ScriptKind::Module));
assert_eq!(
ScriptKind::detect(b"(component)"),
Some(ScriptKind::Component)
);
assert_eq!(ScriptKind::detect(b"module $m"), Some(ScriptKind::Module));
assert_eq!(
ScriptKind::detect(b"component $c"),
Some(ScriptKind::Component)
);
assert_eq!(
ScriptKind::detect(b" \n (module "),
Some(ScriptKind::Module)
);
}
#[test]
fn detect_text_skips_leading_comments() {
assert_eq!(
ScriptKind::detect(b";; header\n(component)"),
Some(ScriptKind::Component)
);
assert_eq!(
ScriptKind::detect(b"(; hi ;) component"),
Some(ScriptKind::Component)
);
assert_eq!(
ScriptKind::detect(b"(;(;nested;) ;) module $m"),
Some(ScriptKind::Module)
);
assert_eq!(ScriptKind::detect(b";; unterminated"), None);
assert_eq!(ScriptKind::detect(b"(; unterminated"), None);
assert_eq!(ScriptKind::detect(b";; c\n"), None);
}
#[test]
fn detect_text_tolerates_a_byte_order_mark() {
let bom_prefixed = "\u{feff}(component)".as_bytes();
assert_eq!(
ScriptKind::detect(bom_prefixed),
Some(ScriptKind::Component)
);
}
#[test]
fn detect_text_enforces_token_boundaries() {
assert_eq!(ScriptKind::detect(b"modulet"), None);
assert_eq!(ScriptKind::detect(b"component-thing"), None);
assert_eq!(ScriptKind::detect(b"components"), None);
assert_eq!(ScriptKind::detect(b"module_name"), None);
assert_eq!(ScriptKind::detect(b""), None);
}
#[test]
fn kind_display_matches_detection_names() {
assert_eq!(ScriptKind::Module.to_string(), "module");
assert_eq!(ScriptKind::Component.to_string(), "component");
}
}