1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum WasmTextError {
10 Empty,
12 Invalid,
14 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
46pub struct WatIdentifier(String);
47
48impl WatIdentifier {
49 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 #[must_use]
60 pub fn as_str(&self) -> &str {
61 &self.0
62 }
63
64 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
101pub struct TextModuleName(String);
102
103impl TextModuleName {
104 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 #[must_use]
111 pub fn as_str(&self) -> &str {
112 &self.0
113 }
114
115 #[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
152pub enum SExpressionMarker {
153 #[default]
155 Module,
156 Func,
158 Import,
160 Export,
162 Memory,
164 Table,
166 Global,
168 Type,
170 Component,
172}
173
174impl SExpressionMarker {
175 #[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#[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#[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}