1use std::{
2 borrow::Cow,
3 io::{self, ErrorKind},
4 path::Path,
5};
6
7use anyhow::{Context, Result, anyhow};
8use turbo_tasks::Vc;
9
10use crate::{DiskFileSystem, FileSystemPath};
11
12pub fn join_path(fs_path: &str, join: &str) -> Option<String> {
17 debug_assert!(
21 !join.contains('\\'),
22 "joined path {join} must not contain a Windows directory '\\', it must be normalized to \
23 Unix '/'"
24 );
25
26 if fs_path.is_empty() {
33 normalize_path(join)
34 } else if join.is_empty() {
35 normalize_path(fs_path)
36 } else {
37 normalize_path(&[fs_path, "/", join].concat())
38 }
39}
40
41#[inline]
44pub fn sys_to_unix(path: &str) -> Cow<'_, str> {
45 #[cfg(not(target_family = "windows"))]
46 {
47 Cow::from(path)
48 }
49 #[cfg(target_family = "windows")]
50 {
51 Cow::Owned(path.replace(std::path::MAIN_SEPARATOR_STR, "/"))
52 }
53}
54
55#[inline]
58pub fn unix_to_sys(path: &str) -> Cow<'_, str> {
59 #[cfg(not(target_family = "windows"))]
60 {
61 Cow::from(path)
62 }
63 #[cfg(target_family = "windows")]
64 {
65 Cow::Owned(path.replace('/', std::path::MAIN_SEPARATOR_STR))
66 }
67}
68
69pub fn normalize_path(str: &str) -> Option<String> {
74 let mut segments = Vec::new();
75 for segment in str.split('/') {
76 match segment {
77 "." | "" => {}
78 ".." => {
79 segments.pop()?;
80 }
81 segment => {
82 segments.push(segment);
83 }
84 }
85 }
86 Some(segments.join("/"))
87}
88
89pub fn normalize_request(str: &str) -> String {
95 let mut segments = vec!["."];
96 let mut depth = 0;
102 let mut popped_dot = false;
103 for segment in str.split('/') {
104 match segment {
105 "." => {}
106 ".." => {
107 if depth > 0 {
108 depth -= 1;
109 segments.pop();
110 } else {
111 if !popped_dot {
114 popped_dot = true;
115 segments.pop();
116 }
117 segments.push(segment);
118 }
119 }
120 segment => {
121 segments.push(segment);
122 depth += 1;
123 }
124 }
125 }
126 segments.join("/")
127}
128
129pub fn extract_disk_access<T>(value: io::Result<T>, path: &Path) -> Result<Option<T>> {
133 match value {
134 Ok(v) => Ok(Some(v)),
135 Err(e) if matches!(e.kind(), ErrorKind::NotFound | ErrorKind::InvalidFilename) => Ok(None),
136 Err(e) => Err(anyhow!(e).context(format!("reading file {}", path.display()))),
137 }
138}
139
140#[cfg(not(target_os = "windows"))]
141pub async fn uri_from_file(root: Vc<FileSystemPath>, path: Option<&str>) -> Result<String> {
142 let root_fs = root.fs();
143 let root_fs = &*Vc::try_resolve_downcast_type::<DiskFileSystem>(root_fs)
144 .await?
145 .context("Expected root to have a DiskFileSystem")?
146 .await?;
147
148 Ok(format!(
149 "file://{}",
150 &sys_to_unix(
151 &root_fs
152 .to_sys_path(match path {
153 Some(path) => root.join(path.into()),
154 None => root,
155 })
156 .await?
157 .to_string_lossy()
158 )
159 .split('/')
160 .map(|s| urlencoding::encode(s))
161 .collect::<Vec<_>>()
162 .join("/")
163 ))
164}
165
166#[cfg(target_os = "windows")]
167pub async fn uri_from_file(root: Vc<FileSystemPath>, path: Option<&str>) -> Result<String> {
168 let root_fs = root.fs();
169 let root_fs = &*Vc::try_resolve_downcast_type::<DiskFileSystem>(root_fs)
170 .await?
171 .context("Expected root to have a DiskFileSystem")?
172 .await?;
173
174 let sys_path = root_fs
175 .to_sys_path(match path {
176 Some(path) => root.join(path.into()),
177 None => root,
178 })
179 .await?;
180
181 let raw_path = sys_path.to_string_lossy().to_string();
182 let normalized_path = raw_path.replace('\\', "/");
183
184 let mut segments = normalized_path.split('/');
185
186 let first = segments.next().unwrap_or_default(); let encoded_path = std::iter::once(first.to_string()) .chain(segments.map(|s| urlencoding::encode(s).into_owned()))
189 .collect::<Vec<_>>()
190 .join("/");
191
192 let uri = format!("file:///{}", encoded_path);
193
194 Ok(uri)
195}
196
197#[cfg(test)]
198mod tests {
199
200 use rstest::*;
201
202 use crate::util::normalize_path;
203
204 #[rstest]
205 #[case("file.js")]
206 #[case("a/b/c/d/e/file.js")]
207 fn test_normalize_path_no_op(#[case] path: &str) {
208 assert_eq!(path, normalize_path(path).unwrap());
209 }
210
211 #[rstest]
212 #[case("/file.js", "file.js")]
213 #[case("./file.js", "file.js")]
214 #[case("././file.js", "file.js")]
215 #[case("a/../c/../file.js", "file.js")]
216 fn test_normalize_path(#[case] path: &str, #[case] normalized: &str) {
217 assert_eq!(normalized, normalize_path(path).unwrap());
218 }
219
220 #[rstest]
221 #[case("../file.js")]
222 #[case("a/../../file.js")]
223 fn test_normalize_path_invalid(#[case] path: &str) {
224 assert_eq!(None, normalize_path(path));
225 }
226}