Skip to main content

use_wasi/
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 WASI primitive labels are invalid.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum WasiError {
10    /// The supplied label was empty.
11    Empty,
12    /// The supplied label contains unsupported characters.
13    Invalid,
14    /// The supplied label is unknown for the requested enum.
15    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            /// Creates a validated WASI text wrapper.
53            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            /// Returns the stored text.
58            #[must_use]
59            pub fn as_str(&self) -> &str {
60                &self.0
61            }
62
63            /// Consumes the wrapper and returns the stored text.
64            #[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/// WASI version family.
104#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
105pub enum WasiVersion {
106    /// WASI Preview 1.
107    #[default]
108    Preview1,
109    /// WASI Preview 2.
110    Preview2,
111}
112
113impl WasiVersion {
114    /// Returns the stable version label.
115    #[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/// WASI execution profile label.
151#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
152pub enum WasiProfile {
153    /// Command-style world.
154    #[default]
155    Command,
156    /// Reactor-style world.
157    Reactor,
158    /// Component Model profile.
159    Component,
160}
161
162impl WasiProfile {
163    /// Returns the stable profile label.
164    #[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            /// Returns the stable label.
209            #[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}