From 4fb35a340e3dd773d2f805c5c4e6bed2e8e71b69 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 10 Oct 2026 09:02:38 +0800 Subject: [PATCH] feat(webidl): add frame integer, dictionary sequence and row conversions --- moli-webidl-derive/src/converter.rs | 9 ++ moli-webidl-derive/src/lib.rs | 1 + moli-webidl/src/buffer_source.rs | 17 +++- moli-webidl/src/convert.rs | 13 +++ moli-webidl/src/lib.rs | 6 +- moli-webidl/src/types.rs | 9 ++ moli-webidl/tests/long_long.rs | 145 ++++++++++++++++++++++++++++ 7 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 moli-webidl/tests/long_long.rs diff --git a/moli-webidl-derive/src/converter.rs b/moli-webidl-derive/src/converter.rs index d52e04ddbb..6242d7f156 100644 --- a/moli-webidl-derive/src/converter.rs +++ b/moli-webidl-derive/src/converter.rs @@ -18,6 +18,7 @@ pub(crate) enum ConverterKind { Octet, Long, EnforceRangeLong, + LongLong, EnforceRangeLongLong, Short, UnsignedShort, @@ -56,6 +57,7 @@ impl ConverterKind { "octet" => Ok(Self::Octet), "long" => Ok(Self::Long), "enforce_range_long" => Ok(Self::EnforceRangeLong), + "long_long" => Ok(Self::LongLong), "enforce_range_long_long" => Ok(Self::EnforceRangeLongLong), "short" => Ok(Self::Short), "unsigned_short" => Ok(Self::UnsignedShort), @@ -71,6 +73,7 @@ impl ConverterKind { "unrestricted_double" => Ok(Self::UnrestrictedDouble), "boolean" => Ok(Self::Boolean), "enum" => Ok(Self::Enum), + "dictionary" => Ok(Self::Dictionary), "raw" => Ok(Self::Raw), _ => Err(Error::new(value.span(), "unsupported converter kind")), } @@ -91,6 +94,7 @@ impl ConverterKind { Self::Octet => quote!(::moli_webidl::Octet), Self::Long => quote!(::moli_webidl::Long), Self::EnforceRangeLong => quote!(::moli_webidl::EnforceRangeLong), + Self::LongLong => quote!(::moli_webidl::LongLong), Self::EnforceRangeLongLong => quote!(::moli_webidl::EnforceRangeLongLong), Self::Short => quote!(::moli_webidl::Short), Self::UnsignedShort => quote!(::moli_webidl::UnsignedShort), @@ -137,6 +141,7 @@ impl ConverterKind { Self::Octet => quote!(::moli_webidl::Octet(#expr)), Self::Long => quote!(::moli_webidl::Long(#expr)), Self::EnforceRangeLong => quote!(::moli_webidl::EnforceRangeLong(#expr)), + Self::LongLong => quote!(::moli_webidl::LongLong(#expr)), Self::EnforceRangeLongLong => quote!(::moli_webidl::EnforceRangeLongLong(#expr)), Self::Short => quote!(::moli_webidl::Short(#expr)), Self::UnsignedShort => quote!(::moli_webidl::UnsignedShort(#expr)), @@ -176,6 +181,7 @@ impl ConverterKind { | Self::Octet | Self::Long | Self::EnforceRangeLong + | Self::LongLong | Self::EnforceRangeLongLong | Self::Short | Self::UnsignedShort @@ -404,6 +410,9 @@ fn infer_converter_kind(ty: &Type) -> Option { if is_type_ident(ty, "i16") { return Some(ConverterKind::Short); } + if is_type_ident(ty, "i64") { + return Some(ConverterKind::LongLong); + } if is_type_ident(ty, "u64") { return Some(ConverterKind::UnsignedLongLong); } diff --git a/moli-webidl-derive/src/lib.rs b/moli-webidl-derive/src/lib.rs index 8b6a517110..2c5f0fa67d 100644 --- a/moli-webidl-derive/src/lib.rs +++ b/moli-webidl-derive/src/lib.rs @@ -20,6 +20,7 @@ use syn::{DeriveInput, parse_macro_input}; /// values parse an empty dictionary; optional fields skip missing/undefined. /// `#[webidl(sequence)]` converts one iterable to `Vec`, using inferred or /// explicit item conversion, including `interface` and `brand_check` metadata. +/// `sequence, converter = "dictionary"` converts each item as a dictionary. /// Uint8Array fields support `allow_shared` for fixed shared backing storage. #[proc_macro_derive(WebIdlArgs, attributes(webidl))] pub fn derive_webidl_args(input: TokenStream) -> TokenStream { diff --git a/moli-webidl/src/buffer_source.rs b/moli-webidl/src/buffer_source.rs index 3e093800d2..199a5d70c0 100644 --- a/moli-webidl/src/buffer_source.rs +++ b/moli-webidl/src/buffer_source.rs @@ -165,7 +165,20 @@ impl AllowSharedBufferSource<'_> { /// Copy into the native view's byte range, preserving surrounding bytes. pub fn write_bytes(&self, scope: &mut v8::PinScope<'_, '_>, bytes: &[u8]) -> bool { - if bytes.len() > self.byte_length() { + self.write_bytes_at(scope, 0, bytes) + } + + /// Copy a row without touching plane padding or other shared-memory bytes. + pub fn write_bytes_at( + &self, + scope: &mut v8::PinScope<'_, '_>, + start: usize, + bytes: &[u8], + ) -> bool { + if start + .checked_add(bytes.len()) + .is_none_or(|end| end > self.byte_length()) + { return false; } if bytes.is_empty() { @@ -174,7 +187,7 @@ impl AllowSharedBufferSource<'_> { let backing = self .backing_store(scope) .expect("BufferSource has native backing"); - let offset = self.byte_offset(); + let offset = self.byte_offset() + start; if backing.is_shared() { // SAFETY: fixed shared storage is retained and V8 validated the // view range; byte stores are unordered as required for BufferSource. diff --git a/moli-webidl/src/convert.rs b/moli-webidl/src/convert.rs index 6231894719..8e022dd84a 100644 --- a/moli-webidl/src/convert.rs +++ b/moli-webidl/src/convert.rs @@ -346,6 +346,19 @@ impl<'s> WebIdlConverter<'s> for crate::ClampedUnsignedLong { } } +impl<'s> WebIdlConverter<'s> for crate::LongLong { + type Options = (); + fn convert( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + context: Context, + _: &(), + ) -> Result { + number_value(scope, value, context, "long long") + .map(|value| Self(unsigned_long_long(value) as i64)) + } +} + impl<'s> WebIdlConverter<'s> for EnforceRangeLongLong { type Options = (); diff --git a/moli-webidl/src/lib.rs b/moli-webidl/src/lib.rs index 7540cf9a7a..66d848346a 100644 --- a/moli-webidl/src/lib.rs +++ b/moli-webidl/src/lib.rs @@ -101,9 +101,9 @@ pub use types::{ Boolean, BufferSource, ByteString, ClampedUnsignedLong, ClampedUnsignedShort, Dictionary, DomString, DomString16, Double, EnforceRangeLong, EnforceRangeLongLong, EnforceRangeUnsignedLong, EnforceRangeUnsignedLongLong, EnumValue, EventListenerOptions, - InterfaceObject, InterfaceOptions, Long, Octet, Record, Sequence, Short, StringOptions, - Uint8ArrayOptions, UnrestrictedDouble, UnrestrictedFloat, UnsignedLong, UnsignedLongLong, - UnsignedShort, UsvString, + InterfaceObject, InterfaceOptions, Long, LongLong, Octet, Record, Sequence, Short, + StringOptions, Uint8ArrayOptions, UnrestrictedDouble, UnrestrictedFloat, UnsignedLong, + UnsignedLongLong, UnsignedShort, UsvString, }; /// Restricted WebIDL float represented as a finite binary32 value. diff --git a/moli-webidl/src/types.rs b/moli-webidl/src/types.rs index 1a0ba93ff4..9ccbbeee08 100644 --- a/moli-webidl/src/types.rs +++ b/moli-webidl/src/types.rs @@ -320,6 +320,15 @@ impl From for u16 { } #[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct LongLong(pub i64); + +impl From for i64 { + fn from(value: LongLong) -> Self { + value.0 + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct EnforceRangeLongLong(pub i64); impl From for i64 { diff --git a/moli-webidl/tests/long_long.rs b/moli-webidl/tests/long_long.rs new file mode 100644 index 0000000000..21b221e515 --- /dev/null +++ b/moli-webidl/tests/long_long.rs @@ -0,0 +1,145 @@ +use moli_webidl::{Context, WebIdlDictionary}; + +#[derive(WebIdlDictionary)] +#[webidl(prefix = "LongLongInit")] +struct Init { + #[webidl(default = 0)] + value: i64, + #[webidl(converter = "long_long", default = 0)] + explicit: i64, +} + +#[test] +fn long_long_derive_wraps_and_preserves_conversion_errors() { + moli_v8_test_util::ensure_v8(); + let mut isolate = v8::Isolate::new(v8::CreateParams::default()); + v8::scope!(let scope, &mut isolate); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + for (expression, expected) in [ + ("-17.9", -17), + ("NaN", 0), + ("Infinity", 0), + ("undefined", 0), + ("2**63", i64::MIN), + ("2**64", 0), + ("-(2**63)-2048", i64::MAX - 2047), + ] { + let source = v8::String::new( + scope, + &format!("({{value:{expression},explicit:{expression}}})"), + ) + .unwrap(); + let value = v8::Script::compile(scope, source, None) + .unwrap() + .run(scope) + .unwrap(); + let init = + moli_webidl::parse_dictionary::(scope, value, Context::argument("Test", 1)) + .unwrap() + .unwrap(); + assert_eq!(init.value, expected, "{expression}"); + assert_eq!(init.explicit, expected, "{expression}"); + } + for source in [ + "({value:Symbol(),explicit:0})", + "({value:0,explicit:1n})", + "({get explicit(){throw 42},value:0})", + ] { + v8::tc_scope!(let scope, scope); + let source = v8::String::new(scope, source).unwrap(); + let value = v8::Script::compile(scope, source, None) + .unwrap() + .run(scope) + .unwrap(); + assert!( + moli_webidl::parse_dictionary::(scope, value, Context::argument("Test", 1)) + .is_err() + ); + } +} + +#[test] +fn buffer_source_row_writes_preserve_padding_and_reject_out_of_bounds() { + moli_v8_test_util::ensure_v8(); + let mut isolate = v8::Isolate::new(v8::CreateParams::default()); + v8::scope!(let scope, &mut isolate); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + for source in [ + "new DataView(new ArrayBuffer(8),2,4)", + "new DataView(new SharedArrayBuffer(8),2,4)", + ] { + let source = v8::String::new(scope, source).unwrap(); + let value = v8::Script::compile(scope, source, None) + .unwrap() + .run(scope) + .unwrap(); + let buffer = moli_webidl::convert::( + scope, + value, + Context::argument("Test", 1), + ) + .unwrap(); + assert!(buffer.write_bytes(scope, &[7, 7, 7, 7])); + assert!(buffer.write_bytes_at(scope, 1, &[1, 2])); + assert_eq!(buffer.to_vec(scope), vec![7, 1, 2, 7]); + assert!(!buffer.write_bytes_at(scope, 3, &[1, 2])); + assert!(!buffer.write_bytes_at(scope, usize::MAX, &[1])); + assert!(buffer.write_bytes_at(scope, 4, &[])); + assert_eq!(buffer.to_vec(scope), vec![7, 1, 2, 7]); + } +} + +#[derive(WebIdlDictionary)] +#[webidl(prefix = "Plane")] +struct Plane { + #[webidl(required)] + offset: u32, +} +#[derive(WebIdlDictionary)] +#[webidl(prefix = "Planes")] +struct Planes { + #[webidl(sequence, converter = "dictionary")] + layout: Option>, +} +#[test] +fn sequence_dictionary_conversion_observes_iteration_and_required_members() { + moli_v8_test_util::ensure_v8(); + let mut isolate = v8::Isolate::new(Default::default()); + v8::scope!(let scope, &mut isolate); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + for (source, expected) in [ + ("({layout:[{offset:3},{offset:7}]})", Some(vec![3, 7])), + ("({layout:[]})", Some(vec![])), + ("({})", None), + ] { + let source = v8::String::new(scope, source).unwrap(); + let value = v8::Script::compile(scope, source, None) + .unwrap() + .run(scope) + .unwrap(); + let planes = + moli_webidl::parse_dictionary::(scope, value, Context::argument("Test", 1)) + .unwrap() + .unwrap(); + assert_eq!( + planes + .layout + .map(|list| list.into_iter().map(|p| p.offset).collect::>()), + expected + ); + } + for source in ["({layout:[null]})", "({layout:[{}]})", "({layout:{}})"] { + let source = v8::String::new(scope, source).unwrap(); + let value = v8::Script::compile(scope, source, None) + .unwrap() + .run(scope) + .unwrap(); + assert!( + moli_webidl::parse_dictionary::(scope, value, Context::argument("Test", 1)) + .is_err() + ); + } +}