Skip to main content

tinywasm_parser/
lib.rs

1#![no_std]
2#![doc(test(
3    no_crate_inject,
4    attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_assignments, unused_variables))
5))]
6#![warn(missing_docs, rust_2018_idioms, unreachable_pub)]
7#![forbid(unsafe_code)]
8//! See [`tinywasm`](https://docs.rs/tinywasm) for documentation.
9
10extern crate alloc;
11
12#[cfg(feature = "std")]
13extern crate std;
14
15// log for logging (optional).
16#[cfg(feature = "log")]
17#[allow(clippy::single_component_path_imports, unused_imports)]
18use log;
19
20// noop fallback if logging is disabled.
21#[cfg(not(feature = "log"))]
22#[allow(unused_imports, unused_macros)]
23pub(crate) mod log {
24    macro_rules! debug    ( ($($tt:tt)*) => {{}} );
25    macro_rules! info    ( ($($tt:tt)*) => {{}} );
26    macro_rules! error    ( ($($tt:tt)*) => {{}} );
27    pub(crate) use debug;
28    pub(crate) use error;
29    pub(crate) use info;
30}
31
32mod conversion;
33mod emitter;
34mod error;
35mod macros;
36mod module;
37mod selection;
38mod validation;
39mod visit;
40
41#[cfg(parallel_parser)]
42mod parallel;
43
44pub use error::*;
45use module::ModuleReader;
46use validation::Validator;
47
48#[cfg(feature = "validate")]
49use wasmparser::WasmFeatures;
50
51pub use tinywasm_types::Module;
52
53/// Parser optimization and lowering options.
54#[non_exhaustive]
55#[derive(Debug, Clone)]
56pub struct ParserOptions {
57    /// Whether to validate modules while parsing. Enabled by default when the
58    /// `validate` feature is enabled.
59    ///
60    /// Requires the `validate` feature to have any effect.
61    ///
62    /// Disable this only for trusted input. Parsing without validation may produce
63    /// a module that violates runtime assumptions.
64    pub validation: bool,
65
66    /// Whether to enable some of the optimizations during lowering, such as bounded instruction selection.
67    pub optimize: bool,
68
69    /// Whether to deduplicate immutable function operands while parsing.
70    pub deduplicate_operands: bool,
71
72    #[cfg(parallel_parser)]
73    /// Number of threads to use for parallel parsing.
74    ///
75    /// Requires the `parallel` feature. Ignored when the feature is disabled.
76    ///
77    /// - `None`: auto-detect based on available parallelism
78    /// - `Some(1)`: force single-threaded
79    /// - `Some(n)`: use up to `n` workers
80    pub threads: Option<usize>,
81}
82
83impl Default for ParserOptions {
84    fn default() -> Self {
85        Self {
86            validation: cfg!(feature = "validate"),
87            optimize: true,
88            deduplicate_operands: true,
89            #[cfg(parallel_parser)]
90            threads: None,
91        }
92    }
93}
94
95impl ParserOptions {
96    /// Create parser options with default settings.
97    pub fn new() -> Self {
98        Self::default()
99    }
100
101    /// Enable or disable WebAssembly validation.
102    ///
103    /// Requires the `validate` feature to have any effect.
104    ///
105    /// Disable this only for trusted input. Parsing without validation may produce
106    /// a module that violates runtime assumptions.
107    pub const fn with_validation(mut self, enabled: bool) -> Self {
108        assert!(!enabled || cfg!(feature = "validate"), "validation requires the `validate` feature");
109        self.validation = enabled;
110        self
111    }
112
113    /// Returns whether WebAssembly validation is enabled.
114    pub const fn validation(&self) -> bool {
115        self.validation
116    }
117
118    /// Enable or disable some of the optimizations during lowering, such as bounded instruction selection.
119    pub const fn with_optimize(mut self, enabled: bool) -> Self {
120        self.optimize = enabled;
121        self
122    }
123
124    /// Returns whether some of the optimizations during lowering, such as bounded instruction selection, are enabled.
125    pub const fn optimize(&self) -> bool {
126        self.optimize
127    }
128
129    /// Enable or disable parse-time deduplication of immutable function operands.
130    pub const fn with_operand_deduplication(mut self, enabled: bool) -> Self {
131        self.deduplicate_operands = enabled;
132        self
133    }
134
135    /// Returns whether immutable function operands are deduplicated while parsing.
136    pub const fn deduplicate_operands(&self) -> bool {
137        self.deduplicate_operands
138    }
139
140    #[cfg(parallel_parser)]
141    /// Set the number of threads for parallel parsing.
142    ///
143    /// Requires the `parallel` feature to have any effect.
144    pub const fn with_threads(mut self, threads: usize) -> Self {
145        self.threads = Some(threads);
146        self
147    }
148
149    #[cfg(parallel_parser)]
150    /// Returns the configured parser thread count, or `None` for auto-detect.
151    pub const fn threads(&self) -> Option<usize> {
152        self.threads
153    }
154}
155
156/// A WebAssembly parser
157#[derive(Debug, Default)]
158pub struct Parser {
159    options: ParserOptions,
160}
161
162impl Parser {
163    /// Create a parser with the given options.
164    pub const fn new(options: ParserOptions) -> Self {
165        Self { options }
166    }
167
168    /// Read back parser options.
169    pub const fn options(&self) -> &ParserOptions {
170        &self.options
171    }
172
173    fn validator(&self) -> Option<Validator> {
174        #[cfg(feature = "validate")]
175        {
176            let features = WasmFeatures::WASM3
177                .difference(WasmFeatures::THREADS)
178                .union(WasmFeatures::CUSTOM_PAGE_SIZES)
179                .union(WasmFeatures::WIDE_ARITHMETIC)
180                .union(WasmFeatures::COMPACT_IMPORTS);
181            self.options.validation().then(|| Validator::new_with_features(features))
182        }
183        #[cfg(not(feature = "validate"))]
184        {
185            assert!(!self.options.validation(), "validation requires the `validate` feature");
186            None
187        }
188    }
189
190    #[cfg(feature = "std")]
191    fn read_more(stream: &mut impl std::io::Read, buffer: &mut alloc::vec::Vec<u8>, hint: usize) -> Result<usize> {
192        let len = buffer.len();
193        // Size hints can come from untrusted section lengths.
194        let increment = hint.clamp(1, 64 * 1024);
195        let new_len =
196            len.checked_add(increment).ok_or_else(|| ParseError::Other("stream buffer is too large".into()))?;
197        buffer
198            .try_reserve(increment)
199            .map_err(|e| ParseError::Other(alloc::format!("Error reserving stream buffer: {e}")))?;
200        buffer.resize(new_len, 0);
201        let read_bytes = loop {
202            match stream.read(&mut buffer[len..]) {
203                Ok(read_bytes) => break read_bytes,
204                Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
205                Err(e) => {
206                    buffer.truncate(len);
207                    return Err(ParseError::Other(alloc::format!("Error reading from stream: {e}")));
208                }
209            }
210        };
211        buffer.truncate(len + read_bytes);
212        Ok(read_bytes)
213    }
214
215    /// Parse a [`Module`] from bytes
216    pub fn parse_module_bytes(&self, wasm: impl AsRef<[u8]>) -> Result<Module> {
217        let wasm = wasm.as_ref();
218        let mut validator = self.validator();
219        let mut reader = ModuleReader::default();
220
221        for payload in wasmparser::Parser::new(0).parse_all(wasm) {
222            match payload? {
223                wasmparser::Payload::CodeSectionStart { count, range, size } => {
224                    reader.begin_code_section(count, range, size, validator.as_mut(), &self.options)?;
225                }
226                wasmparser::Payload::CodeSectionEntry(function) => {
227                    reader.process_borrowed_code_section_entry(function, validator.as_mut(), &self.options)?;
228                }
229                payload => reader.process_payload(payload, validator.as_mut())?,
230            }
231        }
232
233        if !reader.end_reached {
234            return Err(ParseError::EndNotReached);
235        }
236
237        reader.process_pending_functions(&self.options)?;
238        reader.into_module()
239    }
240
241    #[cfg(feature = "std")]
242    /// Parse a [`Module`] from a file. Requires `std` feature.
243    pub fn parse_module_file(&self, path: impl AsRef<crate::std::path::Path> + Clone) -> Result<Module> {
244        let file = crate::std::fs::File::open(&path)
245            .map_err(|e| ParseError::Other(alloc::format!("Error opening file {:?}: {}", path.as_ref(), e)))?;
246        self.parse_module_stream(&mut crate::std::io::BufReader::new(file))
247    }
248
249    #[cfg(feature = "std")]
250    /// Parse a [`Module`] from a stream. Requires `std` feature.
251    pub fn parse_module_stream(&self, mut stream: impl std::io::Read) -> Result<Module> {
252        let mut validator = self.validator();
253        let mut reader = ModuleReader::default();
254        let mut buffer = alloc::vec::Vec::new();
255        let mut parser = wasmparser::Parser::new(0);
256        let mut eof = false;
257        let mut buffer_offset = 0;
258
259        loop {
260            match parser.parse(&buffer[buffer_offset..], eof)? {
261                wasmparser::Chunk::NeedMoreData(hint) => {
262                    if buffer_offset != 0 {
263                        buffer.copy_within(buffer_offset.., 0);
264                        buffer.truncate(buffer.len() - buffer_offset);
265                        buffer_offset = 0;
266                    }
267                    let read_bytes = Self::read_more(&mut stream, &mut buffer, hint)?;
268                    eof = read_bytes == 0;
269                }
270                wasmparser::Chunk::Parsed { consumed, payload } => {
271                    #[cfg(parallel_parser)]
272                    let mut deferred_code_section = None;
273
274                    match payload {
275                        wasmparser::Payload::CodeSectionStart { count, range, size } => {
276                            let defer =
277                                reader.begin_code_section(count, range, size, validator.as_mut(), &self.options)?;
278
279                            #[cfg(parallel_parser)]
280                            if defer {
281                                deferred_code_section = Some((count, size as usize));
282                            }
283
284                            #[cfg(not(parallel_parser))]
285                            let _ = defer;
286                        }
287                        wasmparser::Payload::CodeSectionEntry(function) => {
288                            reader.process_inline_code_section_entry(function, validator.as_mut(), &self.options)?;
289                        }
290                        payload => {
291                            reader.process_payload(payload, validator.as_mut())?;
292                        }
293                    }
294                    buffer_offset += consumed;
295
296                    #[cfg(parallel_parser)]
297                    if let Some((count, section_size)) = deferred_code_section {
298                        while buffer.len() - buffer_offset < section_size {
299                            let remaining = section_size - (buffer.len() - buffer_offset);
300                            let read_bytes = Self::read_more(&mut stream, &mut buffer, remaining)?;
301                            if read_bytes == 0 {
302                                return Err(ParseError::ParseError {
303                                    message: "unexpected end-of-file".into(),
304                                    offset: parser.offset() + (buffer.len() - buffer_offset) as u64,
305                                });
306                            }
307                        }
308
309                        let section_end = buffer_offset + section_size;
310                        let section_bytes =
311                            tinywasm_types::Shared::<[u8]>::from(buffer[buffer_offset..section_end].to_vec());
312                        reader.queue_owned_code_section(count, parser.offset(), section_bytes, validator.as_mut())?;
313                        parser.skip_section();
314                        buffer_offset = section_end;
315                        continue;
316                    }
317
318                    if reader.end_reached {
319                        if buffer_offset != buffer.len() {
320                            return Err(ParseError::Other("trailing bytes after end of module".into()));
321                        }
322
323                        if !eof {
324                            let read_bytes = Self::read_more(&mut stream, &mut buffer, 1)?;
325                            eof = read_bytes == 0;
326
327                            if !eof {
328                                return Err(ParseError::Other("trailing bytes after end of module".into()));
329                            }
330                        }
331                    }
332
333                    if reader.end_reached || eof {
334                        reader.process_pending_functions(&self.options)?;
335                        return reader.into_module();
336                    }
337                }
338            };
339        }
340    }
341}
342
343impl TryFrom<ModuleReader<'_>> for Module {
344    type Error = ParseError;
345
346    fn try_from(reader: ModuleReader<'_>) -> Result<Self> {
347        reader.into_module()
348    }
349}
350
351/// Parse a module from bytes
352pub fn parse_bytes(wasm: &[u8]) -> Result<Module> {
353    Parser::default().parse_module_bytes(wasm)
354}
355
356#[cfg(feature = "std")]
357/// Parse a module from a file. Requires the `std` feature.
358pub fn parse_file(path: impl AsRef<crate::std::path::Path> + Clone) -> Result<Module> {
359    Parser::default().parse_module_file(path)
360}
361
362#[cfg(feature = "std")]
363/// Parse a module from a stream. Requires the `std` feature.
364pub fn parse_stream(stream: impl crate::std::io::Read) -> Result<Module> {
365    Parser::default().parse_module_stream(stream)
366}