Skip to content

Commit

Permalink
feat: add resolve_url_or_path (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret authored Jan 21, 2025
1 parent 0ffdf43 commit 2f37b12
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 6 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ repository = "https://github.com/denoland/deno_path_util"
percent-encoding = "2.3.0"
thiserror = "2"
sys_traits.workspace = true
deno_error = "0.5.2"
deno_error = { version = "0.5.2", features = ["url"] }
url = "2.5.1"

[dev-dependencies]
Expand All @@ -25,4 +25,4 @@ tempfile = "3.4.0"
members = ["."]

[workspace.dependencies]
sys_traits = "0.1.0"
sys_traits = "0.1.7"
2 changes: 1 addition & 1 deletion src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ pub fn atomic_write_file<
data: &[u8],
mode: u32,
) -> std::io::Result<()> {
let mut options = OpenOptions::write();
let mut options = OpenOptions::new_write();
options.mode = Some(mode);
let mut file = sys.fs_open(temp_file_path, &options)?;
file.write_all(data)?;
Expand Down
209 changes: 208 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#![deny(clippy::unused_async)]
#![deny(clippy::unnecessary_wraps)]

use deno_error::JsError;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
Expand Down Expand Up @@ -174,7 +175,7 @@ pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
inner(path.as_ref())
}

#[derive(Debug, Error, deno_error::JsError)]
#[derive(Debug, Clone, Error, deno_error::JsError)]
#[class(uri)]
#[error("Could not convert path to URL.\n Path: {0}")]
pub struct PathToUrlError(pub PathBuf);
Expand Down Expand Up @@ -312,6 +313,75 @@ pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
}
}

/// Returns true if the input string starts with a sequence of characters
/// that could be a valid URI scheme, like 'https:', 'git+ssh:' or 'data:'.
///
/// According to RFC 3986 (https://tools.ietf.org/html/rfc3986#section-3.1),
/// a valid scheme has the following format:
/// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
///
/// We additionally require the scheme to be at least 2 characters long,
/// because otherwise a windows path like c:/foo would be treated as a URL,
/// while no schemes with a one-letter name actually exist.
pub fn specifier_has_uri_scheme(specifier: &str) -> bool {
let mut chars = specifier.chars();
let mut len = 0usize;
// The first character must be a letter.
match chars.next() {
Some(c) if c.is_ascii_alphabetic() => len += 1,
_ => return false,
}
// Second and following characters must be either a letter, number,
// plus sign, minus sign, or dot.
loop {
match chars.next() {
Some(c) if c.is_ascii_alphanumeric() || "+-.".contains(c) => len += 1,
Some(':') if len >= 2 => return true,
_ => return false,
}
}
}

#[derive(Debug, Clone, Error, JsError)]
pub enum ResolveUrlOrPathError {
#[error(transparent)]
#[class(inherit)]
UrlParse(url::ParseError),
#[error(transparent)]
#[class(inherit)]
PathToUrl(PathToUrlError),
}

/// Takes a string representing either an absolute URL or a file path,
/// as it may be passed to deno as a command line argument.
/// The string is interpreted as a URL if it starts with a valid URI scheme,
/// e.g. 'http:' or 'file:' or 'git+ssh:'. If not, it's interpreted as a
/// file path; if it is a relative path it's resolved relative to passed
/// `current_dir`.
pub fn resolve_url_or_path(
specifier: &str,
current_dir: &Path,
) -> Result<Url, ResolveUrlOrPathError> {
if specifier_has_uri_scheme(specifier) {
Url::parse(specifier).map_err(ResolveUrlOrPathError::UrlParse)
} else {
resolve_path(specifier, current_dir)
.map_err(ResolveUrlOrPathError::PathToUrl)
}
}

/// Converts a string representing a relative or absolute path into a
/// ModuleSpecifier. A relative path is considered relative to the passed
/// `current_dir`.
pub fn resolve_path(
path_str: &str,
current_dir: &Path,
) -> Result<Url, PathToUrlError> {
let path = current_dir.join(path_str);
let path = normalize_path(path);
url_from_file_path(&path)
}

