scolapasta_path/
paths.rs

1//! Path routines for interfacing with load paths.
2//!
3//! This module contains functions to manipulate and configure load paths for a
4//! Ruby interpreter.
5//!
6//! These functions are defined in terms of [`Path`] from the Rust Standard
7//! Library.
8
9use std::path::{Component, Path, PathBuf};
10
11mod default;
12#[cfg(any(unix, target_os = "wasi"))]
13mod unix_wasi;
14#[cfg(windows)]
15mod windows;
16
17#[cfg(not(any(unix, windows, target_os = "wasi")))]
18use default as imp;
19#[cfg(any(unix, target_os = "wasi"))]
20use unix_wasi as imp;
21#[cfg(windows)]
22use windows as imp;
23
24/// Directory at which Ruby sources and extensions are stored in the virtual
25/// file system.
26///
27/// Some loaders are not backed by a physical disk. These loaders use the path
28/// returned by this function as a mount point and default working directory.
29///
30/// # Examples
31///
32/// On Windows systems:
33///
34/// ```
35/// # use std::path::Path;
36/// # use scolapasta_path::memory_loader_ruby_load_path;
37/// # #[cfg(windows)]
38/// assert_eq!(
39///     memory_loader_ruby_load_path(),
40///     Path::new("c:/artichoke/virtual_root/src/lib")
41/// );
42/// ```
43///
44/// On non-Windows systems:
45///
46/// ```
47/// # use std::path::Path;
48/// # use scolapasta_path::memory_loader_ruby_load_path;
49/// # #[cfg(not(windows))]
50/// assert_eq!(
51///     memory_loader_ruby_load_path(),
52///     Path::new("/artichoke/virtual_root/src/lib")
53/// );
54/// ```
55#[must_use]
56pub fn memory_loader_ruby_load_path() -> &'static Path {
57    if cfg!(windows) {
58        Path::new("c:/artichoke/virtual_root/src/lib")
59    } else {
60        Path::new("/artichoke/virtual_root/src/lib")
61    }
62}
63
64/// Return whether the given path starts with an explicit relative path.
65///
66/// Explicit relative paths start with `.` or `..` followed immediately by a
67/// directory separator.
68///
69/// Some Ruby source loaders have special handling for explicit relative paths
70/// where explicit relative paths are resolved relative to the process's [current
71/// working directory] rather than the load path.
72///
73/// # Compatibility
74///
75/// On Windows, if the given path contains invalid Unicode code points and
76/// cannot be converted to `&str`, this function will correctly identify these
77/// paths as explicit relative if their prefixes allow.
78///
79/// On platforms that are neither Windows nor Unix, this function may return
80/// incorrect results for paths that do not contain valid UTF-8. See
81/// [`Path::to_str`].
82///
83/// # Examples
84///
85/// ```
86/// # use scolapasta_path::is_explicit_relative;
87/// assert!(is_explicit_relative("./test/loader"));
88/// assert!(is_explicit_relative("../rake/test_task"));
89///
90/// assert!(!is_explicit_relative("json/pure"));
91/// assert!(!is_explicit_relative("/artichoke/src/json/pure"));
92/// ```
93///
94/// # MRI C Declaration
95///
96/// This routine is derived from the [reference implementation] in MRI Ruby:
97///
98/// ```c
99/// static int
100/// is_explicit_relative(const char *path)
101/// {
102///     if (*path++ != '.') return 0;
103///     if (*path == '.') path++;
104///     return isdirsep(*path);
105/// }
106/// ```
107///
108/// [current working directory]: std::env::current_dir
109/// [reference implementation]: https://github.com/artichoke/ruby/blob/v3_0_2/file.c#L6287-L6293
110#[must_use]
111pub fn is_explicit_relative<P: AsRef<Path>>(path: P) -> bool {
112    let path = path.as_ref();
113    imp::is_explicit_relative(path.as_ref())
114}
115
116/// Return whether the given byte string to treat as a path starts with an
117/// explicit relative path.
118///
119/// Explicit relative paths start with `.` or `..` followed immediately by a
120/// directory separator.
121///
122/// Some Ruby source loaders have special handling for explicit relative paths
123/// where explicit relative paths are resolved relative to the process's [current
124/// working directory] rather than the load path.
125///
126/// # Usage
127///
128/// This function can be used instead of [`is_explicit_relative`] if callers
129/// already have a byte string, as they likely do when manipulating a Ruby
130/// [`String`][ruby-string].
131///
132/// # Compatibility
133///
134/// Since this function operates on bytes, it is guaranteed to give a correct
135/// Boolean answer to whether a path is explicit relative on all platforms.
136///
137/// # Examples
138///
139/// ```
140/// # use scolapasta_path::is_explicit_relative_bytes;
141/// assert!(is_explicit_relative_bytes(b"./test/loader"));
142/// assert!(is_explicit_relative_bytes(b"../rake/test_task"));
143///
144/// assert!(!is_explicit_relative_bytes(b"json/pure"));
145/// assert!(!is_explicit_relative_bytes(b"/artichoke/src/json/pure"));
146/// ```
147///
148/// # MRI C Declaration
149///
150/// This routine is derived from the [reference implementation] in MRI Ruby:
151///
152/// ```c
153/// static int
154/// is_explicit_relative(const char *path)
155/// {
156///     if (*path++ != '.') return 0;
157///     if (*path == '.') path++;
158///     return isdirsep(*path);
159/// }
160/// ```
161///
162/// [current working directory]: std::env::current_dir
163/// [ruby-string]: https://ruby-doc.org/core-3.1.2/String.html
164/// [reference implementation]: https://github.com/artichoke/ruby/blob/v3_0_2/file.c#L6287-L6293
165#[must_use]
166pub fn is_explicit_relative_bytes<P: AsRef<[u8]>>(path: P) -> bool {
167    let path = path.as_ref();
168    default::is_explicit_relative_bytes(path)
169}
170
171/// Normalize path separators to all be `/`.
172///
173/// This function is a no-op on all non-Windows platforms. On Windows, this
174/// function will convert `\` separators to `/` if the given [`PathBuf`] is
175/// valid UTF-8.
176///
177/// # Errors
178///
179/// On Unix platforms, this function is infallible. On all other platforms,
180/// including Windows, if the given [`PathBuf`] is not valid UTF-8, the original
181/// `PathBuf` is returned as an error. See [`Path::to_str`] for details.
182pub fn normalize_slashes(path: PathBuf) -> Result<Vec<u8>, PathBuf> {
183    imp::normalize_slashes(path)
184}
185
186/// Translate a relative path into an absolute path, using a secondary path
187/// as the frame of reference.
188pub fn absolutize_relative_to<T, U>(path: T, cwd: U) -> PathBuf
189where
190    T: AsRef<Path>,
191    U: AsRef<Path>,
192{
193    absolutize_relative_to_inner(path.as_ref(), cwd.as_ref())
194}
195
196fn absolutize_relative_to_inner(path: &Path, cwd: &Path) -> PathBuf {
197    let mut iter = path.components().peekable();
198    let hint = iter.size_hint();
199    let (mut components, cwd_is_relative) = if let Some(Component::RootDir) = iter.peek() {
200        (Vec::with_capacity(hint.1.unwrap_or(hint.0)), false)
201    } else {
202        let mut components = cwd.components().map(Component::as_os_str).collect::<Vec<_>>();
203        components.reserve(hint.1.unwrap_or(hint.0));
204        (components, cwd.is_relative())
205    };
206    for component in iter {
207        match component {
208            Component::CurDir => {}
209            Component::ParentDir if cwd_is_relative => {
210                components.pop();
211            }
212            Component::ParentDir => {
213                components.pop();
214                if components.is_empty() {
215                    components.push(Component::RootDir.as_os_str());
216                }
217            }
218            c => {
219                components.push(c.as_os_str());
220            }
221        }
222    }
223    components.into_iter().collect()
224}
225
226#[cfg(test)]
227mod tests {
228    use std::path::Path;
229
230    use super::absolutize_relative_to;
231
232    #[test]
233    fn absolutize_absolute_path() {
234        let path = Path::new("/foo/bar");
235        let cwd = Path::new("/home/artichoke");
236        assert_eq!(absolutize_relative_to(path, cwd), path);
237        let cwd = Path::new("relative/path");
238        assert_eq!(absolutize_relative_to(path, cwd), path);
239    }
240
241    #[test]
242    fn absolutize_absolute_path_dedot_current_dir() {
243        let path = Path::new("/././foo/./bar/./././.");
244        let cwd = Path::new("/home/artichoke");
245        assert_eq!(absolutize_relative_to(path, cwd), Path::new("/foo/bar"));
246        let cwd = Path::new("relative/path");
247        assert_eq!(absolutize_relative_to(path, cwd), Path::new("/foo/bar"));
248    }
249
250    #[test]
251    fn absolutize_absolute_path_dedot_parent_dir() {
252        let path = Path::new("/foo/bar/..");
253        let cwd = Path::new("/home/artichoke");
254        assert_eq!(absolutize_relative_to(path, cwd), Path::new("/foo"));
255        let cwd = Path::new("relative/path");
256        assert_eq!(absolutize_relative_to(path, cwd), Path::new("/foo"));
257
258        let path = Path::new("/foo/../../../../bar/../../../");
259        let cwd = Path::new("/home/artichoke");
260        assert_eq!(absolutize_relative_to(path, cwd), Path::new("/"));
261        let cwd = Path::new("relative/path");
262        assert_eq!(absolutize_relative_to(path, cwd), Path::new("/"));
263
264        let path = Path::new("/foo/../../../../bar/../../../boom/baz");
265        let cwd = Path::new("/home/artichoke");
266        assert_eq!(absolutize_relative_to(path, cwd), Path::new("/boom/baz"));
267        let cwd = Path::new("relative/path");
268        assert_eq!(absolutize_relative_to(path, cwd), Path::new("/boom/baz"));
269    }
270
271    #[test]
272    fn absolutize_relative_path() {
273        let path = Path::new("foo/bar");
274        let cwd = Path::new("/home/artichoke");
275        assert_eq!(absolutize_relative_to(path, cwd), Path::new("/home/artichoke/foo/bar"));
276        let cwd = Path::new("relative/path");
277        assert_eq!(absolutize_relative_to(path, cwd), Path::new("relative/path/foo/bar"));
278    }
279
280    #[test]
281    fn absolutize_relative_path_dedot_current_dir() {
282        let path = Path::new("././././foo/./bar/./././.");
283        let cwd = Path::new("/home/artichoke");
284        assert_eq!(absolutize_relative_to(path, cwd), Path::new("/home/artichoke/foo/bar"));
285        let cwd = Path::new("relative/path");
286        assert_eq!(absolutize_relative_to(path, cwd), Path::new("relative/path/foo/bar"));
287    }
288
289    #[test]
290    #[cfg(unix)]
291    fn absolutize_relative_path_dedot_parent_dir_unix() {
292        let path = Path::new("foo/bar/..");
293        let cwd = Path::new("/home/artichoke");
294        let absolute = absolutize_relative_to(path, cwd);
295        assert_eq!(absolute, Path::new("/home/artichoke/foo"));
296        let cwd = Path::new("relative/path");
297        let absolute = absolutize_relative_to(path, cwd);
298        assert_eq!(absolute, Path::new("relative/path/foo"));
299
300        let path = Path::new("foo/../../../../bar/../../../");
301        let cwd = Path::new("/home/artichoke");
302        let absolute = absolutize_relative_to(path, cwd);
303        assert_eq!(absolute, Path::new("/"));
304        let cwd = Path::new("relative/path");
305        let absolute = absolutize_relative_to(path, cwd);
306        assert_eq!(absolute, Path::new(""));
307
308        let path = Path::new("foo/../../../../bar/../../../boom/baz");
309        let cwd = Path::new("/home/artichoke");
310        let absolute = absolutize_relative_to(path, cwd);
311        assert_eq!(absolute, Path::new("/boom/baz"));
312        let cwd = Path::new("relative/path");
313        let absolute = absolutize_relative_to(path, cwd);
314        assert_eq!(absolute, Path::new("boom/baz"));
315    }
316
317    #[test]
318    #[cfg(windows)]
319    fn absolutize_relative_path_dedot_parent_dir_windows_forward_slash() {
320        let path = Path::new("foo/bar/..");
321        let cwd = Path::new("C:/Users/artichoke");
322        let absolute = absolutize_relative_to(path, cwd);
323        assert_eq!(absolute, Path::new("C:/Users/artichoke/foo"));
324        let cwd = Path::new("relative/path");
325        let absolute = absolutize_relative_to(path, cwd);
326        assert_eq!(absolute, Path::new("relative/path/foo"));
327
328        let path = Path::new("foo/../../../../bar/../../../");
329        let cwd = Path::new("C:/Users/artichoke");
330        let absolute = absolutize_relative_to(path, cwd);
331        assert_eq!(absolute, Path::new("/"));
332        let cwd = Path::new("relative/path");
333        let absolute = absolutize_relative_to(path, cwd);
334        assert_eq!(absolute, Path::new(""));
335
336        let path = Path::new("foo/../../../../bar/../../../boom/baz");
337        let cwd = Path::new("C:/Users/artichoke");
338        let absolute = absolutize_relative_to(path, cwd);
339        assert_eq!(absolute, Path::new("/boom/baz"));
340        let cwd = Path::new("relative/path");
341        let absolute = absolutize_relative_to(path, cwd);
342        assert_eq!(absolute, Path::new("boom/baz"));
343    }
344
345    #[test]
346    #[cfg(windows)]
347    fn absolutize_relative_path_dedot_parent_dir_windows_backward_slash() {
348        let path = Path::new(r"foo\bar\..");
349        let cwd = Path::new(r"C:\Users\artichoke");
350        let absolute = absolutize_relative_to(path, cwd);
351        assert_eq!(absolute, Path::new("C:/Users/artichoke/foo"));
352        let cwd = Path::new(r"relative\path");
353        let absolute = absolutize_relative_to(path, cwd);
354        assert_eq!(absolute, Path::new("relative/path/foo"));
355
356        let path = Path::new(r"foo\..\..\..\..\bar\..\..\..\");
357        let cwd = Path::new(r"C:\Users\artichoke");
358        let absolute = absolutize_relative_to(path, cwd);
359        assert_eq!(absolute, Path::new("/"));
360        let cwd = Path::new(r"relative\path");
361        let absolute = absolutize_relative_to(path, cwd);
362        assert_eq!(absolute, Path::new(""));
363
364        let path = Path::new(r"foo\..\..\..\..\bar\..\..\..\boom\baz");
365        let cwd = Path::new(r"C:\Users\artichoke");
366        let absolute = absolutize_relative_to(path, cwd);
367        assert_eq!(absolute, Path::new("/boom/baz"));
368        let cwd = Path::new(r"relative\path");
369        let absolute = absolutize_relative_to(path, cwd);
370        assert_eq!(absolute, Path::new("boom/baz"));
371    }
372}