From dcf8cb94ee7cc8609efa62e01fa0b92afa59e4a5 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 12 Oct 2026 06:50:47 +0800 Subject: [PATCH] feat(webidl): support lossless DOMString16 in derive bindings --- moli-webidl-derive/src/converter.rs | 9 +++- moli-webidl/tests/dom_string16.rs | 69 ++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/moli-webidl-derive/src/converter.rs b/moli-webidl-derive/src/converter.rs index af76f0d905..b2a9f43442 100644 --- a/moli-webidl-derive/src/converter.rs +++ b/moli-webidl-derive/src/converter.rs @@ -8,6 +8,7 @@ use crate::attrs::FieldAttrs; pub(crate) enum ConverterKind { #[default] DomString, + DomString16, UsvString, ByteString, BufferSource, @@ -45,6 +46,7 @@ impl ConverterKind { pub(crate) fn from_lit(value: &LitStr) -> Result { match value.value().as_str() { "dom_string" => Ok(Self::DomString), + "dom_string16" => Ok(Self::DomString16), "usv_string" => Ok(Self::UsvString), "byte_string" => Ok(Self::ByteString), "buffer_source" => Ok(Self::BufferSource), @@ -80,6 +82,7 @@ impl ConverterKind { pub(crate) fn wrapper_type(self, ty: &Type) -> proc_macro2::TokenStream { match self { Self::DomString => quote!(::moli_webidl::DomString), + Self::DomString16 => quote!(::moli_webidl::DomString16), Self::UsvString => quote!(::moli_webidl::UsvString), Self::ByteString => quote!(::moli_webidl::ByteString), Self::BufferSource => quote!(::moli_webidl::BufferSource), @@ -120,6 +123,9 @@ impl ConverterKind { Self::DomString => { quote!(::moli_webidl::DomString(::std::convert::Into::into(#expr))) } + Self::DomString16 => { + quote!(::moli_webidl::DomString16(::std::convert::Into::into(#expr))) + } Self::UsvString => { quote!(::moli_webidl::UsvString(::std::convert::Into::into(#expr))) } @@ -165,6 +171,7 @@ impl ConverterKind { Self::BufferSource => quote!(::std::convert::Into::into(#value)), Self::CallbackFunction | Self::CallbackInterface => value, Self::DomString + | Self::DomString16 | Self::UsvString | Self::Octet | Self::Long @@ -213,7 +220,7 @@ impl ConverterKind { } if attrs.treat_null_as_empty_string { match self { - Self::DomString | Self::UsvString | Self::ByteString => { + Self::DomString | Self::DomString16 | Self::UsvString | Self::ByteString => { return Ok(quote!(::moli_webidl::StringOptions { treat_null_as_empty_string: true, })); diff --git a/moli-webidl/tests/dom_string16.rs b/moli-webidl/tests/dom_string16.rs index 2efa4f9077..87cafa53f5 100644 --- a/moli-webidl/tests/dom_string16.rs +++ b/moli-webidl/tests/dom_string16.rs @@ -1,5 +1,49 @@ use moli_webidl::{Context, DomString, DomString16, StringOptions, UsvString}; +#[derive(moli_webidl::WebIdlArgs)] +#[webidl(prefix = "derivedLossless")] +struct DerivedLosslessArgs { + #[webidl(required, converter = "dom_string16")] + value: Vec, + #[webidl(nullable, converter = "dom_string16")] + optional: Option>, + #[webidl(converter = "dom_string16", default = vec![0xdfff])] + defaulted: Vec, + #[webidl(converter = "dom_string16", treat_null_as_empty_string, default = Vec::new())] + null_as_empty: Vec, + #[webidl(variadic, converter = "dom_string16")] + tail: Vec>, +} + +fn derived_lossless<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s>, +) { + let Some(parsed) = moli_webidl::parse_args::(scope, &args) else { + return; + }; + let mut values = Vec::new(); + for units in [ + Some(parsed.value), + parsed.optional, + Some(parsed.defaulted), + Some(parsed.null_as_empty), + ] + .into_iter() + .chain(parsed.tail.into_iter().map(Some)) + { + let value = match units { + Some(units) => v8::String::new_from_two_byte(scope, &units, v8::NewStringType::Normal) + .unwrap() + .into(), + None => v8::null(scope).into(), + }; + values.push(value); + } + rv.set(v8::Array::new_with_elements(scope, &values).into()); +} + fn lossless<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -47,13 +91,19 @@ fn dom_string16_preserves_code_units_and_webidl_conversion_semantics() { let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let callee = v8::Context::new(scope, Default::default()); - let (lossless_function, utf8_function, type_error) = { + let (lossless_function, utf8_function, derived_function, type_error) = { let scope = &mut v8::ContextScope::new(scope, callee); let lossless_function = v8::Function::new(scope, lossless).unwrap(); let utf8_function = v8::Function::new(scope, utf8_boundary).unwrap(); + let derived_function = v8::Function::new(scope, derived_lossless).unwrap(); let key = v8::String::new(scope, "TypeError").unwrap(); let type_error = callee.global(scope).get(scope, key.into()).unwrap(); - (lossless_function, utf8_function, type_error) + ( + lossless_function, + utf8_function, + derived_function, + type_error, + ) }; let caller = v8::Context::new(scope, Default::default()); let scope = &mut v8::ContextScope::new(scope, caller); @@ -61,6 +111,7 @@ fn dom_string16_preserves_code_units_and_webidl_conversion_semantics() { for (name, value) in [ ("lossless", lossless_function.into()), ("utf8Boundary", utf8_function.into()), + ("derivedLossless", derived_function.into()), ("CalleeTypeError", type_error), ] { let key = v8::String::new(scope, name).unwrap(); @@ -97,6 +148,20 @@ fn dom_string16_preserves_code_units_and_webidl_conversion_semantics() { assert(utf8Boundary('\ud800x\udfff') === '\ufffdx\ufffd', 'existing Rust String boundary stays lossy'); assert(utf8Boundary('\ud800x\udfff', true) === '\ufffdx\ufffd', 'USVString applies scalar replacement'); assert(utf8Boundary('\ud83d\ude00') === '\ud83d\ude00', 'UTF-8 boundary preserves valid pairs'); + const same = (actual, expected) => actual.length === expected.length && actual.every((value, i) => value === expected[i]); + for (const value of ['\ud800', '\udfff', '\ud800x\udfff', '\ud83d\ude00', '\0']) { + assert(same(derivedLossless(value, value, value, value, value, value), Array(6).fill(value)), 'derived required/optional/variadic UTF-16'); + } + assert(same(derivedLossless(null, null, undefined, null), ['null', null, '\udfff', '']), 'derived nullable/default/null-as-empty'); + assert(same(derivedLossless(undefined, undefined, undefined, undefined), ['undefined', null, '\udfff', '']), 'optional undefined uses defaults'); + assert(thrown(() => derivedLossless()) instanceof CalleeTypeError, 'required arity'); + const derivedConversions = []; + const convert = label => ({toString() { derivedConversions.push(label); return '\ud800' + label; }}); + const derived = derivedLossless(convert('one'), null, undefined, null, convert('two'), convert('three')); + assert(same(derivedConversions, ['one', 'two', 'three']) && same(derived, ['\ud800one', null, '\udfff', '', '\ud800two', '\ud800three']), 'derived variadic conversion ordering'); + derivedConversions.length = 0; + assert(thrown(() => derivedLossless('x', null, undefined, null, convert('one'), {toString() {throw sentinel; }}, convert('last'))) === sentinel && same(derivedConversions, ['one']), 'derived original exception stops remaining conversions'); + assert(thrown(() => derivedLossless('x', null, undefined, null, Symbol())) instanceof CalleeTypeError, 'derived conversion TypeError uses callee realm'); return true; })() "#).unwrap();