deno/cli/lsp/urls.rs

224 lines
5.9 KiB
Rust

// Copyright 2018-2025 the Deno authors. MIT license.
use std::borrow::Cow;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use deno_config::UrlToFilePathError;
use deno_core::error::AnyError;
use deno_core::url::Url;
use deno_path_util::url_to_file_path;
use lsp_types::Uri;
use super::logging::lsp_warn;
/// Matches the `encodeURIComponent()` encoding from JavaScript, which matches
/// the component percent encoding set.
///
/// See: <https://url.spec.whatwg.org/#component-percent-encode-set>
pub const COMPONENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'`')
.add(b'{')
.add(b'}')
.add(b'/')
.add(b':')
.add(b';')
.add(b'=')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'|')
.add(b'$')
.add(b'%')
.add(b'&')
.add(b'+')
.add(b',');
/// Characters that are left unencoded in a `Url` path but will be encoded in a
/// VSCode URI.
const URL_TO_URI_PATH: &percent_encoding::AsciiSet =
&percent_encoding::CONTROLS
.add(b' ')
.add(b'!')
.add(b'$')
.add(b'&')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b'+')
.add(b',')
.add(b':')
.add(b';')
.add(b'=')
.add(b'@')
.add(b'[')
.add(b']')
.add(b'^')
.add(b'|');
/// Characters that may be left unencoded in a `Url` query but not valid in a
/// `Uri` query.
const URL_TO_URI_QUERY: &percent_encoding::AsciiSet =
&URL_TO_URI_PATH.add(b'\\').add(b'`').add(b'{').add(b'}');
/// Characters that may be left unencoded in a `Url` fragment but not valid in
/// a `Uri` fragment.
const URL_TO_URI_FRAGMENT: &percent_encoding::AsciiSet =
&URL_TO_URI_PATH.add(b'#').add(b'\\').add(b'{').add(b'}');
pub fn uri_parse_unencoded(s: &str) -> Result<Uri, AnyError> {
url_to_uri(&Url::parse(s)?)
}
pub fn normalize_uri(uri: &Uri) -> Cow<'_, Uri> {
if !uri.scheme().is_some_and(|s| s.eq_lowercase("file")) {
return Cow::Borrowed(uri);
}
let url = normalize_url(Url::parse(uri.as_str()).unwrap());
let Ok(normalized_uri) = url_to_uri(&url) else {
return Cow::Borrowed(uri);
};
Cow::Owned(normalized_uri)
}
pub fn url_to_uri(url: &Url) -> Result<Uri, AnyError> {
let components = deno_core::url::quirks::internal_components(url);
let mut input = String::with_capacity(url.as_str().len());
input.push_str(&url.as_str()[..components.path_start as usize]);
let path = url.path();
let mut chars = path.chars();
let has_drive_letter = chars.next().is_some_and(|c| c == '/')
&& chars.next().is_some_and(|c| c.is_ascii_alphabetic())
&& chars.next().is_some_and(|c| c == ':')
&& chars.next().is_none_or(|c| c == '/');
if has_drive_letter {
let (dl_part, rest) = path.split_at(2);
input.push_str(&dl_part.to_ascii_lowercase());
input.push_str(
&percent_encoding::utf8_percent_encode(rest, URL_TO_URI_PATH).to_string(),
);
} else {
input.push_str(
&percent_encoding::utf8_percent_encode(path, URL_TO_URI_PATH).to_string(),
);
}
if let Some(query) = url.query() {
input.push('?');
input.push_str(
&percent_encoding::utf8_percent_encode(query, URL_TO_URI_QUERY)
.to_string(),
);
}
if let Some(fragment) = url.fragment() {
input.push('#');
input.push_str(
&percent_encoding::utf8_percent_encode(fragment, URL_TO_URI_FRAGMENT)
.to_string(),
);
}
Ok(Uri::from_str(&input).inspect_err(|err| {
lsp_warn!("Could not convert URL \"{url}\" to URI: {err}")
})?)
}
pub fn uri_to_url(uri: &Uri) -> Url {
(|| {
let scheme = uri.scheme()?;
if !scheme.eq_lowercase("untitled")
&& !scheme.eq_lowercase("vscode-notebook-cell")
&& !scheme.eq_lowercase("deno-notebook-cell")
&& !scheme.eq_lowercase("vscode-userdata")
{
return None;
}
Url::parse(&format!(
"file:///{}",
&uri.as_str()[uri.path_bounds.0 as usize..].trim_start_matches('/'),
))
.ok()
.map(normalize_url)
})()
.unwrap_or_else(|| normalize_url(Url::parse(uri.as_str()).unwrap()))
}
pub fn uri_to_file_path(uri: &Uri) -> Result<PathBuf, UrlToFilePathError> {
url_to_file_path(&uri_to_url(uri))
}
pub fn uri_is_file_like(uri: &Uri) -> bool {
let Some(scheme) = uri.scheme() else {
return false;
};
scheme.eq_lowercase("file")
|| scheme.eq_lowercase("untitled")
|| scheme.eq_lowercase("vscode-notebook-cell")
|| scheme.eq_lowercase("deno-notebook-cell")
|| scheme.eq_lowercase("vscode-userdata")
}
fn normalize_url(url: Url) -> Url {
let Ok(path) = url_to_file_path(&url) else {
return url;
};
let normalized_path = normalize_path(&path);
let Ok(mut normalized_url) = Url::from_file_path(&normalized_path) else {
return url;
};
if let Some(query) = url.query() {
normalized_url.set_query(Some(query));
}
if let Some(fragment) = url.fragment() {
normalized_url.set_fragment(Some(fragment));
}
normalized_url
}
// TODO(nayeemrmn): Change the version of this in deno_path_util to force
// uppercase on drive letters. Then remove this.
fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
fn inner(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret =
if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
let s = c.as_os_str();
if s.len() == 2 {
PathBuf::from(s.to_ascii_uppercase())
} else {
PathBuf::from(s)
}
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
inner(path.as_ref())
}