/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! The script module mod contains common traits and structs //! related to `type=module` for script thread or worker threads. use crate::document_loader::LoadType; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowBinding::WindowMethods; use crate::dom::bindings::conversions::jsstring_to_str; use crate::dom::bindings::error::report_pending_exception; use crate::dom::bindings::error::Error; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::settings_stack::AutoIncumbentScript; use crate::dom::bindings::str::DOMString; use crate::dom::bindings::trace::RootedTraceableBox; use crate::dom::document::Document; use crate::dom::dynamicmoduleowner::{DynamicModuleId, DynamicModuleOwner}; use crate::dom::element::Element; use crate::dom::globalscope::GlobalScope; use crate::dom::htmlscriptelement::{HTMLScriptElement, ScriptId}; use crate::dom::htmlscriptelement::{ScriptOrigin, ScriptType, SCRIPT_JS_MIMES}; use crate::dom::node::document_from_node; use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::promise::Promise; use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler}; use crate::dom::window::Window; use crate::dom::worker::TrustedWorkerAddress; use crate::network_listener::{self, NetworkListener}; use crate::network_listener::{PreInvoke, ResourceTimingListener}; use crate::realms::{enter_realm, AlreadyInRealm, InRealm}; use crate::script_runtime::JSContext as SafeJSContext; use crate::task::TaskBox; use crate::task_source::TaskSourceName; use encoding_rs::UTF_8; use hyper_serde::Serde; use indexmap::IndexSet; use ipc_channel::ipc; use ipc_channel::router::ROUTER; use js::jsapi::Handle as RawHandle; use js::jsapi::HandleObject; use js::jsapi::HandleValue as RawHandleValue; use js::jsapi::MutableHandleValue; use js::jsapi::Value; use js::jsapi::{CompileModule1, ExceptionStackBehavior, FinishDynamicModuleImport}; use js::jsapi::{GetModuleRequestSpecifier, GetRequestedModules, SetModuleMetadataHook}; use js::jsapi::{GetModuleResolveHook, JSRuntime, SetModuleResolveHook}; use js::jsapi::{Heap, JSContext, JS_ClearPendingException, SetModulePrivate}; use js::jsapi::{JSAutoRealm, JSObject, JSString}; use js::jsapi::{JS_DefineProperty4, JS_IsExceptionPending, JS_NewStringCopyN, JSPROP_ENUMERATE}; use js::jsapi::{ModuleErrorBehaviour, ModuleEvaluate, ModuleLink, ThrowOnModuleEvaluationFailure}; use js::jsapi::{SetModuleDynamicImportHook, SetScriptPrivateReferenceHooks}; use js::jsval::{JSVal, PrivateValue, UndefinedValue}; use js::rust::jsapi_wrapped::{GetArrayLength, JS_GetElement}; use js::rust::jsapi_wrapped::{GetRequestedModuleSpecifier, JS_GetPendingException}; use js::rust::transform_str_to_source_text; use js::rust::wrappers::JS_SetPendingException; use js::rust::CompileOptionsWrapper; use js::rust::{Handle, HandleValue, IntoHandle}; use mime::Mime; use net_traits::request::{CredentialsMode, Destination, ParserMetadata}; use net_traits::request::{Referrer, RequestBuilder, RequestMode}; use net_traits::IpcSend; use net_traits::{CoreResourceMsg, FetchChannels}; use net_traits::{FetchMetadata, Metadata, ReferrerPolicy}; use net_traits::{FetchResponseListener, NetworkError}; use net_traits::{ResourceFetchTiming, ResourceTimingType}; use servo_url::ServoUrl; use std::collections::{HashMap, HashSet}; use std::mem; use std::ptr; use std::rc::Rc; use std::str::FromStr; use std::sync::{Arc, Mutex}; use url::ParseError as UrlParseError; use uuid::Uuid; #[allow(unsafe_code)] unsafe fn gen_type_error(global: &GlobalScope, string: String) -> RethrowError { rooted!(in(*GlobalScope::get_cx()) let mut thrown = UndefinedValue()); Error::Type(string).to_jsval(*GlobalScope::get_cx(), &global, thrown.handle_mut()); return RethrowError(RootedTraceableBox::from_box(Heap::boxed(thrown.get()))); } #[derive(JSTraceable)] pub struct ModuleObject(Box>); impl ModuleObject { #[allow(unsafe_code)] pub fn handle(&self) -> HandleObject { unsafe { self.0.handle() } } } #[derive(JSTraceable)] pub struct RethrowError(RootedTraceableBox>); impl RethrowError { fn handle(&self) -> Handle { self.0.handle() } } impl Clone for RethrowError { fn clone(&self) -> Self { Self(RootedTraceableBox::from_box(Heap::boxed( self.0.get().clone(), ))) } } pub(crate) struct ModuleScript { base_url: ServoUrl, options: ScriptFetchOptions, owner: Option, } impl ModuleScript { pub fn new( base_url: ServoUrl, options: ScriptFetchOptions, owner: Option, ) -> Self { ModuleScript { base_url, options, owner, } } } /// Identity for a module which will be /// used to retrieve the module when we'd /// like to get it from module map. /// /// For example, we will save module parents with /// module identity so that we can get module tree /// from a descendant no matter the parent is an /// inline script or a external script #[derive(Clone, Debug, Eq, Hash, JSTraceable, PartialEq)] pub enum ModuleIdentity { ScriptId(ScriptId), ModuleUrl(ServoUrl), } impl ModuleIdentity { pub fn get_module_tree(&self, global: &GlobalScope) -> Rc { match self { ModuleIdentity::ModuleUrl(url) => { let module_map = global.get_module_map().borrow(); module_map.get(&url.clone()).unwrap().clone() }, ModuleIdentity::ScriptId(script_id) => { let inline_module_map = global.get_inline_module_map().borrow(); inline_module_map.get(&script_id).unwrap().clone() }, } } } #[derive(JSTraceable)] pub struct ModuleTree { url: ServoUrl, text: DomRefCell>, record: DomRefCell>, status: DomRefCell, // The spec maintains load order for descendants, so we use an indexset for descendants and // parents. This isn't actually necessary for parents however the IndexSet APIs don't // interop with HashSet, and IndexSet isn't very expensive // (https://github.com/bluss/indexmap/issues/110) // // By default all maps in web specs are ordered maps // (https://infra.spec.whatwg.org/#ordered-map), however we can usually get away with using // stdlib maps and sets because we rarely iterate over them. parent_identities: DomRefCell>, descendant_urls: DomRefCell>, // A set to memoize which descendants are under fetching incomplete_fetch_urls: DomRefCell>, visited_urls: DomRefCell>, rethrow_error: DomRefCell>, network_error: DomRefCell>, // A promise for owners to execute when the module tree // is finished promise: DomRefCell>>, external: bool, } impl ModuleTree { pub fn new(url: ServoUrl, external: bool, visited_urls: HashSet) -> Self { ModuleTree { url, text: DomRefCell::new(Rc::new(DOMString::new())), record: DomRefCell::new(None), status: DomRefCell::new(ModuleStatus::Initial), parent_identities: DomRefCell::new(IndexSet::new()), descendant_urls: DomRefCell::new(IndexSet::new()), incomplete_fetch_urls: DomRefCell::new(IndexSet::new()), visited_urls: DomRefCell::new(visited_urls), rethrow_error: DomRefCell::new(None), network_error: DomRefCell::new(None), promise: DomRefCell::new(None), external, } } pub fn get_status(&self) -> ModuleStatus { self.status.borrow().clone() } pub fn set_status(&self, status: ModuleStatus) { *self.status.borrow_mut() = status; } pub fn get_record(&self) -> &DomRefCell> { &self.record } pub fn set_record(&self, record: ModuleObject) { *self.record.borrow_mut() = Some(record); } pub fn get_rethrow_error(&self) -> &DomRefCell> { &self.rethrow_error } pub fn set_rethrow_error(&self, rethrow_error: RethrowError) { *self.rethrow_error.borrow_mut() = Some(rethrow_error); } pub fn get_network_error(&self) -> &DomRefCell> { &self.network_error } pub fn set_network_error(&self, network_error: NetworkError) { *self.network_error.borrow_mut() = Some(network_error); } pub fn get_text(&self) -> &DomRefCell> { &self.text } pub fn set_text(&self, module_text: Rc) { *self.text.borrow_mut() = module_text; } pub fn get_incomplete_fetch_urls(&self) -> &DomRefCell> { &self.incomplete_fetch_urls } pub fn get_descendant_urls(&self) -> &DomRefCell> { &self.descendant_urls } pub fn get_parent_urls(&self) -> IndexSet { let parent_identities = self.parent_identities.borrow(); parent_identities .iter() .filter_map(|parent_identity| match parent_identity { ModuleIdentity::ScriptId(_) => None, ModuleIdentity::ModuleUrl(url) => Some(url.clone()), }) .collect() } pub fn insert_parent_identity(&self, parent_identity: ModuleIdentity) { self.parent_identities.borrow_mut().insert(parent_identity); } pub fn insert_incomplete_fetch_url(&self, dependency: ServoUrl) { self.incomplete_fetch_urls.borrow_mut().insert(dependency); } pub fn remove_incomplete_fetch_url(&self, dependency: ServoUrl) { self.incomplete_fetch_urls.borrow_mut().remove(&dependency); } /// recursively checks if all of the transitive descendants are /// in the FetchingDescendants or later status fn recursive_check_descendants( module_tree: &ModuleTree, module_map: &HashMap>, discovered_urls: &mut HashSet, ) -> bool { discovered_urls.insert(module_tree.url.clone()); let descendant_urls = module_tree.descendant_urls.borrow(); for descendant_url in descendant_urls.iter() { match module_map.get(&descendant_url.clone()) { None => return false, Some(descendant_module) => { if discovered_urls.contains(&descendant_module.url) { continue; } let descendant_status = descendant_module.get_status(); if descendant_status < ModuleStatus::FetchingDescendants { return false; } let all_ready_descendants = ModuleTree::recursive_check_descendants( &descendant_module, module_map, discovered_urls, ); if !all_ready_descendants { return false; } }, } } return true; } fn has_all_ready_descendants(&self, global: &GlobalScope) -> bool { let module_map = global.get_module_map().borrow(); let mut discovered_urls = HashSet::new(); return ModuleTree::recursive_check_descendants(&self, &module_map, &mut discovered_urls); } // We just leverage the power of Promise to run the task for `finish` the owner. // Thus, we will always `resolve` it and no need to register a callback for `reject` fn append_handler( &self, owner: ModuleOwner, module_identity: ModuleIdentity, fetch_options: ScriptFetchOptions, ) { let this = owner.clone(); let identity = module_identity.clone(); let options = fetch_options.clone(); let handler = PromiseNativeHandler::new( &owner.global(), Some(ModuleHandler::new(Box::new( task!(fetched_resolve: move || { this.notify_owner_to_finish(identity, options); }), ))), None, ); let realm = enter_realm(&*owner.global()); let comp = InRealm::Entered(&realm); let _ais = AutoIncumbentScript::new(&*owner.global()); let mut promise = self.promise.borrow_mut(); match promise.as_ref() { Some(promise) => promise.append_native_handler(&handler, comp), None => { let new_promise = Promise::new_in_current_realm(comp); new_promise.append_native_handler(&handler, comp); *promise = Some(new_promise); }, } } fn append_dynamic_module_handler( &self, owner: ModuleOwner, module_identity: ModuleIdentity, dynamic_module: RootedTraceableBox, ) { let this = owner.clone(); let identity = module_identity.clone(); let module_id = owner.global().dynamic_module_list().push(dynamic_module); let handler = PromiseNativeHandler::new( &owner.global(), Some(ModuleHandler::new(Box::new( task!(fetched_resolve: move || { this.finish_dynamic_module(identity, module_id); }), ))), None, ); let realm = enter_realm(&*owner.global()); let comp = InRealm::Entered(&realm); let _ais = AutoIncumbentScript::new(&*owner.global()); let mut promise = self.promise.borrow_mut(); match promise.as_ref() { Some(promise) => promise.append_native_handler(&handler, comp), None => { let new_promise = Promise::new_in_current_realm(comp); new_promise.append_native_handler(&handler, comp); *promise = Some(new_promise); }, } } } #[derive(Clone, Copy, Debug, JSTraceable, PartialEq, PartialOrd)] pub enum ModuleStatus { Initial, Fetching, FetchingDescendants, Finished, } impl ModuleTree { #[allow(unsafe_code)] /// https://html.spec.whatwg.org/multipage/#creating-a-module-script /// Step 7-11. fn compile_module_script( &self, global: &GlobalScope, owner: ModuleOwner, module_script_text: Rc, url: ServoUrl, options: ScriptFetchOptions, ) -> Result { let cx = GlobalScope::get_cx(); let _ac = JSAutoRealm::new(*cx, *global.reflector().get_jsobject()); let compile_options = unsafe { CompileOptionsWrapper::new(*cx, url.as_str(), 1) }; unsafe { rooted!(in(*cx) let mut module_script = CompileModule1( *cx, compile_options.ptr, &mut transform_str_to_source_text(&module_script_text), )); if module_script.is_null() { warn!("fail to compile module script of {}", url); rooted!(in(*cx) let mut exception = UndefinedValue()); assert!(JS_GetPendingException(*cx, &mut exception.handle_mut())); JS_ClearPendingException(*cx); return Err(RethrowError(RootedTraceableBox::from_box(Heap::boxed( exception.get(), )))); } let module_script_data = Rc::new(ModuleScript::new(url.clone(), options, Some(owner))); SetModulePrivate( module_script.get(), &PrivateValue(Rc::into_raw(module_script_data) as *const _), ); debug!("module script of {} compile done", url); self.resolve_requested_module_specifiers( &global, module_script.handle().into_handle(), url.clone(), ) .map(|_| ModuleObject(Heap::boxed(*module_script))) } } #[allow(unsafe_code)] /// https://html.spec.whatwg.org/multipage/#fetch-the-descendants-of-and-link-a-module-script /// Step 5-2. pub fn instantiate_module_tree( &self, global: &GlobalScope, module_record: HandleObject, ) -> Result<(), RethrowError> { let cx = GlobalScope::get_cx(); let _ac = JSAutoRealm::new(*cx, *global.reflector().get_jsobject()); unsafe { if !ModuleLink(*cx, module_record) { warn!("fail to link & instantiate module"); rooted!(in(*cx) let mut exception = UndefinedValue()); assert!(JS_GetPendingException(*cx, &mut exception.handle_mut())); JS_ClearPendingException(*cx); Err(RethrowError(RootedTraceableBox::from_box(Heap::boxed( exception.get(), )))) } else { debug!("module instantiated successfully"); Ok(()) } } } #[allow(unsafe_code)] pub fn execute_module( &self, global: &GlobalScope, module_record: HandleObject, eval_result: MutableHandleValue, ) -> Result<(), RethrowError> { let cx = GlobalScope::get_cx(); let _ac = JSAutoRealm::new(*cx, *global.reflector().get_jsobject()); unsafe { let ok = ModuleEvaluate(*cx, module_record, eval_result); assert!(ok, "module evaluation failed"); rooted!(in(*cx) let mut evaluation_promise = ptr::null_mut::()); if eval_result.is_object() { evaluation_promise.set(eval_result.to_object()); } let throw_result = ThrowOnModuleEvaluationFailure( *cx, evaluation_promise.handle().into(), ModuleErrorBehaviour::ThrowModuleErrorsSync, ); if !throw_result { warn!("fail to evaluate module"); rooted!(in(*cx) let mut exception = UndefinedValue()); assert!(JS_GetPendingException(*cx, &mut exception.handle_mut())); JS_ClearPendingException(*cx); Err(RethrowError(RootedTraceableBox::from_box(Heap::boxed( exception.get(), )))) } else { debug!("module evaluated successfully"); Ok(()) } } } #[allow(unsafe_code)] pub fn report_error(&self, global: &GlobalScope) { let module_error = self.rethrow_error.borrow(); if let Some(exception) = &*module_error { unsafe { let ar = enter_realm(&*global); JS_SetPendingException( *GlobalScope::get_cx(), exception.handle(), ExceptionStackBehavior::Capture, ); report_pending_exception(*GlobalScope::get_cx(), true, InRealm::Entered(&ar)); } } } #[allow(unsafe_code)] fn resolve_requested_module_specifiers( &self, global: &GlobalScope, module_object: HandleObject, base_url: ServoUrl, ) -> Result, RethrowError> { let cx = GlobalScope::get_cx(); let _ac = JSAutoRealm::new(*cx, *global.reflector().get_jsobject()); let mut specifier_urls = IndexSet::new(); unsafe { rooted!(in(*cx) let requested_modules = GetRequestedModules(*cx, module_object)); let mut length = 0; if !GetArrayLength(*cx, requested_modules.handle(), &mut length) { let module_length_error = gen_type_error(&global, "Wrong length of requested modules".to_owned()); return Err(module_length_error); } for index in 0..length { rooted!(in(*cx) let mut element = UndefinedValue()); if !JS_GetElement( *cx, requested_modules.handle(), index, &mut element.handle_mut(), ) { let get_element_error = gen_type_error(&global, "Failed to get requested module".to_owned()); return Err(get_element_error); } rooted!(in(*cx) let specifier = GetRequestedModuleSpecifier( *cx, element.handle() )); let url = ModuleTree::resolve_module_specifier( *cx, &base_url, specifier.handle().into_handle(), ); if url.is_err() { let specifier_error = gen_type_error(&global, "Wrong module specifier".to_owned()); return Err(specifier_error); } specifier_urls.insert(url.unwrap()); } } Ok(specifier_urls) } /// The following module specifiers are allowed by the spec: /// - a valid absolute URL /// - a valid relative URL that starts with "/", "./" or "../" /// /// Bareword module specifiers are currently disallowed as these may be given /// special meanings in the future. /// https://html.spec.whatwg.org/multipage/#resolve-a-module-specifier #[allow(unsafe_code)] fn resolve_module_specifier( cx: *mut JSContext, url: &ServoUrl, specifier: RawHandle<*mut JSString>, ) -> Result { let specifier_str = unsafe { jsstring_to_str(cx, *specifier) }; // Step 1. if let Ok(specifier_url) = ServoUrl::parse(&specifier_str) { return Ok(specifier_url); } // Step 2. if !specifier_str.starts_with("/") && !specifier_str.starts_with("./") && !specifier_str.starts_with("../") { return Err(UrlParseError::InvalidDomainCharacter); } // Step 3. return ServoUrl::parse_with_base(Some(url), &specifier_str.clone()); } /// https://html.spec.whatwg.org/multipage/#finding-the-first-parse-error fn find_first_parse_error( &self, global: &GlobalScope, discovered_urls: &mut HashSet, ) -> (Option, Option) { // 3. discovered_urls.insert(self.url.clone()); // 4. let record = self.get_record().borrow(); if record.is_none() { return ( self.network_error.borrow().clone(), self.rethrow_error.borrow().clone(), ); } let module_map = global.get_module_map().borrow(); let mut parse_error: Option = None; // 5-6. let descendant_urls = self.descendant_urls.borrow(); for descendant_module in descendant_urls .iter() // 7. .filter_map(|url| module_map.get(&url.clone())) { // 8-2. if discovered_urls.contains(&descendant_module.url) { continue; } // 8-3. let (child_network_error, child_parse_error) = descendant_module.find_first_parse_error(&global, discovered_urls); // Due to network error's priority higher than parse error, // we will return directly when we meet a network error. if child_network_error.is_some() { return (child_network_error, None); } // 8-4. // // In case of having any network error in other descendants, // we will store the "first" parse error and keep running this // loop to ensure we don't have any network error. if child_parse_error.is_some() && parse_error.is_none() { parse_error = child_parse_error; } } // Step 9. return (None, parse_error); } #[allow(unsafe_code)] /// https://html.spec.whatwg.org/multipage/#fetch-the-descendants-of-a-module-script fn fetch_module_descendants( &self, owner: &ModuleOwner, destination: Destination, options: &ScriptFetchOptions, parent_identity: ModuleIdentity, ) { debug!("Start to load dependencies of {}", self.url.clone()); let global = owner.global(); self.set_status(ModuleStatus::FetchingDescendants); let specifier_urls = { let raw_record = self.record.borrow(); match raw_record.as_ref() { // Step 1. None => { self.set_status(ModuleStatus::Finished); debug!( "Module {} doesn't have module record but tried to load descendants.", self.url.clone() ); return; }, // Step 5. Some(raw_record) => self.resolve_requested_module_specifiers( &global, raw_record.handle(), self.url.clone(), ), } }; match specifier_urls { // Step 3. Ok(valid_specifier_urls) if valid_specifier_urls.len() == 0 => { debug!("Module {} doesn't have any dependencies.", self.url.clone()); self.advance_finished_and_link(&global); }, Ok(valid_specifier_urls) => { self.descendant_urls .borrow_mut() .extend(valid_specifier_urls.clone()); let mut urls = IndexSet::new(); let mut visited_urls = self.visited_urls.borrow_mut(); for parsed_url in valid_specifier_urls { // Step 5-3. if !visited_urls.contains(&parsed_url) { // Step 5-3-1. urls.insert(parsed_url.clone()); // Step 5-3-2. visited_urls.insert(parsed_url.clone()); self.insert_incomplete_fetch_url(parsed_url.clone()); } } // Step 3. if urls.len() == 0 { debug!( "After checking with visited urls, module {} doesn't have dependencies to load.", self.url.clone() ); self.advance_finished_and_link(&global); return; } // Step 8. for url in urls { // https://html.spec.whatwg.org/multipage/#internal-module-script-graph-fetching-procedure // Step 1. assert!(visited_urls.get(&url).is_some()); let options = options.descendant_fetch_options(); // Step 2. fetch_single_module_script( owner.clone(), url.clone(), visited_urls.clone(), destination.clone(), options, Some(parent_identity.clone()), false, None, ); } }, Err(error) => { self.set_rethrow_error(error); self.advance_finished_and_link(&global); }, } } /// https://html.spec.whatwg.org/multipage/#fetch-the-descendants-of-and-link-a-module-script /// step 4-7. fn advance_finished_and_link(&self, global: &GlobalScope) { { if !self.has_all_ready_descendants(&global) { return; } } self.set_status(ModuleStatus::Finished); debug!("Going to advance and finish for: {}", self.url.clone()); { // Notify parents of this module to finish // // Before notifying, if the parent module has already had zero incomplete // fetches, then it means we don't need to notify it. let parent_identities = self.parent_identities.borrow(); for parent_identity in parent_identities.iter() { let parent_tree = parent_identity.get_module_tree(&global); let incomplete_count_before_remove = { let incomplete_urls = parent_tree.get_incomplete_fetch_urls().borrow(); incomplete_urls.len() }; if incomplete_count_before_remove > 0 { parent_tree.remove_incomplete_fetch_url(self.url.clone()); parent_tree.advance_finished_and_link(&global); } } } let mut discovered_urls: HashSet = HashSet::new(); let (network_error, rethrow_error) = self.find_first_parse_error(&global, &mut discovered_urls); match (network_error, rethrow_error) { (Some(network_error), _) => { self.set_network_error(network_error); }, (None, None) => { let module_record = self.get_record().borrow(); if let Some(record) = &*module_record { let instantiated = self.instantiate_module_tree(&global, record.handle()); if let Err(exception) = instantiated { self.set_rethrow_error(exception); } } }, (None, Some(error)) => { self.set_rethrow_error(error); }, } let promise = self.promise.borrow(); if let Some(promise) = promise.as_ref() { promise.resolve_native(&()); } } } #[derive(JSTraceable, MallocSizeOf)] struct ModuleHandler { #[ignore_malloc_size_of = "Measuring trait objects is hard"] task: DomRefCell>>, } impl ModuleHandler { pub fn new(task: Box) -> Box { Box::new(Self { task: DomRefCell::new(Some(task)), }) } } impl Callback for ModuleHandler { fn callback(&self, _cx: SafeJSContext, _v: HandleValue, _realm: InRealm) { let task = self.task.borrow_mut().take().unwrap(); task.run_box(); } } /// The owner of the module /// It can be `worker` or `script` element #[derive(Clone)] pub(crate) enum ModuleOwner { #[allow(dead_code)] Worker(TrustedWorkerAddress), Window(Trusted), DynamicModule(Trusted), } impl ModuleOwner { pub fn global(&self) -> DomRoot { match &self { ModuleOwner::Worker(worker) => (*worker.root().clone()).global(), ModuleOwner::Window(script) => (*script.root()).global(), ModuleOwner::DynamicModule(dynamic_module) => (*dynamic_module.root()).global(), } } pub fn notify_owner_to_finish( &self, module_identity: ModuleIdentity, fetch_options: ScriptFetchOptions, ) { match &self { ModuleOwner::Worker(_) => unimplemented!(), ModuleOwner::DynamicModule(_) => unimplemented!(), ModuleOwner::Window(script) => { let global = self.global(); let document = document_from_node(&*script.root()); let load = { let module_tree = module_identity.get_module_tree(&global); let network_error = module_tree.get_network_error().borrow(); match network_error.as_ref() { Some(network_error) => Err(network_error.clone()), None => match module_identity { ModuleIdentity::ModuleUrl(script_src) => Ok(ScriptOrigin::external( Rc::clone(&module_tree.get_text().borrow()), script_src.clone(), fetch_options, ScriptType::Module, )), ModuleIdentity::ScriptId(_) => Ok(ScriptOrigin::internal( Rc::clone(&module_tree.get_text().borrow()), document.base_url().clone(), fetch_options, ScriptType::Module, )), }, } }; let asynch = script .root() .upcast::() .has_attribute(&local_name!("async")); if !asynch && (&*script.root()).get_parser_inserted() { document.deferred_script_loaded(&*script.root(), load); } else if !asynch && !(&*script.root()).get_non_blocking() { document.asap_in_order_script_loaded(&*script.root(), load); } else { document.asap_script_loaded(&*script.root(), load); }; }, } } #[allow(unsafe_code)] /// /// Step 6-9 pub fn finish_dynamic_module( &self, module_identity: ModuleIdentity, dynamic_module_id: DynamicModuleId, ) { let global = self.global(); let module = global.dynamic_module_list().remove(dynamic_module_id); let cx = GlobalScope::get_cx(); let module_tree = module_identity.get_module_tree(&global); // In the timing of executing this `finish_dynamic_module` function, // we've run `find_first_parse_error` which means we've had the highest // priority error in the tree. So, we can just get both `network_error` and // `rethrow_error` directly here. let network_error = module_tree.get_network_error().borrow().as_ref().cloned(); let existing_rethrow_error = module_tree.get_rethrow_error().borrow().as_ref().cloned(); rooted!(in(*cx) let mut rval = UndefinedValue()); if network_error.is_none() && existing_rethrow_error.is_none() { let record = module_tree .get_record() .borrow() .as_ref() .map(|record| record.handle()); if let Some(record) = record { let evaluated = module_tree .execute_module(&global, record, rval.handle_mut().into()) .err(); if let Some(exception) = evaluated.clone() { module_tree.set_rethrow_error(exception); } } } // Ensure any failures related to importing this dynamic module are immediately reported. match (network_error, existing_rethrow_error) { (Some(_), _) => unsafe { let err = gen_type_error(&global, "Dynamic import failed".to_owned()); JS_SetPendingException(*cx, err.handle(), ExceptionStackBehavior::Capture); }, (None, Some(rethrow_error)) => unsafe { JS_SetPendingException( *cx, rethrow_error.handle(), ExceptionStackBehavior::Capture, ); }, // do nothing if there's no errors (None, None) => {}, }; debug!("Finishing dynamic import for {:?}", module_identity); rooted!(in(*cx) let mut evaluation_promise = ptr::null_mut::()); if rval.is_object() { evaluation_promise.set(rval.to_object()); } unsafe { let ok = FinishDynamicModuleImport( *cx, evaluation_promise.handle().into(), module.referencing_private.handle(), module.specifier.handle(), module.promise.reflector().get_jsobject().into_handle(), ); if ok { assert!(!JS_IsExceptionPending(*cx)); } else { warn!("failed to finish dynamic module import"); } } return; } } /// The context required for asynchronously loading an external module script source. struct ModuleContext { /// The owner of the module that initiated the request. owner: ModuleOwner, /// The response body received to date. data: Vec, /// The response metadata received to date. metadata: Option, /// The initial URL requested. url: ServoUrl, /// Destination of current module context destination: Destination, /// Options for the current script fetch options: ScriptFetchOptions, /// Indicates whether the request failed, and why status: Result<(), NetworkError>, /// Timing object for this resource resource_timing: ResourceFetchTiming, } impl FetchResponseListener for ModuleContext { fn process_request_body(&mut self) {} // TODO(cybai): Perhaps add custom steps to perform fetch here? fn process_request_eof(&mut self) {} // TODO(cybai): Perhaps add custom steps to perform fetch here? fn process_response(&mut self, metadata: Result) { self.metadata = metadata.ok().map(|meta| match meta { FetchMetadata::Unfiltered(m) => m, FetchMetadata::Filtered { unsafe_, .. } => unsafe_, }); let status_code = self .metadata .as_ref() .and_then(|m| match m.status { Some((c, _)) => Some(c), _ => None, }) .unwrap_or(0); self.status = match status_code { 0 => Err(NetworkError::Internal( "No http status code received".to_owned(), )), 200..=299 => Ok(()), // HTTP ok status codes _ => Err(NetworkError::Internal(format!( "HTTP error code {}", status_code ))), }; } fn process_response_chunk(&mut self, mut chunk: Vec) { if self.status.is_ok() { self.data.append(&mut chunk); } } /// /// Step 9-12 #[allow(unsafe_code)] fn process_response_eof(&mut self, response: Result) { let global = self.owner.global(); if let Some(window) = global.downcast::() { window .Document() .finish_load(LoadType::Script(self.url.clone())); } // Step 9-1 & 9-2. let load = response.and(self.status.clone()).and_then(|_| { // Step 9-3. let meta = self.metadata.take().unwrap(); if let Some(content_type) = meta.content_type.map(Serde::into_inner) { if let Ok(content_type) = Mime::from_str(&content_type.to_string()) { let essence_mime = content_type.essence_str(); if !SCRIPT_JS_MIMES.contains(&essence_mime) { return Err(NetworkError::Internal(format!( "Invalid MIME type: {}", essence_mime ))); } } else { return Err(NetworkError::Internal(format!( "Failed to parse MIME type: {}", content_type.to_string() ))); } } else { return Err(NetworkError::Internal("No MIME type".into())); } // Step 10. let (source_text, _, _) = UTF_8.decode(&self.data); Ok(ScriptOrigin::external( Rc::new(DOMString::from(source_text)), meta.final_url, self.options.clone(), ScriptType::Module, )) }); let module_tree = { let module_map = global.get_module_map().borrow(); module_map.get(&self.url.clone()).unwrap().clone() }; module_tree.remove_incomplete_fetch_url(self.url.clone()); // Step 12. match load { Err(err) => { error!("Failed to fetch {} with error {:?}", self.url.clone(), err); module_tree.set_network_error(err); module_tree.advance_finished_and_link(&global); }, Ok(ref resp_mod_script) => { module_tree.set_text(resp_mod_script.text()); let compiled_module = module_tree.compile_module_script( &global, self.owner.clone(), resp_mod_script.text(), self.url.clone(), self.options.clone(), ); match compiled_module { Err(exception) => { module_tree.set_rethrow_error(exception); module_tree.advance_finished_and_link(&global); }, Ok(record) => { module_tree.set_record(record); module_tree.fetch_module_descendants( &self.owner, self.destination.clone(), &self.options, ModuleIdentity::ModuleUrl(self.url.clone()), ); }, } }, } } fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming { &mut self.resource_timing } fn resource_timing(&self) -> &ResourceFetchTiming { &self.resource_timing } fn submit_resource_timing(&mut self) { network_listener::submit_timing(self) } } impl ResourceTimingListener for ModuleContext { fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) { let initiator_type = InitiatorType::LocalName("module".to_string()); (initiator_type, self.url.clone()) } fn resource_timing_global(&self) -> DomRoot { self.owner.global() } } impl PreInvoke for ModuleContext {} #[allow(unsafe_code, non_snake_case)] /// A function to register module hooks (e.g. listening on resolving modules, /// getting module metadata, getting script private reference and resolving dynamic import) pub unsafe fn EnsureModuleHooksInitialized(rt: *mut JSRuntime) { if GetModuleResolveHook(rt).is_some() { return; } SetModuleResolveHook(rt, Some(HostResolveImportedModule)); SetModuleMetadataHook(rt, Some(HostPopulateImportMeta)); SetScriptPrivateReferenceHooks( rt, Some(host_add_ref_top_level_script), Some(host_release_top_level_script), ); SetModuleDynamicImportHook(rt, Some(host_import_module_dynamically)); } #[allow(unsafe_code)] unsafe extern "C" fn host_add_ref_top_level_script(value: *const Value) { let val = Rc::from_raw((*value).to_private() as *const ModuleScript); mem::forget(val.clone()); mem::forget(val); } #[allow(unsafe_code)] unsafe extern "C" fn host_release_top_level_script(value: *const Value) { let _val = Rc::from_raw((*value).to_private() as *const ModuleScript); } #[allow(unsafe_code)] /// pub unsafe extern "C" fn host_import_module_dynamically( cx: *mut JSContext, reference_private: RawHandleValue, specifier: RawHandle<*mut JSObject>, promise: RawHandle<*mut JSObject>, ) -> bool { // Step 1. let cx = SafeJSContext::from_ptr(cx); let in_realm_proof = AlreadyInRealm::assert_for_cx(cx); let global_scope = GlobalScope::from_context(*cx, InRealm::Already(&in_realm_proof)); // Step 2. let mut base_url = global_scope.api_base_url(); // Step 3. let mut options = ScriptFetchOptions::default_classic_script(&global_scope); // Step 4. let module_data = module_script_from_reference_private(&reference_private); if let Some(data) = module_data { base_url = data.base_url.clone(); options = data.options.descendant_fetch_options(); } let promise = Promise::new_with_js_promise(Handle::from_raw(promise), cx); //Step 5 & 6. if let Err(e) = fetch_an_import_module_script_graph( &global_scope, specifier, reference_private, base_url, options, promise, ) { JS_SetPendingException(*cx, e.handle(), ExceptionStackBehavior::Capture); return false; } true } #[derive(Clone, JSTraceable, MallocSizeOf)] /// pub struct ScriptFetchOptions { pub referrer: Referrer, pub integrity_metadata: String, pub credentials_mode: CredentialsMode, pub cryptographic_nonce: String, pub parser_metadata: ParserMetadata, pub referrer_policy: Option, } impl ScriptFetchOptions { /// pub fn default_classic_script(global: &GlobalScope) -> ScriptFetchOptions { Self { cryptographic_nonce: String::new(), integrity_metadata: String::new(), referrer: global.get_referrer(), parser_metadata: ParserMetadata::NotParserInserted, credentials_mode: CredentialsMode::CredentialsSameOrigin, referrer_policy: None, } } /// fn descendant_fetch_options(&self) -> ScriptFetchOptions { Self { referrer: self.referrer.clone(), integrity_metadata: String::new(), cryptographic_nonce: self.cryptographic_nonce.clone(), credentials_mode: self.credentials_mode, parser_metadata: self.parser_metadata, referrer_policy: self.referrer_policy, } } } #[allow(unsafe_code)] unsafe fn module_script_from_reference_private<'a>( reference_private: &RawHandle, ) -> Option<&ModuleScript> { if reference_private.get().is_undefined() { return None; } (reference_private.get().to_private() as *const ModuleScript).as_ref() } /// #[allow(unsafe_code)] fn fetch_an_import_module_script_graph( global: &GlobalScope, module_request: RawHandle<*mut JSObject>, reference_private: RawHandleValue, base_url: ServoUrl, options: ScriptFetchOptions, promise: Rc, ) -> Result<(), RethrowError> { // Step 1. let cx = GlobalScope::get_cx(); rooted!(in(*cx) let specifier = unsafe { GetModuleRequestSpecifier(*cx, module_request) }); let url = ModuleTree::resolve_module_specifier(*cx, &base_url, specifier.handle().into()); // Step 2. if url.is_err() { let specifier_error = unsafe { gen_type_error(&global, "Wrong module specifier".to_owned()) }; return Err(specifier_error); } let dynamic_module_id = DynamicModuleId(Uuid::new_v4()); // Step 3. let owner = match unsafe { module_script_from_reference_private(&reference_private) } { Some(module_data) if module_data.owner.is_some() => module_data.owner.clone().unwrap(), _ => ModuleOwner::DynamicModule(Trusted::new(&DynamicModuleOwner::new( global, promise.clone(), dynamic_module_id.clone(), ))), }; let dynamic_module = RootedTraceableBox::new(DynamicModule { promise, specifier: Heap::default(), referencing_private: Heap::default(), id: dynamic_module_id, }); dynamic_module.specifier.set(module_request.get()); dynamic_module .referencing_private .set(reference_private.get()); let url = url.unwrap(); let mut visited_urls = HashSet::new(); visited_urls.insert(url.clone()); fetch_single_module_script( owner, url, visited_urls, Destination::Script, options, None, true, Some(dynamic_module), ); Ok(()) } #[allow(unsafe_code, non_snake_case)] /// https://tc39.github.io/ecma262/#sec-hostresolveimportedmodule /// https://html.spec.whatwg.org/multipage/#hostresolveimportedmodule(referencingscriptormodule%2C-specifier) unsafe extern "C" fn HostResolveImportedModule( cx: *mut JSContext, reference_private: RawHandleValue, specifier: RawHandle<*mut JSObject>, ) -> *mut JSObject { let in_realm_proof = AlreadyInRealm::assert_for_cx(SafeJSContext::from_ptr(cx)); let global_scope = GlobalScope::from_context(cx, InRealm::Already(&in_realm_proof)); // Step 2. let mut base_url = global_scope.api_base_url(); // Step 3. let module_data = module_script_from_reference_private(&reference_private); if let Some(data) = module_data { base_url = data.base_url.clone(); } // Step 5. rooted!(in(*GlobalScope::get_cx()) let specifier = GetModuleRequestSpecifier(cx, specifier)); let url = ModuleTree::resolve_module_specifier( *GlobalScope::get_cx(), &base_url, specifier.handle().into(), ); // Step 6. assert!(url.is_ok()); let parsed_url = url.unwrap(); // Step 4 & 7. let module_map = global_scope.get_module_map().borrow(); let module_tree = module_map.get(&parsed_url); // Step 9. assert!(module_tree.is_some()); let fetched_module_object = module_tree.unwrap().get_record().borrow(); // Step 8. assert!(fetched_module_object.is_some()); // Step 10. if let Some(record) = &*fetched_module_object { return record.handle().get(); } unreachable!() } #[allow(unsafe_code, non_snake_case)] /// https://tc39.es/ecma262/#sec-hostgetimportmetaproperties /// https://html.spec.whatwg.org/multipage/#hostgetimportmetaproperties unsafe extern "C" fn HostPopulateImportMeta( cx: *mut JSContext, reference_private: RawHandleValue, meta_object: RawHandle<*mut JSObject>, ) -> bool { let in_realm_proof = AlreadyInRealm::assert_for_cx(SafeJSContext::from_ptr(cx)); let global_scope = GlobalScope::from_context(cx, InRealm::Already(&in_realm_proof)); // Step 2. let base_url = match module_script_from_reference_private(&reference_private) { Some(module_data) => module_data.base_url.clone(), None => global_scope.api_base_url(), }; rooted!(in(cx) let url_string = JS_NewStringCopyN( cx, base_url.as_str().as_ptr() as *const _, base_url.as_str().len() )); // Step 3. JS_DefineProperty4( cx, meta_object, "url\0".as_ptr() as *const _, url_string.handle().into_handle(), JSPROP_ENUMERATE.into(), ) } /// https://html.spec.whatwg.org/multipage/#fetch-a-module-script-tree pub(crate) fn fetch_external_module_script( owner: ModuleOwner, url: ServoUrl, destination: Destination, options: ScriptFetchOptions, ) { let mut visited_urls = HashSet::new(); visited_urls.insert(url.clone()); // Step 1. fetch_single_module_script( owner, url, visited_urls, destination, options, None, true, None, ) } #[derive(JSTraceable, MallocSizeOf)] #[unrooted_must_root_lint::must_root] pub(crate) struct DynamicModuleList { requests: Vec>, #[ignore_malloc_size_of = "Define in uuid"] next_id: DynamicModuleId, } impl DynamicModuleList { pub fn new() -> Self { Self { requests: vec![], next_id: DynamicModuleId(Uuid::new_v4()), } } fn push(&mut self, mut module: RootedTraceableBox) -> DynamicModuleId { let id = self.next_id; self.next_id = DynamicModuleId(Uuid::new_v4()); module.id = id; self.requests.push(module); id } fn remove(&mut self, id: DynamicModuleId) -> RootedTraceableBox { let index = self .requests .iter() .position(|module| module.id == id) .expect("missing dynamic module"); self.requests.remove(index) } } #[unrooted_must_root_lint::must_root] #[derive(JSTraceable, MallocSizeOf)] struct DynamicModule { #[ignore_malloc_size_of = "Rc is hard"] promise: Rc, #[ignore_malloc_size_of = "GC types are hard"] specifier: Heap<*mut JSObject>, #[ignore_malloc_size_of = "GC types are hard"] referencing_private: Heap, #[ignore_malloc_size_of = "Defined in uuid"] id: DynamicModuleId, } /// https://html.spec.whatwg.org/multipage/#fetch-a-single-module-script fn fetch_single_module_script( owner: ModuleOwner, url: ServoUrl, visited_urls: HashSet, destination: Destination, options: ScriptFetchOptions, parent_identity: Option, top_level_module_fetch: bool, dynamic_module: Option>, ) { { // Step 1. let global = owner.global(); let module_map = global.get_module_map().borrow(); debug!("Start to fetch {}", url); if let Some(module_tree) = module_map.get(&url.clone()) { let status = module_tree.get_status(); debug!("Meet a fetched url {} and its status is {:?}", url, status); match dynamic_module { Some(module) => module_tree.append_dynamic_module_handler( owner.clone(), ModuleIdentity::ModuleUrl(url.clone()), module, ), None if top_level_module_fetch => module_tree.append_handler( owner.clone(), ModuleIdentity::ModuleUrl(url.clone()), options, ), // do nothing if it's neither a dynamic module nor a top level module None => {}, } if let Some(parent_identity) = parent_identity { module_tree.insert_parent_identity(parent_identity); } match status { ModuleStatus::Initial => unreachable!( "We have the module in module map so its status should not be `initial`" ), // Step 2. ModuleStatus::Fetching => {}, // Step 3. ModuleStatus::FetchingDescendants | ModuleStatus::Finished => { module_tree.advance_finished_and_link(&global); }, } return; } } let global = owner.global(); let is_external = true; let module_tree = ModuleTree::new(url.clone(), is_external, visited_urls); module_tree.set_status(ModuleStatus::Fetching); match dynamic_module { Some(module) => module_tree.append_dynamic_module_handler( owner.clone(), ModuleIdentity::ModuleUrl(url.clone()), module, ), None if top_level_module_fetch => module_tree.append_handler( owner.clone(), ModuleIdentity::ModuleUrl(url.clone()), options.clone(), ), // do nothing if it's neither a dynamic module nor a top level module None => {}, } if let Some(parent_identity) = parent_identity { module_tree.insert_parent_identity(parent_identity); } module_tree.insert_incomplete_fetch_url(url.clone()); // Step 4. global.set_module_map(url.clone(), module_tree); // Step 5-6. let mode = match destination.clone() { Destination::Worker | Destination::SharedWorker if top_level_module_fetch => { RequestMode::SameOrigin }, _ => RequestMode::CorsMode, }; let document: Option> = match &owner { ModuleOwner::Worker(_) | ModuleOwner::DynamicModule(_) => None, ModuleOwner::Window(script) => Some(document_from_node(&*script.root())), }; // Step 7-8. let request = RequestBuilder::new(url.clone(), global.get_referrer()) .destination(destination.clone()) .origin(global.origin().immutable().clone()) .parser_metadata(options.parser_metadata) .integrity_metadata(options.integrity_metadata.clone()) .credentials_mode(options.credentials_mode) .mode(mode); let context = Arc::new(Mutex::new(ModuleContext { owner, data: vec![], metadata: None, url: url.clone(), destination: destination.clone(), options, status: Ok(()), resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource), })); let (action_sender, action_receiver) = ipc::channel().unwrap(); let task_source = global.networking_task_source(); let canceller = global.task_canceller(TaskSourceName::Networking); let listener = NetworkListener { context, task_source, canceller: Some(canceller), }; ROUTER.add_route( action_receiver.to_opaque(), Box::new(move |message| { listener.notify_fetch(message.to().unwrap()); }), ); match document { Some(doc) => doc.fetch_async(LoadType::Script(url), request, action_sender), None => { let _ = global .resource_threads() .sender() .send(CoreResourceMsg::Fetch( request, FetchChannels::ResponseMsg(action_sender, None), )) .unwrap(); }, } } #[allow(unsafe_code)] /// https://html.spec.whatwg.org/multipage/#fetch-an-inline-module-script-graph pub(crate) fn fetch_inline_module_script( owner: ModuleOwner, module_script_text: Rc, url: ServoUrl, script_id: ScriptId, options: ScriptFetchOptions, ) { let global = owner.global(); let is_external = false; let module_tree = ModuleTree::new(url.clone(), is_external, HashSet::new()); let compiled_module = module_tree.compile_module_script( &global, owner.clone(), module_script_text, url.clone(), options.clone(), ); match compiled_module { Ok(record) => { module_tree.append_handler( owner.clone(), ModuleIdentity::ScriptId(script_id.clone()), options.clone(), ); module_tree.set_record(record); // We need to set `module_tree` into inline module map in case // of that the module descendants finished right after the // fetch module descendants step. global.set_inline_module_map(script_id, module_tree); // Due to needed to set `module_tree` to inline module_map first, // we will need to retrieve it again so that we can do the fetch // module descendants step. let inline_module_map = global.get_inline_module_map().borrow(); let module_tree = inline_module_map.get(&script_id).unwrap().clone(); module_tree.fetch_module_descendants( &owner, Destination::Script, &options, ModuleIdentity::ScriptId(script_id), ); }, Err(exception) => { module_tree.set_rethrow_error(exception); module_tree.set_status(ModuleStatus::Finished); global.set_inline_module_map(script_id.clone(), module_tree); owner.notify_owner_to_finish(ModuleIdentity::ScriptId(script_id), options); }, } }