wasmer_wasix/fs/
path_posix.rs

1//! Guest POSIX path helpers.
2//!
3//! WASIX implements a system layer, so guest paths must behave identically to
4//! POSIX paths even when the runtime host is not POSIX. A different textual
5//! form of the same path can still be observable to compat tests, so guest path
6//! operations should preserve slash-separated string semantics instead of using
7//! host-native `Path` component rules. Host-native `Path` remains the filesystem
8//! trait boundary type; these types are for guest-visible path math on our side
9//! of that boundary.
10
11use std::{
12    borrow::Cow,
13    path::{Path, PathBuf},
14};
15
16#[cfg(feature = "enable-serde")]
17use serde_derive::{Deserialize, Serialize};
18use wasmer_wasix_types::wasi::Errno;
19
20#[derive(Clone, Copy)]
21pub(crate) enum PosixPathComponent<'a> {
22    RootDir,
23    CurDir,
24    ParentDir,
25    Normal(&'a str),
26}
27
28#[derive(Debug)]
29#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
30pub(crate) struct PosixPath<'a> {
31    #[cfg_attr(feature = "enable-serde", serde(borrow))]
32    path: Cow<'a, str>,
33}
34
35#[derive(Debug)]
36#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
37pub(crate) struct PosixPathBuf {
38    path: String,
39}
40
41impl<'a> PosixPath<'a> {
42    pub(crate) fn new(path: &'a str) -> Self {
43        Self {
44            path: Cow::Borrowed(path),
45        }
46    }
47
48    pub(crate) fn from_path(path: &'a Path) -> Self {
49        Self {
50            path: path.to_string_lossy(),
51        }
52    }
53
54    pub(crate) fn as_str(&self) -> &str {
55        self.path.as_ref()
56    }
57
58    pub(crate) fn is_absolute(&self) -> bool {
59        self.as_str().starts_with('/')
60    }
61
62    pub(crate) fn strip_root_prefix(&self) -> PosixPathBuf {
63        PosixPathBuf::from(self.as_str().strip_prefix('/').unwrap_or(self.as_str()))
64    }
65
66    pub(crate) fn strip_prefix<'b>(&'b self, prefix: &PosixPath<'_>) -> Option<PosixPath<'b>> {
67        let path = self.as_str();
68        let prefix = prefix.as_str();
69
70        if prefix == "/" {
71            return path.strip_prefix('/').map(PosixPath::new);
72        }
73
74        if path == prefix {
75            return Some(PosixPath::new(""));
76        }
77
78        let suffix = path.strip_prefix(prefix)?;
79        suffix.strip_prefix('/').map(PosixPath::new)
80    }
81
82    pub(crate) fn parent(&self) -> PosixPathBuf {
83        let path = self.as_str();
84        let trimmed = path.trim_end_matches('/');
85        let parent = trimmed
86            .rsplit_once('/')
87            .map(|(parent, _)| parent)
88            .unwrap_or_default();
89        PosixPathBuf::from(parent)
90    }
91
92    pub(crate) fn components(
93        &self,
94        include_root: bool,
95        preserve_trailing_slash: bool,
96    ) -> Vec<PosixPathComponent<'_>> {
97        let path = self.as_str();
98        let mut components = Vec::new();
99
100        if include_root && path.starts_with('/') {
101            components.push(PosixPathComponent::RootDir);
102        }
103
104        for component in path.split('/').filter(|component| !component.is_empty()) {
105            components.push(match component {
106                "." => PosixPathComponent::CurDir,
107                ".." => PosixPathComponent::ParentDir,
108                component => PosixPathComponent::Normal(component),
109            });
110        }
111
112        if preserve_trailing_slash && path.ends_with('/') {
113            components.push(PosixPathComponent::CurDir);
114        }
115
116        components
117    }
118
119    pub(crate) fn join(&self, relative: &PosixPath<'_>) -> PosixPathBuf {
120        let base = self.as_str();
121        let relative = relative.as_str();
122
123        if relative.is_empty() || relative == "." {
124            return PosixPathBuf::from(base);
125        }
126
127        if relative.starts_with('/') || base.is_empty() || base == "." {
128            PosixPathBuf::from(relative)
129        } else if base == "/" {
130            PosixPathBuf::from(format!("/{relative}"))
131        } else if base.ends_with('/') {
132            PosixPathBuf::from(format!("{base}{relative}"))
133        } else {
134            PosixPathBuf::from(format!("{base}/{relative}"))
135        }
136    }
137
138    pub(crate) fn parent_path_and_name(&self) -> Result<(PosixPathBuf, String), Errno> {
139        let path = self.as_str();
140        let trimmed = path.trim_end_matches('/');
141        if trimmed.is_empty() {
142            return Err(Errno::Inval);
143        }
144
145        let (parent, name) = match trimmed.rsplit_once('/') {
146            Some(("", name)) if path.starts_with('/') => ("/", name),
147            Some((parent, name)) => (parent, name),
148            None => ("", trimmed),
149        };
150
151        if name.is_empty() {
152            return Err(Errno::Inval);
153        }
154
155        Ok((PosixPathBuf::from(parent), name.to_string()))
156    }
157
158    pub(crate) fn normalize_virtual_symlink_key(&self) -> PosixPathBuf {
159        let mut normalized = Vec::new();
160
161        for component in self.components(false, false) {
162            match component {
163                PosixPathComponent::RootDir | PosixPathComponent::CurDir => {}
164                PosixPathComponent::ParentDir => {
165                    normalized.pop();
166                }
167                PosixPathComponent::Normal(component) => normalized.push(component.to_owned()),
168            }
169        }
170
171        PosixPathBuf::from_components(self.is_absolute(), &normalized, "/")
172    }
173}
174
175impl PosixPathBuf {
176    pub(crate) fn from_components(
177        is_absolute: bool,
178        components: &[String],
179        empty_path: &str,
180    ) -> Self {
181        if components.is_empty() {
182            PosixPathBuf::from(empty_path)
183        } else if is_absolute {
184            PosixPathBuf::from(format!("/{}", components.join("/")))
185        } else {
186            PosixPathBuf::from(components.join("/"))
187        }
188    }
189
190    pub(crate) fn as_posix_path(&self) -> PosixPath<'_> {
191        PosixPath::new(&self.path)
192    }
193
194    pub(crate) fn as_str(&self) -> &str {
195        &self.path
196    }
197
198    pub(crate) fn into_path_buf(self) -> PathBuf {
199        PathBuf::from(self.path)
200    }
201
202    pub(crate) fn resolve_relative(
203        symlink_parent: &PosixPath<'_>,
204        relative_path: &PosixPath<'_>,
205        preserve_after_first_normal: bool,
206    ) -> Result<Self, Errno> {
207        let mut resolved = Vec::new();
208        symlink_parent.push_normalized_relative(&mut resolved)?;
209
210        if !preserve_after_first_normal {
211            relative_path.push_normalized_relative(&mut resolved)?;
212            return Ok(PosixPathBuf::from_components(false, &resolved, "."));
213        }
214
215        let mut validation = resolved.clone();
216        relative_path.push_normalized_relative(&mut validation)?;
217
218        let mut remaining = Vec::new();
219        let mut preserve_remaining = false;
220
221        relative_path.visit_relative_components(|component| {
222            if preserve_remaining {
223                match component {
224                    PosixPathComponent::RootDir => {}
225                    PosixPathComponent::CurDir => remaining.push(".".to_owned()),
226                    PosixPathComponent::ParentDir => remaining.push("..".to_owned()),
227                    PosixPathComponent::Normal(component) => remaining.push(component.to_owned()),
228                }
229                return Ok(());
230            }
231
232            match component {
233                PosixPathComponent::RootDir | PosixPathComponent::CurDir => {}
234                PosixPathComponent::ParentDir => {
235                    resolved.pop().ok_or(Errno::Perm)?;
236                }
237                PosixPathComponent::Normal(component) => {
238                    remaining.push(component.to_owned());
239                    preserve_remaining = true;
240                }
241            }
242            Ok(())
243        })?;
244
245        if !remaining.is_empty() {
246            resolved.extend(remaining);
247        }
248
249        Ok(PosixPathBuf::from_components(false, &resolved, "."))
250    }
251}
252
253impl<'a> PosixPath<'a> {
254    fn visit_relative_components<'b, F>(&'b self, mut visit: F) -> Result<(), Errno>
255    where
256        F: FnMut(PosixPathComponent<'b>) -> Result<(), Errno>,
257    {
258        if self.is_absolute() {
259            return Err(Errno::Perm);
260        }
261
262        for component in self.components(false, false) {
263            visit(component)?;
264        }
265
266        Ok(())
267    }
268
269    fn push_normalized_relative(&self, resolved: &mut Vec<String>) -> Result<(), Errno> {
270        self.visit_relative_components(|component| {
271            match component {
272                PosixPathComponent::RootDir | PosixPathComponent::CurDir => {}
273                PosixPathComponent::Normal(component) => resolved.push(component.to_owned()),
274                PosixPathComponent::ParentDir => {
275                    resolved.pop().ok_or(Errno::Perm)?;
276                }
277            }
278            Ok(())
279        })
280    }
281}
282
283impl From<&str> for PosixPathBuf {
284    fn from(path: &str) -> Self {
285        Self {
286            path: path.to_owned(),
287        }
288    }
289}
290
291impl From<String> for PosixPathBuf {
292    fn from(path: String) -> Self {
293        Self { path }
294    }
295}