From 282455c8b76e2a5e2b2d383fa00fe3b2bcc04089 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 24 Sep 2026 06:34:29 +0800 Subject: [PATCH] fix(service-worker): use shared WebIDL promise bindings --- .../navigator_runtime/navigator.rs | 27 +- .../window_runtime/service_worker.rs | 235 +++++++++--------- .../src/script_vm/tests/browser_api/misc.rs | 58 ++++- .../tests/browser_api/trusted_types.rs | 173 +++++++++++-- .../src/script_vm/tests/webidl_receivers.rs | 98 ++++++++ .../service_worker_runtime/registration.rs | 6 +- 6 files changed, 455 insertions(+), 142 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/navigator.rs b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/navigator.rs index b0d8d2fc81..c28a02738d 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/navigator.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/navigator.rs @@ -416,13 +416,34 @@ struct ServiceWorkerContainerDeclaration { #[webapi(accessor_property = "controller", enumerable, getter = navigator_service_worker_controller_getter_callback)] controller: (), - #[webapi(method, enumerable, callback = navigator_service_worker_register_callback, length = 1)] + #[webapi( + method, + enumerable, + callback = navigator_service_worker_register_callback, + receiver = web_api_interfaces::ServiceWorkerContainer::is_instance, + returns_promise, + length = 1 + )] register: (), - #[webapi(method, enumerable, callback = navigator_service_worker_get_registration_callback, length = 1)] + #[webapi( + method, + enumerable, + callback = navigator_service_worker_get_registration_callback, + receiver = web_api_interfaces::ServiceWorkerContainer::is_instance, + returns_promise, + length = 0 + )] get_registration: (), - #[webapi(method, enumerable, callback = navigator_service_worker_get_registrations_callback, length = 0)] + #[webapi( + method, + enumerable, + callback = navigator_service_worker_get_registrations_callback, + receiver = web_api_interfaces::ServiceWorkerContainer::is_instance, + returns_promise, + length = 0 + )] get_registrations: (), #[webapi(method, enumerable, callback = simple_event_target_add_event_listener_callback)] diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs index 664379c668..6730023ede 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs @@ -331,6 +331,44 @@ struct BackgroundSyncOptions { min_interval: u64, } +#[derive(webidl::WebIdlDictionary)] +#[webidl(prefix = "RegistrationOptions")] +struct ServiceWorkerRegistrationOptions { + #[webidl(name = "scope", converter = "usv_string")] + requested_scope: Option, + + #[webidl( + name = "type", + converter = "enum", + default = WorkerScriptKind::Classic + )] + script_kind: WorkerScriptKind, + + #[webidl( + name = "updateViaCache", + converter = "enum", + default = ServiceWorkerUpdateViaCache::Imports + )] + update_via_cache: ServiceWorkerUpdateViaCache, +} + +impl Default for ServiceWorkerRegistrationOptions { + fn default() -> Self { + Self { + requested_scope: None, + script_kind: WorkerScriptKind::Classic, + update_via_cache: ServiceWorkerUpdateViaCache::Imports, + } + } +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "ServiceWorkerContainer.getRegistration")] +struct ServiceWorkerGetRegistrationArgs { + #[webidl(name = "clientURL", converter = "usv_string", default = "")] + client_url: String, +} + #[derive(Clone, Copy)] enum ServiceWorkerRegistrationPhase<'a> { Snapshot(&'a crate::service_worker_runtime::ServiceWorkerRegistrationSnapshot), @@ -341,6 +379,30 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<' args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { + if args.length() == 0 { + throw_type_error( + scope, + "Failed to execute 'register' on 'ServiceWorkerContainer': 1 argument required, but only 0 present.", + ); + return; + } + // Convert the union argument before the options dictionary. The Trusted + // Types sink algorithm runs only after all WebIDL argument conversion. + let script_argument = args.get(0); + let script_argument = match v8::Local::::try_from(script_argument) { + Ok(object) if web_api_interfaces::TrustedScriptURL::is_instance(scope, object) => { + script_argument + } + _ => { + let Some(script) = script_argument.to_string(scope) else { + return; + }; + script.into() + } + }; + let Some(options) = service_worker_registration_options(scope, &args) else { + return; + }; let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { let Some(resolver) = v8::PromiseResolver::new(scope) else { return; @@ -350,13 +412,6 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<' rv.set(promise.into()); return; }; - if args.length() == 0 { - throw_type_error( - scope, - "Failed to execute 'register' on 'ServiceWorkerContainer': 1 argument required, but only 0 present.", - ); - return; - } let owner = service_worker_container_owner_scope(scope, args.this()); let Some(request_context) = (unsafe { &mut *host_ptr }).service_worker_window_request_context(owner) @@ -390,7 +445,7 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<' }; let Some(script) = crate::context_bootstrap::trusted_script_url_string_or_throw( scope, - args.get(0), + script_argument, requirements, "ServiceWorkerContainer register", "register", @@ -404,7 +459,7 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<' let Some(script_url) = resolve_service_worker_script_url(request_context.document_url(), &script) else { - reject_service_worker_promise( + reject_service_worker_promise_with_type_error( scope, resolver, "failed to resolve service worker script URL", @@ -412,24 +467,22 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<' rv.set(promise.into()); return; }; - let script_kind = match service_worker_script_kind(scope, &args) { - Ok(script_kind) => script_kind, - Err(message) => { - reject_service_worker_promise(scope, resolver, message); + let scope_url = match service_worker_scope_url( + request_context.document_url(), + &script_url, + options.requested_scope.as_deref(), + ) { + Some(scope_url) => scope_url, + None => { + reject_service_worker_promise_with_type_error( + scope, + resolver, + "failed to resolve service worker scope URL", + ); rv.set(promise.into()); return; } }; - let update_via_cache = match service_worker_update_via_cache(scope, &args) { - Ok(update_via_cache) => update_via_cache, - Err(message) => { - reject_service_worker_promise(scope, resolver, message); - rv.set(promise.into()); - return; - } - }; - let scope_url = - service_worker_scope_url(scope, request_context.document_url(), &script_url, &args); let host = unsafe { &mut *host_ptr }; let Some(request_client) = host .document_resource_loader_for_window_owner(request_context.owner().window_document_owner()) @@ -448,8 +501,8 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<' host.start_service_worker_runtime( script_url, scope_url, - script_kind, - update_via_cache, + options.script_kind, + options.update_via_cache, &request_context, request_client, request_id, @@ -1068,6 +1121,9 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_get_registration_ca args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; let Some(resolver) = v8::PromiseResolver::new(scope) else { return; }; @@ -1079,11 +1135,10 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_get_registration_ca let state = host .service_worker_window_request_context(owner) .and_then(|request_context| { - service_worker_client_url(scope, request_context.document_url(), &args).and_then( - |client_url| { + service_worker_client_url(request_context.document_url(), &parsed.client_url) + .and_then(|client_url| { host.service_worker_registration_for_client(&request_context, &client_url) - }, - ) + }) }); if let Some(state) = state { build_service_worker_registration_object_for_container( @@ -2085,98 +2140,46 @@ fn resolve_service_worker_script_url(document_url: &url::Url, script: &str) -> O Some(script_url) } +fn service_worker_registration_options<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, +) -> Option { + let context = webidl::Context::argument("ServiceWorkerContainer.register", 2); + match webidl::dictionary_arg(args, 1, context) { + Ok(Some(options)) => { + match webidl::parse_dictionary_object::( + scope, options, + ) { + Ok(options) => Some(options), + Err(error) => { + webidl::throw_error(scope, &error); + None + } + } + } + Ok(None) => Some(ServiceWorkerRegistrationOptions::default()), + Err(error) => { + webidl::throw_error(scope, &error); + None + } + } +} + fn service_worker_scope_url( - scope: &mut v8::PinScope<'_, '_>, document_url: &url::Url, script_url: &url::Url, - args: &v8::FunctionCallbackArguments<'_>, -) -> url::Url { - if args.length() > 1 - && let Ok(options) = v8::Local::::try_from(args.get(1)) - && let Some(scope_value) = options.get(scope, v8str(scope, "scope").into()) - && !scope_value.is_null_or_undefined() - && let Some(scope_string) = scope_value.to_string(scope) - && let Ok(mut scope_url) = document_url.join(&scope_string.to_rust_string_lossy(scope)) - { - scope_url.set_fragment(None); - return scope_url; - } - default_service_worker_scope_url(script_url) -} - -fn service_worker_script_kind( - scope: &mut v8::PinScope<'_, '_>, - args: &v8::FunctionCallbackArguments<'_>, -) -> Result { - if args.length() <= 1 { - return Ok(WorkerScriptKind::Classic); - } - let value = args.get(1); - if value.is_null_or_undefined() { - return Ok(WorkerScriptKind::Classic); - } - let Ok(options) = v8::Local::::try_from(value) else { - return Ok(WorkerScriptKind::Classic); - }; - let Some(type_value) = options.get(scope, v8str(scope, "type").into()) else { - return Ok(WorkerScriptKind::Classic); - }; - if type_value.is_null_or_undefined() { - return Ok(WorkerScriptKind::Classic); - } - let Some(type_string) = type_value.to_string(scope) else { - return Err("failed to parse service worker type"); - }; - match type_string.to_rust_string_lossy(scope).as_str() { - "classic" => Ok(WorkerScriptKind::Classic), - "module" => Ok(WorkerScriptKind::Module), - _ => Err("invalid service worker type"), - } -} - -fn service_worker_update_via_cache( - scope: &mut v8::PinScope<'_, '_>, - args: &v8::FunctionCallbackArguments<'_>, -) -> Result { - if args.length() <= 1 { - return Ok(ServiceWorkerUpdateViaCache::default()); - } - let value = args.get(1); - if value.is_null_or_undefined() { - return Ok(ServiceWorkerUpdateViaCache::default()); - } - let Ok(options) = v8::Local::::try_from(value) else { - return Ok(ServiceWorkerUpdateViaCache::default()); - }; - let Some(update_via_cache_value) = options.get(scope, v8str(scope, "updateViaCache").into()) - else { - return Ok(ServiceWorkerUpdateViaCache::default()); - }; - if update_via_cache_value.is_null_or_undefined() { - return Ok(ServiceWorkerUpdateViaCache::default()); - } - let Some(update_via_cache_string) = update_via_cache_value.to_string(scope) else { - return Err("failed to parse service worker updateViaCache"); - }; - ServiceWorkerUpdateViaCache::parse_webidl_token( - &update_via_cache_string.to_rust_string_lossy(scope), - ) - .ok_or("invalid service worker updateViaCache") -} - -fn service_worker_client_url( - scope: &mut v8::PinScope<'_, '_>, - document_url: &url::Url, - args: &v8::FunctionCallbackArguments<'_>, + requested_scope: Option<&str>, ) -> Option { - let client = if args.length() > 0 && !args.get(0).is_null_or_undefined() { - args.get(0) - .to_string(scope) - .map(|value| value.to_rust_string_lossy(scope))? - } else { - String::new() + let Some(requested_scope) = requested_scope else { + return Some(default_service_worker_scope_url(script_url)); }; - let mut client_url = document_url.join(&client).ok()?; + let mut scope_url = document_url.join(requested_scope).ok()?; + scope_url.set_fragment(None); + Some(scope_url) +} + +fn service_worker_client_url(document_url: &url::Url, client: &str) -> Option { + let mut client_url = document_url.join(client).ok()?; client_url.set_fragment(None); Some(client_url) } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index 2bd8c1830b..da586e8a3b 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -12032,7 +12032,7 @@ async fn navigator_service_worker_shim_controls_window_fetch() { let expected_fetch_url = format!("{base_url}/app/api/data.txt"); let expected_out_of_scope_url = format!("{base_url}/outside/data.txt"); let expected_result = format!( - r#"{{"containerType":"object","controllerIsNull":true,"readyType":"object","serviceWorkerConstructorType":"function","registrationConstructorType":"function","registerType":"function","registerName":"register","registerLength":1,"getRegistrationType":"function","getRegistrationName":"getRegistration","getRegistrationLength":1,"getRegistrationsType":"function","getRegistrationsName":"getRegistrations","getRegistrationsLength":0,"removeEventListenerType":"function","controllerChangeHandlerType":"object","controllerChangeLog":["listener:controllerchange:true","handler:controllerchange:true"],"registration":true,"registrations":"true|0","afterRegisterController":true,"controllerStable":true,"controllerMatchesActive":true,"controllerBrand":true,"registrationBrand":true,"registrationTag":"[object ServiceWorkerRegistration]","installingBrand":false,"installingScriptURL":null,"installingState":null,"waitingIsNull":true,"activeIsNull":false,"activeBrand":true,"activeScriptURL":"{expected_worker_url}","activeState":"activated","registrationScope":"{expected_scope}","readyMatchesRegister":true,"responseStatusText":"Handled by worker","responseText":"sw:/app/api/data.txt","dataResponseStatusText":"OK","dataResponseContentType":"text/plain","dataResponseText":"data-url","outOfScopeResult":"resolved:Handled by worker:sw:/outside/data.txt","firstUnregister":true,"secondUnregister":false,"controllerRetained":true,"controllerStateAfterUnregister":"activated","registrationRemoved":true,"afterUnregisterText":"sw:/app/after-unregister.txt"}}"# + r#"{{"containerType":"object","controllerIsNull":true,"readyType":"object","serviceWorkerConstructorType":"function","registrationConstructorType":"function","registerType":"function","registerName":"register","registerLength":1,"getRegistrationType":"function","getRegistrationName":"getRegistration","getRegistrationLength":0,"getRegistrationsType":"function","getRegistrationsName":"getRegistrations","getRegistrationsLength":0,"removeEventListenerType":"function","controllerChangeHandlerType":"object","controllerChangeLog":["listener:controllerchange:true","handler:controllerchange:true"],"registration":true,"registrations":"true|0","afterRegisterController":true,"controllerStable":true,"controllerMatchesActive":true,"controllerBrand":true,"registrationBrand":true,"registrationTag":"[object ServiceWorkerRegistration]","installingBrand":false,"installingScriptURL":null,"installingState":null,"waitingIsNull":true,"activeIsNull":false,"activeBrand":true,"activeScriptURL":"{expected_worker_url}","activeState":"activated","registrationScope":"{expected_scope}","readyMatchesRegister":true,"responseStatusText":"Handled by worker","responseText":"sw:/app/api/data.txt","dataResponseStatusText":"OK","dataResponseContentType":"text/plain","dataResponseText":"data-url","outOfScopeResult":"resolved:Handled by worker:sw:/outside/data.txt","firstUnregister":true,"secondUnregister":false,"controllerRetained":true,"controllerStateAfterUnregister":"activated","registrationRemoved":true,"afterUnregisterText":"sw:/app/after-unregister.txt"}}"# ); assert_eq!(result, expected_result); @@ -17381,7 +17381,7 @@ async fn navigator_service_worker_update_via_cache_option_reflects_registration( &browser_context_runtime, &loader, "String(globalThis.__serviceWorkerUpdateViaCacheProbe)", - "all|all|Error", + "all|all|TypeError", ) .await; server @@ -22466,6 +22466,60 @@ async fn navigator_service_worker_registry_queries_are_scope_based() { .expect("service worker registry script server should finish"); } +#[test] +fn navigator_service_worker_argument_errors_reject_with_the_original_exception() { + let mut vm = new_storage_test_vm("https://service-worker-webidl.test/"); + vm.eval( + r#" +(() => { + const sw = navigator.serviceWorker; + const marker = {sentinel: true}; + globalThis.serviceWorkerArgumentFailures = []; + globalThis.serviceWorkerArgumentRejections = 0; + function check(callback, expected) { + const promise = callback(); + if (!(promise instanceof Promise)) serviceWorkerArgumentFailures.push('not a Promise'); + promise.then( + () => serviceWorkerArgumentFailures.push('resolved'), + error => { + serviceWorkerArgumentRejections++; + if (expected === marker ? error !== marker : !(error instanceof TypeError)) { + serviceWorkerArgumentFailures.push('wrong rejection reason'); + } + } + ); + } + check(() => sw.register(), TypeError); + check(() => sw.register(Symbol('script')), TypeError); + check(() => sw.register('https://[', {scope: Symbol('scope')}), TypeError); + check(() => sw.getRegistration({toString() { throw marker; }}), marker); + const steps = ['script', 'scope-get', 'scope-string', 'type-get', 'type-string', 'cache-get', 'cache-string']; + for (let index = 0; index < steps.length; index++) { + const log = []; + const step = name => { log.push(name); if (name === steps[index]) throw marker; }; + check(() => sw.register( + {toString() { step('script'); return 'https://['; }}, + { + get scope() { step('scope-get'); return {toString() { step('scope-string'); return './'; }}; }, + get type() { step('type-get'); return {toString() { step('type-string'); return 'classic'; }}; }, + get updateViaCache() { step('cache-get'); return {toString() { step('cache-string'); return 'imports'; }}; } + } + ), marker); + if (JSON.stringify(log) !== JSON.stringify(steps.slice(0, index + 1))) { + serviceWorkerArgumentFailures.push('conversion order: ' + log); + } + } +})() +"#, + ) + .expect("ServiceWorkerContainer conversion errors should return Promises"); + assert_eq!( + vm.eval("JSON.stringify([serviceWorkerArgumentRejections, serviceWorkerArgumentFailures])") + .unwrap(), + "[11,[]]" + ); +} + #[test] fn window_event_source_constructor_exposes_connecting_instance() { let mut vm = new_storage_test_vm("https://example.com/"); diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs index 8b04ca571d..2f73df4443 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs @@ -1134,27 +1134,19 @@ fn service_worker_register_gates_script_url_before_url_resolution() { let mut vm = new_storage_test_vm("https://service-worker-register-trusted-types.test/"); vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]); - let result = vm - .eval( + vm.eval( r#" (() => { - const errorName = callback => { - try { - callback(); - return "none"; - } catch (error) { - return error && error.name; - } - }; + const errorName = promise => promise.then(() => 'resolved', error => error && error.name); const policy = trustedTypes.createPolicy("service-worker-register", { createHTML: value => value, createScriptURL: value => value }); - const blockedString = errorName(() => navigator.serviceWorker.register("worker.js")); - const blockedWrongType = errorName(() => + const blockedString = errorName(navigator.serviceWorker.register("worker.js")); + const blockedWrongType = errorName( navigator.serviceWorker.register(policy.createHTML("worker.js")) ); - const missing = errorName(() => navigator.serviceWorker.register()); + const missing = errorName(navigator.serviceWorker.register()); const trustedPromise = navigator.serviceWorker.register( policy.createScriptURL("http://[") @@ -1171,19 +1163,24 @@ fn service_worker_register_gates_script_url_before_url_resolution() { const defaultPromise = navigator.serviceWorker.register("worker.potato"); defaultPromise.catch(() => {}); - return JSON.stringify({ - blockedString, - blockedWrongType, - missing, - trustedPromise: trustedPromise instanceof Promise, - defaultPromise: defaultPromise instanceof Promise, - defaultCalls + Promise.all([blockedString, blockedWrongType, missing]).then(([blockedString, blockedWrongType, missing]) => { + globalThis.serviceWorkerTrustedUrlResult = { + blockedString, + blockedWrongType, + missing, + trustedPromise: trustedPromise instanceof Promise, + defaultPromise: defaultPromise instanceof Promise, + defaultCalls + }; }); })() "#, ) .expect("ServiceWorkerContainer.register TrustedScriptURL probe should evaluate"); + let result = vm + .eval("JSON.stringify(globalThis.serviceWorkerTrustedUrlResult)") + .expect("ServiceWorkerContainer.register errors should reject their Promises"); assert_eq!( result, r#"{"blockedString":"TypeError","blockedWrongType":"TypeError","missing":"TypeError","trustedPromise":true,"defaultPromise":true,"defaultCalls":[["worker.potato","TrustedScriptURL","ServiceWorkerContainer register"]]}"# @@ -1270,3 +1267,139 @@ fn dom_parser_gates_converted_union_source_after_webidl_argument_conversion() { r#"{"blocked":["TypeError","TypeError","TypeError"],"accepted":["trusted","root"],"defaultValues":["default","null","root"],"sourceConversions":2,"invalidType":"TypeError","invalidTypeSkippedPolicy":true,"symbolSource":"TypeError","defaultCalls":[["source","TrustedHTML","DOMParser parseFromString"],["null","TrustedHTML","DOMParser parseFromString"],["","TrustedHTML","DOMParser parseFromString"]]}"# ); } + +#[test] +fn service_worker_register_converts_options_before_the_default_policy() { + let mut vm = new_storage_test_vm("https://service-worker-register-options.test/"); + vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]); + vm.eval( + r#" +(async () => { + const sw = navigator.serviceWorker; + const marker = {sentinel: true}; + const rows = []; + async function probe(label, callback, log) { + const promise = callback(); + log.push('returned'); + try { + await promise; + rows.push([label, 'fulfilled']); + } catch (error) { + rows.push([label, promise instanceof Promise, error === marker ? 'sentinel' : error.name, [...log]]); + } + } + const options = log => ({ + get scope() { log.push('scope'); return './'; }, + get type() { log.push('type'); return 'classic'; }, + get updateViaCache() { log.push('cache'); return 'imports'; } + }); + let log = []; + await probe('blocked-after-options', () => sw.register( + {toString() { log.push('script'); return 'worker.js'; }}, options(log) + ), log); + log = []; + await probe('options-exception-before-policy', () => sw.register('worker.js', { + get scope() { log.push('scope'); throw marker; } + }), log); + log = []; + await probe('script-exception', () => sw.register( + {toString() { log.push('script'); throw marker; }}, options(log) + ), log); + const policy = trustedTypes.createPolicy('registration-tests', {createScriptURL: v => v}); + const trusted = policy.createScriptURL('https://['); + trusted.toString = () => { throw marker; }; + log = []; + await probe('trusted-input', () => sw.register(trusted, options(log)), log); + let defaultThrows = false; + let policyLog = []; + trustedTypes.createPolicy('default', { + createScriptURL(value, type, sink) { + policyLog.push(['policy', value, type, sink]); + if (defaultThrows) throw marker; + return 'https://['; + } + }); + log = []; policyLog = log; + await probe('default-after-options', () => sw.register( + {toString() { log.push('script'); return 'worker\ud800.js'; }}, options(log) + ), log); + log = []; policyLog = log; defaultThrows = true; + await probe('default-exception', () => sw.register('worker.js', options(log)), log); + log = []; policyLog = log; + await probe('invalid-options-before-default', () => sw.register('worker.js', { + get type() { log.push('type'); return null; } + }), log); + return rows; +})().then(rows => { globalThis.serviceWorkerOptionsResult = rows; }); +"#, + ) + .expect("ServiceWorkerContainer.register argument and policy probes should evaluate"); + let result = vm + .eval("JSON.stringify(globalThis.serviceWorkerOptionsResult)") + .unwrap(); + assert_eq!( + serde_json::from_str::(&result).unwrap(), + serde_json::json!([ + [ + "blocked-after-options", + true, + "TypeError", + ["script", "scope", "type", "cache", "returned"] + ], + [ + "options-exception-before-policy", + true, + "sentinel", + ["scope", "returned"] + ], + ["script-exception", true, "sentinel", ["script", "returned"]], + [ + "trusted-input", + true, + "TypeError", + ["scope", "type", "cache", "returned"] + ], + [ + "default-after-options", + true, + "TypeError", + [ + "script", + "scope", + "type", + "cache", + [ + "policy", + "worker�.js", + "TrustedScriptURL", + "ServiceWorkerContainer register" + ], + "returned" + ] + ], + [ + "default-exception", + true, + "sentinel", + [ + "scope", + "type", + "cache", + [ + "policy", + "worker.js", + "TrustedScriptURL", + "ServiceWorkerContainer register" + ], + "returned" + ] + ], + [ + "invalid-options-before-default", + true, + "TypeError", + ["type", "returned"] + ] + ]) + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs b/moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs index 90009b7c52..d730c0ea5e 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs @@ -190,3 +190,101 @@ if (parentGetLoaded.call(face) !== face.loaded) realmFailures.push('cross-realm "[2,[]]" ); } + +#[test] +fn webidl_receiver_service_worker_operations_reject_before_argument_conversion() { + let mut vm = new_storage_test_vm("https://service-worker-receiver.test/"); + vm.eval( + r#" +(() => { + const sw = navigator.serviceWorker; + const register = sw.register; + const getRegistration = sw.getRegistration; + const getRegistrations = sw.getRegistrations; + globalThis.serviceWorkerReceiverFailures = []; + globalThis.serviceWorkerReceiverRejections = 0; + let conversions = 0; + const script = {toString() { conversions++; return 'https://['; }}; + const options = {get scope() { conversions++; return './'; }}; + function check(promise) { + if (!(promise instanceof Promise)) serviceWorkerReceiverFailures.push('not a Promise'); + promise.then( + () => serviceWorkerReceiverFailures.push('resolved'), + error => { + serviceWorkerReceiverRejections++; + if (!(error instanceof TypeError)) serviceWorkerReceiverFailures.push('wrong error'); + } + ); + } + for (const receiver of [{}, Object.create(sw), new Proxy(sw, {}), null]) { + check(register.call(receiver, script, options)); + check(getRegistration.call(receiver, script)); + check(getRegistrations.call(receiver)); + } + if (conversions !== 0) serviceWorkerReceiverFailures.push('converted invalid receiver arguments'); + const prototype = Object.getPrototypeOf(sw); + Object.setPrototypeOf(sw, null); + check(register.call(sw, script)); + Object.setPrototypeOf(sw, prototype); + if (conversions !== 1) serviceWorkerReceiverFailures.push('lost native receiver identity'); + if (register.length !== 1 || getRegistration.length !== 0 || getRegistrations.length !== 0) { + serviceWorkerReceiverFailures.push('operation length'); + } +})() +"#, + ) + .expect("ServiceWorkerContainer receiver checks should return rejected Promises"); + assert_eq!( + vm.eval("JSON.stringify([serviceWorkerReceiverRejections, serviceWorkerReceiverFailures])") + .unwrap(), + "[13,[]]" + ); +} + +#[test] +fn webidl_receiver_service_worker_promises_use_the_callee_realm() { + let mut vm = new_storage_test_vm("https://service-worker-receiver.test/"); + vm.eval( + r#" +(() => { + const html = document.appendChild(document.createElement('html')); + html.appendChild(document.createElement('body')); + const child = document.body.appendChild(document.createElement('iframe')).contentWindow; + const parentSw = navigator.serviceWorker; + const childSw = child.navigator.serviceWorker; + const marker = {sentinel: true}; + const throwingScript = {toString() { throw marker; }}; + globalThis.serviceWorkerRealmFailures = []; + globalThis.serviceWorkerRealmRejections = 0; + for (const [callback, P, E, expected] of [ + [() => childSw.register.call(parentSw, Symbol('script')), child.Promise, child.TypeError, null], + [() => parentSw.register.call(childSw, Symbol('script')), Promise, TypeError, null], + [() => childSw.getRegistration.call(parentSw, Symbol('client')), child.Promise, child.TypeError, null], + [() => childSw.register.call({}), child.Promise, child.TypeError, null], + [() => childSw.register.call(parentSw, throwingScript), child.Promise, child.TypeError, marker], + [() => parentSw.register.call(childSw, throwingScript), Promise, TypeError, marker] + ]) { + const promise = callback(); + const otherP = P === Promise ? child.Promise : Promise; + const otherE = E === TypeError ? child.TypeError : TypeError; + if (!(promise instanceof P) || promise instanceof otherP) serviceWorkerRealmFailures.push('Promise realm'); + promise.then( + () => serviceWorkerRealmFailures.push('resolved'), + error => { + serviceWorkerRealmRejections++; + if (expected === marker ? error !== marker : (!(error instanceof E) || error instanceof otherE)) { + serviceWorkerRealmFailures.push('rejection identity or realm'); + } + } + ); + } +})() +"#, + ) + .expect("ServiceWorkerContainer cross-realm operations should return Promises"); + assert_eq!( + vm.eval("JSON.stringify([serviceWorkerRealmRejections, serviceWorkerRealmFailures])") + .unwrap(), + "[6,[]]" + ); +} diff --git a/moli-renderer-v8/src/service_worker_runtime/registration.rs b/moli-renderer-v8/src/service_worker_runtime/registration.rs index 3fe562b086..90de169423 100644 --- a/moli-renderer-v8/src/service_worker_runtime/registration.rs +++ b/moli-renderer-v8/src/service_worker_runtime/registration.rs @@ -9,7 +9,11 @@ use super::{ pub(crate) const DEFAULT_NAVIGATION_PRELOAD_HEADER_VALUE: &str = "true"; -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, crate::webidl::WebIdlEnum)] +#[webidl( + name = "ServiceWorkerUpdateViaCache", + parse_with = Self::parse_webidl_token +)] pub(crate) enum ServiceWorkerUpdateViaCache { #[default] Imports,