Skip to main content

use_wasm_text/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Error returned when WebAssembly text metadata is invalid.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum WasmTextError {
10    /// The supplied value was empty.
11    Empty,
12    /// The supplied value is not accepted by this crate's conservative rules.
13    Invalid,
14    /// The supplied S-expression marker is unknown.
15    UnknownMarker,
16}
17
18impl fmt::Display for WasmTextError {
19    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::Empty => formatter.write_str("WebAssembly text value cannot be empty"),
22            Self::Invalid => formatter.write_str("invalid WebAssembly text value"),
23            Self::UnknownMarker => formatter.write_str("unknown WebAssembly S-expression marker"),
24        }
25    }
26}
27
28impl Error for WasmTextError {}
29
30fn validate_text_name(value: &str) -> Result<&str, WasmTextError> {
31    let trimmed = value.trim();
32    if trimmed.is_empty() {
33        return Err(WasmTextError::Empty);
34    }
35    if trimmed
36        .chars()
37        .any(|character| character.is_control() || character.is_whitespace())
38    {
39        return Err(WasmTextError::Invalid);
40    }
41    Ok(trimmed)
42}
43
44/// WAT identifier such as '$name'.
45#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
46pub struct WatIdentifier(String);
47
48impl WatIdentifier {
49    /// Creates a validated WAT identifier.
50    pub fn new(value: impl AsRef<str>) -> Result<Self, WasmTextError> {
51        let trimmed = validate_text_name(value.as_ref())?;
52        if !trimmed.starts_with('$') || trimmed.len() == 1 {
53            return Err(WasmTextError::Invalid);
54        }
55        Ok(Self(trimmed.to_owned()))
56    }
57
58    /// Returns the stored identifier.
59    #[must_use]
60    pub fn as_str(&self) -> &str {
61        &self.0
62    }
63
64    /// Consumes the wrapper and returns the stored identifier.
65    #[must_use]
66    pub fn into_string(self) -> String {
67        self.0
68    }
69}
70
71impl AsRef<str> for WatIdentifier {
72    fn as_ref(&self) -> &str {
73        self.as_str()
74    }
75}
76
77impl fmt::Display for WatIdentifier {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        formatter.write_str(self.as_str())
80    }
81}
82
83impl FromStr for WatIdentifier {
84    type Err = WasmTextError;
85
86    fn from_str(value: &str) -> Result<Self, Self::Err> {
87        Self::new(value)
88    }
89}
90
91impl TryFrom<&str> for WatIdentifier {
92    type Error = WasmTextError;
93
94    fn try_from(value: &str) -> Result<Self, Self::Error> {
95        Self::new(value)
96    }
97}
98
99/// WAT module name metadata.
100#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
101pub struct TextModuleName(String);
102
103impl TextModuleName {
104    /// Creates a validated text module name.
105    pub fn new(value: impl AsRef<str>) -> Result<Self, WasmTextError> {
106        validate_text_name(value.as_ref()).map(|value| Self(value.to_owned()))
107    }
108
109    /// Returns the stored module name.
110    #[must_use]
111    pub fn as_str(&self) -> &str {
112        &self.0
113    }
114
115    /// Consumes the wrapper and returns the stored module name.
116    #[must_use]
117    pub fn into_string(self) -> String {
118        self.0
119    }
120}
121
122impl AsRef<str> for TextModuleName {
123    fn as_ref(&self) -> &str {
124        self.as_str()
125    }
126}
127
128impl fmt::Display for TextModuleName {
129    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130        formatter.write_str(self.as_str())
131    }
132}
133
134impl FromStr for TextModuleName {
135    type Err = WasmTextError;
136
137    fn from_str(value: &str) -> Result<Self, Self::Err> {
138        Self::new(value)
139    }
140}
141
142impl TryFrom<&str> for TextModuleName {
143    type Error = WasmTextError;
144
145    fn try_from(value: &str) -> Result<Self, Self::Error> {
146        Self::new(value)
147    }
148}
149
150/// Small vocabulary of S-expression markers used by WebAssembly text.
151#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
152pub enum SExpressionMarker {
153    /// Module marker.
154    #[default]
155    Module,
156    /// Function marker.
157    Func,
158    /// Import marker.
159    Import,
160    /// Export marker.
161    Export,
162    /// Memory marker.
163    Memory,
164    /// Table marker.
165    Table,
166    /// Global marker.
167    Global,
168    /// Type marker.
169    Type,
170    /// Component marker.
171    Component,
172}
173
174impl SExpressionMarker {
175    /// Returns the marker label without the opening parenthesis.
176    #[must_use]
177    pub const fn as_str(self) -> &'static str {
178        match self {
179            Self::Module => "module",
180            Self::Func => "func",
181            Self::Import => "import",
182            Self::Export => "export",
183            Self::Memory => "memory",
184            Self::Table => "table",
185            Self::Global => "global",
186            Self::Type => "type",
187            Self::Component => "component",
188        }
189    }
190}
191
192impl fmt::Display for SExpressionMarker {
193    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
194        formatter.write_str(self.as_str())
195    }
196}
197
198impl FromStr for SExpressionMarker {
199    type Err = WasmTextError;
200
201    fn from_str(value: &str) -> Result<Self, Self::Err> {
202        let trimmed = value.trim().trim_start_matches('(');
203        if trimmed.is_empty() {
204            return Err(WasmTextError::Empty);
205        }
206        match trimmed.to_ascii_lowercase().as_str() {
207            "module" => Ok(Self::Module),
208            "func" => Ok(Self::Func),
209            "import" => Ok(Self::Import),
210            "export" => Ok(Self::Export),
211            "memory" => Ok(Self::Memory),
212            "table" => Ok(Self::Table),
213            "global" => Ok(Self::Global),
214            "type" => Ok(Self::Type),
215            "component" => Ok(Self::Component),
216            _ => Err(WasmTextError::UnknownMarker),
217        }
218    }
219}
220
221/// Returns 'true' when the text starts like a WAT module and has balanced parentheses.
222#[must_use]
223pub fn looks_like_wat_module(input: &str) -> bool {
224    let trimmed = input.trim_start();
225    trimmed.starts_with("(module") && has_balanced_parentheses_basic(trimmed)
226}
227
228/// Performs a small balanced-parentheses check for text metadata.
229#[must_use]
230pub fn has_balanced_parentheses_basic(input: &str) -> bool {
231    let mut depth = 0_u32;
232    for character in input.chars() {
233        match character {
234            '(' => depth = depth.saturating_add(1),
235            ')' => {
236                if depth == 0 {
237                    return false;
238                }
239                depth -= 1;
240            },
241            _ => {},
242        }
243    }
244    depth == 0
245}
246
247#[cfg(test)]
248mod tests {
249    use super::{
250        SExpressionMarker, TextModuleName, WasmTextError, WatIdentifier,
251        has_balanced_parentheses_basic, looks_like_wat_module,
252    };
253
254    #[test]
255    fn validates_text_names() {
256        let identifier = WatIdentifier::new("$run").expect("valid identifier");
257        let module = TextModuleName::new("example").expect("valid module name");
258
259        assert_eq!(identifier.as_str(), "$run");
260        assert_eq!(module.to_string(), "example");
261        assert_eq!(WatIdentifier::new("run"), Err(WasmTextError::Invalid));
262    }
263
264    #[test]
265    fn parses_markers_and_checks_text_shape() {
266        assert_eq!(
267            "(module".parse::<SExpressionMarker>(),
268            Ok(SExpressionMarker::Module)
269        );
270        assert!(looks_like_wat_module("(module (func))"));
271        assert!(has_balanced_parentheses_basic("(func (result i32))"));
272        assert!(!has_balanced_parentheses_basic("(func"));
273    }
274}