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 WasiError {
10 Empty,
12 Invalid,
14 Unknown,
16}
17
18impl fmt::Display for WasiError {
19 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Self::Empty => formatter.write_str("WASI label cannot be empty"),
22 Self::Invalid => formatter.write_str("invalid WASI label"),
23 Self::Unknown => formatter.write_str("unknown WASI label"),
24 }
25 }
26}
27
28impl Error for WasiError {}
29
30fn validate_wasi_label(value: &str) -> Result<&str, WasiError> {
31 let trimmed = value.trim();
32 if trimmed.is_empty() {
33 return Err(WasiError::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(WasiError::Invalid);
42 }
43 Ok(trimmed)
44}
45
46macro_rules! wasi_text_newtype {
47 ($name:ident) => {
48 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49 pub struct $name(String);
50
51 impl $name {
52 pub fn new(value: impl AsRef<str>) -> Result<Self, WasiError> {
54 validate_wasi_label(value.as_ref()).map(|value| Self(value.to_owned()))
55 }
56
57 #[must_use]
59 pub fn as_str(&self) -> &str {
60 &self.0
61 }
62
63 #[must_use]
65 pub fn into_string(self) -> String {
66 self.0
67 }
68 }
69
70 impl AsRef<str> for $name {
71 fn as_ref(&self) -> &str {
72 self.as_str()
73 }
74 }
75
76 impl fmt::Display for $name {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 formatter.write_str(self.as_str())
79 }
80 }
81
82 impl FromStr for $name {
83 type Err = WasiError;
84
85 fn from_str(value: &str) -> Result<Self, Self::Err> {
86 Self::new(value)
87 }
88 }
89
90 impl TryFrom<&str> for $name {
91 type Error = WasiError;
92
93 fn try_from(value: &str) -> Result<Self, Self::Error> {
94 Self::new(value)
95 }
96 }
97 };
98}
99
100wasi_text_newtype!(WasiCapabilityLabel);
101wasi_text_newtype!(WasiInterfaceName);
102
103#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
105pub enum WasiVersion {
106 #[default]
108 Preview1,
109 Preview2,
111}
112
113impl WasiVersion {
114 #[must_use]
116 pub const fn as_str(self) -> &'static str {
117 match self {
118 Self::Preview1 => "preview1",
119 Self::Preview2 => "preview2",
120 }
121 }
122}
123
124impl fmt::Display for WasiVersion {
125 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126 formatter.write_str(self.as_str())
127 }
128}
129
130impl FromStr for WasiVersion {
131 type Err = WasiError;
132
133 fn from_str(value: &str) -> Result<Self, Self::Err> {
134 let trimmed = value.trim();
135 if trimmed.is_empty() {
136 return Err(WasiError::Empty);
137 }
138 match trimmed
139 .to_ascii_lowercase()
140 .replace(['-', '_'], "")
141 .as_str()
142 {
143 "preview1" | "wasip1" => Ok(Self::Preview1),
144 "preview2" | "wasip2" => Ok(Self::Preview2),
145 _ => Err(WasiError::Unknown),
146 }
147 }
148}
149
150#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
152pub enum WasiProfile {
153 #[default]
155 Command,
156 Reactor,
158 Component,
160}
161
162impl WasiProfile {
163 #[must_use]
165 pub const fn as_str(self) -> &'static str {
166 match self {
167 Self::Command => "command",
168 Self::Reactor => "reactor",
169 Self::Component => "component",
170 }
171 }
172}
173
174impl fmt::Display for WasiProfile {
175 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176 formatter.write_str(self.as_str())
177 }
178}
179
180impl FromStr for WasiProfile {
181 type Err = WasiError;
182
183 fn from_str(value: &str) -> Result<Self, Self::Err> {
184 let trimmed = value.trim();
185 if trimmed.is_empty() {
186 return Err(WasiError::Empty);
187 }
188 match trimmed.to_ascii_lowercase().as_str() {
189 "command" => Ok(Self::Command),
190 "reactor" => Ok(Self::Reactor),
191 "component" => Ok(Self::Component),
192 _ => Err(WasiError::Unknown),
193 }
194 }
195}
196
197macro_rules! label_enum {
198 ($name:ident { $($variant:ident => $label:literal),+ $(,)? }) => {
199 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
200 pub enum $name {
201 $(
202 #[doc = concat!("'", $label, "' marker.")]
203 $variant,
204 )+
205 }
206
207 impl $name {
208 #[must_use]
210 pub const fn as_str(self) -> &'static str {
211 match self {
212 $(Self::$variant => $label,)+
213 }
214 }
215 }
216
217 impl fmt::Display for $name {
218 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219 formatter.write_str(self.as_str())
220 }
221 }
222 };
223}
224
225label_enum!(FilesystemPermission {
226 Read => "filesystem.read",
227 Write => "filesystem.write",
228 Create => "filesystem.create",
229 Remove => "filesystem.remove",
230});
231
232label_enum!(SocketPermission {
233 Tcp => "socket.tcp",
234 Udp => "socket.udp",
235 IpNameLookup => "socket.ip-name-lookup",
236});
237
238label_enum!(EnvironmentPermission {
239 Args => "environment.args",
240 Environ => "environment.environ",
241});
242
243label_enum!(ClockCapability {
244 Monotonic => "clock.monotonic",
245 WallClock => "clock.wall-clock",
246});
247
248label_enum!(RandomCapability {
249 Insecure => "random.insecure",
250 Secure => "random.secure",
251});
252
253#[cfg(test)]
254mod tests {
255 use super::{
256 ClockCapability, FilesystemPermission, WasiCapabilityLabel, WasiError, WasiInterfaceName,
257 WasiProfile, WasiVersion,
258 };
259
260 #[test]
261 fn parses_versions_and_profiles() {
262 assert_eq!("wasip1".parse::<WasiVersion>(), Ok(WasiVersion::Preview1));
263 assert_eq!(
264 "component".parse::<WasiProfile>(),
265 Ok(WasiProfile::Component)
266 );
267 assert_eq!(WasiVersion::Preview2.to_string(), "preview2");
268 }
269
270 #[test]
271 fn validates_labels_and_permissions() {
272 let capability = WasiCapabilityLabel::new("filesystem.read").expect("valid capability");
273 let interface = WasiInterfaceName::new("wasi:filesystem/types").expect("valid interface");
274
275 assert_eq!(capability.as_str(), "filesystem.read");
276 assert_eq!(interface.as_str(), "wasi:filesystem/types");
277 assert_eq!(
278 WasiCapabilityLabel::new("bad label"),
279 Err(WasiError::Invalid)
280 );
281 assert_eq!(FilesystemPermission::Read.to_string(), "filesystem.read");
282 assert_eq!(ClockCapability::WallClock.to_string(), "clock.wall-clock");
283 }
284}