fix(service-worker): use shared WebIDL promise bindings

This commit is contained in:
ldm0
2026-09-24 06:34:29 +08:00
parent c3b795bff2
commit 282455c8b7
6 changed files with 455 additions and 142 deletions
@@ -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)]
@@ -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<String>,
#[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::<v8::Object>::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::<ServiceWorkerGetRegistrationArgs>(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<ServiceWorkerRegistrationOptions> {
let context = webidl::Context::argument("ServiceWorkerContainer.register", 2);
match webidl::dictionary_arg(args, 1, context) {
Ok(Some(options)) => {
match webidl::parse_dictionary_object::<ServiceWorkerRegistrationOptions>(
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::<v8::Object>::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<WorkerScriptKind, &'static str> {
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::<v8::Object>::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<ServiceWorkerUpdateViaCache, &'static str> {
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::<v8::Object>::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<url::Url> {
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<url::Url> {
let mut client_url = document_url.join(client).ok()?;
client_url.set_fragment(None);
Some(client_url)
}
@@ -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/");
@@ -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"],["<root/>","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::<serde_json::Value>(&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"]
]
])
);
}
@@ -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,[]]"
);
}
@@ -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,