1use std::{collections::BTreeMap, fmt};
9use wasmparser::{Encoding, ExternalKind, Parser, Payload};
10
11#[cfg(test)]
12mod tests;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub struct InspectionLimits {
17 pub module_bytes: usize,
19 pub sections: usize,
21 pub exports: u32,
23 pub custom_sections: usize,
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum InspectionResource {
30 ModuleBytes,
32 Sections,
34 Exports,
36 CustomSections,
38}
39
40#[derive(Debug)]
45pub struct ParseError {
46 source: wasmparser::BinaryReaderError,
47}
48
49impl ParseError {
50 #[must_use]
52 pub fn offset(&self) -> u64 {
53 self.source.offset()
54 }
55
56 #[must_use]
60 pub fn message(&self) -> &str {
61 self.source.message()
62 }
63}
64
65impl fmt::Display for ParseError {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 fmt::Display::fmt(&self.source, f)
68 }
69}
70
71impl std::error::Error for ParseError {}
72
73#[derive(Debug)]
75pub enum InspectionError {
76 LimitExceeded {
78 resource: InspectionResource,
80 actual: u64,
82 limit: u64,
84 },
85 UnsupportedEncoding,
87 Parse(ParseError),
89 DuplicateExport {
91 offset: usize,
93 },
94 UnknownSection {
96 id: u8,
98 offset: usize,
100 },
101}
102
103impl fmt::Display for InspectionError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::LimitExceeded {
107 resource,
108 actual,
109 limit,
110 } => write!(f, "Wasm {resource:?} count {actual} exceeds {limit}"),
111 Self::UnsupportedEncoding => {
112 f.write_str("expected a core Wasm module, received a component")
113 }
114 Self::Parse(source) => write!(f, "malformed Wasm structure: {source}"),
115 Self::DuplicateExport { offset } => write!(f, "duplicate Wasm export at byte {offset}"),
116 Self::UnknownSection { id, offset } => {
117 write!(f, "unknown Wasm section {id} at byte {offset}")
118 }
119 }
120 }
121}
122impl std::error::Error for InspectionError {
123 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
124 match self {
125 Self::Parse(source) => Some(source),
126 _ => None,
127 }
128 }
129}
130const fn parse_error(source: wasmparser::BinaryReaderError) -> InspectionError {
131 InspectionError::Parse(ParseError { source })
132}
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub enum ExportKind {
137 Function,
139 Table,
141 Memory,
143 Global,
145 Tag,
147}
148
149#[derive(Clone, Copy, Debug, Eq, PartialEq)]
151pub struct Export {
152 pub kind: ExportKind,
154 pub index: u32,
156}
157
158#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160pub struct CustomSection<'a> {
161 pub name: &'a str,
163 pub data: &'a [u8],
165}
166
167#[derive(Clone, Debug, Eq, PartialEq)]
169pub struct WasmFacts<'a> {
170 pub raw_bytes: usize,
172 pub code_section_bytes: usize,
174 pub data_section_bytes: usize,
176 pub defined_functions: u32,
178 pub data_segments: u32,
180 pub exports: BTreeMap<&'a str, Export>,
182 pub custom_sections: Vec<CustomSection<'a>>,
184}
185
186pub fn inspect(bytes: &[u8], limits: InspectionLimits) -> Result<WasmFacts<'_>, InspectionError> {
198 enforce(
199 InspectionResource::ModuleBytes,
200 bytes.len() as u64,
201 limits.module_bytes as u64,
202 )?;
203 let mut facts = WasmFacts {
204 raw_bytes: bytes.len(),
205 code_section_bytes: 0,
206 data_section_bytes: 0,
207 defined_functions: 0,
208 data_segments: 0,
209 exports: BTreeMap::new(),
210 custom_sections: Vec::new(),
211 };
212 let mut sections = 0;
213 for payload in Parser::new(0).parse_all(bytes) {
214 let payload = payload.map_err(parse_error)?;
215 if payload.as_section().is_some() {
216 sections += 1;
217 enforce(
218 InspectionResource::Sections,
219 sections,
220 limits.sections as u64,
221 )?;
222 }
223 match payload {
224 Payload::Version { encoding, .. } if encoding != Encoding::Module => {
225 return Err(InspectionError::UnsupportedEncoding);
226 }
227 Payload::FunctionSection(reader) => {
228 facts.defined_functions = reader.count();
229 for index in reader {
232 index.map_err(parse_error)?;
233 }
234 }
235 Payload::CodeSectionStart { range, .. } => {
236 facts.code_section_bytes = host_size(range.end - range.start)?;
237 }
238 Payload::DataSection(reader) => {
239 let range = reader.range();
240 facts.data_section_bytes = host_size(range.end - range.start)?;
241 facts.data_segments = reader.count();
242 for segment in reader {
243 segment.map_err(parse_error)?;
244 }
245 }
246 Payload::ExportSection(reader) => {
247 enforce(
248 InspectionResource::Exports,
249 u64::from(reader.count()),
250 u64::from(limits.exports),
251 )?;
252 for entry in reader.into_iter_with_offsets() {
253 let (offset, entry) = entry.map_err(parse_error)?;
254 let kind = match entry.kind {
255 ExternalKind::Func | ExternalKind::FuncExact => ExportKind::Function,
256 ExternalKind::Table => ExportKind::Table,
257 ExternalKind::Memory => ExportKind::Memory,
258 ExternalKind::Global => ExportKind::Global,
259 ExternalKind::Tag => ExportKind::Tag,
260 };
261 if facts
262 .exports
263 .insert(
264 entry.name,
265 Export {
266 kind,
267 index: entry.index,
268 },
269 )
270 .is_some()
271 {
272 return Err(InspectionError::DuplicateExport {
273 offset: host_size(offset)?,
274 });
275 }
276 }
277 }
278 Payload::CustomSection(reader) => {
279 enforce(
280 InspectionResource::CustomSections,
281 facts.custom_sections.len() as u64 + 1,
282 limits.custom_sections as u64,
283 )?;
284 facts.custom_sections.push(CustomSection {
285 name: reader.name(),
286 data: reader.data(),
287 });
288 }
289 Payload::UnknownSection { id, range, .. } => {
290 return Err(InspectionError::UnknownSection {
291 id,
292 offset: host_size(range.start)?,
293 });
294 }
295 _ => {}
296 }
297 }
298 Ok(facts)
299}
300
301fn host_size(value: u64) -> Result<usize, InspectionError> {
304 usize::try_from(value).map_err(|_| InspectionError::LimitExceeded {
305 resource: InspectionResource::ModuleBytes,
306 actual: value,
307 limit: usize::MAX as u64,
308 })
309}
310
311const fn enforce(
312 resource: InspectionResource,
313 actual: u64,
314 limit: u64,
315) -> Result<(), InspectionError> {
316 if actual > limit {
317 return Err(InspectionError::LimitExceeded {
318 resource,
319 actual,
320 limit,
321 });
322 }
323 Ok(())
324}