From c988e617fe3525ae5dedd1d8eb2d3d2ff4e2f3da Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 16 Sep 2026 15:56:50 +0800 Subject: [PATCH] fix(webidl): preserve record conversion order --- .../src/script_vm/tests/webidl_fetch.rs | 58 ++++++++++++++++ moli-webidl/src/convert.rs | 67 ++++++++++++++++--- 2 files changed, 115 insertions(+), 10 deletions(-) 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 c815e49eb8..3514413298 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs @@ -6811,6 +6811,64 @@ fn webidl_initializer_records_reject_enumerable_symbol_keys() { assert_eq!(result, "ok"); } +#[test] +fn webidl_initializer_records_interleave_descriptor_and_value_access() { + let mut vm = new_storage_test_vm("https://initializer-record-order.test/"); + let result = vm + .eval( + r#" +(() => { + const equal = (actual, expected, label) => { + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(label + ': ' + JSON.stringify(actual)); + } + }; + const consumers = [ + ['Headers', input => Array.from(new Headers(input)).map(pair => pair.join('=')).join('&')], + ['URLSearchParams', input => new URLSearchParams(input).toString()], + ]; + for (const [target, create] of consumers) { + const log = []; + const record = {}; + Object.defineProperties(record, { + a: {value: '1', enumerable: true, configurable: true}, + b: {value: '2', enumerable: true, configurable: true}, + }); + const input = new Proxy(record, { + get(object, key, receiver) { + log.push(['get', key === Symbol.iterator ? '@@iterator' : String(key)]); + if (key === Symbol.iterator) return undefined; + if (key === 'a') { + Object.defineProperty(object, 'b', {enumerable: false}); + } + return Reflect.get(object, key, receiver); + }, + ownKeys(object) { + log.push(['ownKeys']); + return Reflect.ownKeys(object); + }, + getOwnPropertyDescriptor(object, key) { + log.push(['descriptor', String(key)]); + return Reflect.getOwnPropertyDescriptor(object, key); + }, + }); + equal(create(input), 'a=1', target + ' result'); + equal(log, [ + ['get', '@@iterator'], + ['ownKeys'], + ['descriptor', 'a'], + ['get', 'a'], + ['descriptor', 'b'], + ], target + ' operation order'); + } + return 'ok'; +})() +"#, + ) + .unwrap(); + assert_eq!(result, "ok"); +} + #[test] fn webidl_initializer_unions_convert_url_objects_as_records() { let mut vm = new_storage_test_vm("https://initializer-record.test/"); diff --git a/moli-webidl/src/convert.rs b/moli-webidl/src/convert.rs index c7654b7717..b770527221 100644 --- a/moli-webidl/src/convert.rs +++ b/moli-webidl/src/convert.rs @@ -378,7 +378,7 @@ where Ok(Some(Sequence(values))) } -// Records are converted from own property names and then property values. If two +// Records are converted from own property keys and then property values. If two // JavaScript keys become the same WebIDL key after key conversion, the later // property wins, matching the WebIDL record replacement behavior. impl<'s, K, V> WebIdlConverter<'s> for Record @@ -398,12 +398,15 @@ where ) -> Result { let object = v8::Local::::try_from(value) .map_err(|_| WebIdlError::new(context, WebIdlErrorKind::CannotConvert("record")))?; - let properties = own_property_names(scope, object, context)?; - let mut entries: Vec<(K, V)> = Vec::with_capacity(properties.length() as usize); - for index in 0..properties.length() { - let key_value = properties.get_index(scope, index).ok_or_else(|| { + let keys = record_own_keys(scope, object, context)?; + let mut entries: Vec<(K, V)> = Vec::with_capacity(keys.length() as usize); + for index in 0..keys.length() { + let key_value = keys.get_index(scope, index).ok_or_else(|| { WebIdlError::new(context, WebIdlErrorKind::CannotConvert("record")) })?; + if !record_property_is_enumerable(scope, object, key_value, context)? { + continue; + } let key = K::convert(scope, key_value, context, &K::Options::default())?; let value = record_property_value(scope, object, key_value, context)?; let value = V::convert(scope, value, context, &V::Options::default())?; @@ -1200,18 +1203,19 @@ fn call_sequence_function<'s>( } } -fn own_property_names<'s>( +fn record_own_keys<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, context: Context, ) -> Result, WebIdlError> { let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); let mut scope = try_catch.init(); - // WebIDL record conversion starts with [[OwnPropertyKeys]], including symbols, - // and then considers only enumerable properties. V8's default arguments skip - // symbols, which would bypass the record key conversion that rejects them. + // WebIDL record conversion starts with [[OwnPropertyKeys]]. Enumerability is + // checked later, one key at a time, so Proxy descriptor traps stay interleaved + // with key and value conversion. let property_names_args = v8::GetPropertyNamesArgsBuilder::new() - .property_filter(v8::PropertyFilter::ONLY_ENUMERABLE) + .property_filter(v8::PropertyFilter::ALL_PROPERTIES) + .key_conversion(v8::KeyConversionMode::ConvertToString) .build(); match object.get_own_property_names(&scope, property_names_args) { Some(properties) => Ok(properties), @@ -1226,6 +1230,49 @@ fn own_property_names<'s>( } } +fn record_property_is_enumerable<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + key: v8::Local<'s, v8::Value>, + context: Context, +) -> Result { + let key = v8::Local::::try_from(key) + .map_err(|_| WebIdlError::new(context, WebIdlErrorKind::CannotConvert("record")))?; + let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); + let mut scope = try_catch.init(); + let descriptor = match object.get_own_property_descriptor(&scope, key) { + Some(descriptor) => descriptor, + None if scope.has_caught() => { + let _ = scope.rethrow(); + return Err(WebIdlError::pending_exception(context)); + } + None => { + return Err(WebIdlError::new( + context, + WebIdlErrorKind::CannotConvert("record"), + )); + } + }; + if descriptor.is_undefined() { + return Ok(false); + } + let descriptor = v8::Local::::try_from(descriptor) + .map_err(|_| WebIdlError::new(context, WebIdlErrorKind::CannotConvert("record")))?; + let enumerable_key = v8::String::new(&scope, "enumerable") + .ok_or_else(|| WebIdlError::new(context, WebIdlErrorKind::CannotConvert("record")))?; + match descriptor.get(&scope, enumerable_key.into()) { + Some(enumerable) => Ok(enumerable.boolean_value(&scope)), + None if scope.has_caught() => { + let _ = scope.rethrow(); + Err(WebIdlError::pending_exception(context)) + } + None => Err(WebIdlError::new( + context, + WebIdlErrorKind::CannotConvert("record"), + )), + } +} + fn record_property_value<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>,