Skip to main content

node_resolver/
path.rs

1// Copyright 2018-2026 the Deno authors. MIT license.
2
3use std::borrow::Cow;
4use std::path::Component;
5use std::path::Path;
6use std::path::PathBuf;
7
8use url::Url;
9
10#[derive(Debug, Clone)]
11pub enum UrlOrPath {
12  Url(Url),
13  Path(PathBuf),
14}
15
16impl UrlOrPath {
17  pub fn is_file(&self) -> bool {
18    match self {
19      UrlOrPath::Url(url) => url.scheme() == "file",
20      UrlOrPath::Path(_) => true,
21    }
22  }
23
24  pub fn is_node_url(&self) -> bool {
25    match self {
26      UrlOrPath::Url(url) => url.scheme() == "node",
27      UrlOrPath::Path(_) => false,
28    }
29  }
30
31  pub fn into_path(
32    self,
33  ) -> Result<PathBuf, deno_path_util::UrlToFilePathError> {
34    match self {
35      UrlOrPath::Url(url) => deno_path_util::url_to_file_path(&url),
36      UrlOrPath::Path(path) => Ok(path),
37    }
38  }
39
40  pub fn into_url(self) -> Result<Url, deno_path_util::PathToUrlError> {
41    match self {
42      UrlOrPath::Url(url) => Ok(url),
43      UrlOrPath::Path(path) => deno_path_util::url_from_file_path(&path),
44    }
45  }
46
47  pub fn to_string_lossy(&self) -> Cow<'_, str> {
48    match self {
49      UrlOrPath::Url(url) => Cow::Borrowed(url.as_str()),
50      UrlOrPath::Path(path) => path.to_string_lossy(),
51    }
52  }
53}
54
55impl std::fmt::Display for UrlOrPath {
56  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57    match self {
58      UrlOrPath::Url(url) => url.fmt(f),
59      UrlOrPath::Path(path) => {
60        // prefer displaying a url
61        match deno_path_util::url_from_file_path(path) {
62          Ok(url) => url.fmt(f),
63          Err(_) => {
64            write!(f, "{}", path.display())
65          }
66        }
67      }
68    }
69  }
70}
71
72pub struct UrlOrPathRef<'a> {
73  url: once_cell::unsync::OnceCell<Cow<'a, Url>>,
74  path: once_cell::unsync::OnceCell<Cow<'a, Path>>,
75}
76
77impl<'a> UrlOrPathRef<'a> {
78  pub fn from_path(path: &'a Path) -> Self {
79    Self {
80      url: Default::default(),
81      path: once_cell::unsync::OnceCell::with_value(Cow::Borrowed(path)),
82    }
83  }
84
85  pub fn from_url(url: &'a Url) -> Self {
86    Self {
87      path: Default::default(),
88      url: once_cell::unsync::OnceCell::with_value(Cow::Borrowed(url)),
89    }
90  }
91
92  pub fn url(&self) -> Result<&Url, deno_path_util::PathToUrlError> {
93    self
94      .url
95      .get_or_try_init(|| {
96        deno_path_util::url_from_file_path(self.path.get().unwrap())
97          .map(Cow::Owned)
98      })
99      .map(|v| v.as_ref())
100  }
101
102  pub fn path(&self) -> Result<&Path, deno_path_util::UrlToFilePathError> {
103    self
104      .path
105      .get_or_try_init(|| {
106        deno_path_util::url_to_file_path(self.url.get().unwrap())
107          .map(Cow::Owned)
108      })
109      .map(|v| v.as_ref())
110  }
111
112  pub fn display(&self) -> UrlOrPath {
113    // prefer url
114    if let Ok(url) = self.url() {
115      UrlOrPath::Url(url.clone())
116    } else {
117      // this will always be set if url is None
118      UrlOrPath::Path(self.path.get().unwrap().to_path_buf())
119    }
120  }
121}
122
123/// Extension to path_clean::PathClean
124pub trait PathClean<T> {
125  fn clean(&self) -> T;
126}
127
128impl PathClean<PathBuf> for PathBuf {
129  fn clean(&self) -> PathBuf {
130    fn is_clean_path(path: &Path) -> bool {
131      let path = path.to_string_lossy();
132      let mut current_index = 0;
133      while let Some(index) = path[current_index..].find("\\.") {
134        let trailing_index = index + current_index + 2;
135        let mut trailing_chars = path[trailing_index..].chars();
136        match trailing_chars.next() {
137          Some('.') => match trailing_chars.next() {
138            Some('/') | Some('\\') | None => {
139              return false;
140            }
141            _ => {}
142          },
143          Some('/') | Some('\\') => {
144            return false;
145          }
146          _ => {}
147        }
148        current_index = trailing_index;
149      }
150      true
151    }
152
153    let path = path_clean::PathClean::clean(self);
154    if cfg!(windows) && !is_clean_path(&path) {
155      // temporary workaround because path_clean::PathClean::clean is
156      // not good enough on windows
157      let mut components = Vec::new();
158
159      for component in path.components() {
160        match component {
161          Component::CurDir => {
162            // skip
163          }
164          Component::ParentDir => {
165            let maybe_last_component = components.pop();
166            if !matches!(maybe_last_component, Some(Component::Normal(_))) {
167              panic!("Error normalizing: {}", path.display());
168            }
169          }
170          Component::Normal(_) | Component::RootDir | Component::Prefix(_) => {
171            components.push(component);
172          }
173        }
174      }
175      components.into_iter().collect::<PathBuf>()
176    } else {
177      path
178    }
179  }
180}
181
182#[cfg(test)]
183mod test {
184  #[cfg(windows)]
185  #[test]
186  fn test_path_clean() {
187    use super::*;
188
189    run_test("C:\\test\\./file.txt", "C:\\test\\file.txt");
190    run_test("C:\\test\\../other/file.txt", "C:\\other\\file.txt");
191    run_test("C:\\test\\../other\\file.txt", "C:\\other\\file.txt");
192
193    fn run_test(input: &str, expected: &str) {
194      assert_eq!(PathBuf::from(input).clean(), PathBuf::from(expected));
195    }
196  }
197}