use tari_template_abi::{ABI_TEMPLATE_DEF_GLOBAL_NAME, TEMPLATE_DEF_CUSTOM_SECTION, TemplateDef, WASM_PTR_SIZE};
use wasmer::wasmparser::{BinaryReaderError, Data, DataKind, ExternalKind, Operator, Parser, Payload, TypeRef};
pub fn extract_template_def(code: &[u8]) -> Result<TemplateDef, ExtractTemplateDefError> {
let mut imported_global_count: u32 = 0;
let mut globals_init: Vec<Option<i32>> = Vec::new();
let mut export_global_idx: Option<u32> = None;
let mut data_segments = Vec::new();
for payload in Parser::new(0).parse_all(code) {
match payload? {
Payload::CustomSection(reader) if reader.name() == TEMPLATE_DEF_CUSTOM_SECTION => {
return decode_template_def_from_blob(reader.data());
},
Payload::ImportSection(reader) => {
for imports in reader {
for entry in imports? {
let (_, import) = entry?;
if matches!(import.ty, TypeRef::Global(_)) {
imported_global_count = imported_global_count
.checked_add(1)
.ok_or(ExtractTemplateDefError::TooManyImportedGlobals)?;
}
}
}
},
Payload::GlobalSection(reader) => {
for global in reader {
let global = global?;
let mut ops = global.init_expr.get_operators_reader();
let init = match ops.read() {
Ok(Operator::I32Const { value }) => Some(value),
_ => None,
};
globals_init.push(init);
}
},
Payload::ExportSection(reader) => {
for export in reader {
let export = export?;
if export.kind == ExternalKind::Global && export.name == ABI_TEMPLATE_DEF_GLOBAL_NAME {
export_global_idx = Some(export.index);
break;
}
}
},
Payload::DataSection(reader) => {
for segment in reader {
let segment = segment?;
if let DataKind::Active {
memory_index: 0,
offset_expr,
} = &segment.kind
{
let mut ops = offset_expr.get_operators_reader();
if let Ok(Operator::I32Const { value }) = ops.read() {
data_segments.push((u64::from(value as u32), segment));
}
}
}
},
_ => {},
}
}
let global_idx = export_global_idx.ok_or(ExtractTemplateDefError::AbiExportMissing)?;
if global_idx < imported_global_count {
return Err(ExtractTemplateDefError::AbiExportImported);
}
let local_idx = (global_idx - imported_global_count) as usize;
let abi_ptr = globals_init
.get(local_idx)
.copied()
.flatten()
.ok_or(ExtractTemplateDefError::AbiGlobalNotConst)?;
let abi_ptr = u64::from(abi_ptr as u32);
let len_bytes = read_data_at(&data_segments, abi_ptr, WASM_PTR_SIZE)?;
let length = u32::from_le_bytes(len_bytes.try_into().expect("WASM_PTR_SIZE == 4")) as usize;
if length > code.len() {
return Err(ExtractTemplateDefError::AbiLengthExceedsBinary {
length,
binary_size: code.len(),
});
}
let body_offset = abi_ptr
.checked_add(WASM_PTR_SIZE as u64)
.ok_or(ExtractTemplateDefError::OffsetOverflow)?;
let body = read_data_at(&data_segments, body_offset, length)?;
tari_bor::decode(body).map_err(ExtractTemplateDefError::Decode)
}
fn decode_template_def_from_blob(blob: &[u8]) -> Result<TemplateDef, ExtractTemplateDefError> {
if blob.len() < WASM_PTR_SIZE {
return Err(ExtractTemplateDefError::SectionTooShort { len: blob.len() });
}
let prefix: [u8; WASM_PTR_SIZE] = blob[..WASM_PTR_SIZE]
.try_into()
.expect("blob.len() >= WASM_PTR_SIZE checked above");
let full_len = u32::from_le_bytes(prefix) as usize;
if full_len < WASM_PTR_SIZE || full_len > blob.len() {
return Err(ExtractTemplateDefError::SectionLengthMismatch {
declared: full_len,
actual: blob.len(),
});
}
tari_bor::decode(&blob[WASM_PTR_SIZE..full_len]).map_err(ExtractTemplateDefError::Decode)
}
fn read_data_at<'a>(
segments: &[(u64, Data<'a>)],
offset: u64,
len: usize,
) -> Result<&'a [u8], ExtractTemplateDefError> {
let len_u64 = len as u64;
let read_end = offset
.checked_add(len_u64)
.ok_or(ExtractTemplateDefError::OffsetOverflow)?;
for (seg_offset, seg_data) in segments {
let seg_end = seg_offset
.checked_add(seg_data.data.len() as u64)
.ok_or(ExtractTemplateDefError::OffsetOverflow)?;
if offset >= *seg_offset && read_end <= seg_end {
let local = (offset - seg_offset) as usize;
return Ok(&seg_data.data[local..local + len]);
}
}
Err(ExtractTemplateDefError::DataOutOfRange { offset, len })
}
#[derive(Debug, thiserror::Error)]
pub enum ExtractTemplateDefError {
#[error("WASM parse error: {0}")]
Parse(#[from] BinaryReaderError),
#[error(
"Module does not export `{ABI_TEMPLATE_DEF_GLOBAL_NAME}` and has no `{TEMPLATE_DEF_CUSTOM_SECTION}` custom \
section"
)]
AbiExportMissing,
#[error("`{ABI_TEMPLATE_DEF_GLOBAL_NAME}` resolves to an imported global, not a local one")]
AbiExportImported,
#[error("`{ABI_TEMPLATE_DEF_GLOBAL_NAME}` initializer is not an i32.const")]
AbiGlobalNotConst,
#[error("ABI length prefix ({length}) exceeds the WASM binary size ({binary_size})")]
AbiLengthExceedsBinary { length: usize, binary_size: usize },
#[error("Module imports more globals than fit in u32")]
TooManyImportedGlobals,
#[error("Arithmetic overflow computing memory offset")]
OffsetOverflow,
#[error("ABI bytes at offset {offset} (len {len}) are not covered by any active data segment")]
DataOutOfRange { offset: u64, len: usize },
#[error("`{TEMPLATE_DEF_CUSTOM_SECTION}` custom section is too short ({len} bytes) to hold a length prefix")]
SectionTooShort { len: usize },
#[error("`{TEMPLATE_DEF_CUSTOM_SECTION}` declares length {declared} but the section is {actual} bytes")]
SectionLengthMismatch { declared: usize, actual: usize },
#[error("Failed to decode TemplateDef: {0}")]
Decode(#[source] tari_bor::BorError),
}
#[cfg(test)]
mod tests {
use tari_template_abi::{TemplateDef, TemplateDefV1, version};
use tari_template_builtin::all_builtin_templates;
use super::*;
fn write_uleb128(out: &mut Vec<u8>, mut value: u32) {
loop {
let mut byte = (value & 0x7f) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
out.push(byte);
if value == 0 {
break;
}
}
}
fn make_wasm_with_custom_section(name: &str, payload: &[u8]) -> Vec<u8> {
let mut wasm = Vec::with_capacity(8 + 16 + name.len() + payload.len());
wasm.extend_from_slice(&[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
let mut section_body = Vec::with_capacity(name.len() + payload.len() + 8);
write_uleb128(&mut section_body, name.len() as u32);
section_body.extend_from_slice(name.as_bytes());
section_body.extend_from_slice(payload);
wasm.push(0); write_uleb128(&mut wasm, section_body.len() as u32);
wasm.extend_from_slice(§ion_body);
wasm
}
fn synthetic_template_def() -> TemplateDef {
TemplateDef::V1(TemplateDefV1 {
template_name: "Synthetic".to_string(),
abi_version: version::LATEST_TEMPLATE_VERSION,
functions: Vec::new(),
})
}
#[test]
fn extracts_legacy_builtin_template_defs() {
for template in all_builtin_templates() {
let def = extract_template_def(template.binary)
.unwrap_or_else(|e| panic!("extract failed for {}: {}", template.name, e));
assert_eq!(
def.template_name(),
template.name,
"extracted template_name should match the static builtin name",
);
}
}
#[test]
fn extracts_from_custom_section() {
let blob = synthetic_template_def().encode_for_wasm_embedding().expect("encode");
let wasm = make_wasm_with_custom_section(TEMPLATE_DEF_CUSTOM_SECTION, &blob);
let def = extract_template_def(&wasm).expect("extract from custom section");
assert_eq!(def.template_name(), "Synthetic");
}
#[test]
fn rejects_malformed_section_length() {
let mut blob = vec![0u8; 16];
blob[..4].copy_from_slice(&u32::MAX.to_le_bytes());
let wasm = make_wasm_with_custom_section(TEMPLATE_DEF_CUSTOM_SECTION, &blob);
match extract_template_def(&wasm) {
Err(ExtractTemplateDefError::SectionLengthMismatch { .. }) => {},
other => panic!("expected SectionLengthMismatch, got {:?}", other),
}
}
}