use std::collections::BTreeSet;
use thiserror::Error;
use wasm_encoder::{
CodeSection, ConstExpr, DataSection, EntityType, ExportKind, ExportSection, Function,
FunctionSection, ImportSection, Instruction, MemorySection, MemoryType, Module, TypeSection,
ValType,
};
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum ReplyMode {
Query,
Update,
}
#[derive(Clone, Copy, Debug)]
pub struct RawReplyMethod<'a> {
pub name: &'a str,
pub mode: ReplyMode,
pub reply: &'a [u8],
}
#[derive(Clone, Copy, Debug)]
pub struct RawReplyLimits {
pub methods: usize,
pub reply_bytes: usize,
pub method_name_bytes: usize,
}
#[derive(Debug, Eq, Error, PartialEq)]
pub enum RawReplyError {
#[error("raw reply fixture exceeds {resource} limit {limit}")]
Budget {
resource: &'static str,
limit: usize,
},
#[error("invalid method name at index {index}")]
InvalidName {
index: usize,
},
#[error("duplicate method export at index {index}")]
DuplicateExport {
index: usize,
},
#[error("raw reply fixture cannot represent its lengths in Wasm32")]
Wasm32Limits,
}
pub fn raw_reply_canister(
methods: &[RawReplyMethod<'_>],
limits: RawReplyLimits,
) -> Result<Vec<u8>, RawReplyError> {
if methods.len() > limits.methods {
return Err(RawReplyError::Budget {
resource: "methods",
limit: limits.methods,
});
}
let count = u32::try_from(methods.len()).map_err(|_| RawReplyError::Wasm32Limits)?;
count.checked_add(2).ok_or(RawReplyError::Wasm32Limits)?;
let mut exports = BTreeSet::new();
let mut bytes = 0_usize;
let mut names = 0_usize;
for (index, method) in methods.iter().enumerate() {
if method.name.is_empty() || method.name.chars().any(char::is_control) {
return Err(RawReplyError::InvalidName { index });
}
if !exports.insert(method.name) {
return Err(RawReplyError::DuplicateExport { index });
}
names = charge(
names,
method.name.len(),
limits.method_name_bytes,
"method names",
)?;
bytes = charge(bytes, method.reply.len(), limits.reply_bytes, "reply bytes")?;
}
i32::try_from(bytes).map_err(|_| RawReplyError::Wasm32Limits)?;
let pages =
u64::try_from(bytes.div_ceil(65_536).max(1)).map_err(|_| RawReplyError::Wasm32Limits)?;
let mut module = Module::new();
let mut types = TypeSection::new();
types.ty().function([ValType::I32, ValType::I32], []);
types.ty().function([], []);
module.section(&types);
let mut imports = ImportSection::new();
imports.import("ic0", "msg_reply_data_append", EntityType::Function(0));
imports.import("ic0", "msg_reply", EntityType::Function(1));
module.section(&imports);
let mut functions = FunctionSection::new();
for _ in methods {
functions.function(1);
}
module.section(&functions);
let mut memory = MemorySection::new();
memory.memory(MemoryType {
minimum: pages,
maximum: Some(pages),
memory64: false,
shared: false,
page_size_log2: None,
});
module.section(&memory);
let mut exported = ExportSection::new();
exported.export("memory", ExportKind::Memory, 0);
let mut code = CodeSection::new();
let mut data = DataSection::new();
let mut offset = 0_i32;
for (index, method) in (2..count + 2).zip(methods) {
let prefix = match method.mode {
ReplyMode::Query => "canister_query",
ReplyMode::Update => "canister_update",
};
exported.export(
&format!("{prefix} {}", method.name),
ExportKind::Func,
index,
);
let length = i32::try_from(method.reply.len()).map_err(|_| RawReplyError::Wasm32Limits)?;
let mut body = Function::new([]);
body.instruction(&Instruction::I32Const(offset));
body.instruction(&Instruction::I32Const(length));
body.instruction(&Instruction::Call(0));
body.instruction(&Instruction::Call(1));
body.instruction(&Instruction::End);
code.function(&body);
data.active(
0,
&ConstExpr::i32_const(offset),
method.reply.iter().copied(),
);
offset += length;
}
module.section(&exported);
module.section(&code);
module.section(&data);
Ok(module.finish())
}
fn charge(
total: usize,
amount: usize,
limit: usize,
resource: &'static str,
) -> Result<usize, RawReplyError> {
total
.checked_add(amount)
.filter(|sum| *sum <= limit)
.ok_or(RawReplyError::Budget { resource, limit })
}
#[cfg(test)]
mod tests;