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