diff options
author | Luca Casonato <lucacasonato@yahoo.com> | 2021-04-07 15:22:14 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-07 15:22:14 +0200 |
commit | 966ce7de8a23f63d0f30b1748fe69ccaf07519e0 (patch) | |
tree | 3275ca96a835fe91a62a73d5a4c83bf6ca917b66 | |
parent | 2865f39bec6da135a2d2d679a65e7ff139131bd7 (diff) |
feat: blob URL support (#10045)
This commit adds blob URL support. Blob URLs are stored in a process
global storage, that can be accessed from all workers, and the module
loader. Blob URLs can be created using `URL.createObjectURL` and revoked
using `URL.revokeObjectURL`.
This commit does not add support for `fetch`ing blob URLs. This will be
added in a follow up commit.
33 files changed, 488 insertions, 33 deletions
diff --git a/Cargo.lock b/Cargo.lock index f5af15f8c..75b328418 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -621,6 +621,7 @@ name = "deno_file" version = "0.1.0" dependencies = [ "deno_core", + "uuid", ] [[package]] diff --git a/cli/disk_cache.rs b/cli/disk_cache.rs index 2f2cc9e4f..0f5d16f98 100644 --- a/cli/disk_cache.rs +++ b/cli/disk_cache.rs @@ -67,7 +67,7 @@ impl DiskCache { out.push(path_seg); } } - "http" | "https" | "data" => out = url_to_filename(url)?, + "http" | "https" | "data" | "blob" => out = url_to_filename(url)?, "file" => { let path = match url.to_file_path() { Ok(path) => path, diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index 1c97d7018..0c1d9519d 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -18,9 +18,11 @@ use deno_core::futures; use deno_core::futures::future::FutureExt; use deno_core::ModuleSpecifier; use deno_runtime::deno_fetch::reqwest; +use deno_runtime::deno_file::BlobUrlStore; use deno_runtime::permissions::Permissions; use log::debug; use log::info; +use std::borrow::Borrow; use std::collections::HashMap; use std::env; use std::fs; @@ -32,7 +34,8 @@ use std::sync::Arc; use std::sync::Mutex; static DENO_AUTH_TOKENS: &str = "DENO_AUTH_TOKENS"; -pub const SUPPORTED_SCHEMES: [&str; 4] = ["data", "file", "http", "https"]; +pub const SUPPORTED_SCHEMES: [&str; 5] = + ["data", "blob", "file", "http", "https"]; /// A structure representing a source file. #[derive(Debug, Clone, Eq, PartialEq)] @@ -317,6 +320,7 @@ pub struct FileFetcher { cache_setting: CacheSetting, http_cache: HttpCache, http_client: reqwest::Client, + blob_url_store: BlobUrlStore, } impl FileFetcher { @@ -325,6 +329,7 @@ impl FileFetcher { cache_setting: CacheSetting, allow_remote: bool, ca_data: Option<Vec<u8>>, + blob_url_store: BlobUrlStore, ) -> Result<Self, AnyError> { Ok(Self { auth_tokens: AuthTokens::new(env::var(DENO_AUTH_TOKENS).ok()), @@ -333,6 +338,7 @@ impl FileFetcher { cache_setting, http_cache, http_client: create_http_client(get_user_agent(), ca_data)?, + blob_url_store, }) } @@ -446,6 +452,62 @@ impl FileFetcher { }) } + /// Get a blob URL. + fn fetch_blob_url( + &self, + specifier: &ModuleSpecifier, + ) -> Result<File, AnyError> { + debug!("FileFetcher::fetch_blob_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 blob_url_storage = self.blob_url_store.borrow(); + let blob = blob_url_storage.get(specifier)?.ok_or_else(|| { + custom_error( + "NotFound", + format!("Blob URL not found: \"{}\".", specifier), + ) + })?; + + let content_type = blob.media_type; + + let (media_type, maybe_charset) = + map_content_type(specifier, Some(content_type.clone())); + let source = + strip_shebang(get_source_from_bytes(blob.data, maybe_charset)?); + + let local = + self + .http_cache + .get_cache_filename(specifier) + .ok_or_else(|| { + generic_error("Cannot convert specifier to cached filename.") + })?; + let mut headers = HashMap::new(); + headers.insert("content-type".to_string(), content_type); + self.http_cache.set(specifier, 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. /// @@ -555,6 +617,12 @@ impl FileFetcher { self.cache.insert(specifier.clone(), file.clone()); } result + } else if scheme == "blob" { + let result = self.fetch_blob_url(specifier); + if let Ok(file) = &result { + self.cache.insert(specifier.clone(), file.clone()); + } + result } else if !self.allow_remote { Err(custom_error( "NoRemote", @@ -604,6 +672,7 @@ mod tests { use deno_core::error::get_custom_error_class; use deno_core::resolve_url; use deno_core::resolve_url_or_path; + use deno_runtime::deno_file::Blob; use std::rc::Rc; use tempfile::TempDir; @@ -611,14 +680,29 @@ mod tests { cache_setting: CacheSetting, maybe_temp_dir: Option<Rc<TempDir>>, ) -> (FileFetcher, Rc<TempDir>) { + let (file_fetcher, temp_dir, _) = + setup_with_blob_url_store(cache_setting, maybe_temp_dir); + (file_fetcher, temp_dir) + } + + fn setup_with_blob_url_store( + cache_setting: CacheSetting, + maybe_temp_dir: Option<Rc<TempDir>>, + ) -> (FileFetcher, Rc<TempDir>, BlobUrlStore) { let temp_dir = maybe_temp_dir.unwrap_or_else(|| { Rc::new(TempDir::new().expect("failed to create temp directory")) }); let location = temp_dir.path().join("deps"); - let file_fetcher = - FileFetcher::new(HttpCache::new(&location), cache_setting, true, None) - .expect("setup failed"); - (file_fetcher, temp_dir) + let blob_url_store = BlobUrlStore::default(); + let file_fetcher = FileFetcher::new( + HttpCache::new(&location), + cache_setting, + true, + None, + blob_url_store.clone(), + ) + .expect("setup failed"); + (file_fetcher, temp_dir, blob_url_store) } macro_rules! file_url { @@ -989,6 +1073,36 @@ mod tests { } #[tokio::test] + async fn test_fetch_blob_url() { + let (file_fetcher, _, blob_url_store) = + setup_with_blob_url_store(CacheSetting::Use, None); + + let specifier = blob_url_store.insert( + Blob { + data: + "export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n" + .as_bytes() + .to_vec(), + media_type: "application/typescript".to_string(), + }, + None, + ); + + 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); @@ -1061,6 +1175,7 @@ mod tests { CacheSetting::ReloadAll, true, None, + BlobUrlStore::default(), ) .expect("setup failed"); let result = file_fetcher @@ -1087,6 +1202,7 @@ mod tests { CacheSetting::Use, true, None, + BlobUrlStore::default(), ) .expect("could not create file fetcher"); let specifier = @@ -1114,6 +1230,7 @@ mod tests { CacheSetting::Use, true, None, + BlobUrlStore::default(), ) .expect("could not create file fetcher"); let result = file_fetcher_02 @@ -1274,6 +1391,7 @@ mod tests { CacheSetting::Use, true, None, + BlobUrlStore::default(), ) .expect("could not create file fetcher"); let specifier = @@ -1304,6 +1422,7 @@ mod tests { CacheSetting::Use, true, None, + BlobUrlStore::default(), ) .expect("could not create file fetcher"); let result = file_fetcher_02 @@ -1413,6 +1532,7 @@ mod tests { CacheSetting::Use, false, None, + BlobUrlStore::default(), ) .expect("could not create file fetcher"); let specifier = @@ -1439,6 +1559,7 @@ mod tests { CacheSetting::Only, true, None, + BlobUrlStore::default(), ) .expect("could not create file fetcher"); let file_fetcher_02 = FileFetcher::new( @@ -1446,6 +1567,7 @@ mod tests { CacheSetting::Use, true, None, + BlobUrlStore::default(), ) .expect("could not create file fetcher"); let specifier = diff --git a/cli/http_cache.rs b/cli/http_cache.rs index 7eff2b9be..37da00983 100644 --- a/cli/http_cache.rs +++ b/cli/http_cache.rs @@ -39,7 +39,7 @@ fn base_url_to_filename(url: &Url) -> Option<PathBuf> { }; out.push(host_port); } - "data" => (), + "data" | "blob" => (), scheme => { error!("Don't know how to create cache name for scheme: {}", scheme); return None; diff --git a/cli/main.rs b/cli/main.rs index e9e29c9db..7146d8548 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -122,6 +122,7 @@ fn create_web_worker_callback( ts_version: version::TYPESCRIPT.to_string(), no_color: !colors::use_color(), get_error_class_fn: Some(&crate::errors::get_error_class_name), + blob_url_store: program_state.blob_url_store.clone(), }; let mut worker = WebWorker::from_options( @@ -199,6 +200,7 @@ pub fn create_main_worker( no_color: !colors::use_color(), get_error_class_fn: Some(&crate::errors::get_error_class_name), location: program_state.flags.location.clone(), + blob_url_store: program_state.blob_url_store.clone(), }; let mut worker = MainWorker::from_options(main_module, permissions, &options); diff --git a/cli/program_state.rs b/cli/program_state.rs index f47dcf0e9..a41acdec3 100644 --- a/cli/program_state.rs +++ b/cli/program_state.rs @@ -14,6 +14,7 @@ use crate::module_graph::TypeLib; use crate::source_maps::SourceMapGetter; use crate::specifier_handler::FetchHandler; use crate::version; +use deno_runtime::deno_file::BlobUrlStore; use deno_runtime::inspector::InspectorServer; use deno_runtime::permissions::Permissions; @@ -56,6 +57,7 @@ pub struct ProgramState { pub maybe_import_map: Option<ImportMap>, pub maybe_inspector_server: Option<Arc<InspectorServer>>, pub ca_data: Option<Vec<u8>>, + pub blob_url_store: BlobUrlStore, } impl ProgramState { @@ -80,11 +82,14 @@ impl ProgramState { CacheSetting::Use }; + let blob_url_store = BlobUrlStore::default(); + let file_fetcher = FileFetcher::new( http_cache, cache_usage, !flags.no_remote, ca_data.clone(), + blob_url_store.clone(), )?; let lockfile = if let Some(filename) = &flags.lock { @@ -131,6 +136,7 @@ impl ProgramState { maybe_import_map, maybe_inspector_server, ca_data, + blob_url_store, }; Ok(Arc::new(program_state)) } @@ -257,7 +263,7 @@ impl ProgramState { match url.scheme() { // we should only be looking for emits for schemes that denote external // modules, which the disk_cache supports - "wasm" | "file" | "http" | "https" | "data" => (), + "wasm" | "file" | "http" | "https" | "data" | "blob" => (), _ => { return None; } diff --git a/cli/source_maps.rs b/cli/source_maps.rs index 8bc070418..d62c0f3e0 100644 --- a/cli/source_maps.rs +++ b/cli/source_maps.rs @@ -142,6 +142,7 @@ pub fn get_orig_position<G: SourceMapGetter>( // around it. Use the `file_name` we get from V8 if // `source_file_name` does not parse as a URL. let file_name = match deno_core::resolve_url(source_file_name) { + Ok(m) if m.scheme() == "blob" => file_name, Ok(m) => m.to_string(), Err(_) => file_name, }; diff --git a/cli/specifier_handler.rs b/cli/specifier_handler.rs index 9f329819e..58815e15c 100644 --- a/cli/specifier_handler.rs +++ b/cli/specifier_handler.rs @@ -306,7 +306,9 @@ impl SpecifierHandler for FetchHandler { } })?; let url = &source_file.specifier; - let is_remote = !(url.scheme() == "file" || url.scheme() == "data"); + let is_remote = !(url.scheme() == "file" + || url.scheme() == "data" + || url.scheme() == "blob"); let filename = disk_cache.get_cache_filename_with_extension(url, "meta"); let maybe_version = if let Some(filename) = filename { if let Ok(bytes) = disk_cache.get(&filename) { @@ -569,6 +571,7 @@ pub mod tests { use crate::file_fetcher::CacheSetting; use crate::http_cache::HttpCache; use deno_core::resolve_url_or_path; + use deno_runtime::deno_file::BlobUrlStore; use tempfile::TempDir; macro_rules! map ( @@ -593,6 +596,7 @@ pub mod tests { CacheSetting::Use, true, None, + BlobUrlStore::default(), ) .expect("could not setup"); let disk_cache = deno_dir.gen_cache; diff --git a/cli/standalone.rs b/cli/standalone.rs index e3fb6fe43..83879a163 100644 --- a/cli/standalone.rs +++ b/cli/standalone.rs @@ -15,6 +15,7 @@ use deno_core::v8_set_flags; use deno_core::ModuleLoader; use deno_core::ModuleSpecifier; use deno_core::OpState; +use deno_runtime::deno_file::BlobUrlStore; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsOptions; use deno_runtime::worker::MainWorker; @@ -158,6 +159,7 @@ pub async fn run( ) -> Result<(), AnyError> { let main_module = resolve_url(SPECIFIER)?; let permissions = Permissions::from_options(&metadata.permissions); + let blob_url_store = BlobUrlStore::default(); let module_loader = Rc::new(EmbeddedModuleLoader(source_code)); let create_web_worker_cb = Arc::new(|_| { todo!("Worker are currently not supported in standalone binaries"); @@ -189,6 +191,7 @@ pub async fn run( no_color: !colors::use_color(), get_error_class_fn: Some(&get_error_class_name), location: metadata.location, + blob_url_store, }; let mut worker = MainWorker::from_options(main_module.clone(), permissions, &options); diff --git a/cli/tests/import_blob_url.ts b/cli/tests/import_blob_url.ts new file mode 100644 index 000000000..86bb634e1 --- /dev/null +++ b/cli/tests/import_blob_url.ts @@ -0,0 +1,13 @@ +const blob = new Blob( + ['export const a = "a";\n\nexport enum A {\n A,\n B,\n C,\n}\n'], + { + type: "application/typescript", + }, +); +const url = URL.createObjectURL(blob); + +const a = await import(url); + +console.log(a.a); +console.log(a.A); +console.log(a.A.A); diff --git a/cli/tests/import_blob_url.ts.out b/cli/tests/import_blob_url.ts.out new file mode 100644 index 000000000..bfa0b9d94 --- /dev/null +++ b/cli/tests/import_blob_url.ts.out @@ -0,0 +1,3 @@ +a +{ "0": "A", "1": "B", "2": "C", A: 0, B: 1, C: 2 } +0 diff --git a/cli/tests/import_blob_url_error_stack.ts b/cli/tests/import_blob_url_error_stack.ts new file mode 100644 index 000000000..f9c4f2e9d --- /dev/null +++ b/cli/tests/import_blob_url_error_stack.ts @@ -0,0 +1,13 @@ +const blob = new Blob( + [ + "enum A {\n A,\n B,\n C,\n }\n \n export function a() {\n throw new Error(`Hello ${A.C}`);\n }\n ", + ], + { + type: "application/typescript", + }, +); +const url = URL.createObjectURL(blob); + +const { a } = await import(url); + +a(); diff --git a/cli/tests/import_blob_url_error_stack.ts.out b/cli/tests/import_blob_url_error_stack.ts.out new file mode 100644 index 000000000..18ca023e3 --- /dev/null +++ b/cli/tests/import_blob_url_error_stack.ts.out @@ -0,0 +1,6 @@ +[WILDCARD]error: Uncaught (in promise) Error: Hello 2 + throw new Error(`Hello ${A.C}`); + ^ + at a (blob:null/[WILDCARD]:8:10) + at file:///[WILDCARD]/cli/tests/import_blob_url_error_stack.ts:13:1 +[WILDCARD] diff --git a/cli/tests/import_blob_url_import_relative.ts b/cli/tests/import_blob_url_import_relative.ts new file mode 100644 index 000000000..ad130bdac --- /dev/null +++ b/cli/tests/import_blob_url_import_relative.ts @@ -0,0 +1,8 @@ +const blob = new Blob(['export { a } from "./a.ts";'], { + type: "application/javascript", +}); +const url = URL.createObjectURL(blob); + +const a = await import(url); + +console.log(a); diff --git a/cli/tests/import_blob_url_import_relative.ts.out b/cli/tests/import_blob_url_import_relative.ts.out new file mode 100644 index 000000000..9d47ab719 --- /dev/null +++ b/cli/tests/import_blob_url_import_relative.ts.out @@ -0,0 +1,4 @@ +error: Uncaught (in promise) TypeError: invalid URL: relative URL with a cannot-be-a-base base +const a = await import(url); + ^ + at async file://[WILDCARD]/cli/tests/import_blob_url_import_relative.ts:6:11 diff --git a/cli/tests/import_blob_url_imports.ts b/cli/tests/import_blob_url_imports.ts new file mode 100644 index 000000000..7c2b10865 --- /dev/null +++ b/cli/tests/import_blob_url_imports.ts @@ -0,0 +1,11 @@ +const blob = new Blob( + [ + 'export { printHello } from "http://localhost:4545/cli/tests/subdir/mod2.ts"', + ], + { type: "application/javascript" }, +); +const url = URL.createObjectURL(blob); + +const { printHello } = await import(url); + +printHello(); diff --git a/cli/tests/import_blob_url_imports.ts.out b/cli/tests/import_blob_url_imports.ts.out new file mode 100644 index 000000000..e965047ad --- /dev/null +++ b/cli/tests/import_blob_url_imports.ts.out @@ -0,0 +1 @@ +Hello diff --git a/cli/tests/import_blob_url_jsx.ts b/cli/tests/import_blob_url_jsx.ts new file mode 100644 index 000000000..8d645796a --- /dev/null +++ b/cli/tests/import_blob_url_jsx.ts @@ -0,0 +1,16 @@ +const blob = new Blob( + ["export default function() {\n return <div>Hello Deno!</div>\n}\n"], + { type: "text/jsx" }, +); +const url = URL.createObjectURL(blob); + +const { default: render } = await import(url); + +// deno-lint-ignore no-explicit-any +(globalThis as any).React = { + createElement(...args: unknown[]) { + console.log(...args); + }, +}; + +render(); diff --git a/cli/tests/import_blob_url_jsx.ts.out b/cli/tests/import_blob_url_jsx.ts.out new file mode 100644 index 000000000..c1c85f250 --- /dev/null +++ b/cli/tests/import_blob_url_jsx.ts.out @@ -0,0 +1 @@ +div null Hello Deno! diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 5f8d5d403..73fdcc994 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -3683,6 +3683,34 @@ console.log("finish"); output: "import_dynamic_data_url.ts.out", }); + itest!(import_blob_url_error_stack { + args: "run --quiet --reload import_blob_url_error_stack.ts", + output: "import_blob_url_error_stack.ts.out", + exit_code: 1, + }); + + itest!(import_blob_url_import_relative { + args: "run --quiet --reload import_blob_url_import_relative.ts", + output: "import_blob_url_import_relative.ts.out", + exit_code: 1, + }); + + itest!(import_blob_url_imports { + args: "run --quiet --reload import_blob_url_imports.ts", + output: "import_blob_url_imports.ts.out", + http_server: true, + }); + + itest!(import_blob_url_jsx { + args: "run --quiet --reload import_blob_url_jsx.ts", + output: "import_blob_url_jsx.ts.out", + }); + + itest!(import_blob_url { + args: "run --quiet --reload import_blob_url.ts", + output: "import_blob_url.ts.out", + }); + itest!(import_file_with_colon { args: "run --quiet --reload import_file_with_colon.ts", output: "import_file_with_colon.ts.out", diff --git a/cli/tests/unsupported_dynamic_import_scheme.out b/cli/tests/unsupported_dynamic_import_scheme.out index 9160f935f..c708fced4 100644 --- a/cli/tests/unsupported_dynamic_import_scheme.out +++ b/cli/tests/unsupported_dynamic_import_scheme.out @@ -1,5 +1,6 @@ error: Uncaught (in promise) TypeError: Unsupported scheme "xxx" for module "xxx:". Supported schemes: [ "data", + "blob", "file", "http", "https", diff --git a/cli/tsc.rs b/cli/tsc.rs index 7c54905c5..834b0cc58 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -110,6 +110,19 @@ fn hash_data_url( format!("data:///{}{}", hash, media_type.as_ts_extension()) } +fn hash_blob_url( + specifier: &ModuleSpecifier, + media_type: &MediaType, +) -> String { + assert_eq!( + specifier.scheme(), + "blob", + "Specifier must be a blob: specifier." + ); + let hash = crate::checksum::gen(&[specifier.path().as_bytes()]); + format!("blob:///{}{}", hash, media_type.as_ts_extension()) +} + /// tsc only supports `.ts`, `.tsx`, `.d.ts`, `.js`, or `.jsx` as root modules /// and so we have to detect the apparent media type based on extensions it /// supports. @@ -378,15 +391,19 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> { resolved_specifier ) }; - let resolved_specifier_str = if resolved_specifier.scheme() == "data" - { - let specifier_str = hash_data_url(&resolved_specifier, &media_type); - state - .data_url_map - .insert(specifier_str.clone(), resolved_specifier); - specifier_str - } else { - resolved_specifier.to_string() + let resolved_specifier_str = match resolved_specifier.scheme() { + "data" | "blob" => { + let specifier_str = if resolved_specifier.scheme() == "data" { + hash_data_url(&resolved_specifier, &media_type) + } else { + hash_blob_url(&resolved_specifier, &media_type) + }; + state + .data_url_map + .insert(specifier_str.clone(), resolved_specifier); + specifier_str + } + _ => resolved_specifier.to_string(), }; resolved.push(( resolved_specifier_str, @@ -439,12 +456,17 @@ pub fn exec(request: Request) -> Result<Response, AnyError> { let root_names: Vec<String> = request .root_names .iter() - .map(|(s, mt)| { - if s.scheme() == "data" { - let specifier_str = hash_data_url(s, mt); + .map(|(s, mt)| match s.scheme() { + "data" | "blob" => { + let specifier_str = if s.scheme() == "data" { + hash_data_url(&s, &mt) + } else { + hash_blob_url(&s, &mt) + }; data_url_map.insert(specifier_str.clone(), s.clone()); specifier_str - } else { + } + _ => { let ext_media_type = get_tsc_media_type(s); if mt != &ext_media_type { let new_specifier = format!("{}{}", s, mt.as_ts_extension()); diff --git a/op_crates/file/03_blob_url.js b/op_crates/file/03_blob_url.js new file mode 100644 index 000000000..29019cd84 --- /dev/null +++ b/op_crates/file/03_blob_url.js @@ -0,0 +1,63 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// @ts-check +/// <reference no-default-lib="true" /> +/// <reference path="../../core/lib.deno_core.d.ts" /> +/// <reference path="../webidl/internal.d.ts" /> +/// <reference path="../web/internal.d.ts" /> +/// <reference path="../web/lib.deno_web.d.ts" /> +/// <reference path="../url/internal.d.ts" /> +/// <reference path="../url/lib.deno_url.d.ts" /> +/// <reference path="./internal.d.ts" /> +/// <reference path="./lib.deno_file.d.ts" /> +/// <reference lib="esnext" /> +"use strict"; + +((window) => { + const core = Deno.core; + // const webidl = window.__bootstrap.webidl; + const { _byteSequence } = window.__bootstrap.file; + const { URL } = window.__bootstrap.url; + + /** + * @param {Blob} blob + * @returns {string} + */ + function createObjectURL(blob) { + // const prefix = "Failed to execute 'createObjectURL' on 'URL'"; + // webidl.requiredArguments(arguments.length, 1, { prefix }); + // blob = webidl.converters["Blob"](blob, { + // context: "Argument 1", + // prefix, + // }); + + const url = core.jsonOpSync( + "op_file_create_object_url", + blob.type, + blob[_byteSequence], + ); + + return url; + } + + /** + * @param {string} url + * @returns {void} + */ + function revokeObjectURL(url) { + // const prefix = "Failed to execute 'revokeObjectURL' on 'URL'"; + // webidl.requiredArguments(arguments.length, 1, { prefix }); + // url = webidl.converters["DOMString"](url, { + // context: "Argument 1", + // prefix, + // }); + + core.jsonOpSync( + "op_file_revoke_object_url", + url, + ); + } + + URL.createObjectURL = createObjectURL; + URL.revokeObjectURL = revokeObjectURL; +})(globalThis); diff --git a/op_crates/file/Cargo.toml b/op_crates/file/Cargo.toml index 54476c783..9897e8cf4 100644 --- a/op_crates/file/Cargo.toml +++ b/op_crates/file/Cargo.toml @@ -15,3 +15,4 @@ path = "lib.rs" [dependencies] deno_core = { version = "0.83.0", path = "../../core" } +uuid = { version = "0.8.2", features = ["v4"] } diff --git a/op_crates/file/lib.rs b/op_crates/file/lib.rs index c7e690433..4cfe4eed4 100644 --- a/op_crates/file/lib.rs +++ b/op_crates/file/lib.rs @@ -1,7 +1,85 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +use deno_core::error::null_opbuf; +use deno_core::error::AnyError; +use deno_core::url::Url; use deno_core::JsRuntime; +use deno_core::ModuleSpecifier; +use deno_core::ZeroCopyBuf; +use std::collections::HashMap; use std::path::PathBuf; +use std::sync::Arc; +use std::sync::Mutex; +use uuid::Uuid; + +#[derive(Clone)] +pub struct Blob { + pub data: Vec<u8>, + pub media_type: String, +} + +pub struct Location(pub Url); + +#[derive(Default, Clone)] +pub struct BlobUrlStore(Arc<Mutex<HashMap<Url, Blob>>>); + +impl BlobUrlStore { + pub fn get(&self, url: &ModuleSpecifier) -> Result<Option<Blob>, AnyError> { + let blob_store = self.0.lock().unwrap(); + Ok(blob_store.get(url).cloned()) + } + + pub fn insert(&self, blob: Blob, maybe_location: Option<Url>) -> Url { + let origin = if let Some(location) = maybe_location { + location.origin().ascii_serialization() + } else { + "null".to_string() + }; + let id = Uuid::new_v4(); + let url = Url::parse(&format!("blob:{}/{}", origin, id)).unwrap(); + + let mut blob_store = self.0.lock().unwrap(); + blob_store.insert(url.clone(), blob); + + url + } + + pub fn remove(&self, url: &ModuleSpecifier) { + let mut blob_store = self.0.lock().unwrap(); + blob_store.remove(&url); + } +} + +pub fn op_file_create_object_url( + state: &mut deno_core::OpState, + media_type: String, + zero_copy: Option<ZeroCopyBuf>, +) -> Result<String, AnyError> { + let data = zero_copy.ok_or_else(null_opbuf)?; + let blob = Blob { + data: data.to_vec(), + media_type, + }; + + let maybe_location = state.try_borrow::<Location>(); + let blob_store = state.borrow::<BlobUrlStore>(); + + let url = + blob_store.insert(blob, maybe_location.map(|location| location.0.clone())); + + Ok(url.to_string()) +} + +pub fn op_file_revoke_object_url( + state: &mut deno_core::OpState, + url: String, + _zero_copy: Option<ZeroCopyBuf>, +) -> Result<(), AnyError> { + let url = Url::parse(&url)?; + let blob_store = state.borrow::<BlobUrlStore>(); + blob_store.remove(&url); + Ok(()) +} /// Load and execute the javascript code. pub fn init(isolate: &mut JsRuntime) { @@ -11,6 +89,10 @@ pub fn init(isolate: &mut JsRuntime) { "deno:op_crates/file/02_filereader.js", include_str!("02_filereader.js"), ), + ( + "deno:op_crates/file/03_blob_url.js", + include_str!("03_blob_url.js"), + ), ]; for (url, source_code) in files { isolate.execute(url, source_code).unwrap(); diff --git a/op_crates/url/00_url.js b/op_crates/url/00_url.js index 9dd2b7800..bf1ed6059 100644 --- a/op_crates/url/00_url.js +++ b/op_crates/url/00_url.js @@ -389,14 +389,6 @@ toJSON() { return this.href; } - - static createObjectURL() { - throw new Error("Not implemented"); - } - - static revokeObjectURL() { - throw new Error("Not implemented"); - } } window.__bootstrap.url = { diff --git a/op_crates/url/lib.deno_url.d.ts b/op_crates/url/lib.deno_url.d.ts index 2a27fe693..3f9745352 100644 --- a/op_crates/url/lib.deno_url.d.ts +++ b/op_crates/url/lib.deno_url.d.ts @@ -155,8 +155,8 @@ declare class URLSearchParams { /** The URL interface represents an object providing static methods used for creating object URLs. */ declare class URL { constructor(url: string, base?: string | URL); - createObjectURL(object: any): string; - revokeObjectURL(url: string): void; + static createObjectURL(blob: Blob): string; + static revokeObjectURL(url: string): void; hash: string; host: string; diff --git a/runtime/examples/hello_runtime.rs b/runtime/examples/hello_runtime.rs index e8bf9841e..9c3cbd67e 100644 --- a/runtime/examples/hello_runtime.rs +++ b/runtime/examples/hello_runtime.rs @@ -2,6 +2,7 @@ use deno_core::error::AnyError; use deno_core::FsModuleLoader; +use deno_runtime::deno_file::BlobUrlStore; use deno_runtime::permissions::Permissions; use deno_runtime::worker::MainWorker; use deno_runtime::worker::WorkerOptions; @@ -39,6 +40,7 @@ async fn main() -> Result<(), AnyError> { no_color: false, get_error_class_fn: Some(&get_error_class_name), location: None, + blob_url_store: BlobUrlStore::default(), }; let js_path = diff --git a/runtime/ops/file.rs b/runtime/ops/file.rs new file mode 100644 index 000000000..e7f68b207 --- /dev/null +++ b/runtime/ops/file.rs @@ -0,0 +1,31 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +use deno_core::url::Url; +use deno_file::op_file_create_object_url; +use deno_file::op_file_revoke_object_url; +use deno_file::BlobUrlStore; +use deno_file::Location; + +pub fn init( + rt: &mut deno_core::JsRuntime, + blob_url_store: BlobUrlStore, + maybe_location: Option<Url>, +) { + { + let op_state = rt.op_state(); + let mut op_state = op_state.borrow_mut(); + op_state.put(blob_url_store); + if let Some(location) = maybe_location { + op_state.put(Location(location)); + } + } + super::reg_json_sync( + rt, + "op_file_create_object_url", + op_file_create_object_url, + ); + super::reg_json_sync( + rt, + "op_file_revoke_object_url", + op_file_revoke_object_url, + ); +} diff --git a/runtime/ops/mod.rs b/runtime/ops/mod.rs index 6574546b1..67eae27b2 100644 --- a/runtime/ops/mod.rs +++ b/runtime/ops/mod.rs @@ -2,6 +2,7 @@ pub mod crypto; pub mod fetch; +pub mod file; pub mod fs; pub mod fs_events; pub mod io; diff --git a/runtime/permissions.rs b/runtime/permissions.rs index 6e9000e16..45b7c0bf1 100644 --- a/runtime/permissions.rs +++ b/runtime/permissions.rs @@ -587,6 +587,7 @@ impl Permissions { ))), }, "data" => Ok(()), + "blob" => Ok(()), _ => self.net.check_url(specifier), } } diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index f5c81e6d4..c30b715e4 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -23,6 +23,7 @@ use deno_core::JsRuntime; use deno_core::ModuleLoader; use deno_core::ModuleSpecifier; use deno_core::RuntimeOptions; +use deno_file::BlobUrlStore; use log::debug; use std::env; use std::rc::Rc; @@ -155,6 +156,7 @@ pub struct WebWorkerOptions { /// Sets `Deno.noColor` in JS runtime. pub no_color: bool, pub get_error_class_fn: Option<GetErrorClassFn>, + pub blob_url_store: BlobUrlStore, } impl WebWorker { @@ -217,7 +219,7 @@ impl WebWorker { } ops::web_worker::init(js_runtime, sender.clone(), handle); - ops::runtime::init(js_runtime, main_module); + ops::runtime::init(js_runtime, main_module.clone()); ops::fetch::init( js_runtime, options.user_agent.clone(), @@ -232,6 +234,11 @@ impl WebWorker { ops::reg_json_sync(js_runtime, "op_close", deno_core::op_close); ops::reg_json_sync(js_runtime, "op_resources", deno_core::op_resources); ops::url::init(js_runtime); + ops::file::init( + js_runtime, + options.blob_url_store.clone(), + Some(main_module), + ); ops::io::init(js_runtime); ops::webgpu::init(js_runtime); ops::websocket::init( @@ -518,6 +525,7 @@ mod tests { ts_version: "x".to_string(), no_color: true, get_error_class_fn: None, + blob_url_store: BlobUrlStore::default(), }; let mut worker = WebWorker::from_options( diff --git a/runtime/worker.rs b/runtime/worker.rs index 00434b313..982198d65 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -21,6 +21,7 @@ use deno_core::ModuleId; use deno_core::ModuleLoader; use deno_core::ModuleSpecifier; use deno_core::RuntimeOptions; +use deno_file::BlobUrlStore; use log::debug; use std::env; use std::rc::Rc; @@ -66,6 +67,7 @@ pub struct WorkerOptions { pub no_color: bool, pub get_error_class_fn: Option<GetErrorClassFn>, pub location: Option<Url>, + pub blob_url_store: BlobUrlStore, } impl MainWorker { @@ -129,6 +131,11 @@ impl MainWorker { ops::reg_json_sync(js_runtime, "op_close", deno_core::op_close); ops::reg_json_sync(js_runtime, "op_resources", deno_core::op_resources); ops::url::init(js_runtime); + ops::file::init( + js_runtime, + options.blob_url_store.clone(), + options.location.clone(), + ); ops::fs_events::init(js_runtime); ops::fs::init(js_runtime); ops::io::init(js_runtime); @@ -298,6 +305,7 @@ mod tests { no_color: true, get_error_class_fn: None, location: None, + blob_url_store: BlobUrlStore::default(), }; MainWorker::from_options(main_module, permissions, &options) |