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 WasmExportError {
10 Empty,
12 InvalidName,
14 UnknownKind,
16}
17
18impl fmt::Display for WasmExportError {
19 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Self::Empty => formatter.write_str("WebAssembly export value cannot be empty"),
22 Self::InvalidName => formatter.write_str("invalid WebAssembly export name"),
23 Self::UnknownKind => formatter.write_str("unknown WebAssembly export kind"),
24 }
25 }
26}
27
28impl Error for WasmExportError {}
29
30fn validate_export_text(value: &str) -> Result<&str, WasmExportError> {
31 let trimmed = value.trim();
32 if trimmed.is_empty() {
33 return Err(WasmExportError::Empty);
34 }
35 if trimmed.chars().any(|character| {
36 character.is_control()
37 || character.is_whitespace()
38 || !(character.is_ascii_alphanumeric()
39 || matches!(character, '_' | '-' | '.' | '/' | ':' | '$'))
40 }) {
41 return Err(WasmExportError::InvalidName);
42 }
43 Ok(trimmed)
44}
45
46#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
48pub struct ExportName(String);
49
50impl ExportName {
51 pub fn new(value: impl AsRef<str>) -> Result<Self, WasmExportError> {
53 validate_export_text(value.as_ref()).map(|value| Self(value.to_owned()))
54 }
55
56 #[must_use]
58 pub fn as_str(&self) -> &str {
59 &self.0
60 }
61
62 #[must_use]
64 pub fn into_string(self) -> String {
65 self.0
66 }
67}
68
69impl AsRef<str> for ExportName {
70 fn as_ref(&self) -> &str {
71 self.as_str()
72 }
73}
74
75impl fmt::Display for ExportName {
76 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77 formatter.write_str(self.as_str())
78 }
79}
80
81impl FromStr for ExportName {
82 type Err = WasmExportError;
83
84 fn from_str(value: &str) -> Result<Self, Self::Err> {
85 Self::new(value)
86 }
87}
88
89impl TryFrom<&str> for ExportName {
90 type Error = WasmExportError;
91
92 fn try_from(value: &str) -> Result<Self, Self::Error> {
93 Self::new(value)
94 }
95}
96
97#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
99pub enum ExportKind {
100 #[default]
102 Function,
103 Memory,
105 Table,
107 Global,
109}
110
111impl ExportKind {
112 #[must_use]
114 pub const fn as_str(self) -> &'static str {
115 match self {
116 Self::Function => "function",
117 Self::Memory => "memory",
118 Self::Table => "table",
119 Self::Global => "global",
120 }
121 }
122}
123
124impl fmt::Display for ExportKind {
125 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126 formatter.write_str(self.as_str())
127 }
128}
129
130impl FromStr for ExportKind {
131 type Err = WasmExportError;
132
133 fn from_str(value: &str) -> Result<Self, Self::Err> {
134 let trimmed = value.trim();
135 if trimmed.is_empty() {
136 return Err(WasmExportError::Empty);
137 }
138 match trimmed.to_ascii_lowercase().as_str() {
139 "function" | "func" => Ok(Self::Function),
140 "memory" | "mem" => Ok(Self::Memory),
141 "table" => Ok(Self::Table),
142 "global" => Ok(Self::Global),
143 _ => Err(WasmExportError::UnknownKind),
144 }
145 }
146}
147
148#[derive(Clone, Debug, Eq, Hash, PartialEq)]
150pub struct ExportedFunction {
151 name: ExportName,
152 index: u32,
153}
154
155impl ExportedFunction {
156 #[must_use]
158 pub const fn new(name: ExportName, index: u32) -> Self {
159 Self { name, index }
160 }
161
162 #[must_use]
164 pub const fn name(&self) -> &ExportName {
165 &self.name
166 }
167
168 #[must_use]
170 pub const fn index(&self) -> u32 {
171 self.index
172 }
173}
174
175#[derive(Clone, Debug, Eq, Hash, PartialEq)]
177pub struct ExportedMemory {
178 name: ExportName,
179 index: u32,
180}
181
182impl ExportedMemory {
183 #[must_use]
185 pub const fn new(name: ExportName, index: u32) -> Self {
186 Self { name, index }
187 }
188}
189
190#[derive(Clone, Debug, Eq, Hash, PartialEq)]
192pub struct ExportedTable {
193 name: ExportName,
194 index: u32,
195}
196
197impl ExportedTable {
198 #[must_use]
200 pub const fn new(name: ExportName, index: u32) -> Self {
201 Self { name, index }
202 }
203}
204
205#[derive(Clone, Debug, Eq, Hash, PartialEq)]
207pub struct ExportedGlobal {
208 name: ExportName,
209 index: u32,
210 mutable: bool,
211}
212
213impl ExportedGlobal {
214 #[must_use]
216 pub const fn new(name: ExportName, index: u32, mutable: bool) -> Self {
217 Self {
218 name,
219 index,
220 mutable,
221 }
222 }
223
224 #[must_use]
226 pub const fn is_mutable(&self) -> bool {
227 self.mutable
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::{ExportKind, ExportName, ExportedFunction, WasmExportError};
234
235 #[test]
236 fn validates_export_names() {
237 let name = ExportName::new("run").expect("valid export name");
238
239 assert_eq!(name.as_str(), "run");
240 assert_eq!(name.to_string(), "run");
241 assert_eq!(
242 ExportName::new("bad name"),
243 Err(WasmExportError::InvalidName)
244 );
245 }
246
247 #[test]
248 fn parses_export_kinds_and_metadata() {
249 let kind = "mem".parse::<ExportKind>().expect("known export kind");
250 let function = ExportedFunction::new(ExportName::new("run").expect("valid export"), 3);
251
252 assert_eq!(kind, ExportKind::Memory);
253 assert_eq!(kind.to_string(), "memory");
254 assert_eq!(function.name().as_str(), "run");
255 assert_eq!(function.index(), 3);
256 }
257}