turbo_unix_path/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::borrow::Cow;
4
5/// Converts system paths into Unix paths. This is a noop on Unix systems, and replaces backslash
6/// directory separators with forward slashes on Windows.
7#[inline]
8pub fn sys_to_unix(path: &str) -> Cow<'_, str> {
9    #[cfg(not(target_family = "windows"))]
10    {
11        Cow::from(path)
12    }
13    #[cfg(target_family = "windows")]
14    {
15        Cow::Owned(path.replace(std::path::MAIN_SEPARATOR_STR, "/"))
16    }
17}
18
19/// Converts Unix paths into system paths. This is a noop on Unix systems, and replaces forward
20/// slash directory separators with backslashes on Windows.
21#[inline]
22pub fn unix_to_sys(path: &str) -> Cow<'_, str> {
23    #[cfg(not(target_family = "windows"))]
24    {
25        Cow::from(path)
26    }
27    #[cfg(target_family = "windows")]
28    {
29        Cow::Owned(path.replace('/', std::path::MAIN_SEPARATOR_STR))
30    }
31}
32
33/// Joins two /-separated paths into a normalized path.
34/// Paths are concatenated with /.
35///
36/// see also [normalize_path] for normalization.
37pub fn join_path(fs_path: &str, join: &str) -> Option<String> {
38    // Paths that we join are written as source code (eg, `join_path(fs_path, "foo/bar.js")`) and
39    // it's expected that they will never contain a backslash.
40    debug_assert!(
41        !join.contains('\\'),
42        "joined path {join} must not contain a Windows directory '\\', it must be normalized to \
43         Unix '/'"
44    );
45
46    // TODO: figure out why this freezes the benchmarks.
47    // // an absolute path would leave the file system root
48    // if Path::new(join).is_absolute() {
49    //     return None;
50    // }
51
52    if fs_path.is_empty() {
53        normalize_path(join)
54    } else if join.is_empty() {
55        normalize_path(fs_path)
56    } else {
57        normalize_path(&[fs_path, "/", join].concat())
58    }
59}
60
61/// Normalizes a /-separated path into a form that contains no leading /, no double /, no "."
62/// segment, no ".." segment.
63///
64/// Returns None if the path would need to start with ".." to be equal.
65pub fn normalize_path(str: &str) -> Option<String> {
66    let mut segments = Vec::new();
67    for segment in str.split('/') {
68        match segment {
69            "." | "" => {}
70            ".." => {
71                segments.pop()?;
72            }
73            segment => {
74                segments.push(segment);
75            }
76        }
77    }
78    Some(segments.join("/"))
79}
80
81/// Normalizes a /-separated request into a form that contains no leading /, no double /, and no "."
82/// or ".." segments in the middle of the request.
83///
84/// A request might only start with a single "." segment and no ".." segments, or any positive
85/// number of ".." segments but no "." segment.
86pub fn normalize_request(str: &str) -> String {
87    let mut segments = vec!["."];
88    // Keeps track of our directory depth so that we can pop directories when encountering a "..".
89    // If this is positive, then we're inside a directory and we can pop that. If it's 0, then we
90    // can't pop the directory and we must keep the ".." in our segments. This is not the same as
91    // the segments.len(), because we cannot pop a kept ".." when encountering another "..".
92    let mut depth = 0;
93    let mut popped_dot = false;
94    for segment in str.split('/') {
95        match segment {
96            "." => {}
97            ".." => {
98                if depth > 0 {
99                    depth -= 1;
100                    segments.pop();
101                } else {
102                    // The first time we push a "..", we need to remove the "." we include by
103                    // default.
104                    if !popped_dot {
105                        popped_dot = true;
106                        segments.pop();
107                    }
108                    segments.push(segment);
109                }
110            }
111            segment => {
112                segments.push(segment);
113                depth += 1;
114            }
115        }
116    }
117    segments.join("/")
118}
119
120pub fn get_relative_path_to(from: &str, target: &str) -> String {
121    fn split(s: &str) -> impl Iterator<Item = &str> {
122        let empty = s.is_empty();
123        let mut iterator = s.split('/');
124        if empty {
125            iterator.next();
126        }
127        iterator
128    }
129
130    let mut from_segments = split(from).peekable();
131    let mut target_segments = split(target).peekable();
132    while from_segments.peek() == target_segments.peek() {
133        from_segments.next();
134        if target_segments.next().is_none() {
135            return ".".to_string();
136        }
137    }
138    let mut result = Vec::new();
139    if from_segments.peek().is_none() {
140        result.push(".");
141    } else {
142        while from_segments.next().is_some() {
143            result.push("..");
144        }
145    }
146    for segment in target_segments {
147        result.push(segment);
148    }
149    result.join("/")
150}
151
152pub fn get_parent_path(path: &str) -> &str {
153    match str::rfind(path, '/') {
154        Some(index) => &path[..index],
155        None => "",
156    }
157}
158
159#[cfg(test)]
160mod tests {
161
162    use rstest::*;
163
164    use super::*;
165
166    #[rstest]
167    #[case("file.js")]
168    #[case("a/b/c/d/e/file.js")]
169    fn test_normalize_path_no_op(#[case] path: &str) {
170        assert_eq!(path, normalize_path(path).unwrap());
171    }
172
173    #[rstest]
174    #[case("/file.js", "file.js")]
175    #[case("./file.js", "file.js")]
176    #[case("././file.js", "file.js")]
177    #[case("a/../c/../file.js", "file.js")]
178    fn test_normalize_path(#[case] path: &str, #[case] normalized: &str) {
179        assert_eq!(normalized, normalize_path(path).unwrap());
180    }
181
182    #[rstest]
183    #[case("../file.js")]
184    #[case("a/../../file.js")]
185    fn test_normalize_path_invalid(#[case] path: &str) {
186        assert_eq!(None, normalize_path(path));
187    }
188}