Skip to main content

use_wasm_binary/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6
7/// WebAssembly binary magic bytes: '\0asm'.
8pub const WASM_MAGIC_BYTES: [u8; 4] = [0x00, b'a', b's', b'm'];
9
10/// WebAssembly version 1 encoded as little-endian bytes.
11pub const WASM_VERSION_1_BYTES: [u8; 4] = [0x01, 0x00, 0x00, 0x00];
12
13/// The byte length of a minimal WebAssembly binary header.
14pub const WASM_BINARY_HEADER_LEN: usize = 8;
15
16/// Coarse WebAssembly binary format marker.
17#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
18pub enum WasmBinaryFormat {
19    /// Core WebAssembly module binary format.
20    #[default]
21    CoreModule,
22}
23
24impl WasmBinaryFormat {
25    /// Returns the stable format label.
26    #[must_use]
27    pub const fn as_str(self) -> &'static str {
28        match self {
29            Self::CoreModule => "core-module",
30        }
31    }
32
33    /// Returns the magic bytes associated with this format.
34    #[must_use]
35    pub const fn magic_bytes(self) -> [u8; 4] {
36        match self {
37            Self::CoreModule => WASM_MAGIC_BYTES,
38        }
39    }
40
41    /// Returns the supported version bytes associated with this format.
42    #[must_use]
43    pub const fn version_bytes(self) -> [u8; 4] {
44        match self {
45            Self::CoreModule => WASM_VERSION_1_BYTES,
46        }
47    }
48}
49
50impl fmt::Display for WasmBinaryFormat {
51    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52        formatter.write_str(self.as_str())
53    }
54}
55
56/// Error returned when a WebAssembly binary header is not recognized.
57#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum WasmBinaryError {
59    /// Fewer than eight bytes were provided.
60    TooShort,
61    /// The first four bytes were not the WebAssembly magic bytes.
62    InvalidMagic,
63    /// The version bytes do not describe WebAssembly version 1.
64    UnsupportedVersion,
65}
66
67impl fmt::Display for WasmBinaryError {
68    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            Self::TooShort => formatter.write_str("WebAssembly binary header is too short"),
71            Self::InvalidMagic => formatter.write_str("missing WebAssembly magic bytes"),
72            Self::UnsupportedVersion => {
73                formatter.write_str("unsupported WebAssembly binary version")
74            },
75        }
76    }
77}
78
79impl Error for WasmBinaryError {}
80
81/// Minimal WebAssembly binary header metadata.
82#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
83pub struct WasmBinaryHeader {
84    version: u32,
85}
86
87impl WasmBinaryHeader {
88    /// Creates header metadata for a known version.
89    #[must_use]
90    pub const fn new(version: u32) -> Self {
91        Self { version }
92    }
93
94    /// Returns the decoded WebAssembly binary version.
95    #[must_use]
96    pub const fn version(self) -> u32 {
97        self.version
98    }
99
100    /// Returns 'true' for WebAssembly version 1.
101    #[must_use]
102    pub const fn is_version_1(self) -> bool {
103        self.version == 1
104    }
105
106    /// Returns the binary format marker for this header.
107    #[must_use]
108    pub const fn format(self) -> WasmBinaryFormat {
109        WasmBinaryFormat::CoreModule
110    }
111}
112
113/// Returns 'true' when the byte slice starts with the WebAssembly magic bytes.
114#[must_use]
115pub fn has_wasm_magic(bytes: &[u8]) -> bool {
116    bytes.get(..WASM_MAGIC_BYTES.len()) == Some(WASM_MAGIC_BYTES.as_slice())
117}
118
119/// Returns 'true' when the byte slice has the magic bytes and version-1 header.
120#[must_use]
121pub fn looks_like_wasm_binary(bytes: &[u8]) -> bool {
122    validate_wasm_header(bytes).is_ok()
123}
124
125/// Validates a minimal WebAssembly binary header.
126pub fn validate_wasm_header(bytes: &[u8]) -> Result<WasmBinaryHeader, WasmBinaryError> {
127    if bytes.len() < WASM_BINARY_HEADER_LEN {
128        return Err(WasmBinaryError::TooShort);
129    }
130
131    if !has_wasm_magic(bytes) {
132        return Err(WasmBinaryError::InvalidMagic);
133    }
134
135    let version = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
136    if version != 1 {
137        return Err(WasmBinaryError::UnsupportedVersion);
138    }
139
140    Ok(WasmBinaryHeader::new(version))
141}
142
143/// Parses a minimal WebAssembly binary header.
144pub fn parse_wasm_binary_header(bytes: &[u8]) -> Result<WasmBinaryHeader, WasmBinaryError> {
145    validate_wasm_header(bytes)
146}
147
148/// Validates a minimal WebAssembly binary header.
149pub fn validate_wasm_binary_header(bytes: &[u8]) -> Result<WasmBinaryHeader, WasmBinaryError> {
150    validate_wasm_header(bytes)
151}
152
153#[cfg(test)]
154mod tests {
155    use super::{
156        WASM_MAGIC_BYTES, WASM_VERSION_1_BYTES, WasmBinaryError, WasmBinaryFormat, has_wasm_magic,
157        looks_like_wasm_binary, parse_wasm_binary_header, validate_wasm_binary_header,
158        validate_wasm_header,
159    };
160
161    #[test]
162    fn detects_magic_bytes() {
163        assert!(has_wasm_magic(&WASM_MAGIC_BYTES));
164        assert!(!has_wasm_magic(b"wat"));
165    }
166
167    #[test]
168    fn validates_version_one_header() {
169        let mut bytes = Vec::from(WASM_MAGIC_BYTES);
170        bytes.extend(WASM_VERSION_1_BYTES);
171        let header = validate_wasm_header(&bytes).expect("valid version-one header");
172
173        assert_eq!(header.version(), 1);
174        assert!(header.is_version_1());
175        assert_eq!(header.format(), WasmBinaryFormat::CoreModule);
176        assert_eq!(header.format().to_string(), "core-module");
177        assert_eq!(WasmBinaryFormat::CoreModule.magic_bytes(), WASM_MAGIC_BYTES);
178        assert_eq!(parse_wasm_binary_header(&bytes), Ok(header));
179        assert_eq!(validate_wasm_binary_header(&bytes), Ok(header));
180        assert!(looks_like_wasm_binary(&bytes));
181    }
182
183    #[test]
184    fn rejects_invalid_headers() {
185        assert_eq!(
186            validate_wasm_header(b"\0asm"),
187            Err(WasmBinaryError::TooShort)
188        );
189        assert_eq!(
190            validate_wasm_header(b"nope\x01\0\0\0"),
191            Err(WasmBinaryError::InvalidMagic)
192        );
193        assert_eq!(
194            validate_wasm_header(b"\0asm\x02\0\0\0"),
195            Err(WasmBinaryError::UnsupportedVersion)
196        );
197    }
198}