use std::{collections::BTreeSet, fmt};
use wasm_encoder::{
CodeSection, ConstExpr, DataSection, EntityType, ExportKind, ExportSection, Function,
FunctionSection, ImportSection, Instruction, MemorySection, MemoryType, Module, TypeSection,
ValType,
};
#[derive(Clone, Copy, Debug)]
pub struct FunctionImport<'a> {
pub module: &'a str,
pub name: &'a str,
pub parameters: u32,
}
#[derive(Clone, Debug)]
pub struct Call {
pub import: u32,
pub arguments: Vec<i32>,
}
#[derive(Clone, Debug)]
pub struct ExportedFunction<'a> {
pub name: &'a str,
pub calls: Vec<Call>,
}
#[derive(Clone, Copy, Debug)]
pub struct Memory32<'a> {
pub pages: u32,
pub export: &'a str,
}
#[derive(Clone, Copy, Debug)]
pub struct DataSegment<'a> {
pub offset: u32,
pub bytes: &'a [u8],
}
#[derive(Clone, Copy, Debug)]
pub struct CallModule<'a> {
pub imports: &'a [FunctionImport<'a>],
pub functions: &'a [ExportedFunction<'a>],
pub memory: Memory32<'a>,
pub data: &'a [DataSegment<'a>],
}
#[derive(Debug, Eq, PartialEq)]
pub enum EncodingError {
LengthOverflow,
LimitExceeded {
limit: usize,
},
InvalidMemory,
InvalidCall {
function: usize,
call: usize,
},
DuplicateExport,
DataOutOfBounds {
segment: usize,
},
}
impl fmt::Display for EncodingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LengthOverflow => f.write_str("Wasm encoding extent exceeds u32"),
Self::LimitExceeded { limit } => write!(f, "encoded Wasm exceeds {limit} bytes"),
Self::InvalidMemory => f.write_str("fixed Wasm32 memory exceeds 65536 pages"),
Self::InvalidCall { function, call } => {
write!(f, "invalid import call {call} in function {function}")
}
Self::DuplicateExport => f.write_str("duplicate Wasm export name"),
Self::DataOutOfBounds { segment } => {
write!(f, "data segment {segment} exceeds initial memory")
}
}
}
}
impl std::error::Error for EncodingError {}
pub fn encode(module: &CallModule<'_>, max_bytes: usize) -> Result<Vec<u8>, EncodingError> {
admit(module, max_bytes)?;
let import_count = length(module.imports.len())?;
let void_type = module
.imports
.iter()
.position(|import| import.parameters == 0)
.map_or(Ok(import_count), length)?;
let mut types = TypeSection::new();
let mut imports = ImportSection::new();
for (index, import) in module.imports.iter().enumerate() {
types
.ty()
.function((0..import.parameters).map(|_| ValType::I32), []);
imports.import(
import.module,
import.name,
EntityType::Function(length(index)?),
);
}
if void_type == import_count {
types.ty().function([], []);
}
let mut functions = FunctionSection::new();
let mut exports = ExportSection::new();
let mut code = CodeSection::new();
exports.export(module.memory.export, ExportKind::Memory, 0);
for (index, function) in module.functions.iter().enumerate() {
functions.function(void_type);
exports.export(
function.name,
ExportKind::Func,
import_count + length(index)?,
);
let mut body = Function::new([]);
for call in &function.calls {
for &argument in &call.arguments {
body.instruction(&Instruction::I32Const(argument));
}
body.instruction(&Instruction::Call(call.import));
}
body.instruction(&Instruction::End);
code.function(&body);
}
let mut memory = MemorySection::new();
memory.memory(MemoryType {
minimum: u64::from(module.memory.pages),
maximum: Some(u64::from(module.memory.pages)),
memory64: false,
shared: false,
page_size_log2: None,
});
let mut data = DataSection::new();
for segment in module.data {
let address = i32::from_le_bytes(segment.offset.to_le_bytes());
data.active(
0,
&ConstExpr::i32_const(address),
segment.bytes.iter().copied(),
);
}
let mut encoded = Module::new();
encoded
.section(&types)
.section(&imports)
.section(&functions)
.section(&memory)
.section(&exports)
.section(&code)
.section(&data);
let bytes = encoded.finish();
length(bytes.len())?;
if bytes.len() > max_bytes {
return Err(EncodingError::LimitExceeded { limit: max_bytes });
}
Ok(bytes)
}
fn length(value: usize) -> Result<u32, EncodingError> {
u32::try_from(value).map_err(|_| EncodingError::LengthOverflow)
}
fn charge(total: &mut usize, amount: usize, limit: usize) -> Result<(), EncodingError> {
*total = total
.checked_add(amount)
.filter(|value| *value <= limit)
.ok_or(EncodingError::LimitExceeded { limit })?;
Ok(())
}
fn admit(module: &CallModule<'_>, limit: usize) -> Result<(), EncodingError> {
let mut minimum = 0;
charge(&mut minimum, 8, limit)?;
length(
module
.imports
.len()
.checked_add(1)
.ok_or(EncodingError::LengthOverflow)?,
)?;
length(
module
.functions
.len()
.checked_add(module.imports.len())
.ok_or(EncodingError::LengthOverflow)?,
)?;
length(
module
.functions
.len()
.checked_add(1)
.ok_or(EncodingError::LengthOverflow)?,
)?;
length(module.data.len())?;
if module.memory.pages > 65_536 {
return Err(EncodingError::InvalidMemory);
}
length(module.memory.export.len())?;
charge(&mut minimum, module.memory.export.len(), limit)?;
for import in module.imports {
length(import.module.len())?;
length(import.name.len())?;
charge(&mut minimum, import.module.len(), limit)?;
charge(&mut minimum, import.name.len(), limit)?;
charge(
&mut minimum,
usize::try_from(import.parameters).map_err(|_| EncodingError::LengthOverflow)?,
limit,
)?;
}
for (index, function) in module.functions.iter().enumerate() {
length(function.name.len())?;
charge(&mut minimum, function.name.len(), limit)?;
charge(&mut minimum, function.calls.len(), limit)?;
for (position, call) in function.calls.iter().enumerate() {
let import = usize::try_from(call.import)
.ok()
.and_then(|index| module.imports.get(index));
if import.is_none_or(|import| {
Some(import.parameters) != u32::try_from(call.arguments.len()).ok()
}) {
return Err(EncodingError::InvalidCall {
function: index,
call: position,
});
}
charge(&mut minimum, call.arguments.len(), limit)?;
}
}
for (index, segment) in module.data.iter().enumerate() {
let bytes = length(segment.bytes.len())?;
if u64::from(segment.offset) + u64::from(bytes) > u64::from(module.memory.pages) * 65_536 {
return Err(EncodingError::DataOutOfBounds { segment: index });
}
charge(&mut minimum, segment.bytes.len(), limit)?;
}
let mut exports = BTreeSet::new();
exports.insert(module.memory.export);
for function in module.functions {
if !exports.insert(function.name) {
return Err(EncodingError::DuplicateExport);
}
}
Ok(())
}
#[cfg(test)]
mod tests;