1#![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 sys_traits::impls::is_windows;
15use thiserror::Error;
16use url::Url;
17
18pub mod fs;
19
20pub fn url_parent(url: &Url) -> Url {
22 let mut url = url.clone();
23 let mut segments = url.path().split('/').collect::<Vec<_>>();
25 if segments.iter().all(|s| s.is_empty()) {
26 return url;
27 }
28 if let Some(last) = segments.last() {
29 if last.is_empty() {
30 segments.pop();
31 }
32 segments.pop();
33 let new_path = format!("{}/", segments.join("/"));
34 url.set_path(&new_path);
35 }
36 url
37}
38
39#[derive(Debug, Error, deno_error::JsError)]
40#[class(uri)]
41#[error("Could not convert URL to file path.\n URL: {0}")]
42pub struct UrlToFilePathError(pub Url);
43
44pub fn url_to_file_path(url: &Url) -> Result<PathBuf, UrlToFilePathError> {
48 let result = if url.scheme() != "file" {
49 Err(())
50 } else {
51 url_to_file_path_inner(url)
52 };
53 match result {
54 Ok(path) => Ok(path),
55 Err(()) => Err(UrlToFilePathError(url.clone())),
56 }
57}
58
59fn url_to_file_path_inner(url: &Url) -> Result<PathBuf, ()> {
60 #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
61 return url_to_file_path_real(url);
62 #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))]
63 url_to_file_path_wasm(url)
64}
65
66#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
67fn url_to_file_path_real(url: &Url) -> Result<PathBuf, ()> {
68 if cfg!(windows) {
69 match url.to_file_path() {
70 Ok(path) => Ok(path),
71 Err(()) => {
72 if url.scheme() == "file"
76 && url.host().is_none()
77 && url.port().is_none()
78 && url.path_segments().is_some()
79 {
80 let path_str = url.path();
81 match String::from_utf8(
82 percent_encoding::percent_decode(path_str.as_bytes()).collect(),
83 ) {
84 Ok(path_str) => Ok(PathBuf::from(path_str)),
85 Err(_) => Err(()),
86 }
87 } else {
88 Err(())
89 }
90 }
91 }
92 } else {
93 url.to_file_path()
94 }
95}
96
97#[cfg(any(
98 test,
99 not(any(unix, windows, target_os = "redox", target_os = "wasi"))
100))]
101#[allow(clippy::unnecessary_wraps)]
102fn url_to_file_path_wasm(url: &Url) -> Result<PathBuf, ()> {
103 fn is_windows_path_segment(url: &str) -> bool {
104 let mut chars = url.chars();
105
106 let first_char = chars.next();
107 if first_char.is_none() || !first_char.unwrap().is_ascii_alphabetic() {
108 return false;
109 }
110
111 if chars.next() != Some(':') {
112 return false;
113 }
114
115 chars.next().is_none()
116 }
117
118 let path_segments = url.path_segments().unwrap().collect::<Vec<_>>();
119 let mut final_text = String::new();
120 let mut is_windows_share = false;
121 if let Some(host) = url.host_str() {
122 final_text.push_str("\\\\");
123 final_text.push_str(host);
124 is_windows_share = true;
125 }
126 for segment in path_segments.iter() {
127 if is_windows_share {
128 final_text.push('\\');
129 } else if !final_text.is_empty() {
130 final_text.push('/');
131 }
132 final_text.push_str(
133 &percent_encoding::percent_decode_str(segment).decode_utf8_lossy(),
134 );
135 }
136 if !is_windows_share && !is_windows_path_segment(path_segments[0]) {
137 final_text = format!("/{}", final_text);
138 }
139 Ok(PathBuf::from(final_text))
140}
141
142#[inline]
148pub fn normalize_path(path: Cow<Path>) -> Cow<Path> {
149 fn should_normalize(path: &Path) -> bool {
150 if path_has_trailing_separator(path) {
151 return true;
152 }
153
154 let mut last_part = None;
155 for component in path.components() {
156 match component {
157 Component::CurDir | Component::ParentDir => {
158 return true;
159 }
160 Component::Prefix(..) | Component::RootDir => {
161 }
163 Component::Normal(component) => {
164 last_part = Some(component);
165 }
166 }
167 }
168
169 if is_windows()
170 && let Some(last_part) = last_part
171 {
172 let bytes = last_part.as_encoded_bytes();
173 if bytes.ends_with(b".") || bytes.ends_with(b" ") {
174 return true;
175 }
176 }
177
178 path_has_cur_dir_separator(path)
179 }
180
181 fn path_has_trailing_separator(path: &Path) -> bool {
182 #[cfg(unix)]
183 let raw = std::os::unix::ffi::OsStrExt::as_bytes(path.as_os_str());
184 #[cfg(windows)]
185 let raw = path.as_os_str().as_encoded_bytes();
186 #[cfg(target_arch = "wasm32")]
187 let raw = path.to_string_lossy();
188 #[cfg(target_arch = "wasm32")]
189 let raw = raw.as_bytes();
190
191 if sys_traits::impls::is_windows() {
192 raw.contains(&b'/') || raw.ends_with(b"\\")
193 } else {
194 raw.ends_with(b"/")
195 }
196 }
197
198 fn path_has_cur_dir_separator(path: &Path) -> bool {
201 #[cfg(unix)]
202 let raw = std::os::unix::ffi::OsStrExt::as_bytes(path.as_os_str());
203 #[cfg(windows)]
204 let raw = path.as_os_str().as_encoded_bytes();
205 #[cfg(target_arch = "wasm32")]
206 let raw = path.to_string_lossy();
207 #[cfg(target_arch = "wasm32")]
208 let raw = raw.as_bytes();
209
210 if raw.ends_with(b"\\.") || raw.ends_with(b"/.") {
211 return true;
212 }
213
214 if sys_traits::impls::is_windows() {
215 for window in raw.windows(3) {
216 if matches!(window, [b'\\', b'.', b'\\']) {
217 return true;
218 }
219 }
220 } else {
221 for window in raw.windows(3) {
222 if matches!(window, [b'/', b'.', b'/']) {
223 return true;
224 }
225 }
226 }
227
228 false
229 }
230
231 fn inner(path: &Path) -> PathBuf {
232 let mut components = path.components().peekable();
233 let mut ret =
234 if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
235 components.next();
236 PathBuf::from(c.as_os_str())
237 } else {
238 PathBuf::new()
239 };
240
241 for component in components {
242 match component {
243 Component::Prefix(..) => unreachable!(),
244 Component::RootDir => {
245 ret.push(component.as_os_str());
246 }
247 Component::CurDir => {}
248 Component::ParentDir => {
249 ret.pop();
250 }
251 Component::Normal(c) => {
252 if is_windows() {
253 let bytes = c.as_encoded_bytes();
254 let mut end = bytes.len();
256 while end > 0 && (bytes[end - 1] == b'.' || bytes[end - 1] == b' ')
257 {
258 end -= 1;
259 }
260 if end == bytes.len() {
261 ret.push(c);
262 } else if end > 0 {
263 #[cfg(windows)]
264 {
265 use std::os::windows::ffi::{OsStrExt, OsStringExt};
266 let wide: Vec<u16> = c.encode_wide().collect();
267 let trimmed = std::ffi::OsString::from_wide(&wide[..end]);
268 ret.push(trimmed);
269 }
270 #[cfg(not(windows))]
272 unsafe {
273 let trimmed =
274 std::ffi::OsStr::from_encoded_bytes_unchecked(&bytes[..end]);
275 ret.push(trimmed);
276 }
277 }
278 } else {
279 ret.push(c);
280 }
281 }
282 }
283 }
284 ret
285 }
286
287 if should_normalize(&path) {
288 Cow::Owned(inner(&path))
289 } else {
290 path
291 }
292}
293
294#[derive(Debug, Clone, Error, deno_error::JsError, PartialEq, Eq)]
295#[class(uri)]
296#[error("Could not convert path to URL.\n Path: {0}")]
297pub struct PathToUrlError(pub PathBuf);
298
299#[allow(clippy::result_unit_err)]
300pub fn url_from_file_path(path: &Path) -> Result<Url, PathToUrlError> {
301 let path = normalize_path(Cow::Borrowed(path));
302 #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
303 return Url::from_file_path(&path)
304 .map_err(|()| PathToUrlError(path.to_path_buf()));
305 #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))]
306 url_from_file_path_wasm(&path)
307 .map_err(|()| PathToUrlError(path.to_path_buf()))
308}
309
310#[allow(clippy::result_unit_err)]
311pub fn url_from_directory_path(path: &Path) -> Result<Url, PathToUrlError> {
312 let path = normalize_path(Cow::Borrowed(path));
313 #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
314 return Url::from_directory_path(&path)
315 .map_err(|()| PathToUrlError(path.to_path_buf()));
316 #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))]
317 url_from_directory_path_wasm(&path)
318 .map_err(|()| PathToUrlError(path.to_path_buf()))
319}
320
321#[cfg(any(
322 test,
323 not(any(unix, windows, target_os = "redox", target_os = "wasi"))
324))]
325fn url_from_directory_path_wasm(path: &Path) -> Result<Url, ()> {
326 let mut url = url_from_file_path_wasm(path)?;
327 url.path_segments_mut().unwrap().push("");
328 Ok(url)
329}
330
331#[cfg(any(
332 test,
333 not(any(unix, windows, target_os = "redox", target_os = "wasi"))
334))]
335fn url_from_file_path_wasm(path: &Path) -> Result<Url, ()> {
336 use std::path::Component;
337
338 let original_path = path.to_string_lossy();
339 let mut path_str = original_path;
340 if path_str.contains('\\') {
342 let mut url = Url::parse("file://").unwrap();
343 if let Some(next) = path_str.strip_prefix(r#"\\?\UNC\"#) {
344 if let Some((host, rest)) = next.split_once('\\')
345 && url.set_host(Some(host)).is_ok()
346 {
347 path_str = rest.to_string().into();
348 }
349 } else if let Some(next) = path_str.strip_prefix(r#"\\?\"#) {
350 path_str = next.to_string().into();
351 } else if let Some(next) = path_str.strip_prefix(r#"\\"#)
352 && let Some((host, rest)) = next.split_once('\\')
353 && url.set_host(Some(host)).is_ok()
354 {
355 path_str = rest.to_string().into();
356 }
357
358 for component in path_str.split('\\') {
359 url.path_segments_mut().unwrap().push(component);
360 }
361
362 Ok(url)
363 } else {
364 let mut url = Url::parse("file://").unwrap();
365 for component in path.components() {
366 match component {
367 Component::RootDir => {
368 url.path_segments_mut().unwrap().push("");
369 }
370 Component::Normal(segment) => {
371 url
372 .path_segments_mut()
373 .unwrap()
374 .push(&segment.to_string_lossy());
375 }
376 Component::Prefix(_) | Component::CurDir | Component::ParentDir => {
377 return Err(());
378 }
379 }
380 }
381
382 Ok(url)
383 }
384}
385
386#[cfg(not(windows))]
387#[inline]
388pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
389 path
390}
391
392#[cfg(windows)]
394pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
395 use std::path::Component;
396 use std::path::Prefix;
397
398 let mut components = path.components();
399 match components.next() {
400 Some(Component::Prefix(prefix)) => {
401 match prefix.kind() {
402 Prefix::Verbatim(device) => {
404 let mut path = PathBuf::new();
405 path.push(format!(r"\\{}\", device.to_string_lossy()));
406 path.extend(components.filter(|c| !matches!(c, Component::RootDir)));
407 path
408 }
409 Prefix::VerbatimDisk(_) => {
411 let mut path = PathBuf::new();
412 path.push(prefix.as_os_str().to_string_lossy().replace(r"\\?\", ""));
413 path.extend(components);
414 path
415 }
416 Prefix::VerbatimUNC(hostname, share_name) => {
418 let mut path = PathBuf::new();
419 path.push(format!(
420 r"\\{}\{}\",
421 hostname.to_string_lossy(),
422 share_name.to_string_lossy()
423 ));
424 path.extend(components.filter(|c| !matches!(c, Component::RootDir)));
425 path
426 }
427 _ => path,
428 }
429 }
430 _ => path,
431 }
432}
433
434pub fn specifier_has_uri_scheme(specifier: &str) -> bool {
445 let mut chars = specifier.chars();
446 let mut len = 0usize;
447 match chars.next() {
449 Some(c) if c.is_ascii_alphabetic() => len += 1,
450 _ => return false,
451 }
452 loop {
455 match chars.next() {
456 Some(c) if c.is_ascii_alphanumeric() || "+-.".contains(c) => len += 1,
457 Some(':') if len >= 2 => return true,
458 _ => return false,
459 }
460 }
461}
462
463#[derive(Debug, Clone, Error, JsError, PartialEq, Eq)]
464pub enum ResolveUrlOrPathError {
465 #[error(transparent)]
466 #[class(inherit)]
467 UrlParse(url::ParseError),
468 #[error(transparent)]
469 #[class(inherit)]
470 PathToUrl(PathToUrlError),
471}
472
473pub fn resolve_url_or_path(
480 specifier: &str,
481 current_dir: &Path,
482) -> Result<Url, ResolveUrlOrPathError> {
483 if specifier_has_uri_scheme(specifier) {
484 Url::parse(specifier).map_err(ResolveUrlOrPathError::UrlParse)
485 } else {
486 resolve_path(specifier, current_dir)
487 .map_err(ResolveUrlOrPathError::PathToUrl)
488 }
489}
490
491pub fn resolve_path(
495 path_str: &str,
496 current_dir: &Path,
497) -> Result<Url, PathToUrlError> {
498 let path = current_dir.join(path_str);
499 url_from_file_path(&path)
500}
501
502#[derive(Debug, Error, Clone, PartialEq, Eq, deno_error::JsError)]
503pub enum SpecifierError {
504 #[class(inherit)]
507 #[error("invalid URL: {0}")]
508 InvalidUrl(url::ParseError),
509 #[class(type)]
510 #[error("Import \"{specifier}\" not a dependency")]
511 ImportPrefixMissing { specifier: String },
512}
513
514pub fn resolve_import(
520 specifier: &str,
521 referrer: &Url,
522) -> Result<Url, SpecifierError> {
523 match Url::parse(specifier) {
524 Ok(url) => Ok(url),
527
528 Err(url::ParseError::RelativeUrlWithoutBase)
533 if !(specifier.starts_with('/')
534 || specifier.starts_with("./")
535 || specifier.starts_with("../")) =>
536 {
537 Err(SpecifierError::ImportPrefixMissing {
538 specifier: specifier.to_string(),
539 })
540 }
541
542 Err(url::ParseError::RelativeUrlWithoutBase) => {
545 referrer.join(specifier).map_err(SpecifierError::InvalidUrl)
546 }
547
548 Err(err) => Err(SpecifierError::InvalidUrl(err)),
553 }
554}
555
556pub fn get_atomic_path(sys: &impl SystemRandom, path: &Path) -> PathBuf {
557 let rand = gen_rand_path_component(sys);
558 let extension = format!("{rand}.tmp");
559 path.with_extension(extension)
560}
561
562fn gen_rand_path_component(sys: &impl SystemRandom) -> String {
563 use std::fmt::Write;
564 (0..4).fold(String::with_capacity(8), |mut output, _| {
565 write!(&mut output, "{:02x}", sys.sys_random_u8().unwrap()).unwrap();
566 output
567 })
568}
569
570pub fn is_relative_specifier(specifier: &str) -> bool {
571 let mut specifier_chars = specifier.chars();
572 let Some(first_char) = specifier_chars.next() else {
573 return false;
574 };
575 if first_char != '.' {
576 return false;
577 }
578 let Some(second_char) = specifier_chars.next() else {
579 return true;
580 };
581 if second_char == '/' {
582 return true;
583 }
584 let Some(third_char) = specifier_chars.next() else {
585 return second_char == '.';
586 };
587 second_char == '.' && third_char == '/'
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593
594 #[test]
595 fn test_url_parent() {
596 run_test("file:///", "file:///");
597 run_test("file:///test", "file:///");
598 run_test("file:///test/", "file:///");
599 run_test("file:///test/other", "file:///test/");
600 run_test("file:///test/other.txt", "file:///test/");
601 run_test("file:///test/other/", "file:///test/");
602
603 fn run_test(url: &str, expected: &str) {
604 let result = url_parent(&Url::parse(url).unwrap());
605 assert_eq!(result.to_string(), expected);
606 }
607 }
608
609 #[test]
610 fn test_url_to_file_path() {
611 run_success_test("file:///", "/");
612 run_success_test("file:///test", "/test");
613 run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt");
614 #[cfg(not(debug_assertions))]
615 run_success_test(
616 "file:///dir/test%20test/test.txt",
617 "/dir/test test/test.txt",
618 );
619
620 assert_no_panic_url_to_file_path("file:/");
621 assert_no_panic_url_to_file_path("file://");
622 #[cfg(not(debug_assertions))]
623 assert_no_panic_url_to_file_path("file://asdf/");
624 assert_no_panic_url_to_file_path("file://asdf/66666/a.ts");
625
626 #[track_caller]
627 fn run_success_test(url: &str, expected_path: &str) {
628 let result = url_to_file_path(&Url::parse(url).unwrap()).unwrap();
629 assert_eq!(result, PathBuf::from(expected_path));
630 }
631
632 #[track_caller]
633 fn assert_no_panic_url_to_file_path(url: &str) {
634 let _result = url_to_file_path(&Url::parse(url).unwrap());
635 }
636 }
637
638 #[test]
639 fn test_url_to_file_path_wasm() {
640 #[track_caller]
641 fn convert(path: &str) -> String {
642 url_to_file_path_wasm(&Url::parse(path).unwrap())
643 .unwrap()
644 .to_string_lossy()
645 .into_owned()
646 }
647
648 assert_eq!(convert("file:///a/b/c.json"), "/a/b/c.json");
649 assert_eq!(convert("file:///D:/test/other.json"), "D:/test/other.json");
650 assert_eq!(
651 convert("file:///path%20with%20spaces/and%23special%25chars!.json"),
652 "/path with spaces/and#special%chars!.json",
653 );
654 assert_eq!(
655 convert("file:///C:/My%20Documents/file.txt"),
656 "C:/My Documents/file.txt"
657 );
658 assert_eq!(
659 convert("file:///a/b/%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80.txt"),
660 "/a/b/пример.txt"
661 );
662 assert_eq!(
663 convert("file://server/share/folder/file.txt"),
664 "\\\\server\\share\\folder\\file.txt"
665 );
666 }
667
668 #[test]
669 fn test_url_from_file_path_wasm() {
670 #[track_caller]
671 fn convert(path: &str) -> String {
672 url_from_file_path_wasm(Path::new(path))
673 .unwrap()
674 .to_string()
675 }
676
677 assert_eq!(convert("/a/b/c.json"), "file:///a/b/c.json");
678 assert_eq!(
679 convert("D:\\test\\other.json"),
680 "file:///D:/test/other.json"
681 );
682 assert_eq!(
683 convert("/path with spaces/and#special%chars!.json"),
684 "file:///path%20with%20spaces/and%23special%25chars!.json"
685 );
686 assert_eq!(
687 convert("C:\\My Documents\\file.txt"),
688 "file:///C:/My%20Documents/file.txt"
689 );
690 assert_eq!(
691 convert("/a/b/пример.txt"),
692 "file:///a/b/%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80.txt"
693 );
694 assert_eq!(
695 convert("\\\\server\\share\\folder\\file.txt"),
696 "file://server/share/folder/file.txt"
697 );
698 assert_eq!(convert(r#"\\?\UNC\server\share"#), "file://server/share");
699 assert_eq!(
700 convert(r"\\?\cat_pics\subfolder\file.jpg"),
701 "file:///cat_pics/subfolder/file.jpg"
702 );
703 assert_eq!(convert(r"\\?\cat_pics"), "file:///cat_pics");
704 }
705
706 #[test]
707 fn test_url_from_directory_path_wasm() {
708 #[track_caller]
709 fn convert(path: &str) -> String {
710 url_from_directory_path_wasm(Path::new(path))
711 .unwrap()
712 .to_string()
713 }
714
715 assert_eq!(convert("/a/b/c"), "file:///a/b/c/");
716 assert_eq!(convert("D:\\test\\other"), "file:///D:/test/other/");
717 }
718
719 #[cfg(windows)]
720 #[test]
721 fn test_strip_unc_prefix() {
722 use std::path::PathBuf;
723
724 run_test(r"C:\", r"C:\");
725 run_test(r"C:\test\file.txt", r"C:\test\file.txt");
726
727 run_test(r"\\?\C:\", r"C:\");
728 run_test(r"\\?\C:\test\file.txt", r"C:\test\file.txt");
729
730 run_test(r"\\.\C:\", r"\\.\C:\");
731 run_test(r"\\.\C:\Test\file.txt", r"\\.\C:\Test\file.txt");
732
733 run_test(r"\\?\UNC\localhost\", r"\\localhost");
734 run_test(r"\\?\UNC\localhost\c$\", r"\\localhost\c$");
735 run_test(
736 r"\\?\UNC\localhost\c$\Windows\file.txt",
737 r"\\localhost\c$\Windows\file.txt",
738 );
739 run_test(r"\\?\UNC\wsl$\deno.json", r"\\wsl$\deno.json");
740
741 run_test(r"\\?\server1", r"\\server1");
742 run_test(r"\\?\server1\e$\", r"\\server1\e$\");
743 run_test(
744 r"\\?\server1\e$\test\file.txt",
745 r"\\server1\e$\test\file.txt",
746 );
747
748 fn run_test(input: &str, expected: &str) {
749 assert_eq!(
750 super::strip_unc_prefix(PathBuf::from(input)),
751 PathBuf::from(expected)
752 );
753 }
754 }
755
756 #[test]
757 fn test_normalize_path_basic() {
758 let run_test = run_normalize_path_test;
759 run_test("a/../b", "b");
760 run_test("a/./b/", &PathBuf::from("a").join("b").to_string_lossy());
761 run_test(
762 "a/./b/../c",
763 &PathBuf::from("a").join("c").to_string_lossy(),
764 );
765 }
766
767 #[cfg(windows)]
768 #[test]
769 fn test_normalize_path_win() {
770 let run_test = run_normalize_path_test;
771
772 run_test("C:\\test\\file.txt", "C:\\test\\file.txt");
773 run_test("C:\\test\\./file.txt", "C:\\test\\file.txt");
774 run_test("C:\\test\\../other/file.txt", "C:\\other\\file.txt");
775 run_test("C:\\test\\../other\\file.txt", "C:\\other\\file.txt");
776 run_test(
777 "C:\\test\\removes_trailing_slash\\",
778 "C:\\test\\removes_trailing_slash",
779 );
780 run_test("C:\\a\\.\\b\\..\\c", "C:\\a\\c");
781 run_test("C:\\test\\.", "C:\\test");
782 run_test("C:\\test\\test...", "C:\\test\\test");
783 run_test("C:\\test\\test ", "C:\\test\\test");
784 }
785
786 #[track_caller]
787 fn run_normalize_path_test(input: &str, expected: &str) {
788 assert_eq!(
789 normalize_path(Cow::Owned(PathBuf::from(input))).to_string_lossy(),
790 expected
791 );
792 }
793
794 #[test]
795 fn test_atomic_path() {
796 let sys = sys_traits::impls::InMemorySys::default();
797 sys.set_seed(Some(10));
798 let path = Path::new("/a/b/c.txt");
799 let atomic_path = get_atomic_path(&sys, path);
800 assert_eq!(atomic_path.parent().unwrap(), path.parent().unwrap());
801 assert_eq!(atomic_path.file_name().unwrap(), "c.3d3d3d3d.tmp");
802 }
803
804 #[test]
805 fn test_specifier_has_uri_scheme() {
806 let tests = vec![
807 ("http://foo.bar/etc", true),
808 ("HTTP://foo.bar/etc", true),
809 ("http:ftp:", true),
810 ("http:", true),
811 ("hTtP:", true),
812 ("ftp:", true),
813 ("mailto:spam@please.me", true),
814 ("git+ssh://git@github.com/denoland/deno", true),
815 ("blob:https://whatwg.org/mumbojumbo", true),
816 ("abc.123+DEF-ghi:", true),
817 ("abc.123+def-ghi:@", true),
818 ("", false),
819 (":not", false),
820 ("http", false),
821 ("c:dir", false),
822 ("X:", false),
823 ("./http://not", false),
824 ("1abc://kinda/but/no", false),
825 ("schluẞ://no/more", false),
826 ];
827
828 for (specifier, expected) in tests {
829 let result = specifier_has_uri_scheme(specifier);
830 assert_eq!(result, expected);
831 }
832 }
833
834 #[test]
835 fn test_resolve_url_or_path() {
836 let mut tests: Vec<(&str, String)> = vec![
838 (
839 "http://deno.land/core/tests/006_url_imports.ts",
840 "http://deno.land/core/tests/006_url_imports.ts".to_string(),
841 ),
842 (
843 "https://deno.land/core/tests/006_url_imports.ts",
844 "https://deno.land/core/tests/006_url_imports.ts".to_string(),
845 ),
846 ];
847
848 let cwd = if cfg!(miri) {
851 PathBuf::from("/miri")
852 } else {
853 std::env::current_dir().unwrap()
854 };
855 let cwd_str = cwd.to_str().unwrap();
856
857 if cfg!(target_os = "windows") {
858 let expected_url = "file:///C:/deno/tests/006_url_imports.ts";
860 tests.extend(vec![
861 (
862 r"C:/deno/tests/006_url_imports.ts",
863 expected_url.to_string(),
864 ),
865 (
866 r"C:\deno\tests\006_url_imports.ts",
867 expected_url.to_string(),
868 ),
869 (
870 r"\\?\C:\deno\tests\006_url_imports.ts",
871 expected_url.to_string(),
872 ),
873 ]);
878
879 let expected_url = format!(
881 "file:///{}:/deno/tests/006_url_imports.ts",
882 cwd_str.get(..1).unwrap(),
883 );
884 tests.extend(vec![
885 (r"/deno/tests/006_url_imports.ts", expected_url.to_string()),
886 (r"\deno\tests\006_url_imports.ts", expected_url.to_string()),
887 (
888 r"\deno\..\deno\tests\006_url_imports.ts",
889 expected_url.to_string(),
890 ),
891 (r"\deno\.\tests\006_url_imports.ts", expected_url),
892 ]);
893
894 let expected_url = format!(
896 "file:///{}/tests/006_url_imports.ts",
897 cwd_str.replace('\\', "/")
898 );
899 tests.extend(vec![
900 (r"tests/006_url_imports.ts", expected_url.to_string()),
901 (r"tests\006_url_imports.ts", expected_url.to_string()),
902 (r"./tests/006_url_imports.ts", (*expected_url).to_string()),
903 (r".\tests\006_url_imports.ts", (*expected_url).to_string()),
904 ]);
905
906 let expected_url = "file://server/share/deno/cool";
908 tests.extend(vec![
909 (r"\\server\share\deno\cool", expected_url.to_string()),
910 (r"\\server/share/deno/cool", expected_url.to_string()),
911 ]);
914 } else {
915 let expected_url = "file:///deno/tests/006_url_imports.ts";
917 tests.extend(vec![
918 ("/deno/tests/006_url_imports.ts", expected_url.to_string()),
919 ("//deno/tests/006_url_imports.ts", expected_url.to_string()),
920 ]);
921
922 let expected_url = format!("file://{cwd_str}/tests/006_url_imports.ts");
924 tests.extend(vec![
925 ("tests/006_url_imports.ts", expected_url.to_string()),
926 ("./tests/006_url_imports.ts", expected_url.to_string()),
927 (
928 "tests/../tests/006_url_imports.ts",
929 expected_url.to_string(),
930 ),
931 ("tests/./006_url_imports.ts", expected_url),
932 ]);
933 }
934
935 for (specifier, expected_url) in tests {
936 let url = resolve_url_or_path(specifier, &cwd).unwrap().to_string();
937 assert_eq!(url, expected_url);
938 }
939 }
940
941 #[test]
942 fn test_resolve_url_or_path_deprecated_error() {
943 use ResolveUrlOrPathError::*;
944 use url::ParseError::*;
945
946 let mut tests = vec![
947 ("https://eggplant:b/c", UrlParse(InvalidPort)),
948 ("https://:8080/a/b/c", UrlParse(EmptyHost)),
949 ];
950 if cfg!(target_os = "windows") {
951 let p = r"\\.\c:/stuff/deno/script.ts";
952 tests.push((p, PathToUrl(PathToUrlError(PathBuf::from(p)))));
953 }
954
955 for (specifier, expected_err) in tests {
956 let err =
957 resolve_url_or_path(specifier, &PathBuf::from("/")).unwrap_err();
958 assert_eq!(err, expected_err);
959 }
960 }
961
962 #[test]
963 fn test_is_relative_specifier() {
964 assert!(is_relative_specifier("."));
965 assert!(is_relative_specifier(".."));
966 assert!(is_relative_specifier("../"));
967 assert!(is_relative_specifier("../test"));
968 assert!(is_relative_specifier("./"));
969 assert!(is_relative_specifier("./test"));
970 assert!(!is_relative_specifier(""));
971 assert!(!is_relative_specifier("a"));
972 assert!(!is_relative_specifier(".test"));
973 assert!(!is_relative_specifier("..test"));
974 assert!(!is_relative_specifier("/test"));
975 assert!(!is_relative_specifier("test"));
976 }
977
978 #[test]
979 fn test_resolve_import() {
980 fn run_test(specifier: &str, base: &str, expected: &str) {
981 let actual =
982 resolve_import(specifier, &Url::parse(base).unwrap()).unwrap();
983 assert_eq!(actual.as_str(), expected);
984 }
985
986 run_test(
987 "./test.js",
988 "https://example.com",
989 "https://example.com/test.js",
990 );
991 run_test(
992 "https://deno.land/x/mod.ts",
993 "https://example.com",
994 "https://deno.land/x/mod.ts",
995 );
996 run_test(
997 "../test.js",
998 "https://example.com/sub",
999 "https://example.com/test.js",
1000 );
1001 run_test(
1002 "/test.js",
1003 "https://example.com/sub/dir/test",
1004 "https://example.com/test.js",
1005 );
1006
1007 match resolve_import("test.js", &Url::parse("https://example.com").unwrap())
1008 {
1009 Err(SpecifierError::ImportPrefixMissing { specifier }) => {
1010 assert_eq!(specifier, "test.js");
1011 }
1012 _ => unreachable!(),
1013 }
1014 }
1015}