summaryrefslogtreecommitdiff
path: root/cli/file_fetcher.rs
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2021-01-06 13:22:38 +1100
committerGitHub <noreply@github.com>2021-01-06 13:22:38 +1100
commit54240c22af6233d1d977d469868b0d9050cad6da (patch)
tree81af9892f8728852387fad9c2d243ab933de927f /cli/file_fetcher.rs
parent60c9c857584bf5180dd0f7b937683dd9691aef84 (diff)
feat(cli): support data urls (#8866)
Closes: #5059 Co-authored-by: Valentin Anger <syrupthinker@gryphno.de>
Diffstat (limited to 'cli/file_fetcher.rs')
-rw-r--r--cli/file_fetcher.rs182
1 files changed, 163 insertions, 19 deletions
diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs
index f10574c2d..5a31ee6cc 100644
--- a/cli/file_fetcher.rs
+++ b/cli/file_fetcher.rs
@@ -27,7 +27,7 @@ use std::pin::Pin;
use std::sync::Arc;
use std::sync::Mutex;
-pub const SUPPORTED_SCHEMES: [&str; 3] = ["http", "https", "file"];
+pub const SUPPORTED_SCHEMES: [&str; 4] = ["data", "file", "http", "https"];
/// A structure representing a source file.
#[derive(Debug, Clone, Eq, PartialEq)]
@@ -145,6 +145,41 @@ pub fn get_source_from_bytes(
Ok(source)
}
+fn get_source_from_data_url(
+ specifier: &ModuleSpecifier,
+) -> Result<(String, MediaType, String), AnyError> {
+ let url = specifier.as_url();
+ if url.scheme() != "data" {
+ return Err(custom_error(
+ "BadScheme",
+ format!("Unexpected scheme of \"{}\"", url.scheme()),
+ ));
+ }
+ let path = url.path();
+ let mut parts = path.splitn(2, ',');
+ let media_type_part =
+ percent_encoding::percent_decode_str(parts.next().unwrap())
+ .decode_utf8()?;
+ let data_part = if let Some(data) = parts.next() {
+ data
+ } else {
+ return Err(custom_error(
+ "BadUrl",
+ "The data URL is badly formed, missing a comma.",
+ ));
+ };
+ let (media_type, maybe_charset) =
+ map_content_type(specifier, Some(media_type_part.to_string()));
+ let is_base64 = media_type_part.rsplit(';').any(|p| p == "base64");
+ let bytes = if is_base64 {
+ base64::decode(data_part)?
+ } else {
+ percent_encoding::percent_decode_str(data_part).collect()
+ };
+ let source = strip_shebang(get_source_from_bytes(bytes, maybe_charset)?);
+ Ok((source, media_type, media_type_part.to_string()))
+}
+
/// Return a validated scheme for a given module specifier.
fn get_validated_scheme(
specifier: &ModuleSpecifier,
@@ -185,6 +220,8 @@ pub fn map_content_type(
| "application/node" => {
map_js_like_extension(specifier, MediaType::JavaScript)
}
+ "text/jsx" => MediaType::JSX,
+ "text/tsx" => MediaType::TSX,
"application/json" | "text/json" => MediaType::Json,
"application/wasm" => MediaType::Wasm,
// Handle plain and possibly webassembly
@@ -354,6 +391,47 @@ impl FileFetcher {
Ok(Some(file))
}
+ /// Convert a data URL into a file, resulting in an error if the URL is
+ /// invalid.
+ fn fetch_data_url(
+ &self,
+ specifier: &ModuleSpecifier,
+ ) -> Result<File, AnyError> {
+ debug!("FileFetcher::fetch_data_url() - specifier: {}", specifier);
+ match self.fetch_cached(specifier, 0) {
+ Ok(Some(file)) => return Ok(file),
+ Ok(None) => {}
+ Err(err) => return Err(err),
+ }
+
+ if self.cache_setting == CacheSetting::Only {
+ return Err(custom_error(
+ "NotFound",
+ format!(
+ "Specifier not found in cache: \"{}\", --cached-only is specified.",
+ specifier
+ ),
+ ));
+ }
+
+ let (source, media_type, content_type) =
+ get_source_from_data_url(specifier)?;
+ let local = self.http_cache.get_cache_filename(specifier.as_url());
+ let mut headers = HashMap::new();
+ headers.insert("content-type".to_string(), content_type);
+ self
+ .http_cache
+ .set(specifier.as_url(), headers, source.as_bytes())?;
+
+ Ok(File {
+ local,
+ maybe_types: None,
+ media_type,
+ source,
+ specifier: specifier.clone(),
+ })
+ }
+
/// Asynchronously fetch remote source file specified by the URL following
/// redirects.
///
@@ -450,26 +528,27 @@ impl FileFetcher {
permissions.check_specifier(specifier)?;
if let Some(file) = self.cache.get(specifier) {
Ok(file)
+ } else if scheme == "file" {
+ // we do not in memory cache files, as this would prevent files on the
+ // disk changing effecting things like workers and dynamic imports.
+ fetch_local(specifier)
+ } else if scheme == "data" {
+ let result = self.fetch_data_url(specifier);
+ if let Ok(file) = &result {
+ self.cache.insert(specifier.clone(), file.clone());
+ }
+ result
+ } else if !self.allow_remote {
+ Err(custom_error(
+ "NoRemote",
+ format!("A remote specifier was requested: \"{}\", but --no-remote is specified.", specifier),
+ ))
} else {
- let is_local = scheme == "file";
- if is_local {
- fetch_local(specifier)
- } else if !self.allow_remote {
- Err(custom_error(
- "NoRemote",
- format!("A remote specifier was requested: \"{}\", but --no-remote is specified.", specifier),
- ))
- } else {
- let result = self.fetch_remote(specifier, permissions, 10).await;
- // only cache remote resources, as they are the only things that would
- // be "expensive" to fetch multiple times during an invocation, and it
- // also allows local file sources to be changed, enabling things like
- // dynamic import and workers to be updated while Deno is running.
- if let Ok(file) = &result {
- self.cache.insert(specifier.clone(), file.clone());
- }
- result
+ let result = self.fetch_remote(specifier, permissions, 10).await;
+ if let Ok(file) = &result {
+ self.cache.insert(specifier.clone(), file.clone());
}
+ result
}
}
@@ -582,12 +661,46 @@ mod tests {
}
#[test]
+ fn test_get_source_from_data_url() {
+ let fixtures = vec![
+ ("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=", true, MediaType::TypeScript, "application/typescript;base64", "export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n"),
+ ("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=?a=b&b=c", true, MediaType::TypeScript, "application/typescript;base64", "export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n"),
+ ("data:text/plain,Hello%2C%20Deno!", true, MediaType::Unknown, "text/plain", "Hello, Deno!"),
+ ("data:,Hello%2C%20Deno!", true, MediaType::Unknown, "", "Hello, Deno!"),
+ ("data:application/javascript,console.log(\"Hello, Deno!\");%0A", true, MediaType::JavaScript, "application/javascript", "console.log(\"Hello, Deno!\");\n"),
+ ("data:text/jsx;base64,ZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24oKSB7CiAgcmV0dXJuIDxkaXY+SGVsbG8gRGVubyE8L2Rpdj4KfQo=", true, MediaType::JSX, "text/jsx;base64", "export default function() {\n return <div>Hello Deno!</div>\n}\n"),
+ ("data:text/tsx;base64,ZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24oKSB7CiAgcmV0dXJuIDxkaXY+SGVsbG8gRGVubyE8L2Rpdj4KfQo=", true, MediaType::TSX, "text/tsx;base64", "export default function() {\n return <div>Hello Deno!</div>\n}\n"),
+ ];
+
+ for (
+ url_str,
+ expected_ok,
+ expected_media_type,
+ expected_media_type_str,
+ expected,
+ ) in fixtures
+ {
+ let specifier = ModuleSpecifier::resolve_url(url_str).unwrap();
+ let actual = get_source_from_data_url(&specifier);
+ assert_eq!(actual.is_ok(), expected_ok);
+ if expected_ok {
+ let (actual, actual_media_type, actual_media_type_str) =
+ actual.unwrap();
+ assert_eq!(actual, expected);
+ assert_eq!(actual_media_type, expected_media_type);
+ assert_eq!(actual_media_type_str, expected_media_type_str);
+ }
+ }
+ }
+
+ #[test]
fn test_get_validated_scheme() {
let fixtures = vec![
("https://deno.land/x/mod.ts", true, "https"),
("http://deno.land/x/mod.ts", true, "http"),
("file:///a/b/c.ts", true, "file"),
("file:///C:/a/b/c.ts", true, "file"),
+ ("data:,some%20text", true, "data"),
("ftp://a/b/c.ts", false, ""),
("mailto:dino@deno.land", false, ""),
];
@@ -692,6 +805,18 @@ mod tests {
),
(
"https://deno.land/x/mod",
+ Some("text/jsx".to_string()),
+ MediaType::JSX,
+ None,
+ ),
+ (
+ "https://deno.land/x/mod",
+ Some("text/tsx".to_string()),
+ MediaType::TSX,
+ None,
+ ),
+ (
+ "https://deno.land/x/mod",
Some("text/json".to_string()),
MediaType::Json,
None,
@@ -828,6 +953,25 @@ mod tests {
}
#[tokio::test]
+ async fn test_fetch_data_url() {
+ let (file_fetcher, _) = setup(CacheSetting::Use, None);
+ let specifier = ModuleSpecifier::resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap();
+
+ let result = file_fetcher
+ .fetch(&specifier, &Permissions::allow_all())
+ .await;
+ assert!(result.is_ok());
+ let file = result.unwrap();
+ assert_eq!(
+ file.source,
+ "export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n"
+ );
+ assert_eq!(file.media_type, MediaType::TypeScript);
+ assert_eq!(file.maybe_types, None);
+ assert_eq!(file.specifier, specifier);
+ }
+
+ #[tokio::test]
async fn test_fetch_complex() {
let _http_server_guard = test_util::http_server();
let (file_fetcher, temp_dir) = setup(CacheSetting::Use, None);