use crate::error::WasmError;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use wasmtime::{Engine, Module};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum WasmCapability {
WasiFs,
WasiEnv,
WasiArgs,
WasiStdio,
WasiNet,
HostFunctions,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleMetadata {
pub hash: String,
pub size: usize,
pub capabilities: HashSet<WasmCapability>,
pub exports: Vec<String>,
pub imports: Vec<WasmImport>,
pub is_wasi: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WasmImport {
pub module: String,
pub name: String,
}
#[derive(Debug, Clone)]
pub struct WasmModule {
pub bytes: Vec<u8>,
pub metadata: ModuleMetadata,
compiled: Option<Module>,
}
impl WasmModule {
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, WasmError> {
Self::validate_basic_format(&bytes)?;
let metadata = Self::extract_metadata(&bytes)?;
Self::validate_module(&bytes, &metadata)?;
Ok(WasmModule {
bytes,
metadata,
compiled: None,
})
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, WasmError> {
let bytes = fs::read(path)?;
Self::from_bytes(bytes)
}
pub fn get_compiled(&mut self, engine: &Engine) -> Result<&Module, WasmError> {
if self.compiled.is_none() {
let module = Module::from_binary(engine, &self.bytes)?;
self.compiled = Some(module);
}
Ok(self.compiled.as_ref().unwrap())
}
pub fn hash(&self) -> &str {
&self.metadata.hash
}
pub fn requires_capability(&self, capability: &WasmCapability) -> bool {
self.metadata.capabilities.contains(capability)
}
pub fn is_wasi(&self) -> bool {
self.metadata.is_wasi
}
fn extract_metadata(bytes: &[u8]) -> Result<ModuleMetadata, WasmError> {
let mut hasher = Sha256::new();
hasher.update(bytes);
let hash = format!("{:x}", hasher.finalize());
let engine = Engine::default();
let module = Module::from_binary(&engine, bytes)
.map_err(|e| WasmError::ModuleLoad(e.to_string()))?;
let mut capabilities = HashSet::new();
let mut exports = Vec::new();
let mut imports = Vec::new();
let mut is_wasi = false;
for export in module.exports() {
exports.push(export.name().to_string());
}
for import in module.imports() {
let import_info = WasmImport {
module: import.module().to_string(),
name: import.name().to_string(),
};
if import.module().starts_with("wasi_") {
is_wasi = true;
match import.name() {
name if name.starts_with("fd_") => {
capabilities.insert(WasmCapability::WasiFs);
capabilities.insert(WasmCapability::WasiStdio);
}
name if name.starts_with("environ_") => {
capabilities.insert(WasmCapability::WasiEnv);
}
name if name.starts_with("args_") => {
capabilities.insert(WasmCapability::WasiArgs);
}
name if name.starts_with("sock_") => {
capabilities.insert(WasmCapability::WasiNet);
}
_ => {}
}
} else if import.module() != "env" {
capabilities.insert(WasmCapability::HostFunctions);
}
imports.push(import_info);
}
if is_wasi {
capabilities.insert(WasmCapability::WasiStdio);
}
Ok(ModuleMetadata {
hash,
size: bytes.len(),
capabilities,
exports,
imports,
is_wasi,
})
}
fn validate_basic_format(bytes: &[u8]) -> Result<(), WasmError> {
if bytes.len() < 8 {
return Err(WasmError::InvalidFormat(
"WASM module too small (minimum 8 bytes)".to_string()
));
}
if &bytes[0..4] != b"\0asm" {
return Err(WasmError::InvalidFormat(
"Invalid WASM magic number".to_string()
));
}
const MAX_MODULE_SIZE: usize = 64 * 1024 * 1024;
if bytes.len() > MAX_MODULE_SIZE {
return Err(WasmError::ModuleValidation(format!(
"Module too large: {} bytes (max: {} bytes)",
bytes.len(), MAX_MODULE_SIZE
)));
}
Ok(())
}
fn validate_module(_bytes: &[u8], metadata: &ModuleMetadata) -> Result<(), WasmError> {
if metadata.capabilities.contains(&WasmCapability::WasiNet) {
return Err(WasmError::UnsupportedCapability(
"WASI networking is not supported".to_string()
));
}
if metadata.is_wasi && !metadata.exports.contains(&"_start".to_string()) {
return Err(WasmError::ModuleValidation(
"WASI module must export '_start' function".to_string()
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::test_modules::{minimal_wasm, simple_function_wasm, wasi_hello_wasm, INVALID_MAGIC_WASM};
#[test]
fn test_minimal_wasm_module() {
let module = WasmModule::from_bytes(minimal_wasm().to_vec()).unwrap();
assert_eq!(module.metadata.size, minimal_wasm().len());
assert!(!module.is_wasi());
assert!(module.metadata.exports.is_empty());
assert!(module.metadata.imports.is_empty());
}
#[test]
fn test_simple_function_wasm() {
let module = WasmModule::from_bytes(simple_function_wasm().to_vec()).unwrap();
assert!(!module.is_wasi());
assert!(module.metadata.exports.contains(&"add".to_string()));
assert!(!module.requires_capability(&WasmCapability::WasiStdio));
}
#[test]
fn test_wasi_module_detection() {
let module = WasmModule::from_bytes(wasi_hello_wasm().to_vec()).unwrap();
assert!(module.is_wasi());
assert!(module.metadata.exports.contains(&"_start".to_string()));
assert!(module.metadata.exports.contains(&"memory".to_string()));
assert!(module.requires_capability(&WasmCapability::WasiStdio));
let has_fd_write = module.metadata.imports.iter()
.any(|imp| imp.module == "wasi_snapshot_preview1" && imp.name == "fd_write");
assert!(has_fd_write);
let has_environ_get = module.metadata.imports.iter()
.any(|imp| imp.module == "wasi_snapshot_preview1" && imp.name == "environ_get");
assert!(has_environ_get);
assert!(module.requires_capability(&WasmCapability::WasiEnv));
}
#[test]
fn test_invalid_wasm_magic() {
let result = WasmModule::from_bytes(INVALID_MAGIC_WASM.to_vec());
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), WasmError::InvalidFormat(_)));
}
#[test]
fn test_empty_bytes() {
let empty_bytes = vec![];
let result = WasmModule::from_bytes(empty_bytes);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), WasmError::InvalidFormat(_)));
}
#[test]
fn test_module_too_large() {
let mut large_bytes = vec![0x00, 0x61, 0x73, 0x6d]; large_bytes.extend(vec![0x01, 0x00, 0x00, 0x00]); large_bytes.extend(vec![0x00; 65 * 1024 * 1024]);
let result = WasmModule::from_bytes(large_bytes);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), WasmError::ModuleValidation(_)));
}
#[test]
fn test_hash_calculation() {
let module1 = WasmModule::from_bytes(minimal_wasm().to_vec()).unwrap();
let module2 = WasmModule::from_bytes(simple_function_wasm().to_vec()).unwrap();
assert_ne!(module1.hash(), module2.hash());
let module1_copy = WasmModule::from_bytes(minimal_wasm().to_vec()).unwrap();
assert_eq!(module1.hash(), module1_copy.hash());
}
#[test]
fn test_capability_detection() {
let wasi_module = WasmModule::from_bytes(wasi_hello_wasm().to_vec()).unwrap();
assert!(wasi_module.requires_capability(&WasmCapability::WasiStdio));
assert!(wasi_module.requires_capability(&WasmCapability::WasiEnv));
assert!(!wasi_module.requires_capability(&WasmCapability::WasiNet));
let simple_module = WasmModule::from_bytes(simple_function_wasm().to_vec()).unwrap();
assert!(!simple_module.requires_capability(&WasmCapability::WasiStdio));
}
#[test]
fn test_compiled_module_caching() {
let mut module = WasmModule::from_bytes(minimal_wasm().to_vec()).unwrap();
let engine = wasmtime::Engine::default();
let _compiled1 = module.get_compiled(&engine).unwrap();
assert!(module.compiled.is_some());
let _compiled2 = module.get_compiled(&engine).unwrap();
assert!(module.compiled.is_some());
}
#[test]
fn test_from_file_nonexistent() {
let result = WasmModule::from_file("/nonexistent/path/module.wasm");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), WasmError::Io(_)));
}
}