From 927573de9715e6bf1458ffe5bf201d7fd7b76019 Mon Sep 17 00:00:00 2001 From: "Ngo Iok Ui (Wu Yu Wei)" Date: Sat, 21 Jun 2025 15:17:27 +0900 Subject: [PATCH] script: complete `resolve_module_specifier` (#37552) Implement whole spec of `resolve_module_specifier`. Servo can now support script element with import map type! Testing: `tests/wpt/tests/import-map` Fixes: #37316 #36394 --------- Signed-off-by: Wu Yu Wei --- components/script/dom/globalscope.rs | 44 +- components/script/dom/htmlscriptelement.rs | 4 +- components/script/script_module.rs | 381 ++++++++++++++---- components/url/lib.rs | 11 + .../acquiring/dynamic-import.html.ini | 3 - .../modulepreload-link-header.html.ini | 3 - .../acquiring/modulepreload.html.ini | 3 - .../acquiring/script-tag-inline.html.ini | 3 - .../import-maps/acquiring/script-tag.html.ini | 3 - .../acquiring/worker-request.html.ini | 3 - .../import-maps/bare-specifiers.sub.html.ini | 27 -- .../applied-to-target-dynamic.sub.html.ini | 6 - .../csp/applied-to-target.sub.html.ini | 6 - tests/wpt/meta/import-maps/csp/hash.html.ini | 3 - tests/wpt/meta/import-maps/csp/nonce.html.ini | 3 - .../import-maps/csp/unsafe-inline.html.ini | 3 - .../data-url-specifiers.sub.html.ini | 36 -- .../import-maps/dynamic-integrity.html.ini | 9 +- .../http-url-like-specifiers.sub.html.ini | 36 -- .../import-maps-base-url.sub.html.ini | 18 - .../meta/import-maps/module-map-key.html.ini | 3 - .../already-resolved-dropped.html.ini | 2 + .../multiple-import-maps/basic.html.ini | 3 - .../conflict-first-persists.html.ini | 9 - ...consistency-in-module-tree-inline.html.ini | 3 - ...lution-consistency-in-module-tree.html.ini | 3 - .../url-resolution-conflict.html.ini | 3 - .../multiple-import-maps/with-errors.html.ini | 3 - .../not-overridden/dynamic.html.ini | 1 + .../external-script-bare-descendent.html.ini | 9 +- .../not-overridden/integrity.html.ini | 3 - .../not-overridden/prefix.html.ini | 1 + .../import-maps/static-integrity.html.ini | 3 - .../script-enforcement-006.html.ini | 3 - .../script-enforcement-008.https.html.ini | 3 - 35 files changed, 376 insertions(+), 281 deletions(-) delete mode 100644 tests/wpt/meta/import-maps/acquiring/dynamic-import.html.ini delete mode 100644 tests/wpt/meta/import-maps/acquiring/modulepreload-link-header.html.ini delete mode 100644 tests/wpt/meta/import-maps/acquiring/modulepreload.html.ini delete mode 100644 tests/wpt/meta/import-maps/acquiring/script-tag-inline.html.ini delete mode 100644 tests/wpt/meta/import-maps/acquiring/script-tag.html.ini delete mode 100644 tests/wpt/meta/import-maps/acquiring/worker-request.html.ini delete mode 100644 tests/wpt/meta/import-maps/bare-specifiers.sub.html.ini delete mode 100644 tests/wpt/meta/import-maps/csp/applied-to-target-dynamic.sub.html.ini delete mode 100644 tests/wpt/meta/import-maps/csp/applied-to-target.sub.html.ini delete mode 100644 tests/wpt/meta/import-maps/csp/hash.html.ini delete mode 100644 tests/wpt/meta/import-maps/csp/nonce.html.ini delete mode 100644 tests/wpt/meta/import-maps/csp/unsafe-inline.html.ini delete mode 100644 tests/wpt/meta/import-maps/data-url-specifiers.sub.html.ini delete mode 100644 tests/wpt/meta/import-maps/http-url-like-specifiers.sub.html.ini delete mode 100644 tests/wpt/meta/import-maps/import-maps-base-url.sub.html.ini delete mode 100644 tests/wpt/meta/import-maps/module-map-key.html.ini create mode 100644 tests/wpt/meta/import-maps/multiple-import-maps/already-resolved-dropped.html.ini delete mode 100644 tests/wpt/meta/import-maps/multiple-import-maps/basic.html.ini delete mode 100644 tests/wpt/meta/import-maps/multiple-import-maps/conflict-first-persists.html.ini delete mode 100644 tests/wpt/meta/import-maps/multiple-import-maps/resolution-consistency-in-module-tree-inline.html.ini delete mode 100644 tests/wpt/meta/import-maps/multiple-import-maps/resolution-consistency-in-module-tree.html.ini delete mode 100644 tests/wpt/meta/import-maps/multiple-import-maps/url-resolution-conflict.html.ini delete mode 100644 tests/wpt/meta/import-maps/multiple-import-maps/with-errors.html.ini delete mode 100644 tests/wpt/meta/import-maps/not-overridden/integrity.html.ini delete mode 100644 tests/wpt/meta/trusted-types/script-enforcement-006.html.ini diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs index bf9e8ae923c..f3b6e8acd6e 100644 --- a/components/script/dom/globalscope.rs +++ b/components/script/dom/globalscope.rs @@ -4,7 +4,7 @@ use std::cell::{Cell, OnceCell, Ref}; use std::collections::hash_map::Entry; -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::ops::Index; use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -137,7 +137,7 @@ use crate::microtask::{Microtask, MicrotaskQueue, UserMicrotask}; use crate::network_listener::{NetworkListener, PreInvoke}; use crate::realms::{InRealm, enter_realm}; use crate::script_module::{ - DynamicModuleList, ImportMap, ModuleScript, ModuleTree, ScriptFetchOptions, + DynamicModuleList, ImportMap, ModuleScript, ModuleTree, ResolvedModule, ScriptFetchOptions, }; use crate::script_runtime::{CanGc, JSContext as SafeJSContext, ThreadSafeJSContext}; use crate::script_thread::{ScriptThread, with_script_thread}; @@ -385,6 +385,9 @@ pub(crate) struct GlobalScope { /// /// import_map: DomRefCell, + + /// + resolved_module_set: DomRefCell>, } /// A wrapper for glue-code between the ipc router and the event-loop. @@ -789,6 +792,7 @@ impl GlobalScope { count_queuing_strategy_size_function: OnceCell::new(), notification_permission_request_callback_map: Default::default(), import_map: Default::default(), + resolved_module_set: Default::default(), } } @@ -3487,8 +3491,40 @@ impl GlobalScope { } } - pub(crate) fn import_map(&self) -> &DomRefCell { - &self.import_map + pub(crate) fn import_map(&self) -> Ref<'_, ImportMap> { + self.import_map.borrow() + } + + pub(crate) fn import_map_mut(&self) -> RefMut<'_, ImportMap> { + self.import_map.borrow_mut() + } + + pub(crate) fn resolved_module_set(&self) -> Ref<'_, HashSet> { + self.resolved_module_set.borrow() + } + + pub(crate) fn resolved_module_set_mut(&self) -> RefMut<'_, HashSet> { + self.resolved_module_set.borrow_mut() + } + + /// + pub(crate) fn add_module_to_resolved_module_set( + &self, + base_url: &str, + specifier: &str, + specifier_url: Option, + ) { + // Step 1. Let global be settingsObject's global object. + // Step 2. If global does not implement Window, then return. + if self.is::() { + // Step 3. Let record be a new specifier resolution record, with serialized base URL + // set to serializedBaseURL, specifier set to normalizedSpecifier, and specifier as + // a URL set to asURL. + let record = + ResolvedModule::new(base_url.to_owned(), specifier.to_owned(), specifier_url); + // Step 4. Append record to global's resolved module set. + self.resolved_module_set.borrow_mut().insert(record); + } } } diff --git a/components/script/dom/htmlscriptelement.rs b/components/script/dom/htmlscriptelement.rs index 12dcf6e4057..17838ace9fc 100644 --- a/components/script/dom/htmlscriptelement.rs +++ b/components/script/dom/htmlscriptelement.rs @@ -856,7 +856,9 @@ impl HTMLScriptElement { credentials_mode: module_credentials_mode, }; - // TODO: Step 30. Environment settings object. + // Step 30. Let settings object be el's node document's relevant settings object. + // This is done by passing ModuleOwner in step 31.11 and step 32.2. + // What we actually need is global's import map eventually. let base_url = doc.base_url(); if let Some(src) = element.get_attribute(&ns!(), &local_name!("src")) { diff --git a/components/script/script_module.rs b/components/script/script_module.rs index 62f286eab1d..f292a62e071 100644 --- a/components/script/script_module.rs +++ b/components/script/script_module.rs @@ -22,13 +22,13 @@ use js::jsapi::{ GetModuleResolveHook, GetRequestedModuleSpecifier, GetRequestedModulesCount, Handle as RawHandle, HandleObject, HandleValue as RawHandleValue, Heap, JS_ClearPendingException, JS_DefineProperty4, JS_IsExceptionPending, JS_NewStringCopyN, - JSAutoRealm, JSContext, JSObject, JSPROP_ENUMERATE, JSRuntime, JSString, ModuleErrorBehaviour, + JSAutoRealm, JSContext, JSObject, JSPROP_ENUMERATE, JSRuntime, ModuleErrorBehaviour, ModuleEvaluate, ModuleLink, MutableHandleValue, SetModuleDynamicImportHook, SetModuleMetadataHook, SetModulePrivate, SetModuleResolveHook, SetScriptPrivateReferenceHooks, ThrowOnModuleEvaluationFailure, Value, }; use js::jsval::{JSVal, PrivateValue, UndefinedValue}; -use js::rust::wrappers::{JS_GetPendingException, JS_SetPendingException}; +use js::rust::wrappers::{JS_GetModulePrivate, JS_GetPendingException, JS_SetPendingException}; use js::rust::{ CompileOptionsWrapper, Handle, HandleObject as RustHandleObject, HandleValue, IntoHandle, MutableHandleObject as RustMutableHandleObject, transform_str_to_source_text, @@ -512,7 +512,6 @@ impl ModuleTree { self.resolve_requested_module_specifiers( global, module_script.handle().into_handle(), - url, can_gc, ) .map(|_| ()) @@ -616,7 +615,6 @@ impl ModuleTree { &self, global: &GlobalScope, module_object: HandleObject, - base_url: &ServoUrl, can_gc: CanGc, ) -> Result, RethrowError> { let cx = GlobalScope::get_cx(); @@ -628,17 +626,19 @@ impl ModuleTree { let length = GetRequestedModulesCount(*cx, module_object); for index in 0..length { - rooted!(in(*cx) let specifier = GetRequestedModuleSpecifier( - *cx, module_object, index - )); - - let url = ModuleTree::resolve_module_specifier( + let specifier = jsstring_to_str( *cx, - base_url, - specifier.handle().into_handle(), + ptr::NonNull::new(GetRequestedModuleSpecifier(*cx, module_object, index)) + .unwrap(), ); - if url.is_none() { + rooted!(in(*cx) let mut private = UndefinedValue()); + JS_GetModulePrivate(module_object.get(), private.handle_mut()); + let private = private.handle().into_handle(); + let script = module_script_from_reference_private(&private); + let url = ModuleTree::resolve_module_specifier(global, script, specifier, can_gc); + + if url.is_err() { let specifier_error = gen_type_error(global, "Wrong module specifier".to_owned(), can_gc); @@ -652,24 +652,111 @@ impl ModuleTree { 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. /// #[allow(unsafe_code)] fn resolve_module_specifier( - cx: *mut JSContext, - url: &ServoUrl, - specifier: RawHandle<*mut JSString>, - ) -> Option { - let specifier_str = unsafe { jsstring_to_str(cx, ptr::NonNull::new(*specifier).unwrap()) }; + global: &GlobalScope, + script: Option<&ModuleScript>, + specifier: DOMString, + can_gc: CanGc, + ) -> Fallible { + // Step 1~3 to get settingsObject and baseURL + let script_global = script.and_then(|s| s.owner.as_ref().map(|o| o.global())); + // Step 1. Let settingsObject and baseURL be null. + let (global, base_url): (&GlobalScope, &ServoUrl) = match script { + // Step 2. If referringScript is not null, then: + // Set settingsObject to referringScript's settings object. + // Set baseURL to referringScript's base URL. + Some(s) => (script_global.as_ref().map_or(global, |g| g), &s.base_url), + // Step 3. Otherwise: + // Set settingsObject to the current settings object. + // Set baseURL to settingsObject's API base URL. + // FIXME(#37553): Is this the correct current settings object? + None => (global, &global.api_base_url()), + }; - // TODO: We return the url here to keep the origianl behavior. should fix when we implement the full spec. + // Step 4. Let importMap be an empty import map. + // Step 5. If settingsObject's global object implements Window, then set importMap to settingsObject's + // global object's import map. + let import_map = if global.is::() { + Some(global.import_map()) + } else { + None + }; + + // Step 6. Let serializedBaseURL be baseURL, serialized. + let serialized_base_url = base_url.as_str(); // Step 7. Let asURL be the result of resolving a URL-like module specifier given specifier and baseURL. - Self::resolve_url_like_module_specifier(&specifier_str, url) + let as_url = Self::resolve_url_like_module_specifier(&specifier, base_url); + // Step 8. Let normalizedSpecifier be the serialization of asURL, if asURL is non-null; + // otherwise, specifier. + let normalized_specifier = match &as_url { + Some(url) => url.as_str(), + None => &specifier, + }; + + // Step 9. Let result be a URL-or-null, initially null. + let mut result = None; + if let Some(map) = import_map { + // Step 10. For each scopePrefix → scopeImports of importMap's scopes: + for (prefix, imports) in &map.scopes { + // Step 10.1 If scopePrefix is serializedBaseURL, or if scopePrefix ends with U+002F (/) + // and scopePrefix is a code unit prefix of serializedBaseURL, then: + let prefix = prefix.as_str(); + if prefix == serialized_base_url || + (serialized_base_url.starts_with(prefix) && prefix.ends_with('\u{002f}')) + { + // Step 10.1.1 Let scopeImportsMatch be the result of resolving an imports match + // given normalizedSpecifier, asURL, and scopeImports. + // Step 10.1.2 If scopeImportsMatch is not null, then set result to scopeImportsMatch, + // and break. + result = resolve_imports_match( + normalized_specifier, + as_url.as_ref(), + imports, + can_gc, + )?; + break; + } + } + + // Step 11. If result is null, set result to the result of resolving an imports match given + // normalizedSpecifier, asURL, and importMap's imports. + if result.is_none() { + result = resolve_imports_match( + normalized_specifier, + as_url.as_ref(), + &map.imports, + can_gc, + )?; + } + } + + // Step 12. If result is null, set it to asURL. + if result.is_none() { + result = as_url.clone(); + } + + // Step 13. If result is not null, then: + match result { + Some(result) => { + // Step 13.1 Add module to resolved module set given settingsObject, serializedBaseURL, + // normalizedSpecifier, and asURL. + global.add_module_to_resolved_module_set( + serialized_base_url, + normalized_specifier, + as_url.clone(), + ); + // Step 13.2 Return result. + Ok(result) + }, + // Step 14. Throw a TypeError indicating that specifier was a bare specifier, + // but was not remapped to anything by importMap. + None => Err(Error::Type( + "Specifier was a bare specifier, but was not remapped to anything by importMap." + .to_owned(), + )), + } } /// @@ -773,12 +860,9 @@ impl ModuleTree { return; }, // Step 5. - Some(raw_record) => self.resolve_requested_module_specifiers( - &global, - raw_record.handle(), - &self.url, - can_gc, - ), + Some(raw_record) => { + self.resolve_requested_module_specifiers(&global, raw_record.handle(), can_gc) + }, } }; @@ -1333,6 +1417,7 @@ unsafe extern "C" fn host_release_top_level_script(value: *const Value) { } #[allow(unsafe_code)] +/// /// pub(crate) unsafe extern "C" fn host_import_module_dynamically( cx: *mut JSContext, @@ -1344,20 +1429,6 @@ pub(crate) unsafe extern "C" fn host_import_module_dynamically( 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. @@ -1365,8 +1436,6 @@ pub(crate) unsafe extern "C" fn host_import_module_dynamically( &global_scope, specifier, reference_private, - base_url, - options, promise, CanGc::note(), ) { @@ -1434,18 +1503,26 @@ fn fetch_an_import_module_script_graph( global: &GlobalScope, module_request: RawHandle<*mut JSObject>, reference_private: RawHandleValue, - base_url: ServoUrl, - options: ScriptFetchOptions, promise: Rc, can_gc: CanGc, ) -> 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()); + let specifier = unsafe { + jsstring_to_str( + *cx, + ptr::NonNull::new(GetModuleRequestSpecifier(*cx, module_request)).unwrap(), + ) + }; + let mut options = ScriptFetchOptions::default_classic_script(global); + let module_data = unsafe { module_script_from_reference_private(&reference_private) }; + if let Some(data) = module_data { + options = data.options.descendant_fetch_options(); + } + let url = ModuleTree::resolve_module_specifier(global, module_data, specifier, can_gc); // Step 2. - if url.is_none() { + if url.is_err() { let specifier_error = gen_type_error(global, "Wrong module specifier".to_owned(), can_gc); return Err(specifier_error); } @@ -1494,8 +1571,8 @@ fn fetch_an_import_module_script_graph( } #[allow(unsafe_code, non_snake_case)] -/// -/// +/// +/// unsafe extern "C" fn HostResolveImportedModule( cx: *mut JSContext, reference_private: RawHandleValue, @@ -1504,25 +1581,17 @@ unsafe extern "C" fn HostResolveImportedModule( 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(), + let module_data = module_script_from_reference_private(&reference_private); + let specifier = jsstring_to_str( + cx, + ptr::NonNull::new(GetModuleRequestSpecifier(cx, specifier)).unwrap(), ); + let url = + ModuleTree::resolve_module_specifier(&global_scope, module_data, specifier, CanGc::note()); // Step 6. - assert!(url.is_some()); + assert!(url.is_ok()); let parsed_url = url.unwrap(); @@ -1870,6 +1939,32 @@ pub(crate) fn fetch_inline_module_script( pub(crate) type ModuleSpecifierMap = IndexMap>; pub(crate) type ModuleIntegrityMap = IndexMap; +/// +#[derive(Default, Eq, Hash, JSTraceable, MallocSizeOf, PartialEq)] +pub(crate) struct ResolvedModule { + /// + base_url: String, + /// + specifier: String, + /// + #[no_trace] + specifier_url: Option, +} + +impl ResolvedModule { + pub(crate) fn new( + base_url: String, + specifier: String, + specifier_url: Option, + ) -> Self { + Self { + base_url, + specifier, + specifier_url, + } + } +} + /// #[derive(Default, JSTraceable, MallocSizeOf)] pub(crate) struct ImportMap { @@ -1910,15 +2005,52 @@ fn merge_existing_and_new_import_maps( let new_import_map_scopes = new_import_map.scopes; // Step 2. Let oldImportMap be global's import map. - let mut old_import_map = global.import_map().borrow_mut(); + let mut old_import_map = global.import_map_mut(); // Step 3. Let newImportMapImports be a deep copy of newImportMap's imports. - let new_import_map_imports = new_import_map.imports; + let mut new_import_map_imports = new_import_map.imports; + let resolved_module_set = global.resolved_module_set(); // Step 4. For each scopePrefix → scopeImports of newImportMapScopes: - for (scope_prefix, scope_imports) in new_import_map_scopes { - // TODO: implement after we complete `resolve_module_specifier` + for (scope_prefix, mut scope_imports) in new_import_map_scopes { // Step 4.1. For each record of global's resolved module set: + for record in resolved_module_set.iter() { + // If scopePrefix is record's serialized base URL, or if scopePrefix ends with + // U+002F (/) and scopePrefix is a code unit prefix of record's serialized base URL, then: + let prefix = scope_prefix.as_str(); + if prefix == record.base_url || + (record.base_url.starts_with(prefix) && prefix.ends_with('\u{002f}')) + { + // For each specifierKey → resolutionResult of scopeImports: + scope_imports.retain(|key, val| { + // If specifierKey is record's specifier, or if all of the following conditions are true: + // specifierKey ends with U+002F (/); + // specifierKey is a code unit prefix of record's specifier; + // either record's specifier as a URL is null or is special, + if *key == record.specifier || + (key.ends_with('\u{002f}') && + record.specifier.starts_with(key) && + (record.specifier_url.is_none() || + record + .specifier_url + .as_ref() + .map(|u| u.is_special_scheme()) + .unwrap_or_default())) + { + // The user agent may report a warning to the console indicating the ignored rule. + // They may choose to avoid reporting if the rule is identical to an existing one. + Console::internal_warn( + global, + DOMString::from(format!("Ignored rule: {key} -> {val:?}.")), + ); + // Remove scopeImports[specifierKey]. + false + } else { + true + } + }) + } + } // Step 4.2 If scopePrefix exists in oldImportMap's scopes if old_import_map.scopes.contains_key(&scope_prefix) { @@ -1959,8 +2091,25 @@ fn merge_existing_and_new_import_maps( .insert(url.clone(), integrity.clone()); } - // TODO: implement after we complete `resolve_module_specifier` // Step 6. For each record of global's resolved module set: + for record in resolved_module_set.iter() { + // For each specifier → url of newImportMapImports: + new_import_map_imports.retain(|specifier, val| { + // If specifier starts with record's specifier, then: + if specifier.starts_with(&record.specifier) { + // The user agent may report a warning to the console indicating the ignored rule. + // They may choose to avoid reporting if the rule is identical to an existing one. + Console::internal_warn( + global, + DOMString::from(format!("Ignored rule: {specifier} -> {val:?}.")), + ); + // Remove newImportMapImports[specifier]. + false + } else { + true + } + }); + } // Step 7. Set oldImportMap's imports to the result of merge module specifier maps, // given newImportMapImports and oldImportMap's imports. @@ -2318,3 +2467,87 @@ fn normalize_specifier_key( // step 4. Return specifierKey. Some(specifier_key.to_string()) } + +/// +/// +/// When the error is thrown, it will terminate the entire resolve a module specifier algorithm +/// without any further fallbacks. +pub(crate) fn resolve_imports_match( + normalized_specifier: &str, + as_url: Option<&ServoUrl>, + specifier_map: &ModuleSpecifierMap, + _can_gc: CanGc, +) -> Fallible> { + // Step 1. For each specifierKey → resolutionResult of specifierMap: + for (specifier_key, resolution_result) in specifier_map { + // Step 1.1 If specifierKey is normalizedSpecifier, then: + if specifier_key == normalized_specifier { + if let Some(resolution_result) = resolution_result { + // Step 1.1.2 Assert: resolutionResult is a URL. + // This is checked by Url type already. + // Step 1.1.3 Return resolutionResult. + return Ok(Some(resolution_result.clone())); + } else { + // Step 1.1.1 If resolutionResult is null, then throw a TypeError. + return Err(Error::Type( + "Resolution of specifierKey was blocked by a null entry.".to_owned(), + )); + } + } + + // Step 1.2 If all of the following are true: + // - specifierKey ends with U+002F (/) + // - specifierKey is a code unit prefix of normalizedSpecifier + // - either asURL is null, or asURL is special, then: + if specifier_key.ends_with('\u{002f}') && + normalized_specifier.starts_with(specifier_key) && + (as_url.is_none() || as_url.map(|u| u.is_special_scheme()).unwrap_or_default()) + { + // Step 1.2.1 If resolutionResult is null, then throw a TypeError. + // Step 1.2.2 Assert: resolutionResult is a URL. + let Some(resolution_result) = resolution_result else { + return Err(Error::Type( + "Resolution of specifierKey was blocked by a null entry.".to_owned(), + )); + }; + + // Step 1.2.3 Let afterPrefix be the portion of normalizedSpecifier after the initial specifierKey prefix. + let after_prefix = normalized_specifier + .strip_prefix(specifier_key) + .expect("specifier_key should be the prefix of normalized_specifier"); + + // Step 1.2.4 Assert: resolutionResult, serialized, ends with U+002F (/), as enforced during parsing. + debug_assert!(resolution_result.as_str().ends_with('\u{002f}')); + + // Step 1.2.5 Let url be the result of URL parsing afterPrefix with resolutionResult. + let url = ServoUrl::parse_with_base(Some(resolution_result), after_prefix); + + // Step 1.2.6 If url is failure, then throw a TypeError + // Step 1.2.7 Assert: url is a URL. + let Ok(url) = url else { + return Err(Error::Type( + "Resolution of normalizedSpecifier was blocked since + the afterPrefix portion could not be URL-parsed relative to + the resolutionResult mapped to by the specifierKey prefix." + .to_owned(), + )); + }; + + // Step 1.2.8 If the serialization of resolutionResult is not + // a code unit prefix of the serialization of url, then throw a TypeError + if !url.as_str().starts_with(resolution_result.as_str()) { + return Err(Error::Type( + "Resolution of normalizedSpecifier was blocked due to + it backtracking above its prefix specifierKey." + .to_owned(), + )); + } + + // Step 1.2.9 Return url. + return Ok(Some(url)); + } + } + + // Step 2. Return null. + Ok(None) +} diff --git a/components/url/lib.rs b/components/url/lib.rs index b9da9855fee..8296f5a79c4 100644 --- a/components/url/lib.rs +++ b/components/url/lib.rs @@ -105,6 +105,17 @@ impl ServoUrl { scheme == "about" || scheme == "blob" || scheme == "data" } + /// + pub fn is_special_scheme(&self) -> bool { + let scheme = self.scheme(); + scheme == "ftp" || + scheme == "file" || + scheme == "http" || + scheme == "https" || + scheme == "ws" || + scheme == "wss" + } + pub fn as_str(&self) -> &str { self.0.as_str() } diff --git a/tests/wpt/meta/import-maps/acquiring/dynamic-import.html.ini b/tests/wpt/meta/import-maps/acquiring/dynamic-import.html.ini deleted file mode 100644 index b488e81d346..00000000000 --- a/tests/wpt/meta/import-maps/acquiring/dynamic-import.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[dynamic-import.html] - [After a dynamic import(), import maps work fine] - expected: FAIL diff --git a/tests/wpt/meta/import-maps/acquiring/modulepreload-link-header.html.ini b/tests/wpt/meta/import-maps/acquiring/modulepreload-link-header.html.ini deleted file mode 100644 index 641fcc1c299..00000000000 --- a/tests/wpt/meta/import-maps/acquiring/modulepreload-link-header.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[modulepreload-link-header.html] - [With modulepreload link header, import maps work fine] - expected: FAIL diff --git a/tests/wpt/meta/import-maps/acquiring/modulepreload.html.ini b/tests/wpt/meta/import-maps/acquiring/modulepreload.html.ini deleted file mode 100644 index 86ef2264330..00000000000 --- a/tests/wpt/meta/import-maps/acquiring/modulepreload.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[modulepreload.html] - [After import maps should work fine] - expected: FAIL diff --git a/tests/wpt/meta/import-maps/acquiring/script-tag-inline.html.ini b/tests/wpt/meta/import-maps/acquiring/script-tag-inline.html.ini deleted file mode 100644 index ca97b388d28..00000000000 --- a/tests/wpt/meta/import-maps/acquiring/script-tag-inline.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[script-tag-inline.html] - [After inline