1#![doc = include_str!("../README.md")]
2
3use std::borrow::Cow;
4
5use smallvec::SmallVec;
6
7#[inline]
10pub fn sys_to_unix(path: &str) -> Cow<'_, str> {
11 #[cfg(not(windows))]
12 {
13 Cow::from(path)
14 }
15 #[cfg(windows)]
16 {
17 Cow::Owned(path.replace(std::path::MAIN_SEPARATOR_STR, "/"))
18 }
19}
20
21#[inline]
24pub fn unix_to_sys(path: &str) -> Cow<'_, str> {
25 #[cfg(not(windows))]
26 {
27 Cow::from(path)
28 }
29 #[cfg(windows)]
30 {
31 Cow::Owned(path.replace('/', std::path::MAIN_SEPARATOR_STR))
32 }
33}
34
35pub fn join_path(fs_path: &str, join: &str) -> Option<String> {
41 debug_assert!(
42 !cfg!(windows) || !join.contains('\\'),
43 "joined path {join} must not contain a Windows directory '\\', it must be normalized to \
44 Unix '/'"
45 );
46
47 if fs_path.is_empty() {
54 normalize_path(join)
55 } else if join.is_empty() {
56 normalize_path(fs_path)
57 } else {
58 normalize_path(&[fs_path, "/", join].concat())
59 }
60}
61
62pub fn normalize_path(str: &str) -> Option<String> {
67 let mut segments = SmallVec::<[&str; 8]>::new();
68 for segment in str.split('/') {
69 match segment {
70 "." | "" => {}
71 ".." => {
72 segments.pop()?;
73 }
74 segment => {
75 segments.push(segment);
76 }
77 }
78 }
79 Some(segments.join("/"))
80}
81
82pub fn normalize_request(str: &str) -> String {
88 let mut segments = SmallVec::<[&str; 8]>::new();
89 segments.push(".");
90 let mut depth = 0;
95 let mut popped_dot = false;
96 for segment in str.split('/') {
97 match segment {
98 "." => {}
99 ".." => {
100 if depth > 0 {
101 depth -= 1;
102 segments.pop();
103 } else {
104 if !popped_dot {
107 popped_dot = true;
108 segments.pop();
109 }
110 segments.push(segment);
111 }
112 }
113 segment => {
114 segments.push(segment);
115 depth += 1;
116 }
117 }
118 }
119 segments.join("/")
120}
121
122pub fn get_relative_path_to<'a>(from: &str, target: &'a str) -> Cow<'a, str> {
132 if from.is_empty() && !target.is_empty() {
133 return Cow::Borrowed(target);
134 }
135
136 relative_to(from, target, false)
137}
138
139pub fn get_relative_request_to<'a>(from: &str, target: &'a str) -> Cow<'a, str> {
148 relative_to(from, target, true)
149}
150
151fn relative_to<'a>(from: &str, target: &'a str, explicitly_relative: bool) -> Cow<'a, str> {
154 fn split(s: &str) -> impl Iterator<Item = &str> {
155 let mut iterator = s.split('/');
156 if s.is_empty() {
157 iterator.next();
158 }
159 iterator
160 }
161
162 let mut from_segments = split(from).peekable();
163 let mut target_segments = split(target).peekable();
164 while from_segments.peek() == target_segments.peek() {
165 from_segments.next();
166 if target_segments.next().is_none() {
167 return Cow::Borrowed(".");
168 }
169 }
170 let mut result = SmallVec::<[&str; 8]>::new();
171 if from_segments.peek().is_some() {
172 while from_segments.next().is_some() {
173 result.push("..");
174 }
175 } else if explicitly_relative {
176 result.push(".");
178 }
179 for segment in target_segments {
180 result.push(segment);
181 }
182 Cow::Owned(result.join("/"))
183}
184
185pub fn get_parent_path(path: &str) -> &str {
186 match str::rfind(path, '/') {
187 Some(index) => &path[..index],
188 None => "",
189 }
190}
191
192#[cfg(test)]
193mod tests {
194
195 use rstest::*;
196
197 use super::*;
198
199 #[rstest]
200 #[case("file.js")]
201 #[case("a/b/c/d/e/file.js")]
202 fn test_normalize_path_no_op(#[case] path: &str) {
203 assert_eq!(path, normalize_path(path).unwrap());
204 }
205
206 #[rstest]
207 #[case("/file.js", "file.js")]
208 #[case("./file.js", "file.js")]
209 #[case("././file.js", "file.js")]
210 #[case("a/../c/../file.js", "file.js")]
211 fn test_normalize_path(#[case] path: &str, #[case] normalized: &str) {
212 assert_eq!(normalized, normalize_path(path).unwrap());
213 }
214
215 #[rstest]
216 #[case("../file.js")]
217 #[case("a/../../file.js")]
218 fn test_normalize_path_invalid(#[case] path: &str) {
219 assert_eq!(None, normalize_path(path));
220 }
221
222 #[rstest]
223 #[case("a/b/c", "a/b/c", ".", true)]
224 #[case("a/c/d", "a/b/c", "../../b/c", false)]
225 #[case("", "a/b/c", "a/b/c", true)]
226 #[case("", "", ".", true)]
227 #[case("a/b", "a/b/c", "c", false)]
228 #[case("a/b/c", "", "../../..", false)]
229 #[case("a/b/c", "c/b/a", "../../../c/b/a", false)]
230 #[case("file:///a/b/c", "file:///c/b/a", "../../../c/b/a", false)]
231 fn test_get_relative_path_to(
232 #[case] from: &str,
233 #[case] target: &str,
234 #[case] expected: &str,
235 #[case] borrowed: bool,
236 ) {
237 let relative = get_relative_path_to(from, target);
238 assert_eq!(relative, expected);
239 assert_eq!(matches!(relative, Cow::Borrowed(_)), borrowed);
240 }
241
242 #[rstest]
246 #[case("a/b/c", "a/b/c", ".", true)]
247 #[case("a/c/d", "a/b/c", "../../b/c", false)]
248 #[case("", "a/b/c", "./a/b/c", false)]
249 #[case("", "", ".", true)]
250 #[case("a/b", "a/b/c", "./c", false)]
251 #[case("a/b", "a/b/c/d", "./c/d", false)]
252 #[case("a/b/c", "", "../../..", false)]
253 #[case("a/b/c", "c/b/a", "../../../c/b/a", false)]
254 #[case("file:///a/b/c", "file:///c/b/a", "../../../c/b/a", false)]
255 fn test_get_relative_request_to(
256 #[case] from: &str,
257 #[case] target: &str,
258 #[case] expected: &str,
259 #[case] borrowed: bool,
260 ) {
261 let relative = get_relative_request_to(from, target);
262 assert_eq!(relative, expected);
263 assert_eq!(matches!(relative, Cow::Borrowed(_)), borrowed);
264 }
265}