use std::{collections::BTreeMap, fmt};
use wasmparser::{Encoding, ExternalKind, Parser, Payload};
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InspectionLimits {
pub module_bytes: usize,
pub sections: usize,
pub exports: u32,
pub custom_sections: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InspectionResource {
ModuleBytes,
Sections,
Exports,
CustomSections,
}
#[derive(Debug)]
pub enum InspectionError {
LimitExceeded {
resource: InspectionResource,
actual: u64,
limit: u64,
},
UnsupportedEncoding,
Parse(wasmparser::BinaryReaderError),
DuplicateExport {
offset: usize,
},
UnknownSection {
id: u8,
offset: usize,
},
}
impl fmt::Display for InspectionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LimitExceeded {
resource,
actual,
limit,
} => write!(f, "Wasm {resource:?} count {actual} exceeds {limit}"),
Self::UnsupportedEncoding => {
f.write_str("expected a core Wasm module, received a component")
}
Self::Parse(source) => write!(f, "malformed Wasm structure: {source}"),
Self::DuplicateExport { offset } => write!(f, "duplicate Wasm export at byte {offset}"),
Self::UnknownSection { id, offset } => {
write!(f, "unknown Wasm section {id} at byte {offset}")
}
}
}
}
impl std::error::Error for InspectionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Parse(source) => Some(source),
_ => None,
}
}
}
impl From<wasmparser::BinaryReaderError> for InspectionError {
fn from(source: wasmparser::BinaryReaderError) -> Self {
Self::Parse(source)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExportKind {
Function,
Table,
Memory,
Global,
Tag,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Export {
pub kind: ExportKind,
pub index: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CustomSection<'a> {
pub name: &'a str,
pub data: &'a [u8],
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WasmFacts<'a> {
pub raw_bytes: usize,
pub code_section_bytes: usize,
pub data_section_bytes: usize,
pub defined_functions: u32,
pub data_segments: u32,
pub exports: BTreeMap<&'a str, Export>,
pub custom_sections: Vec<CustomSection<'a>>,
}
pub fn inspect(bytes: &[u8], limits: InspectionLimits) -> Result<WasmFacts<'_>, InspectionError> {
enforce(
InspectionResource::ModuleBytes,
bytes.len() as u64,
limits.module_bytes as u64,
)?;
let mut facts = WasmFacts {
raw_bytes: bytes.len(),
code_section_bytes: 0,
data_section_bytes: 0,
defined_functions: 0,
data_segments: 0,
exports: BTreeMap::new(),
custom_sections: Vec::new(),
};
let mut sections = 0;
for payload in Parser::new(0).parse_all(bytes) {
let payload = payload?;
if payload.as_section().is_some() {
sections += 1;
enforce(
InspectionResource::Sections,
sections,
limits.sections as u64,
)?;
}
match payload {
Payload::Version { encoding, .. } if encoding != Encoding::Module => {
return Err(InspectionError::UnsupportedEncoding);
}
Payload::FunctionSection(reader) => {
facts.defined_functions = reader.count();
for index in reader {
index?;
}
}
Payload::CodeSectionStart { range, .. } => {
facts.code_section_bytes = range.len();
}
Payload::DataSection(reader) => {
facts.data_section_bytes = reader.range().len();
facts.data_segments = reader.count();
for segment in reader {
segment?;
}
}
Payload::ExportSection(reader) => {
enforce(
InspectionResource::Exports,
u64::from(reader.count()),
u64::from(limits.exports),
)?;
for entry in reader.into_iter_with_offsets() {
let (offset, entry) = entry?;
let kind = match entry.kind {
ExternalKind::Func | ExternalKind::FuncExact => ExportKind::Function,
ExternalKind::Table => ExportKind::Table,
ExternalKind::Memory => ExportKind::Memory,
ExternalKind::Global => ExportKind::Global,
ExternalKind::Tag => ExportKind::Tag,
};
if facts
.exports
.insert(
entry.name,
Export {
kind,
index: entry.index,
},
)
.is_some()
{
return Err(InspectionError::DuplicateExport { offset });
}
}
}
Payload::CustomSection(reader) => {
enforce(
InspectionResource::CustomSections,
facts.custom_sections.len() as u64 + 1,
limits.custom_sections as u64,
)?;
facts.custom_sections.push(CustomSection {
name: reader.name(),
data: reader.data(),
});
}
Payload::UnknownSection { id, range, .. } => {
return Err(InspectionError::UnknownSection {
id,
offset: range.start,
});
}
_ => {}
}
}
Ok(facts)
}
const fn enforce(
resource: InspectionResource,
actual: u64,
limit: u64,
) -> Result<(), InspectionError> {
if actual > limit {
return Err(InspectionError::LimitExceeded {
resource,
actual,
limit,
});
}
Ok(())
}