deno_path_util/
lib.rs

1// Copyright 2018-2025 the Deno authors. MIT license.
2
3#![deny(clippy::print_stderr)]
4#![deny(clippy::print_stdout)]
5#![deny(clippy::unused_async)]
6#![deny(clippy::unnecessary_wraps)]
7
8use deno_error::JsError;
9use std::borrow::Cow;
10use std::path::Component;
11use std::path::Path;
12use std::path::PathBuf;
13use sys_traits::SystemRandom;
14use thiserror::Error;
15use url::Url;
16
17pub mod fs;
18
19/// Gets the parent of this url.
20pub fn url_parent(url: &Url) -> Url {
21  let mut url = url.clone();
22  // don't use url.segments() because it will strip the leading slash
23  let mut segments = url.path().split('/').collect::<Vec<_>>();
24  if segments.iter().all(|s| s.is_empty()) {
25    return url;
26  }
27  if let Some(last) = segments.last() {
28    if last.is_empty() {
29      segments.pop();
30    }
31    segments.pop();
32    let new_path = format!("{}/", segments.join("/"));
33    url.set_path(&new_path);
34  }
35  url
36}
37
38#[derive(Debug, Error, deno_error::JsError)]
39#[class(uri)]
40#[error("Could not convert URL to file path.\n  URL: {0}")]
41pub struct UrlToFilePathError(pub Url);
42
43/// Attempts to convert a url to a file path. By default, uses the Url
44/// crate's `to_file_path()` method, but falls back to try and resolve unix-style
45/// paths on Windows.
46pub fn url_to_file_path(url: &Url) -> Result<PathBuf, UrlToFilePathError> {
47  let result = if url.scheme() != "file" {
48    Err(())
49  } else {
50    url_to_file_path_inner(url)
51  };
52  match result {
53    Ok(path) => Ok(path),
54    Err(()) => Err(UrlToFilePathError(url.clone())),
55  }
56}
57
58fn url_to_file_path_inner(url: &Url) -> Result<PathBuf, ()> {
59  #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
60  return url_to_file_path_real(url);
61  #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))]
62  url_to_file_path_wasm(url)
63}
64
65#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
66fn url_to_file_path_real(url: &Url) -> Result<PathBuf, ()> {
67  if cfg!(windows) {
68    match url.to_file_path() {
69      Ok(path) => Ok(path),
70      Err(()) => {
71        // This might be a unix-style path which is used in the tests even on Windows.
72        // Attempt to see if we can convert it to a `PathBuf`. This code should be removed
73        // once/if https://github.com/servo/rust-url/issues/730 is implemented.
74        if url.scheme() == "file"
75          && url.host().is_none()
76          && url.port().is_none()
77          && url.path_segments().is_some()
78        {
79          let path_str = url.path();
80          match String::from_utf8(
81            percent_encoding::percent_decode(path_str.as_bytes()).collect(),
82          ) {
83            Ok(path_str) => Ok(PathBuf::from(path_str)),
84            Err(_) => Err(()),
85          }
86        } else {
87          Err(())
88        }
89      }
90    }
91  } else {
92    url.to_file_path()
93  }
94}
95
96#[cfg(any(
97  test,
98  not(any(unix, windows, target_os = "redox", target_os = "wasi"))
99))]
100#[allow(clippy::unnecessary_wraps)]
101fn url_to_file_path_wasm(url: &Url) -> Result<PathBuf, ()> {
102  fn is_windows_path_segment(url: &str) -> bool {
103    let mut chars = url.chars();
104
105    let first_char = chars.next();
106    if first_char.is_none() || !first_char.unwrap().is_ascii_alphabetic() {
107      return false;
108    }
109
110    if chars.next() != Some(':') {
111      return false;
112    }
113
114    chars.next().is_none()
115  }
116
117  let path_segments = url.path_segments().unwrap().collect::<Vec<_>>();
118  let mut final_text = String::new();
119  let mut is_windows_share = false;
120  if let Some(host) = url.host_str() {
121    final_text.push_str("\\\\");
122    final_text.push_str(host);
123    is_windows_share = true;
124  }
125  for segment in path_segments.iter() {
126    if is_windows_share {
127      final_text.push('\\');
128    } else if !final_text.is_empty() {
129      final_text.push('/');
130    }
131    final_text.push_str(
132      &percent_encoding::percent_decode_str(segment).decode_utf8_lossy(),
133    );
134  }
135  if !is_windows_share && !is_windows_path_segment(path_segments[0]) {
136    final_text = format!("/{}", final_text);
137  }
138  Ok(PathBuf::from(final_text))
139}
140
141/// Normalize all intermediate components of the path (ie. remove "./" and "../" components).
142/// Similar to `fs::canonicalize()` but doesn't resolve symlinks.
143///
144/// Adapted from Cargo
145/// <https://github.com/rust-lang/cargo/blob/af307a38c20a753ec60f0ad18be5abed3db3c9ac/src/cargo/util/paths.rs#L60-L85>
146#[inline]
147pub fn normalize_path(path: Cow<Path>) -> Cow<Path> {
148  fn should_normalize(path: &Path) -> bool {
149    if path_has_trailing_separator(path) {
150      return true;
151    }
152
153    for component in path.components() {
154      match component {
155        Component::CurDir | Component::ParentDir => {
156          return true;
157        }
158        Component::Prefix(..) | Component::RootDir | Component::Normal(_) => {
159          // ok
160        }
161      }
162    }
163
164    path_has_cur_dir_separator(path)
165  }
166
167  fn path_has_trailing_separator(path: &Path) -> bool {
168    #[cfg(unix)]
169    let raw = std::os::unix::ffi::OsStrExt::as_bytes(path.as_os_str());
170    #[cfg(windows)]
171    let raw = path.as_os_str().as_encoded_bytes();
172    #[cfg(target_arch = "wasm32")]
173    let raw = path.to_string_lossy();
174    #[cfg(target_arch = "wasm32")]
175    let raw = raw.as_bytes();
176
177    if sys_traits::impls::is_windows() {
178      raw.contains(&b'/') || raw.ends_with(b"\\")
179    } else {
180      raw.ends_with(b"/")
181    }
182  }
183
184  // Rust normalizes away `Component::CurDir` most of the time
185  // so we need to explicitly check for it in the bytes
186  fn path_has_cur_dir_separator(path: &Path) -> bool {
187    #[cfg(unix)]
188    let raw = std::os::unix::ffi::OsStrExt::as_bytes(path.as_os_str());
189    #[cfg(windows)]
190    let raw = path.as_os_str().as_encoded_bytes();
191    #[cfg(target_arch = "wasm32")]
192    let raw = path.to_string_lossy();
193    #[cfg(target_arch = "wasm32")]
194    let raw = raw.as_bytes();
195
196    if sys_traits::impls::is_windows() {
197      for window in raw.windows(3) {
198        if matches!(window, [b'\\', b'.', b'\\']) {
199          return true;
200        }
201      }
202    } else {
203      for window in raw.windows(3) {
204        if matches!(window, [b'/', b'.', b'/']) {
205          return true;
206        }
207      }
208    }
209
210    false
211  }
212
213  fn inner(path: &Path) -> PathBuf {
214    let mut components = path.components().peekable();
215    let mut ret =
216      if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
217        components.next();
218        PathBuf::from(c.as_os_str())
219      } else {
220        PathBuf::new()
221      };
222
223    for component in components {
224      match component {
225        Component::Prefix(..) => unreachable!(),
226        Component::RootDir => {
227          ret.push(component.as_os_str());
228        }
229        Component::CurDir => {}
230        Component::ParentDir => {
231          ret.pop();
232        }
233        Component::Normal(c) => {
234          ret.push(c);
235        }
236      }
237    }
238    ret
239  }
240
241  if should_normalize(&path) {
242    Cow::Owned(inner(&path))
243  } else {
244    path
245  }
246}
247
248#[derive(Debug, Clone, Error, deno_error::JsError, PartialEq, Eq)]
249#[class(uri)]
250#[error("Could not convert path to URL.\n  Path: {0}")]
251pub struct PathToUrlError(pub PathBuf);
252
253#[allow(clippy::result_unit_err)]
254pub fn url_from_file_path(path: &Path) -> Result<Url, PathToUrlError> {
255  let path = normalize_path(Cow::Borrowed(path));
256  #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
257  return Url::from_file_path(&path)
258    .map_err(|()| PathToUrlError(path.to_path_buf()));
259  #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))]
260  url_from_file_path_wasm(&path)
261    .map_err(|()| PathToUrlError(path.to_path_buf()))
262}
263
264#[allow(clippy::result_unit_err)]
265pub fn url_from_directory_path(path: &Path) -> Result<Url, PathToUrlError> {
266  let path = normalize_path(Cow::Borrowed(path));
267  #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
268  return Url::from_directory_path(&path)
269    .map_err(|()| PathToUrlError(path.to_path_buf()));
270  #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))]
271  url_from_directory_path_wasm(&path)
272    .map_err(|()| PathToUrlError(path.to_path_buf()))
273}
274
275#[cfg(any(
276  test,
277  not(any(unix, windows, target_os = "redox", target_os = "wasi"))
278))]
279fn url_from_directory_path_wasm(path: &Path) -> Result<Url, ()> {
280  let mut url = url_from_file_path_wasm(path)?;
281  url.path_segments_mut().unwrap().push("");
282  Ok(url)
283}
284
285#[cfg(any(
286  test,
287  not(any(unix, windows, target_os = "redox", target_os = "wasi"))
288))]
289fn url_from_file_path_wasm(path: &Path) -> Result<Url, ()> {
290  use std::path::Component;
291
292  let original_path = path.to_string_lossy();
293  let mut path_str = original_path;
294  // assume paths containing backslashes are windows paths
295  if path_str.contains('\\') {
296    let mut url = Url::parse("file://").unwrap();
297    if let Some(next) = path_str.strip_prefix(r#"\\?\UNC\"#) {
298      if let Some((host, rest)) = next.split_once('\\') {
299        if url.set_host(Some(host)).is_ok() {
300          path_str = rest.to_string().into();
301        }
302      }
303    } else if let Some(next) = path_str.strip_prefix(r#"\\?\"#) {
304      path_str = next.to_string().into();
305    } else if let Some(next) = path_str.strip_prefix(r#"\\"#) {
306      if let Some((host, rest)) = next.split_once('\\') {
307        if url.set_host(Some(host)).is_ok() {
308          path_str = rest.to_string().into();
309        }
310      }
311    }
312
313    for component in path_str.split('\\') {
314      url.path_segments_mut().unwrap().push(component);
315    }
316
317    Ok(url)
318  } else {
319    let mut url = Url::parse("file://").unwrap();
320    for component in path.components() {
321      match component {
322        Component::RootDir => {
323          url.path_segments_mut().unwrap().push("");
324        }
325        Component::Normal(segment) => {
326          url
327            .path_segments_mut()
328            .unwrap()
329            .push(&segment.to_string_lossy());
330        }
331        Component::Prefix(_) | Component::CurDir | Component::ParentDir => {
332          return Err(());
333        }
334      }
335    }
336
337    Ok(url)
338  }
339}
340
341#[cfg(not(windows))]
342#[inline]
343pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
344  path
345}
346
347/// Strips the unc prefix (ex. \\?\) from Windows paths.
348#[cfg(windows)]
349pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
350  use std::path::Component;
351  use std::path::Prefix;
352
353  let mut components = path.components();
354  match components.next() {
355    Some(Component::Prefix(prefix)) => {
356      match prefix.kind() {
357        // \\?\device
358        Prefix::Verbatim(device) => {
359          let mut path = PathBuf::new();
360          path.push(format!(r"\\{}\", device.to_string_lossy()));
361          path.extend(components.filter(|c| !matches!(c, Component::RootDir)));
362          path
363        }
364        // \\?\c:\path
365        Prefix::VerbatimDisk(_) => {
366          let mut path = PathBuf::new();
367          path.push(prefix.as_os_str().to_string_lossy().replace(r"\\?\", ""));
368          path.extend(components);
369          path
370        }
371        // \\?\UNC\hostname\share_name\path
372        Prefix::VerbatimUNC(hostname, share_name) => {
373          let mut path = PathBuf::new();
374          path.push(format!(
375            r"\\{}\{}\",
376            hostname.to_string_lossy(),
377            share_name.to_string_lossy()
378          ));
379          path.extend(components.filter(|c| !matches!(c, Component::RootDir)));
380          path
381        }
382        _ => path,
383      }
384    }
385    _ => path,
386  }
387}
388
389/// Returns true if the input string starts with a sequence of characters
390/// that could be a valid URI scheme, like 'https:', 'git+ssh:' or 'data:'.
391///
392/// According to RFC 3986 (https://tools.ietf.org/html/rfc3986#section-3.1),
393/// a valid scheme has the following format:
394///   scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
395///
396/// We additionally require the scheme to be at least 2 characters long,
397/// because otherwise a windows path like c:/foo would be treated as a URL,
398/// while no schemes with a one-letter name actually exist.
399pub fn specifier_has_uri_scheme(specifier: &str) -> bool {
400  let mut chars = specifier.chars();
401  let mut len = 0usize;
402  // The first character must be a letter.
403  match chars.next() {
404    Some(c) if c.is_ascii_alphabetic() => len += 1,
405    _ => return false,
406  }
407  // Second and following characters must be either a letter, number,
408  // plus sign, minus sign, or dot.
409  loop {
410    match chars.next() {
411      Some(c) if c.is_ascii_alphanumeric() || "+-.".contains(c) => len += 1,
412      Some(':') if len >= 2 => return true,
413      _ => return false,
414    }
415  }
416}
417
418#[derive(Debug, Clone, Error, JsError, PartialEq, Eq)]
419pub enum ResolveUrlOrPathError {
420  #[error(transparent)]
421  #[class(inherit)]
422  UrlParse(url::ParseError),
423  #[error(transparent)]
424  #[class(inherit)]
425  PathToUrl(PathToUrlError),
426}
427
428/// Takes a string representing either an absolute URL or a file path,
429/// as it may be passed to deno as a command line argument.
430/// The string is interpreted as a URL if it starts with a valid URI scheme,
431/// e.g. 'http:' or 'file:' or 'git+ssh:'. If not, it's interpreted as a
432/// file path; if it is a relative path it's resolved relative to passed
433/// `current_dir`.
434pub fn resolve_url_or_path(
435  specifier: &str,
436  current_dir: &Path,
437) -> Result<Url, ResolveUrlOrPathError> {
438  if specifier_has_uri_scheme(specifier) {
439    Url::parse(specifier).map_err(ResolveUrlOrPathError::UrlParse)
440  } else {
441    resolve_path(specifier, current_dir)
442      .map_err(ResolveUrlOrPathError::PathToUrl)
443  }
444}
445
446/// Converts a string representing a relative or absolute path into a
447/// ModuleSpecifier. A relative path is considered relative to the passed
448/// `current_dir`.
449pub fn resolve_path(
450  path_str: &str,
451  current_dir: &Path,
452) -> Result<Url, PathToUrlError> {
453  let path = current_dir.join(path_str);
454  url_from_file_path(&path)
455}
456
457pub fn get_atomic_path(sys: &impl SystemRandom, path: &Path) -> PathBuf {
458  let rand = gen_rand_path_component(sys);
459  let extension = format!("{rand}.tmp");
460  path.with_extension(extension)
461}
462
463fn gen_rand_path_component(sys: &impl SystemRandom) -> String {
464  use std::fmt::Write;
465  (0..4).fold(String::with_capacity(8), |mut output, _| {
466    write!(&mut output, "{:02x}", sys.sys_random_u8().unwrap()).unwrap();
467    output
468  })
469}
470
471pub fn is_relative_specifier(specifier: &str) -> bool {
472  let mut specifier_chars = specifier.chars();
473  let Some(first_char) = specifier_chars.next() else {
474    return false;
475  };
476  if first_char != '.' {
477    return false;
478  }
479  let Some(second_char) = specifier_chars.next() else {
480    return true;
481  };
482  if second_char == '/' {
483    return true;
484  }
485  let Some(third_char) = specifier_chars.next() else {
486    return second_char == '.';
487  };
488  second_char == '.' && third_char == '/'
489}
490
491#[cfg(test)]
492mod tests {
493  use super::*;
494
495  #[test]
496  fn test_url_parent() {
497    run_test("file:///", "file:///");
498    run_test("file:///test", "file:///");
499    run_test("file:///test/", "file:///");
500    run_test("file:///test/other", "file:///test/");
501    run_test("file:///test/other.txt", "file:///test/");
502    run_test("file:///test/other/", "file:///test/");
503
504    fn run_test(url: &str, expected: &str) {
505      let result = url_parent(&Url::parse(url).unwrap());
506      assert_eq!(result.to_string(), expected);
507    }
508  }
509
510  #[test]
511  fn test_url_to_file_path() {
512    run_success_test("file:///", "/");
513    run_success_test("file:///test", "/test");
514    run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt");
515    #[cfg(not(debug_assertions))]
516    run_success_test(
517      "file:///dir/test%20test/test.txt",
518      "/dir/test test/test.txt",
519    );
520
521    assert_no_panic_url_to_file_path("file:/");
522    assert_no_panic_url_to_file_path("file://");
523    #[cfg(not(debug_assertions))]
524    assert_no_panic_url_to_file_path("file://asdf/");
525    assert_no_panic_url_to_file_path("file://asdf/66666/a.ts");
526
527    #[track_caller]
528    fn run_success_test(url: &str, expected_path: &str) {
529      let result = url_to_file_path(&Url::parse(url).unwrap()).unwrap();
530      assert_eq!(result, PathBuf::from(expected_path));
531    }
532
533    #[track_caller]
534    fn assert_no_panic_url_to_file_path(url: &str) {
535      let _result = url_to_file_path(&Url::parse(url).unwrap());
536    }
537  }
538
539  #[test]
540  fn test_url_to_file_path_wasm() {
541    #[track_caller]
542    fn convert(path: &str) -> String {
543      url_to_file_path_wasm(&Url::parse(path).unwrap())
544        .unwrap()
545        .to_string_lossy()
546        .into_owned()
547    }
548
549    assert_eq!(convert("file:///a/b/c.json"), "/a/b/c.json");
550    assert_eq!(convert("file:///D:/test/other.json"), "D:/test/other.json");
551    assert_eq!(
552      convert("file:///path%20with%20spaces/and%23special%25chars!.json"),
553      "/path with spaces/and#special%chars!.json",
554    );
555    assert_eq!(
556      convert("file:///C:/My%20Documents/file.txt"),
557      "C:/My Documents/file.txt"
558    );
559    assert_eq!(
560      convert("file:///a/b/%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80.txt"),
561      "/a/b/пример.txt"
562    );
563    assert_eq!(
564      convert("file://server/share/folder/file.txt"),
565      "\\\\server\\share\\folder\\file.txt"
566    );
567  }
568
569  #[test]
570  fn test_url_from_file_path_wasm() {
571    #[track_caller]
572    fn convert(path: &str) -> String {
573      url_from_file_path_wasm(Path::new(path))
574        .unwrap()
575        .to_string()
576    }
577
578    assert_eq!(convert("/a/b/c.json"), "file:///a/b/c.json");
579    assert_eq!(
580      convert("D:\\test\\other.json"),
581      "file:///D:/test/other.json"
582    );
583    assert_eq!(
584      convert("/path with spaces/and#special%chars!.json"),
585      "file:///path%20with%20spaces/and%23special%25chars!.json"
586    );
587    assert_eq!(
588      convert("C:\\My Documents\\file.txt"),
589      "file:///C:/My%20Documents/file.txt"
590    );
591    assert_eq!(
592      convert("/a/b/пример.txt"),
593      "file:///a/b/%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80.txt"
594    );
595    assert_eq!(
596      convert("\\\\server\\share\\folder\\file.txt"),
597      "file://server/share/folder/file.txt"
598    );
599    assert_eq!(convert(r#"\\?\UNC\server\share"#), "file://server/share");
600    assert_eq!(
601      convert(r"\\?\cat_pics\subfolder\file.jpg"),
602      "file:///cat_pics/subfolder/file.jpg"
603    );
604    assert_eq!(convert(r"\\?\cat_pics"), "file:///cat_pics");
605  }
606
607  #[test]
608  fn test_url_from_directory_path_wasm() {
609    #[track_caller]
610    fn convert(path: &str) -> String {
611      url_from_directory_path_wasm(Path::new(path))
612        .unwrap()
613        .to_string()
614    }
615
616    assert_eq!(convert("/a/b/c"), "file:///a/b/c/");
617    assert_eq!(convert("D:\\test\\other"), "file:///D:/test/other/");
618  }
619
620  #[cfg(windows)]
621  #[test]
622  fn test_strip_unc_prefix() {
623    use std::path::PathBuf;
624
625    run_test(r"C:\", r"C:\");
626    run_test(r"C:\test\file.txt", r"C:\test\file.txt");
627
628    run_test(r"\\?\C:\", r"C:\");
629    run_test(r"\\?\C:\test\file.txt", r"C:\test\file.txt");
630
631    run_test(r"\\.\C:\", r"\\.\C:\");
632    run_test(r"\\.\C:\Test\file.txt", r"\\.\C:\Test\file.txt");
633
634    run_test(r"\\?\UNC\localhost\", r"\\localhost");
635    run_test(r"\\?\UNC\localhost\c$\", r"\\localhost\c$");
636    run_test(
637      r"\\?\UNC\localhost\c$\Windows\file.txt",
638      r"\\localhost\c$\Windows\file.txt",
639    );
640    run_test(r"\\?\UNC\wsl$\deno.json", r"\\wsl$\deno.json");
641
642    run_test(r"\\?\server1", r"\\server1");
643    run_test(r"\\?\server1\e$\", r"\\server1\e$\");
644    run_test(
645      r"\\?\server1\e$\test\file.txt",
646      r"\\server1\e$\test\file.txt",
647    );
648
649    fn run_test(input: &str, expected: &str) {
650      assert_eq!(
651        super::strip_unc_prefix(PathBuf::from(input)),
652        PathBuf::from(expected)
653      );
654    }
655  }
656
657  #[test]
658  fn test_normalize_path_basic() {
659    let run_test = run_normalize_path_test;
660    run_test("a/../b", "b");
661    run_test("a/./b/", &PathBuf::from("a").join("b").to_string_lossy());
662    run_test(
663      "a/./b/../c",
664      &PathBuf::from("a").join("c").to_string_lossy(),
665    );
666  }
667
668  #[cfg(windows)]
669  #[test]
670  fn test_normalize_path_win() {
671    let run_test = run_normalize_path_test;
672
673    run_test("C:\\test\\file.txt", "C:\\test\\file.txt");
674    run_test("C:\\test\\./file.txt", "C:\\test\\file.txt");
675    run_test("C:\\test\\../other/file.txt", "C:\\other\\file.txt");
676    run_test("C:\\test\\../other\\file.txt", "C:\\other\\file.txt");
677    run_test(
678      "C:\\test\\removes_trailing_slash\\",
679      "C:\\test\\removes_trailing_slash",
680    );
681    run_test("C:\\a\\.\\b\\..\\c", "C:\\a\\c");
682  }
683
684  #[track_caller]
685  fn run_normalize_path_test(input: &str, expected: &str) {
686    assert_eq!(
687      normalize_path(Cow::Owned(PathBuf::from(input))).to_string_lossy(),
688      expected
689    );
690  }
691
692  #[test]
693  fn test_atomic_path() {
694    let sys = sys_traits::impls::InMemorySys::default();
695    sys.set_seed(Some(10));
696    let path = Path::new("/a/b/c.txt");
697    let atomic_path = get_atomic_path(&sys, path);
698    assert_eq!(atomic_path.parent().unwrap(), path.parent().unwrap());
699    assert_eq!(atomic_path.file_name().unwrap(), "c.3d3d3d3d.tmp");
700  }
701
702  #[test]
703  fn test_specifier_has_uri_scheme() {
704    let tests = vec![
705      ("http://foo.bar/etc", true),
706      ("HTTP://foo.bar/etc", true),
707      ("http:ftp:", true),
708      ("http:", true),
709      ("hTtP:", true),
710      ("ftp:", true),
711      ("mailto:spam@please.me", true),
712      ("git+ssh://git@github.com/denoland/deno", true),
713      ("blob:https://whatwg.org/mumbojumbo", true),
714      ("abc.123+DEF-ghi:", true),
715      ("abc.123+def-ghi:@", true),
716      ("", false),
717      (":not", false),
718      ("http", false),
719      ("c:dir", false),
720      ("X:", false),
721      ("./http://not", false),
722      ("1abc://kinda/but/no", false),
723      ("schluẞ://no/more", false),
724    ];
725
726    for (specifier, expected) in tests {
727      let result = specifier_has_uri_scheme(specifier);
728      assert_eq!(result, expected);
729    }
730  }
731
732  #[test]
733  fn test_resolve_url_or_path() {
734    // Absolute URL.
735    let mut tests: Vec<(&str, String)> = vec![
736      (
737        "http://deno.land/core/tests/006_url_imports.ts",
738        "http://deno.land/core/tests/006_url_imports.ts".to_string(),
739      ),
740      (
741        "https://deno.land/core/tests/006_url_imports.ts",
742        "https://deno.land/core/tests/006_url_imports.ts".to_string(),
743      ),
744    ];
745
746    // The local path tests assume that the cwd is the deno repo root. Note
747    // that we can't use `cwd` in miri tests, so we just use `/miri` instead.
748    let cwd = if cfg!(miri) {
749      PathBuf::from("/miri")
750    } else {
751      std::env::current_dir().unwrap()
752    };
753    let cwd_str = cwd.to_str().unwrap();
754
755    if cfg!(target_os = "windows") {
756      // Absolute local path.
757      let expected_url = "file:///C:/deno/tests/006_url_imports.ts";
758      tests.extend(vec![
759        (
760          r"C:/deno/tests/006_url_imports.ts",
761          expected_url.to_string(),
762        ),
763        (
764          r"C:\deno\tests\006_url_imports.ts",
765          expected_url.to_string(),
766        ),
767        (
768          r"\\?\C:\deno\tests\006_url_imports.ts",
769          expected_url.to_string(),
770        ),
771        // Not supported: `Url::from_file_path()` fails.
772        // (r"\\.\C:\deno\tests\006_url_imports.ts", expected_url.to_string()),
773        // Not supported: `Url::from_file_path()` performs the wrong conversion.
774        // (r"//./C:/deno/tests/006_url_imports.ts", expected_url.to_string()),
775      ]);
776
777      // Rooted local path without drive letter.
778      let expected_url = format!(
779        "file:///{}:/deno/tests/006_url_imports.ts",
780        cwd_str.get(..1).unwrap(),
781      );
782      tests.extend(vec![
783        (r"/deno/tests/006_url_imports.ts", expected_url.to_string()),
784        (r"\deno\tests\006_url_imports.ts", expected_url.to_string()),
785        (
786          r"\deno\..\deno\tests\006_url_imports.ts",
787          expected_url.to_string(),
788        ),
789        (r"\deno\.\tests\006_url_imports.ts", expected_url),
790      ]);
791
792      // Relative local path.
793      let expected_url = format!(
794        "file:///{}/tests/006_url_imports.ts",
795        cwd_str.replace('\\', "/")
796      );
797      tests.extend(vec![
798        (r"tests/006_url_imports.ts", expected_url.to_string()),
799        (r"tests\006_url_imports.ts", expected_url.to_string()),
800        (r"./tests/006_url_imports.ts", (*expected_url).to_string()),
801        (r".\tests\006_url_imports.ts", (*expected_url).to_string()),
802      ]);
803
804      // UNC network path.
805      let expected_url = "file://server/share/deno/cool";
806      tests.extend(vec![
807        (r"\\server\share\deno\cool", expected_url.to_string()),
808        (r"\\server/share/deno/cool", expected_url.to_string()),
809        // Not supported: `Url::from_file_path()` performs the wrong conversion.
810        // (r"//server/share/deno/cool", expected_url.to_string()),
811      ]);
812    } else {
813      // Absolute local path.
814      let expected_url = "file:///deno/tests/006_url_imports.ts";
815      tests.extend(vec![
816        ("/deno/tests/006_url_imports.ts", expected_url.to_string()),
817        ("//deno/tests/006_url_imports.ts", expected_url.to_string()),
818      ]);
819
820      // Relative local path.
821      let expected_url = format!("file://{cwd_str}/tests/006_url_imports.ts");
822      tests.extend(vec![
823        ("tests/006_url_imports.ts", expected_url.to_string()),
824        ("./tests/006_url_imports.ts", expected_url.to_string()),
825        (
826          "tests/../tests/006_url_imports.ts",
827          expected_url.to_string(),
828        ),
829        ("tests/./006_url_imports.ts", expected_url),
830      ]);
831    }
832
833    for (specifier, expected_url) in tests {
834      let url = resolve_url_or_path(specifier, &cwd).unwrap().to_string();
835      assert_eq!(url, expected_url);
836    }
837  }
838
839  #[test]
840  fn test_resolve_url_or_path_deprecated_error() {
841    use url::ParseError::*;
842    use ResolveUrlOrPathError::*;
843
844    let mut tests = vec![
845      ("https://eggplant:b/c", UrlParse(InvalidPort)),
846      ("https://:8080/a/b/c", UrlParse(EmptyHost)),
847    ];
848    if cfg!(target_os = "windows") {
849      let p = r"\\.\c:/stuff/deno/script.ts";
850      tests.push((p, PathToUrl(PathToUrlError(PathBuf::from(p)))));
851    }
852
853    for (specifier, expected_err) in tests {
854      let err =
855        resolve_url_or_path(specifier, &PathBuf::from("/")).unwrap_err();
856      assert_eq!(err, expected_err);
857    }
858  }
859
860  #[test]
861  fn test_is_relative_specifier() {
862    assert!(is_relative_specifier("."));
863    assert!(is_relative_specifier(".."));
864    assert!(is_relative_specifier("../"));
865    assert!(is_relative_specifier("../test"));
866    assert!(is_relative_specifier("./"));
867    assert!(is_relative_specifier("./test"));
868    assert!(!is_relative_specifier(""));
869    assert!(!is_relative_specifier("a"));
870    assert!(!is_relative_specifier(".test"));
871    assert!(!is_relative_specifier("..test"));
872    assert!(!is_relative_specifier("/test"));
873    assert!(!is_relative_specifier("test"));
874  }
875}