pub fn get_atomic_path(sys: &impl SystemRandom, path: &Path) -> PathBuf {
let rand = gen_rand_path_component(sys);
let extension = format!("{rand}.tmp");
Expand Down Expand Up @@ -514,4 +584,141 @@ mod tests {
assert_eq!(atomic_path.parent().unwrap(), path.parent().unwrap());
assert_eq!(atomic_path.file_name().unwrap(), "c.3d3d3d3d.tmp");
}

#[test]
fn test_specifier_has_uri_scheme() {
let tests = vec![
("http://foo.bar/etc", true),
("HTTP://foo.bar/etc", true),
("http:ftp:", true),
("http:", true),
("hTtP:", true),
("ftp:", true),
("mailto:[email protected]", true),
("git+ssh://[email protected]/denoland/deno", true),
("blob:https://whatwg.org/mumbojumbo", true),
("abc.123+DEF-ghi:", true),
("abc.123+def-ghi:@", true),
("", false),
(":not", false),
("http", false),
("c:dir", false),
("X:", false),
("./http://not", false),
("1abc://kinda/but/no", false),
("schluẞ://no/more", false),
];

for (specifier, expected) in tests {
let result = specifier_has_uri_scheme(specifier);
assert_eq!(result, expected);
}
}

#[test]
fn test_resolve_url_or_path() {
// Absolute URL.
let mut tests: Vec<(&str, String)> = vec![
(
"http://deno.land/core/tests/006_url_imports.ts",
"http://deno.land/core/tests/006_url_imports.ts".to_string(),
),
(
"https://deno.land/core/tests/006_url_imports.ts",
"https://deno.land/core/tests/006_url_imports.ts".to_string(),
),
];

// The local path tests assume that the cwd is the deno repo root. Note
// that we can't use `cwd` in miri tests, so we just use `/miri` instead.
let cwd = if cfg!(miri) {
PathBuf::from("/miri")
} else {
std::env::current_dir().unwrap()
};
let cwd_str = cwd.to_str().unwrap();

if cfg!(target_os = "windows") {
// Absolute local path.
let expected_url = "file:///C:/deno/tests/006_url_imports.ts";
tests.extend(vec![
(
r"C:/deno/tests/006_url_imports.ts",
expected_url.to_string(),
),
(
r"C:\deno\tests\006_url_imports.ts",
expected_url.to_string(),
),
(
r"\\?\C:\deno\tests\006_url_imports.ts",
expected_url.to_string(),
),
// Not supported: `Url::from_file_path()` fails.
// (r"\\.\C:\deno\tests\006_url_imports.ts", expected_url.to_string()),
// Not supported: `Url::from_file_path()` performs the wrong conversion.
// (r"//./C:/deno/tests/006_url_imports.ts", expected_url.to_string()),
]);

// Rooted local path without drive letter.
let expected_url = format!(
"file:///{}:/deno/tests/006_url_imports.ts",
cwd_str.get(..1).unwrap(),
);
tests.extend(vec![
(r"/deno/tests/006_url_imports.ts", expected_url.to_string()),
(r"\deno\tests\006_url_imports.ts", expected_url.to_string()),
(
r"\deno\..\deno\tests\006_url_imports.ts",
expected_url.to_string(),
),
(r"\deno\.\tests\006_url_imports.ts", expected_url),
]);

// Relative local path.
let expected_url = format!(
"file:///{}/tests/006_url_imports.ts",
cwd_str.replace('\\', "/")
);
tests.extend(vec![
(r"tests/006_url_imports.ts", expected_url.to_string()),
(r"tests\006_url_imports.ts", expected_url.to_string()),
(r"./tests/006_url_imports.ts", (*expected_url).to_string()),
(r".\tests\006_url_imports.ts", (*expected_url).to_string()),
]);

// UNC network path.
let expected_url = "file://server/share/deno/cool";
tests.extend(vec![
(r"\\server\share\deno\cool", expected_url.to_string()),
(r"\\server/share/deno/cool", expected_url.to_string()),
// Not supported: `Url::from_file_path()` performs the wrong conversion.
// (r"//server/share/deno/cool", expected_url.to_string()),
]);
} else {
// Absolute local path.
let expected_url = "file:///deno/tests/006_url_imports.ts";
tests.extend(vec![
("/deno/tests/006_url_imports.ts", expected_url.to_string()),
("//deno/tests/006_url_imports.ts", expected_url.to_string()),
]);

// Relative local path.
let expected_url = format!("file://{cwd_str}/tests/006_url_imports.ts");
tests.extend(vec![
("tests/006_url_imports.ts", expected_url.to_string()),
("./tests/006_url_imports.ts", expected_url.to_string()),
(
"tests/../tests/006_url_imports.ts",
expected_url.to_string(),
),
("tests/./006_url_imports.ts", expected_url),
]);
}

for (specifier, expected_url) in tests {
let url = resolve_url_or_path(specifier, &cwd).unwrap().to_string();
assert_eq!(url, expected_url);
}
}
}

0 comments on commit 2f37b12

Please sign in to comment.