├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── deno.json ├── rust-toolchain.toml ├── src └── lib.rs └── tests ├── integration_test.rs └── testdata ├── export_const_global.wasm ├── export_imported_func.wasm ├── export_memory.wasm ├── export_mutable_global.wasm ├── export_only.wasm ├── import_export.wasm ├── import_module.wasm ├── import_mutable_global.wasm ├── import_table.wasm └── import_tag.wasm /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | rust: 7 | name: wasm_dep_analyzer-ubuntu-latest 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 30 10 | env: 11 | CARGO_INCREMENTAL: 0 12 | GH_ACTIONS: 1 13 | RUST_BACKTRACE: full 14 | RUSTFLAGS: -D warnings 15 | 16 | steps: 17 | - name: Clone repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Install rust 21 | uses: dsherret/rust-toolchain-file@v1 22 | 23 | - uses: Swatinem/rust-cache@v2 24 | with: 25 | save-if: ${{ github.ref == 'refs/heads/main' }} 26 | 27 | - name: Install up Deno 28 | uses: denoland/setup-deno@v1 29 | 30 | - name: Format 31 | run: | 32 | cargo fmt -- --check 33 | deno fmt --check 34 | 35 | - name: Lint 36 | run: cargo clippy --all-features --all-targets -- -D clippy::all 37 | 38 | - name: Cargo Build 39 | run: cargo build --all-features --all-targets 40 | 41 | - name: Cargo Test 42 | run: cargo test --all-features --all-targets 43 | 44 | - name: Cargo publish 45 | if: | 46 | github.repository == 'denoland/wasm_dep_analyzer' && 47 | startsWith(github.ref, 'refs/tags/') 48 | env: 49 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 50 | run: cargo publish 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseKind: 7 | description: 'Kind of release' 8 | default: 'minor' 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | required: true 14 | 15 | jobs: 16 | rust: 17 | name: release 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 30 20 | 21 | steps: 22 | - name: Clone repository 23 | uses: actions/checkout@v3 24 | with: 25 | token: ${{ secrets.DENOBOT_PAT }} 26 | 27 | - uses: denoland/setup-deno@v1 28 | - uses: dsherret/rust-toolchain-file@v1 29 | 30 | - name: Tag and release 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.DENOBOT_PAT }} 33 | GH_WORKFLOW_ACTOR: ${{ github.actor }} 34 | run: | 35 | git config user.email "denobot@users.noreply.github.com" 36 | git config user.name "denobot" 37 | deno run -A https://raw.githubusercontent.com/denoland/automation/0.14.2/tasks/publish_release.ts --${{github.event.inputs.releaseKind}} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /target 3 | /Cargo.lock 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | tab_spaces = 2 3 | edition = "2021" 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm_dep_analyzer" 3 | version = "0.4.0" 4 | edition = "2021" 5 | description = "Wasm module dependency analysis for module resolution" 6 | repository = "https://github.com/denoland/wasm_dep_analyzer" 7 | documentation = "https://docs.rs/wasm_dep_analyzer" 8 | authors = ["the Deno authors"] 9 | license = "MIT" 10 | 11 | [dependencies] 12 | thiserror = "2" 13 | deno_error = "0.7.0" 14 | 15 | [dev-dependencies] 16 | pretty_assertions = "1.0.0" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2024 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasm_dep_analyzer 2 | 3 | An extremely lightweight Wasm module parser used in Deno to get the dependencies 4 | of a Wasm module from its bytes for the purpose of ECMAScript module resolution 5 | and TypeScript type checking. 6 | 7 | ```rs 8 | let deps = WasmDeps::parse(&wasm_module_bytes, ParseOptions::default())?; 9 | 10 | eprintln!("{:#?}", deps.imports); 11 | eprintln!("{:#?}", deps.exports); 12 | ``` 13 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "target/" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.83.0" 3 | components = [ "clippy", "rustfmt" ] -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use std::collections::HashMap; 4 | use std::str::Utf8Error; 5 | 6 | use thiserror::Error; 7 | 8 | #[derive(Default, Debug, Clone)] 9 | pub struct ParseOptions { 10 | /// Skip getting the type information. 11 | pub skip_types: bool, 12 | } 13 | 14 | #[derive(Debug, PartialEq, Eq)] 15 | pub struct WasmDeps<'a> { 16 | pub imports: Vec>, 17 | pub exports: Vec>, 18 | } 19 | 20 | impl<'a> WasmDeps<'a> { 21 | /// Parses a Wasm module's bytes discovering the imports, exports, and types. 22 | /// 23 | /// The parser will try to parse even when it doesn't understand something 24 | /// and will only parse out the information necessary for dependency analysis. 25 | pub fn parse( 26 | input: &'a [u8], 27 | options: ParseOptions, 28 | ) -> Result { 29 | parse(input, !options.skip_types) 30 | } 31 | } 32 | 33 | #[derive(Debug, PartialEq, Eq)] 34 | pub enum ImportType { 35 | Function(u32), 36 | Table(TableType), 37 | Memory(MemoryType), 38 | Global(GlobalType), 39 | Tag(TagType), 40 | } 41 | 42 | #[derive(Debug, PartialEq, Eq)] 43 | pub struct Limits { 44 | pub initial: u32, 45 | pub maximum: Option, 46 | } 47 | 48 | #[derive(Debug, PartialEq, Eq)] 49 | pub struct TableType { 50 | pub element_type: u8, 51 | pub limits: Limits, 52 | } 53 | 54 | #[derive(Debug, PartialEq, Eq)] 55 | pub struct MemoryType { 56 | pub limits: Limits, 57 | } 58 | 59 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 60 | pub enum ValueType { 61 | I32, 62 | I64, 63 | F32, 64 | F64, 65 | /// A value currently not understood by this parser. 66 | Unknown, 67 | } 68 | 69 | #[derive(Debug, Clone, PartialEq, Eq)] 70 | pub struct GlobalType { 71 | pub value_type: ValueType, 72 | pub mutability: bool, 73 | } 74 | 75 | #[derive(Debug, PartialEq, Eq)] 76 | pub struct TagType { 77 | pub kind: u8, 78 | pub type_index: u32, 79 | } 80 | 81 | #[derive(Debug, PartialEq, Eq)] 82 | pub struct Import<'a> { 83 | pub name: &'a str, 84 | pub module: &'a str, 85 | pub import_type: ImportType, 86 | } 87 | 88 | #[derive(Debug, PartialEq, Eq)] 89 | pub struct Export<'a> { 90 | pub name: &'a str, 91 | pub index: u32, 92 | pub export_type: ExportType, 93 | } 94 | 95 | #[derive(Debug, PartialEq, Eq)] 96 | pub enum ExportType { 97 | Function(Result), 98 | Table, 99 | Memory, 100 | Global(Result), 101 | Tag, 102 | Unknown, 103 | } 104 | 105 | #[derive(Debug, Clone, PartialEq, Eq)] 106 | pub struct FunctionSignature { 107 | pub params: Vec, 108 | pub returns: Vec, 109 | } 110 | 111 | #[derive(Error, Debug, Clone, PartialEq, Eq, deno_error::JsError)] 112 | #[class(type)] 113 | pub enum ParseError { 114 | #[error("not a Wasm module")] 115 | NotWasm, 116 | #[error("unsupported Wasm version: {0}")] 117 | UnsupportedVersion(u32), 118 | #[error("unexpected end of file")] 119 | UnexpectedEof, 120 | #[error("integer overflow")] 121 | IntegerOverflow, 122 | #[error("invalid utf-8. {0:#}")] 123 | InvalidUtf8(Utf8Error), 124 | #[error("unknown import type '{0:X}'")] 125 | UnknownImportType(u8), 126 | #[error("unknown element type '{0:X}'")] 127 | UnknownElementType(u8), 128 | #[error("invalid mutability flag '{0:X}'")] 129 | InvalidMutabilityFlag(u8), 130 | #[error("unknown attribute '{0:X}'")] 131 | UnknownTagKind(u8), 132 | #[error("invalid type indicator '{0:X}'")] 133 | InvalidTypeIndicator(u8), 134 | #[error("unresolved export type")] 135 | UnresolvedExportType, 136 | } 137 | 138 | type ParseResult<'a, T> = Result<(&'a [u8], T), ParseError>; 139 | 140 | struct ParserState<'a> { 141 | imports: Option>>, 142 | exports: Option>>, 143 | types_section: Option<&'a [u8]>, 144 | globals_section: Option<&'a [u8]>, 145 | functions_section: Option<&'a [u8]>, 146 | search_for_types: bool, 147 | search_for_fns: bool, 148 | search_for_globals: bool, 149 | } 150 | 151 | impl<'a> ParserState<'a> { 152 | pub fn keep_searching(&self) -> bool { 153 | self.imports.is_none() 154 | || self.exports.is_none() 155 | || self.search_for_types 156 | || self.search_for_fns 157 | || self.search_for_globals 158 | } 159 | 160 | pub fn set_exports(&mut self, exports: Vec>) { 161 | // check if there are any exports with functions or globals 162 | let mut had_global_export = false; 163 | let mut had_function_export = false; 164 | for export in &exports { 165 | match export.export_type { 166 | ExportType::Function(_) => { 167 | had_function_export = true; 168 | if had_global_export { 169 | break; 170 | } 171 | } 172 | ExportType::Global(_) => { 173 | had_global_export = true; 174 | if had_function_export { 175 | break; 176 | } 177 | } 178 | _ => {} 179 | } 180 | } 181 | 182 | if !had_function_export { 183 | // no need to search for this then 184 | self.search_for_types = false; 185 | self.search_for_fns = false; 186 | self.types_section = None; 187 | self.functions_section = None; 188 | } 189 | if !had_global_export { 190 | // no need to search for the globals then 191 | self.search_for_globals = false; 192 | self.globals_section = None; 193 | } 194 | 195 | self.exports = Some(exports); 196 | } 197 | 198 | pub fn fill_type_information(&mut self) { 199 | let Some(exports) = &mut self.exports else { 200 | return; 201 | }; 202 | // nothing to fill 203 | if self.types_section.is_none() 204 | && self.functions_section.is_none() 205 | && self.globals_section.is_none() 206 | { 207 | return; 208 | }; 209 | 210 | let mut parsed_types = None; 211 | let mut parsed_globals = None; 212 | let mut function_indexes = None; 213 | for export in exports { 214 | if let ExportType::Function(sig) = &mut export.export_type { 215 | let func_export_idx_to_type_idx = 216 | function_indexes.get_or_insert_with(|| { 217 | build_func_export_idx_to_type_idx( 218 | self.imports.as_ref(), 219 | self.functions_section, 220 | ) 221 | }); 222 | match &func_export_idx_to_type_idx { 223 | Ok(func_export_idx_to_type_idx) => { 224 | let parsed_types = parsed_types.get_or_insert_with(|| { 225 | parse_type_section(self.types_section.unwrap_or_default()) 226 | }); 227 | match &parsed_types { 228 | Ok(types) => { 229 | if let Some(types_index) = 230 | func_export_idx_to_type_idx.get(&export.index) 231 | { 232 | let types_index = *types_index as usize; 233 | if types_index < types.len() { 234 | *sig = Ok(types[types_index].clone()); 235 | } 236 | } 237 | } 238 | Err(err) => { 239 | *sig = Err(err.clone()); 240 | } 241 | } 242 | } 243 | Err(err) => { 244 | *sig = Err(err.clone()); 245 | } 246 | } 247 | } else if let ExportType::Global(global) = &mut export.export_type { 248 | let parsed_globals = parsed_globals.get_or_insert_with(|| { 249 | parse_global_section(self.globals_section.unwrap_or_default()) 250 | }); 251 | let export_index = export.index as usize; 252 | match &parsed_globals { 253 | Ok(globals) => { 254 | if let Some(global_type) = globals.get(export_index) { 255 | *global = Ok(global_type.clone()); 256 | } 257 | } 258 | Err(err) => { 259 | *global = Err(err.clone()); 260 | } 261 | } 262 | } 263 | } 264 | } 265 | } 266 | 267 | /// Builds the function index space when iterating the function imports 268 | /// and then the function section to create an export index to type index map. 269 | fn build_func_export_idx_to_type_idx( 270 | imports: Option<&Vec>, 271 | functions_section: Option<&[u8]>, 272 | ) -> Result, ParseError> { 273 | let parsed_functions = 274 | parse_function_section(functions_section.unwrap_or_default()); 275 | let parsed_functions = match parsed_functions.as_ref() { 276 | Ok(f) => f, 277 | Err(err) => return Err(err.clone()), 278 | }; 279 | let mut space = HashMap::with_capacity( 280 | imports.map(|i| i.len()).unwrap_or(0) + parsed_functions.len(), 281 | ); 282 | let mut i = 0; 283 | if let Some(imports) = imports { 284 | for import in imports { 285 | if let ImportType::Function(final_index) = &import.import_type { 286 | space.insert(i, *final_index); 287 | i += 1; 288 | } 289 | } 290 | } 291 | for index in parsed_functions.iter() { 292 | space.insert(i, *index); 293 | i += 1; 294 | } 295 | Ok(space) 296 | } 297 | 298 | fn parse(input: &[u8], include_types: bool) -> Result { 299 | let mut state = ParserState { 300 | imports: None, 301 | exports: None, 302 | types_section: None, 303 | globals_section: None, 304 | functions_section: None, 305 | search_for_types: include_types, 306 | search_for_fns: include_types, 307 | search_for_globals: include_types, 308 | }; 309 | 310 | let (input, _) = parse_magic_bytes(input)?; 311 | let (mut input, _) = ensure_known_version(input)?; 312 | while !input.is_empty() && state.keep_searching() { 313 | let (rest, section) = parse_section(input)?; 314 | input = rest; 315 | match section.kind { 316 | 0x02 if state.imports.is_none() => { 317 | state.imports = Some(parse_import_section(section.bytes)?); 318 | } 319 | 0x07 if state.exports.is_none() => { 320 | state.set_exports(parse_export_section(section.bytes)?); 321 | } 322 | 0x01 if state.search_for_types => { 323 | state.types_section = Some(section.bytes); 324 | state.search_for_types = false; 325 | } 326 | 0x03 if state.search_for_fns => { 327 | state.functions_section = Some(section.bytes); 328 | state.search_for_fns = false; 329 | } 330 | 0x06 if state.search_for_globals => { 331 | state.globals_section = Some(section.bytes); 332 | state.search_for_globals = false; 333 | } 334 | _ => {} 335 | } 336 | } 337 | 338 | state.fill_type_information(); 339 | 340 | Ok(WasmDeps { 341 | imports: state.imports.unwrap_or_default(), 342 | exports: state.exports.unwrap_or_default(), 343 | }) 344 | } 345 | 346 | fn parse_magic_bytes(input: &[u8]) -> ParseResult<()> { 347 | // \0asm 348 | if input.starts_with(&[0, 97, 115, 109]) { 349 | Ok((&input[4..], ())) 350 | } else { 351 | Err(ParseError::NotWasm) 352 | } 353 | } 354 | 355 | fn ensure_known_version(input: &[u8]) -> ParseResult<()> { 356 | if input.len() < 4 { 357 | return Err(ParseError::UnexpectedEof); 358 | } 359 | 360 | let version = u32::from_le_bytes([input[0], input[1], input[2], input[3]]); 361 | if version != 1 { 362 | return Err(ParseError::UnsupportedVersion(version)); 363 | } 364 | 365 | Ok((&input[4..], ())) 366 | } 367 | 368 | fn parse_import_section(input: &[u8]) -> Result, ParseError> { 369 | let (mut input, count) = parse_var_uint(input)?; 370 | let mut imports = Vec::with_capacity(count as usize); 371 | 372 | for _ in 0..count { 373 | let (rest, import) = parse_import(input)?; 374 | input = rest; 375 | imports.push(import); 376 | } 377 | 378 | debug_assert!(input.is_empty()); 379 | 380 | Ok(imports) 381 | } 382 | 383 | fn parse_import(input: &[u8]) -> ParseResult { 384 | let (input, module) = parse_length_prefixed_string(input)?; 385 | let (input, name) = parse_length_prefixed_string(input)?; 386 | let (input, import_type) = parse_import_type(input)?; 387 | 388 | Ok(( 389 | input, 390 | Import { 391 | module, 392 | name, 393 | import_type, 394 | }, 395 | )) 396 | } 397 | 398 | fn parse_import_type(input: &[u8]) -> ParseResult { 399 | let (input, kind_byte) = read_byte(input)?; 400 | match kind_byte { 401 | 0x00 => { 402 | let (input, type_index) = parse_var_uint(input)?; 403 | Ok((input, ImportType::Function(type_index))) 404 | } 405 | 0x01 => { 406 | let (input, table_type) = parse_table_type(input)?; 407 | Ok((input, ImportType::Table(table_type))) 408 | } 409 | 0x02 => { 410 | let (input, memory_type) = parse_memory_type(input)?; 411 | Ok((input, ImportType::Memory(memory_type))) 412 | } 413 | 0x03 => { 414 | let (input, global_type) = parse_global_type(input)?; 415 | Ok((input, ImportType::Global(global_type))) 416 | } 417 | 0x04 => { 418 | let (input, tag_type) = parse_tag_type(input)?; 419 | Ok((input, ImportType::Tag(tag_type))) 420 | } 421 | _ => Err(ParseError::UnknownImportType(kind_byte)), 422 | } 423 | } 424 | 425 | fn parse_table_type(input: &[u8]) -> ParseResult { 426 | // element type 427 | let (input, element_type) = read_byte(input)?; 428 | if element_type != /* funref */ 0x70 { 429 | return Err(ParseError::UnknownElementType(element_type)); 430 | } 431 | 432 | // limits 433 | let (input, limits) = parse_limits(input)?; 434 | 435 | Ok(( 436 | input, 437 | TableType { 438 | element_type, 439 | limits, 440 | }, 441 | )) 442 | } 443 | 444 | fn parse_memory_type(input: &[u8]) -> ParseResult { 445 | let (input, limits) = parse_limits(input)?; 446 | Ok((input, MemoryType { limits })) 447 | } 448 | 449 | fn parse_global_type(input: &[u8]) -> ParseResult { 450 | let (input, value_type) = parse_value_type(input)?; 451 | let (input, mutability_byte) = read_byte(input)?; 452 | let mutability = match mutability_byte { 453 | 0x00 => false, 454 | 0x01 => true, 455 | _ => return Err(ParseError::InvalidMutabilityFlag(mutability_byte)), 456 | }; 457 | 458 | Ok(( 459 | input, 460 | GlobalType { 461 | value_type, 462 | mutability, 463 | }, 464 | )) 465 | } 466 | 467 | fn skip_init_expr(input: &[u8]) -> ParseResult<()> { 468 | let mut input = input; 469 | 470 | loop { 471 | if input.is_empty() { 472 | return Err(ParseError::UnexpectedEof); 473 | } 474 | 475 | let (next_input, opcode) = read_byte(input)?; 476 | input = next_input; 477 | 478 | // end op code 479 | if opcode == 0x0b { 480 | break; 481 | } 482 | } 483 | 484 | Ok((input, ())) 485 | } 486 | 487 | fn parse_value_type(input: &[u8]) -> ParseResult { 488 | let (input, byte) = read_byte(input)?; 489 | Ok(( 490 | input, 491 | match byte { 492 | 0x7F => ValueType::I32, 493 | 0x7E => ValueType::I64, 494 | 0x7D => ValueType::F32, 495 | 0x7C => ValueType::F64, 496 | _ => ValueType::Unknown, 497 | }, 498 | )) 499 | } 500 | 501 | fn parse_limits(input: &[u8]) -> ParseResult { 502 | fn maybe_parse_maximum(input: &[u8], flags: u8) -> ParseResult> { 503 | if flags == 0x01 { 504 | let (input, max) = parse_var_uint(input)?; 505 | Ok((input, Some(max))) 506 | } else { 507 | Ok((input, None)) 508 | } 509 | } 510 | 511 | let (input, flags) = read_byte(input)?; 512 | let (input, initial) = parse_var_uint(input)?; 513 | let (input, maximum) = maybe_parse_maximum(input, flags)?; 514 | 515 | Ok((input, Limits { initial, maximum })) 516 | } 517 | 518 | fn parse_tag_type(input: &[u8]) -> ParseResult { 519 | let (input, kind) = read_byte(input)?; 520 | if kind != 0x00 { 521 | return Err(ParseError::UnknownTagKind(kind)); 522 | } 523 | 524 | let (input, type_index) = parse_var_uint(input)?; 525 | Ok((input, TagType { kind, type_index })) 526 | } 527 | 528 | fn parse_export_section(input: &[u8]) -> Result, ParseError> { 529 | let (mut input, count) = parse_var_uint(input)?; 530 | let mut exports = Vec::with_capacity(count as usize); 531 | 532 | for _ in 0..count { 533 | let (rest, export) = parse_export_type(input)?; 534 | input = rest; 535 | exports.push(export); 536 | } 537 | 538 | debug_assert!(input.is_empty()); 539 | 540 | Ok(exports) 541 | } 542 | 543 | fn parse_export_type(input: &[u8]) -> ParseResult { 544 | let (input, name) = parse_length_prefixed_string(input)?; 545 | let (input, kind_byte) = read_byte(input)?; 546 | let (input, index) = parse_var_uint(input)?; 547 | 548 | let export_type = match kind_byte { 549 | 0x00 => ExportType::Function(Err(ParseError::UnresolvedExportType)), 550 | 0x01 => ExportType::Table, 551 | 0x02 => ExportType::Memory, 552 | 0x03 => ExportType::Global(Err(ParseError::UnresolvedExportType)), 553 | 0x04 => ExportType::Tag, 554 | _ => ExportType::Unknown, 555 | }; 556 | 557 | Ok(( 558 | input, 559 | Export { 560 | name, 561 | index, 562 | export_type, 563 | }, 564 | )) 565 | } 566 | 567 | fn parse_type_section( 568 | input: &[u8], 569 | ) -> Result, ParseError> { 570 | if input.is_empty() { 571 | return Ok(Vec::new()); 572 | } 573 | 574 | let (mut input, count) = parse_var_uint(input)?; 575 | let mut function_signatures = Vec::with_capacity(count as usize); 576 | 577 | for _ in 0..count { 578 | let (rest, signature) = parse_function_signature(input)?; 579 | function_signatures.push(signature); 580 | input = rest; 581 | } 582 | 583 | debug_assert!(input.is_empty()); 584 | 585 | Ok(function_signatures) 586 | } 587 | 588 | fn parse_function_signature(input: &[u8]) -> ParseResult { 589 | let (input, type_byte) = read_byte(input)?; 590 | if type_byte != 0x60 { 591 | return Err(ParseError::InvalidTypeIndicator(type_byte)); 592 | } 593 | 594 | let (mut input, param_count) = parse_var_uint(input)?; 595 | let mut params = Vec::with_capacity(param_count as usize); 596 | 597 | for _ in 0..param_count { 598 | let (rest, param_type) = parse_value_type(input)?; 599 | input = rest; 600 | params.push(param_type); 601 | } 602 | 603 | let (mut input, return_count) = parse_var_uint(input)?; 604 | let mut returns = Vec::with_capacity(return_count as usize); 605 | 606 | for _ in 0..return_count { 607 | let (rest, return_type) = parse_value_type(input)?; 608 | input = rest; 609 | returns.push(return_type); 610 | } 611 | Ok((input, FunctionSignature { params, returns })) 612 | } 613 | 614 | fn parse_function_section(input: &[u8]) -> Result, ParseError> { 615 | if input.is_empty() { 616 | return Ok(Vec::new()); 617 | } 618 | 619 | let (mut input, count) = parse_var_uint(input)?; 620 | let mut function_indices = Vec::with_capacity(count as usize); 621 | 622 | for _ in 0..count { 623 | let (rest, index) = parse_var_uint(input)?; 624 | function_indices.push(index); 625 | input = rest; 626 | } 627 | 628 | debug_assert!(input.is_empty()); 629 | 630 | Ok(function_indices) 631 | } 632 | 633 | fn parse_global_section(input: &[u8]) -> Result, ParseError> { 634 | if input.is_empty() { 635 | return Ok(Vec::new()); 636 | } 637 | 638 | let (mut input, count) = parse_var_uint(input)?; 639 | let mut globals = Vec::with_capacity(count as usize); 640 | 641 | for _ in 0..count { 642 | let (rest, global_type) = parse_global_type(input)?; 643 | let (rest, _) = skip_init_expr(rest)?; 644 | globals.push(global_type); 645 | input = rest; 646 | } 647 | 648 | debug_assert!(input.is_empty()); 649 | 650 | Ok(globals) 651 | } 652 | 653 | struct WasmSection<'a> { 654 | kind: u8, 655 | bytes: &'a [u8], 656 | } 657 | 658 | fn parse_section(input: &[u8]) -> ParseResult { 659 | let (input, kind) = read_byte(input)?; 660 | let (input, payload_len) = parse_var_uint(input)?; 661 | let payload_len = payload_len as usize; 662 | if input.len() < payload_len { 663 | return Err(ParseError::UnexpectedEof); 664 | } 665 | let section_bytes = &input[..payload_len]; 666 | Ok(( 667 | &input[payload_len..], 668 | WasmSection { 669 | kind, 670 | bytes: section_bytes, 671 | }, 672 | )) 673 | } 674 | 675 | fn parse_length_prefixed_string( 676 | input: &[u8], 677 | ) -> Result<(&[u8], &str), ParseError> { 678 | let (input, length) = parse_var_uint(input)?; 679 | if input.len() < length as usize { 680 | return Err(ParseError::UnexpectedEof); 681 | } 682 | let string_bytes = &input[..length as usize]; 683 | match std::str::from_utf8(string_bytes) { 684 | Ok(s) => Ok((&input[length as usize..], s)), 685 | Err(err) => Err(ParseError::InvalidUtf8(err)), 686 | } 687 | } 688 | 689 | /// Parse a variable length ULEB128 unsigned integer. 690 | fn parse_var_uint(input: &[u8]) -> ParseResult { 691 | let mut result = 0; 692 | let mut shift = 0; 693 | let mut input = input; 694 | loop { 695 | let (rest, byte) = read_byte(input)?; 696 | input = rest; 697 | if shift >= 32 || (shift == 28 && byte > 0b1111) { 698 | return Err(ParseError::IntegerOverflow); 699 | } 700 | result |= ((byte & 0x7f) as u32) << shift; 701 | if byte & 0x80 == 0 { 702 | break; 703 | } 704 | shift += 7; 705 | } 706 | Ok((input, result)) 707 | } 708 | 709 | fn read_byte(input: &[u8]) -> ParseResult { 710 | if input.is_empty() { 711 | return Err(ParseError::UnexpectedEof); 712 | } 713 | Ok((&input[1..], input[0])) 714 | } 715 | 716 | #[cfg(test)] 717 | mod test { 718 | use super::*; 719 | 720 | #[test] 721 | fn test_parse_length_prefixed_string() { 722 | // normal string 723 | { 724 | let input = [0x05, b'H', b'e', b'l', b'l', b'o']; 725 | let (rest, string) = parse_length_prefixed_string(&input).unwrap(); 726 | assert_eq!(string, "Hello"); 727 | assert!(rest.is_empty()); 728 | } 729 | 730 | // empty string 731 | { 732 | let input = [0x00]; 733 | let (rest, string) = parse_length_prefixed_string(&input).unwrap(); 734 | assert_eq!(string, ""); 735 | assert!(rest.is_empty()); 736 | } 737 | 738 | // non-ASCII characters (UTF-8) 739 | { 740 | let input = [0x03, 0xC3, 0xA9, b'm']; // "é" in UTF-8 + 'm' 741 | let (rest, string) = parse_length_prefixed_string(&input).unwrap(); 742 | assert_eq!(string, "ém"); 743 | assert!(rest.is_empty()); 744 | } 745 | 746 | // incorrect UTF-8 sequence 747 | { 748 | let input = [0x01, 0xFF]; // 0xFF is not valid in UTF-8 749 | assert!(parse_length_prefixed_string(&input).is_err()); 750 | } 751 | 752 | // insufficient length (claims 5 bytes, only 4 provided) 753 | { 754 | let input = [0x05, b't', b'e', b's', b't']; // length prefix says 5, but only 4 bytes follow 755 | assert!(parse_length_prefixed_string(&input).is_err()); 756 | } 757 | 758 | // excessive length (claims 2 bytes, 3 provided) 759 | { 760 | let input = [0x02, b'x', b'y', b'z']; 761 | let (rest, string) = parse_length_prefixed_string(&input).unwrap(); 762 | assert_eq!(string, "xy"); 763 | assert_eq!(rest, b"z"); 764 | } 765 | } 766 | 767 | #[test] 768 | fn test_parse_var_uint() { 769 | // single byte 770 | { 771 | let input = [0x01]; 772 | let (rest, value) = parse_var_uint(&input).unwrap(); 773 | assert_eq!(value, 1); 774 | assert_eq!(rest.len(), 0); 775 | } 776 | 777 | // number that spans multiple bytes 778 | { 779 | let input = [0x80, 0x01]; 780 | let (rest, value) = parse_var_uint(&input).unwrap(); 781 | assert_eq!(value, 128); 782 | assert_eq!(rest.len(), 0); 783 | } 784 | 785 | // the maximum 32-bit value 786 | { 787 | let input = [0xFF, 0xFF, 0xFF, 0xFF, 0x0F]; 788 | let (rest, value) = parse_var_uint(&input).unwrap(); 789 | assert_eq!(value, 0xFFFF_FFFF); 790 | assert_eq!(rest.len(), 0); 791 | } 792 | 793 | // input longer than 5 bytes (overflow protection) 794 | { 795 | let input = [0x80, 0x80, 0x80, 0x80, 0x80, 0x01]; 796 | assert!(parse_var_uint(&input).is_err()); 797 | } 798 | 799 | // non-terminated sequence 800 | { 801 | let input = [0x80, 0x80, 0x80]; 802 | assert!(parse_var_uint(&input).is_err()); 803 | } 804 | 805 | // input where the final byte would cause an overflow 806 | { 807 | let input = [0xFF, 0xFF, 0xFF, 0xFF, 0x4F]; 808 | assert!(parse_var_uint(&input).is_err()); 809 | } 810 | } 811 | } 812 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use pretty_assertions::assert_eq; 4 | 5 | use wasm_dep_analyzer::Export; 6 | use wasm_dep_analyzer::ExportType; 7 | use wasm_dep_analyzer::FunctionSignature; 8 | use wasm_dep_analyzer::GlobalType; 9 | use wasm_dep_analyzer::Import; 10 | use wasm_dep_analyzer::ImportType; 11 | use wasm_dep_analyzer::Limits; 12 | use wasm_dep_analyzer::ParseError; 13 | use wasm_dep_analyzer::ParseOptions; 14 | use wasm_dep_analyzer::TableType; 15 | use wasm_dep_analyzer::TagType; 16 | use wasm_dep_analyzer::ValueType; 17 | use wasm_dep_analyzer::WasmDeps; 18 | 19 | #[test] 20 | fn wasm_export_only() { 21 | // The following Rust code compiled: 22 | // 23 | // #[no_mangle] 24 | // pub fn add(left: usize, right: Usize) -> usize { 25 | // left + right 26 | // } 27 | let input = std::fs::read("tests/testdata/export_only.wasm").unwrap(); 28 | let module = WasmDeps::parse(&input, ParseOptions::default()).unwrap(); 29 | assert_eq!( 30 | module, 31 | WasmDeps { 32 | imports: vec![], 33 | exports: vec![ 34 | Export { 35 | name: "memory", 36 | index: 0, 37 | export_type: ExportType::Memory, 38 | }, 39 | Export { 40 | name: "add", 41 | index: 0, 42 | export_type: ExportType::Function(Ok(FunctionSignature { 43 | params: vec![ValueType::I32, ValueType::I32], 44 | returns: vec![ValueType::I32], 45 | })), 46 | }, 47 | Export { 48 | name: "__data_end", 49 | index: 1, 50 | export_type: ExportType::Global(Ok(GlobalType { 51 | value_type: ValueType::I32, 52 | mutability: false, 53 | })), 54 | }, 55 | Export { 56 | name: "__heap_base", 57 | index: 2, 58 | export_type: ExportType::Global(Ok(GlobalType { 59 | value_type: ValueType::I32, 60 | mutability: false, 61 | })), 62 | } 63 | ], 64 | } 65 | ); 66 | } 67 | 68 | #[test] 69 | fn wasm_import_and_export() { 70 | // The following Rust code compiled: 71 | // 72 | // extern "C" { 73 | // fn get_random_value() -> usize; 74 | // } 75 | 76 | // #[no_mangle] 77 | // pub fn add(left: usize) -> usize { 78 | // left + unsafe { get_random_value() } 79 | // } 80 | let input = std::fs::read("tests/testdata/import_export.wasm").unwrap(); 81 | let module = WasmDeps::parse(&input, ParseOptions::default()).unwrap(); 82 | assert_eq!( 83 | module, 84 | WasmDeps { 85 | imports: vec![Import { 86 | name: "get_random_value", 87 | module: "env", 88 | import_type: ImportType::Function(0), 89 | }], 90 | exports: vec![ 91 | Export { 92 | name: "memory", 93 | index: 0, 94 | export_type: ExportType::Memory, 95 | }, 96 | Export { 97 | name: "add", 98 | index: 1, 99 | export_type: ExportType::Function(Ok(FunctionSignature { 100 | params: vec![ValueType::I32], 101 | returns: vec![ValueType::I32], 102 | })), 103 | }, 104 | Export { 105 | name: "__data_end", 106 | index: 1, 107 | export_type: ExportType::Global(Ok(GlobalType { 108 | value_type: ValueType::I32, 109 | mutability: false, 110 | })), 111 | }, 112 | Export { 113 | name: "__heap_base", 114 | index: 2, 115 | export_type: ExportType::Global(Ok(GlobalType { 116 | value_type: ValueType::I32, 117 | mutability: false, 118 | })), 119 | } 120 | ], 121 | } 122 | ); 123 | } 124 | 125 | #[test] 126 | fn wasm_import_module() { 127 | let input = std::fs::read("tests/testdata/import_module.wasm").unwrap(); 128 | let module = WasmDeps::parse(&input, ParseOptions::default()).unwrap(); 129 | assert_eq!( 130 | module, 131 | WasmDeps { 132 | imports: vec![Import { 133 | name: "add", 134 | module: "./import_inner.mjs", 135 | import_type: ImportType::Function(0), 136 | }], 137 | exports: vec![Export { 138 | name: "exported_add", 139 | index: 1, 140 | export_type: ExportType::Function(Ok(FunctionSignature { 141 | params: vec![], // this module actually just adds two constants 142 | returns: vec![ValueType::I32], 143 | })), 144 | }], 145 | } 146 | ); 147 | } 148 | 149 | #[test] 150 | fn wasm_mutable_global_import() { 151 | // from wat2wasm "mutable globals" example 152 | // (module 153 | // (import "env" "g" (global (mut i32))) 154 | // (func (export "f") 155 | // i32.const 100 156 | // global.set 0)) 157 | let input = 158 | std::fs::read("tests/testdata/import_mutable_global.wasm").unwrap(); 159 | let module = WasmDeps::parse(&input, ParseOptions::default()).unwrap(); 160 | assert_eq!( 161 | module, 162 | WasmDeps { 163 | imports: vec![Import { 164 | name: "g", 165 | module: "env", 166 | import_type: ImportType::Global(GlobalType { 167 | value_type: ValueType::I32, 168 | mutability: true, 169 | }), 170 | }], 171 | exports: vec![Export { 172 | name: "f", 173 | index: 0, 174 | export_type: ExportType::Function(Ok(FunctionSignature { 175 | params: vec![], 176 | returns: vec![], 177 | })), 178 | }], 179 | } 180 | ); 181 | } 182 | 183 | #[test] 184 | fn wasm_import_table() { 185 | // (module 186 | // ;; Import a table named "tbl" from a module named "js". 187 | // ;; The table is of type "funcref" with initial size 2 and no maximum size. 188 | // (import "js" "tbl" (table 2 funcref)) 189 | // ) 190 | let input = std::fs::read("tests/testdata/import_table.wasm").unwrap(); 191 | let module = WasmDeps::parse(&input, ParseOptions::default()).unwrap(); 192 | assert_eq!( 193 | module, 194 | WasmDeps { 195 | imports: vec![Import { 196 | name: "table", 197 | module: "env", 198 | import_type: ImportType::Table(TableType { 199 | element_type: 112, 200 | limits: Limits { 201 | initial: 2, 202 | maximum: None, 203 | }, 204 | }), 205 | }], 206 | exports: vec![], 207 | } 208 | ); 209 | } 210 | 211 | #[test] 212 | fn wasm_import_tag() { 213 | // (module 214 | // ;; Import an exception tag 215 | // (import "env" "exception_tag" (tag (type 0))) 216 | 217 | // ;; Define an exception type (a list of value types that the exception can carry) 218 | // (type (func (param i32))) 219 | 220 | // ;; Define a tag that uses the above exception type 221 | // (tag (type 0)) 222 | 223 | // ;; Export the defined tag 224 | // (export "exported_tag" (tag 0)) 225 | // ) 226 | let input = std::fs::read("tests/testdata/import_tag.wasm").unwrap(); 227 | let module = WasmDeps::parse(&input, ParseOptions::default()).unwrap(); 228 | assert_eq!( 229 | module, 230 | WasmDeps { 231 | imports: vec![Import { 232 | name: "exception_tag", 233 | module: "env", 234 | import_type: ImportType::Tag(TagType { 235 | kind: 0, 236 | type_index: 0, 237 | }), 238 | }], 239 | exports: vec![Export { 240 | name: "exported_tag", 241 | index: 0, 242 | export_type: ExportType::Tag, 243 | }], 244 | } 245 | ); 246 | } 247 | 248 | #[test] 249 | fn wasm_export_memory() { 250 | // (module 251 | // ;; Define a memory with an initial size of 1 page (64KiB) 252 | // ;; and a maximum size of 10 pages (640KiB). 253 | // (memory (export "mem") 1 10) 254 | // ) 255 | let input = std::fs::read("tests/testdata/export_memory.wasm").unwrap(); 256 | let module = WasmDeps::parse(&input, ParseOptions::default()).unwrap(); 257 | assert_eq!( 258 | module, 259 | WasmDeps { 260 | imports: vec![], 261 | exports: vec![Export { 262 | name: "mem", 263 | index: 0, 264 | export_type: ExportType::Memory, 265 | }], 266 | } 267 | ); 268 | } 269 | 270 | #[test] 271 | fn wasm_export_mutable_global() { 272 | // (module 273 | // ;; Define a mutable global of type i32 initialized to 42 274 | // (global $myGlobal (export "myExportedGlobal") (mut i32) (i32.const 42)) 275 | // ) 276 | let input = 277 | std::fs::read("tests/testdata/export_mutable_global.wasm").unwrap(); 278 | let module = WasmDeps::parse(&input, ParseOptions::default()).unwrap(); 279 | assert_eq!( 280 | module, 281 | WasmDeps { 282 | imports: vec![], 283 | exports: vec![Export { 284 | name: "myExportedGlobal", 285 | index: 0, 286 | export_type: ExportType::Global(Ok(GlobalType { 287 | value_type: ValueType::I32, 288 | mutability: true, 289 | })), 290 | }], 291 | } 292 | ); 293 | } 294 | 295 | #[test] 296 | fn wasm_export_const_global() { 297 | // (module 298 | // ;; Define a constant global of type i32 initialized to 42 299 | // (global $myGlobal (export "myExportedGlobal") i32 (i32.const 42)) 300 | // ) 301 | let input = std::fs::read("tests/testdata/export_const_global.wasm").unwrap(); 302 | let module = WasmDeps::parse(&input, ParseOptions::default()).unwrap(); 303 | assert_eq!( 304 | module, 305 | WasmDeps { 306 | imports: vec![], 307 | exports: vec![Export { 308 | name: "myExportedGlobal", 309 | index: 0, 310 | export_type: ExportType::Global(Ok(GlobalType { 311 | value_type: ValueType::I32, 312 | mutability: false, 313 | })), 314 | }], 315 | } 316 | ); 317 | } 318 | 319 | #[test] 320 | fn wasm_export_imported_func() { 321 | // (module 322 | // ;; Import a function named 'external_func' from the 'env' module. 323 | // ;; The function takes two i32 integers and returns an i32 integer. 324 | // (import "env" "external_func" (func $imported_func (param i32 i32) (result i32))) 325 | 326 | // ;; Export the imported function under the name 'exported_func'. 327 | // (export "exported_func" (func $imported_func)) 328 | // ) 329 | let input = 330 | std::fs::read("tests/testdata/export_imported_func.wasm").unwrap(); 331 | let module = WasmDeps::parse(&input, ParseOptions::default()).unwrap(); 332 | assert_eq!( 333 | module, 334 | WasmDeps { 335 | imports: vec![Import { 336 | name: "external_func", 337 | module: "env", 338 | import_type: ImportType::Function(0), 339 | }], 340 | exports: vec![Export { 341 | name: "exported_func", 342 | index: 0, 343 | export_type: ExportType::Function(Ok(FunctionSignature { 344 | params: vec![ValueType::I32, ValueType::I32], 345 | returns: vec![ValueType::I32], 346 | })), 347 | }], 348 | } 349 | ); 350 | } 351 | 352 | #[test] 353 | fn wasm_skip_types() { 354 | // function 355 | let input = 356 | std::fs::read("tests/testdata/export_imported_func.wasm").unwrap(); 357 | let module = 358 | WasmDeps::parse(&input, ParseOptions { skip_types: true }).unwrap(); 359 | assert_eq!( 360 | module, 361 | WasmDeps { 362 | imports: vec![Import { 363 | name: "external_func", 364 | module: "env", 365 | import_type: ImportType::Function(0), 366 | }], 367 | exports: vec![Export { 368 | name: "exported_func", 369 | index: 0, 370 | // won't be resolved 371 | export_type: ExportType::Function(Err( 372 | ParseError::UnresolvedExportType 373 | )), 374 | }], 375 | } 376 | ); 377 | 378 | // global 379 | let input = std::fs::read("tests/testdata/export_const_global.wasm").unwrap(); 380 | let module = 381 | WasmDeps::parse(&input, ParseOptions { skip_types: true }).unwrap(); 382 | assert_eq!( 383 | module, 384 | WasmDeps { 385 | imports: vec![], 386 | exports: vec![Export { 387 | name: "myExportedGlobal", 388 | index: 0, 389 | export_type: ExportType::Global(Err(ParseError::UnresolvedExportType)), 390 | }], 391 | } 392 | ); 393 | } 394 | -------------------------------------------------------------------------------- /tests/testdata/export_const_global.wasm: -------------------------------------------------------------------------------- 1 | asmA* myExportedGlobalname myGlobal -------------------------------------------------------------------------------- /tests/testdata/export_imported_func.wasm: -------------------------------------------------------------------------------- 1 | asm`env external_func exported_funcname imported_func -------------------------------------------------------------------------------- /tests/testdata/export_memory.wasm: -------------------------------------------------------------------------------- 1 | asm 2 | memname -------------------------------------------------------------------------------- /tests/testdata/export_mutable_global.wasm: -------------------------------------------------------------------------------- 1 | asmA* myExportedGlobalname myGlobal -------------------------------------------------------------------------------- /tests/testdata/export_only.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denoland/wasm_dep_analyzer/6ff78eebdcb4cf28a99d8fea511a09467dfc5432/tests/testdata/export_only.wasm -------------------------------------------------------------------------------- /tests/testdata/import_export.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denoland/wasm_dep_analyzer/6ff78eebdcb4cf28a99d8fea511a09467dfc5432/tests/testdata/import_export.wasm -------------------------------------------------------------------------------- /tests/testdata/import_module.wasm: -------------------------------------------------------------------------------- 1 | asm ``./import_inner.mjsadd exported_add 2 | 3 | AA -------------------------------------------------------------------------------- /tests/testdata/import_mutable_global.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denoland/wasm_dep_analyzer/6ff78eebdcb4cf28a99d8fea511a09467dfc5432/tests/testdata/import_mutable_global.wasm -------------------------------------------------------------------------------- /tests/testdata/import_table.wasm: -------------------------------------------------------------------------------- 1 | asmenvtablepname -------------------------------------------------------------------------------- /tests/testdata/import_tag.wasm: -------------------------------------------------------------------------------- 1 | asm`env exception_tag  exported_tagname --------------------------------------------------------------------------------