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> {
39 debug_assert!(
42 !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 = Vec::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 = vec!["."];
89 let mut depth = 0;
94 let mut popped_dot = false;
95 for segment in str.split('/') {
96 match segment {
97 "." => {}
98 ".." => {
99 if depth > 0 {
100 depth -= 1;
101 segments.pop();
102 } else {
103 if !popped_dot {
106 popped_dot = true;
107 segments.pop();
108 }
109 segments.push(segment);
110 }
111 }
112 segment => {
113 segments.push(segment);
114 depth += 1;
115 }
116 }
117 }
118 segments.join("/")
119}
120
121pub fn get_relative_path_to(from: &str, target: &str) -> String {
122 fn split(s: &str) -> impl Iterator<Item = &str> {
123 let empty = s.is_empty();
124 let mut iterator = s.split('/');
125 if empty {
126 iterator.next();
127 }
128 iterator
129 }
130
131 let mut from_segments = split(from).peekable();
132 let mut target_segments = split(target).peekable();
133 while from_segments.peek() == target_segments.peek() {
134 from_segments.next();
135 if target_segments.next().is_none() {
136 return ".".to_string();
137 }
138 }
139 let mut result = Vec::new();
140 if from_segments.peek().is_none() {
141 result.push(".");
142 } else {
143 while from_segments.next().is_some() {
144 result.push("..");
145 }
146 }
147 for segment in target_segments {
148 result.push(segment);
149 }
150 result.join("/")
151}
152
153pub fn get_parent_path(path: &str) -> &str {
154 match str::rfind(path, '/') {
155 Some(index) => &path[..index],
156 None => "",
157 }
158}
159
160#[cfg(test)]
161mod tests {
162
163 use rstest::*;
164
165 use super::*;
166
167 #[rstest]
168 #[case("file.js")]
169 #[case("a/b/c/d/e/file.js")]
170 fn test_normalize_path_no_op(#[case] path: &str) {
171 assert_eq!(path, normalize_path(path).unwrap());
172 }
173
174 #[rstest]
175 #[case("/file.js", "file.js")]
176 #[case("./file.js", "file.js")]
177 #[case("././file.js", "file.js")]
178 #[case("a/../c/../file.js", "file.js")]
179 fn test_normalize_path(#[case] path: &str, #[case] normalized: &str) {
180 assert_eq!(normalized, normalize_path(path).unwrap());
181 }
182
183 #[rstest]
184 #[case("../file.js")]
185 #[case("a/../../file.js")]
186 fn test_normalize_path_invalid(#[case] path: &str) {
187 assert_eq!(None, normalize_path(path));
188 }
189}