From 97ec28a0fe030579c976a2c2e2f079287e907f0a Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 13 Sep 2026 05:45:54 +0800 Subject: [PATCH] fix(webidl): honor initializer iterators on platform objects Use sequence/record discrimination for native Headers, URLSearchParams, FormData, and URL initializers. Read the author iterator once, preserve its result or exception, and use own record properties when it is absent. Cover Request and Response headers, enumerable iterator symbols, and FormData File conversion. Remove unused imports and the URL stringification helper. --- .../context_bootstrap/form_data_runtime.rs | 2 +- .../src/context_bootstrap/url_form.rs | 5 +- .../src/context_bootstrap/url_form/helpers.rs | 15 -- .../url_search_params_runtime.rs | 5 +- .../url_search_params_runtime/parse.rs | 24 --- .../src/network_host/headers/store/init.rs | 6 +- .../src/script_vm/tests/webidl_fetch.rs | 138 ++++++++++++++++++ 7 files changed, 142 insertions(+), 53 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/form_data_runtime.rs b/moli-renderer-v8/src/context_bootstrap/form_data_runtime.rs index e43fba0a44..23443c310d 100644 --- a/moli-renderer-v8/src/context_bootstrap/form_data_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/form_data_runtime.rs @@ -15,7 +15,7 @@ pub(crate) use request_body::{ form_data_entries_multipart_body_with_prefix, form_data_request_body, }; pub(crate) use serialize::form_data_entries_to_string_pairs; -pub(in crate::context_bootstrap) use storage::{form_data_entries, form_data_is_object}; +pub(in crate::context_bootstrap) use storage::form_data_is_object; pub(super) use template::build_form_data_constructor_template; pub(crate) fn form_data_object_from_entries<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/url_form.rs b/moli-renderer-v8/src/context_bootstrap/url_form.rs index c9fb922a76..e34d86f9d1 100644 --- a/moli-renderer-v8/src/context_bootstrap/url_form.rs +++ b/moli-renderer-v8/src/context_bootstrap/url_form.rs @@ -6,8 +6,5 @@ mod callbacks; mod helpers; mod template; -pub(super) use helpers::{ - apply_url_update, callback_arg_url_like_string, callback_value_string, url_href_slot, - url_object_value, -}; +pub(super) use helpers::{apply_url_update, callback_value_string, url_object_value}; pub(super) use template::build_url_constructor_template; diff --git a/moli-renderer-v8/src/context_bootstrap/url_form/helpers.rs b/moli-renderer-v8/src/context_bootstrap/url_form/helpers.rs index a922ada112..37957893e3 100644 --- a/moli-renderer-v8/src/context_bootstrap/url_form/helpers.rs +++ b/moli-renderer-v8/src/context_bootstrap/url_form/helpers.rs @@ -11,21 +11,6 @@ pub(in crate::context_bootstrap) fn callback_value_string( .map(|value| value.to_rust_string_lossy(scope)) } -pub(in crate::context_bootstrap) fn callback_arg_url_like_string<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, -) -> Option { - if value.is_null_or_undefined() { - return None; - } - if let Ok(object) = v8::Local::::try_from(value) - && let Some(href) = url_href_slot(scope, object) - { - return Some(href); - } - callback_value_string(scope, value) -} - pub(super) fn resolve_url_constructor_input( input: &str, base: Option<&str>, diff --git a/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime.rs b/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime.rs index a719bf1fde..82f9a469b4 100644 --- a/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime.rs @@ -1,7 +1,4 @@ -use super::form_data_runtime::{form_data_entries, form_data_is_object}; -use super::url_form::{ - apply_url_update, callback_arg_url_like_string, callback_value_string, url_object_value, -}; +use super::url_form::{apply_url_update, callback_value_string, url_object_value}; use super::*; use crate::util::{get_private_object, get_private_value, set_private_value}; use crate::web_api_interfaces; diff --git a/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime/parse.rs b/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime/parse.rs index 963c30a751..a79e6efcf8 100644 --- a/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime/parse.rs +++ b/moli-renderer-v8/src/context_bootstrap/url_search_params_runtime/parse.rs @@ -1,6 +1,4 @@ -use super::storage::{url_search_params_is_object, url_search_params_pairs}; use super::*; -use crate::context_bootstrap::url_form::url_href_slot; use crate::webidl; use moli_url::search_params::{SearchParamPair, parse_search_params}; @@ -38,17 +36,6 @@ pub(super) fn url_search_params_pairs_from_constructor<'s>( return Some(Vec::new()); } if let Ok(object) = v8::Local::::try_from(value) { - if form_data_is_object(scope, object) { - return Some( - form_data_entries(scope, object) - .into_iter() - .filter_map(|(key, value)| { - callback_value_string(scope, v8::Local::new(scope, &value)) - .map(|value| (key, value)) - }) - .collect(), - ); - } let sequence = match webidl::convert_optional_sequence::( scope, value, @@ -64,17 +51,6 @@ pub(super) fn url_search_params_pairs_from_constructor<'s>( if let Some(sequence) = sequence { return Some(sequence.0.into_iter().map(|pair| pair.0).collect()); } - if url_search_params_is_object(scope, object) { - return Some(url_search_params_pairs(scope, object)); - } - if url_href_slot(scope, object).is_some() { - return Some( - callback_arg_url_like_string(scope, value) - .as_deref() - .map(parse_search_params) - .unwrap_or_default(), - ); - } return record_string_pairs(scope, object.into()); } Some( diff --git a/moli-renderer-v8/src/network_host/headers/store/init.rs b/moli-renderer-v8/src/network_host/headers/store/init.rs index 43ac8c3724..43028e2a66 100644 --- a/moli-renderer-v8/src/network_host/headers/store/init.rs +++ b/moli-renderer-v8/src/network_host/headers/store/init.rs @@ -1,4 +1,4 @@ -use super::entries::{headers_entries_if_present, normalized_header_entry_or_throw}; +use super::entries::normalized_header_entry_or_throw; use crate::webidl; pub(in crate::network_host) fn headers_entries_from_init<'s>( @@ -24,10 +24,6 @@ pub(in crate::network_host) fn headers_entries_from_init<'s>( return Ok(entries); } - if let Some(entries) = headers_entries_if_present(scope, init_obj) { - return Ok(entries); - } - let record = webidl::convert::>( scope, init_obj.into(), diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs index 48a28c5baf..e97e0ac1f4 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs @@ -6708,6 +6708,144 @@ fn vtt_cue_constructor_applies_webidl_conversion() { ); } +#[test] +fn webidl_initializer_unions_observe_iterators_on_platform_objects() { + let mut vm = new_storage_test_vm("https://initializer-union.test/"); + let result = vm.eval(r#" +(() => { + const check = (condition, label) => { if (!condition) throw new Error(label); }; + const equal = (actual, expected, label) => check( + JSON.stringify(actual) === JSON.stringify(expected), label + ': ' + JSON.stringify(actual)); + const factories = [ + ['Headers', () => new Headers([['x-original', 'original']])], + ['URLSearchParams', () => new URLSearchParams([['x-original', 'original']])], + ['FormData', () => { + const form = new FormData(); + form.append('x-original', 'original'); + return form; + }] + ]; + const consumers = [ + ['Headers', input => new Headers(input)], + ['URLSearchParams', input => new URLSearchParams(input)], + ['Request', input => new Request('https://initializer-union.test/', {headers: input}).headers], + ['Response', input => new Response(null, {headers: input}).headers] + ]; + for (const [target, create] of consumers) { + for (const [source, factory] of factories) { + for (const mode of ['original', 'custom', 'undefined', 'null', 'throw', 'noncallable']) { + const label = target + ' from ' + source + ' with ' + mode + ' iterator'; + const input = factory(); + const original = input[Symbol.iterator]; + const marker = {}; + const log = []; + Object.defineProperty(input, 'x-record', {enumerable: true, get() { + log.push('record'); + return 'record'; + }}); + Object.defineProperty(input, Symbol.iterator, {get() { + log.push('iterator'); + if (mode === 'throw') throw marker; + if (mode === 'noncallable') return 42; + if (mode === 'undefined') return undefined; + if (mode === 'null') return null; + if (mode === 'original') return original; + return function*() { + check(this === input, label + ' receiver'); + log.push('iterate'); + yield ['x-custom', 'custom']; + }; + }}); + if (mode === 'throw' || mode === 'noncallable') { + let caught; + try { create(input); } catch (error) { caught = error; } + check(mode === 'throw' ? caught === marker : caught instanceof TypeError, label + ' exception'); + } else { + const expected = mode === 'original' ? [['x-original', 'original']] : + mode === 'custom' ? [['x-custom', 'custom']] : [['x-record', 'record']]; + equal(Array.from(create(input)), expected, label); + } + const expectedLog = ['iterator']; + if (mode === 'custom') expectedLog.push('iterate'); + if (mode === 'undefined' || mode === 'null') expectedLog.push('record'); + equal(log, expectedLog, label + ' conversion order'); + } + } + } + return 'ok'; +})() +"#).unwrap(); + assert_eq!(result, "ok"); +} + +#[test] +fn webidl_initializer_unions_convert_platform_objects_without_iterators_as_records() { + let mut vm = new_storage_test_vm("https://initializer-record.test/"); + let result = vm.eval(r#" +(() => { + for (const Constructor of [Headers, URLSearchParams]) { + const url = new URL('https://initializer-record.test/path?query=value'); + Object.defineProperty(url, Symbol.toPrimitive, {value() { + throw new Error('URL object must use record conversion'); + }}); + if (Array.from(new Constructor(url)).length !== 0) throw new Error('empty URL record'); + url['x-record'] = 'record'; + if (JSON.stringify(Array.from(new Constructor(url))) !== '[["x-record","record"]]') { + throw new Error(Constructor.name + ' must use own URL properties'); + } + for (const factory of [() => new Headers(), () => new URLSearchParams(), () => new FormData()]) { + for (const value of [undefined, null]) { + const input = factory(); + let iteratorReads = 0; + Object.defineProperty(input, Symbol.iterator, {enumerable: true, get() { + iteratorReads++; + return value; + }}); + let caught; + try { new Constructor(input); } catch (error) { caught = error; } + if (!(caught instanceof TypeError) || iteratorReads !== 1) { + throw new Error(Constructor.name + ' must reject the enumerable Symbol key before reading its value'); + } + } + } + } + return 'ok'; +})() +"#).unwrap(); + assert_eq!(result, "ok"); +} + +#[test] +fn url_search_params_form_data_initializers_convert_file_values_through_the_iterator() { + let mut vm = new_storage_test_vm("https://form-data-initializer.test/"); + let result = vm.eval(r#" +(() => { + const file = new File(['contents'], 'entry.txt'); + const form = new FormData(); + form.append('a', 'first'); + form.append('file', file); + form.append('a', 'second'); + const params = new URLSearchParams(form); + form.append('later', 'ignored'); + if (JSON.stringify(Array.from(params)) !== '[["a","first"],["file","[object File]"],["a","second"]]') { + throw new Error('FormData entries must be copied in iteration order with File stringification'); + } + const marker = {}; + let conversions = 0; + Object.defineProperty(file, Symbol.toPrimitive, {value(hint) { + if (hint !== 'string') throw new Error('USVString must use the string hint'); + conversions++; + throw marker; + }}); + let caught; + try { new URLSearchParams(form); } catch (error) { caught = error; } + if (caught !== marker || conversions !== 1) throw new Error('File conversion exception must propagate'); + return 'ok'; +})() +"#).unwrap(); + assert_eq!(result, "ok"); +} + #[test] fn webidl_string_records_observe_descriptors_and_values_in_key_order() { let mut vm = new_storage_test_vm("https://webidl-record-order.test/");