1#![doc = include_str!("../README.md")]
2
3use std::borrow::Cow;
4
5#[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#[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
33pub fn join_path(fs_path: &str, join: &str) -> Option<String> {
38 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 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
61pub 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
81pub fn normalize_request(str: &str) -> String {
87 let mut segments = vec!["."];
88 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 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}