Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add resolve_url_or_path #7

Merged
merged 2 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}
Loading