use ic_host_artifacts::wasm_encoding::{
Call, CallModule, DataSegment, ExportedFunction, FunctionImport, Memory32, encode,
};
use std::collections::BTreeSet;
use thiserror::Error;
#[cfg(feature = "held-reply-fixtures")]
pub mod held;
#[cfg(feature = "held-reply-fixtures")]
pub fn cycle_reply_canister(
methods: &[RawReplyMethod<'_>],
limits: RawReplyLimits,
accept_cycles: u128,
module_bytes: usize,
) -> Result<Vec<u8>, held::HeldReplyError> {
held::module(methods, limits, None, Some(accept_cycles), module_bytes)
}
#[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 budget = CatalogBudget::default();
for (index, method) in methods.iter().enumerate() {
if !exports.insert(method.name) {
return Err(RawReplyError::DuplicateExport { index });
}
budget.admit(index, method, limits)?;
}
let bytes = budget.replies;
i32::try_from(bytes).map_err(|_| RawReplyError::Wasm32Limits)?;
let pages =
u32::try_from(bytes.div_ceil(65_536).max(1)).map_err(|_| RawReplyError::Wasm32Limits)?;
let export_names: Vec<_> = methods
.iter()
.map(|method| {
let prefix = match method.mode {
ReplyMode::Query => "canister_query",
ReplyMode::Update => "canister_update",
};
format!("{prefix} {}", method.name)
})
.collect();
let mut functions = Vec::with_capacity(methods.len());
let mut data = Vec::with_capacity(methods.len());
let mut offset = 0_i32;
for (name, method) in export_names.iter().zip(methods) {
let length = i32::try_from(method.reply.len()).map_err(|_| RawReplyError::Wasm32Limits)?;
functions.push(ExportedFunction {
name,
calls: vec![
Call {
import: 0,
arguments: vec![offset, length],
},
Call {
import: 1,
arguments: vec![],
},
],
});
data.push(DataSegment {
offset: offset.cast_unsigned(),
bytes: method.reply,
});
offset += length;
}
encode(
&CallModule {
imports: &[
FunctionImport {
module: "ic0",
name: "msg_reply_data_append",
parameters: 2,
},
FunctionImport {
module: "ic0",
name: "msg_reply",
parameters: 0,
},
],
functions: &functions,
memory: Memory32 {
pages,
export: "memory",
},
data: &data,
},
usize::MAX,
)
.map_err(|_| RawReplyError::Wasm32Limits)
}
#[derive(Default)]
struct CatalogBudget {
names: usize,
replies: usize,
}
impl CatalogBudget {
fn admit(
&mut self,
index: usize,
method: &RawReplyMethod<'_>,
limits: RawReplyLimits,
) -> Result<(), RawReplyError> {
if method.name.is_empty() || method.name.chars().any(char::is_control) {
return Err(RawReplyError::InvalidName { index });
}
self.names = charge(
self.names,
method.name.len(),
limits.method_name_bytes,
"method names",
)?;
self.replies = charge(
self.replies,
method.reply.len(),
limits.reply_bytes,
"reply bytes",
)?;
Ok(())
}
}
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;