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.
This commit is contained in:
ldm0
2026-09-13 17:17:16 +08:00
parent 242aee9dcc
commit 97ec28a0fe
7 changed files with 142 additions and 53 deletions
@@ -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>(
@@ -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;
@@ -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<String> {
if value.is_null_or_undefined() {
return None;
}
if let Ok(object) = v8::Local::<v8::Object>::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>,
@@ -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;
@@ -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::<v8::Object>::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::<UrlSearchParamsSequencePair>(
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(
@@ -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::<webidl::Record<webidl::ByteString, webidl::ByteString>>(
scope,
init_obj.into(),
@@ -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/");