diff --git a/moli-webidl-derive/src/converter.rs b/moli-webidl-derive/src/converter.rs index 4bcf252304..f2ec9999d8 100644 --- a/moli-webidl-derive/src/converter.rs +++ b/moli-webidl-derive/src/converter.rs @@ -20,6 +20,7 @@ pub(crate) enum ConverterKind { ClampedUnsignedShort, UnsignedLong, EnforceRangeUnsignedLong, + EnforceRangeLongLong, UnsignedLongLong, EnforceRangeUnsignedLongLong, Float, @@ -54,6 +55,7 @@ impl ConverterKind { "clamped_unsigned_short" => Ok(Self::ClampedUnsignedShort), "unsigned_long" => Ok(Self::UnsignedLong), "enforce_range_unsigned_long" => Ok(Self::EnforceRangeUnsignedLong), + "enforce_range_long_long" => Ok(Self::EnforceRangeLongLong), "unsigned_long_long" => Ok(Self::UnsignedLongLong), "enforce_range_unsigned_long_long" => Ok(Self::EnforceRangeUnsignedLongLong), "float" => Ok(Self::Float), @@ -87,6 +89,7 @@ impl ConverterKind { Self::EnforceRangeUnsignedLong => { quote!(::moli_webidl::EnforceRangeUnsignedLong) } + Self::EnforceRangeLongLong => quote!(::moli_webidl::EnforceRangeLongLong), Self::UnsignedLongLong => quote!(::moli_webidl::UnsignedLongLong), Self::EnforceRangeUnsignedLongLong => { quote!(::moli_webidl::EnforceRangeUnsignedLongLong) @@ -129,6 +132,7 @@ impl ConverterKind { Self::EnforceRangeUnsignedLong => { quote!(::moli_webidl::EnforceRangeUnsignedLong(#expr)) } + Self::EnforceRangeLongLong => quote!(::moli_webidl::EnforceRangeLongLong(#expr)), Self::UnsignedLongLong => quote!(::moli_webidl::UnsignedLongLong(#expr)), Self::EnforceRangeUnsignedLongLong => { quote!(::moli_webidl::EnforceRangeUnsignedLongLong(#expr)) @@ -160,6 +164,7 @@ impl ConverterKind { | Self::ClampedUnsignedShort | Self::UnsignedLong | Self::EnforceRangeUnsignedLong + | Self::EnforceRangeLongLong | Self::UnsignedLongLong | Self::EnforceRangeUnsignedLongLong | Self::Float diff --git a/moli-webidl/src/convert.rs b/moli-webidl/src/convert.rs index 4978fb0f6d..75f806846a 100644 --- a/moli-webidl/src/convert.rs +++ b/moli-webidl/src/convert.rs @@ -1,10 +1,10 @@ use crate::{ Boolean, BufferSource, ByteString, ClampedUnsignedShort, Context, Dictionary, DomString, - DomString16, Double, EnforceRangeLong, EnforceRangeUnsignedLong, EnforceRangeUnsignedLongLong, - EnumValue, Long, Record, Sequence, StringOptions, UnrestrictedDouble, UnsignedLong, - UnsignedLongLong, UnsignedShort, UsvString, WebIdlArguments, WebIdlConverter, WebIdlDictionary, - WebIdlEnum, WebIdlError, WebIdlErrorKind, dictionary_value, is_nullish, property_result, - symbol_property_result, throw_error, throw_type_error, + DomString16, Double, EnforceRangeLong, EnforceRangeLongLong, EnforceRangeUnsignedLong, + EnforceRangeUnsignedLongLong, EnumValue, Long, Record, Sequence, StringOptions, + UnrestrictedDouble, UnsignedLong, UnsignedLongLong, UnsignedShort, UsvString, WebIdlArguments, + WebIdlConverter, WebIdlDictionary, WebIdlEnum, WebIdlError, WebIdlErrorKind, dictionary_value, + is_nullish, property_result, symbol_property_result, throw_error, throw_type_error, }; impl<'s> WebIdlConverter<'s> for DomString { @@ -331,6 +331,20 @@ impl<'s> WebIdlConverter<'s> for UnsignedLongLong { } } +impl<'s> WebIdlConverter<'s> for EnforceRangeLongLong { + type Options = (); + + fn convert( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + context: Context, + _options: &Self::Options, + ) -> Result { + let value = number_value(scope, value, context, "[EnforceRange] long long")?; + enforce_range_long_long(value, context).map(Self) + } +} + impl<'s> WebIdlConverter<'s> for EnforceRangeUnsignedLongLong { type Options = (); @@ -1402,6 +1416,19 @@ fn unsigned_long_long(value: f64) -> u64 { } } +fn enforce_range_long_long(value: f64, context: Context) -> Result { + // ConvertToInt uses the exact JavaScript integer range for 64-bit types. + let value = value.trunc(); + if !value.is_finite() || !(-9_007_199_254_740_991.0..=9_007_199_254_740_991.0).contains(&value) + { + return Err(WebIdlError::new( + context, + WebIdlErrorKind::CannotConvert("[EnforceRange] long long"), + )); + } + Ok(value as i64) +} + fn enforce_range_unsigned_long_long(value: f64, context: Context) -> Result { // ConvertToInt uses the exact JavaScript integer range for 64-bit types. let value = value.trunc(); diff --git a/moli-webidl/src/lib.rs b/moli-webidl/src/lib.rs index 11324ece94..19de1aa414 100644 --- a/moli-webidl/src/lib.rs +++ b/moli-webidl/src/lib.rs @@ -98,5 +98,6 @@ pub use types::{ UnsignedShort, UsvString, }; +pub use types::EnforceRangeLongLong; /// Restricted WebIDL float represented as a finite binary32 value. pub use types::Float; diff --git a/moli-webidl/src/types.rs b/moli-webidl/src/types.rs index a155e87a1c..c5be05eaf7 100644 --- a/moli-webidl/src/types.rs +++ b/moli-webidl/src/types.rs @@ -316,6 +316,14 @@ impl From for u64 { } } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct EnforceRangeLongLong(pub i64); +impl From for i64 { + fn from(value: EnforceRangeLongLong) -> Self { + value.0 + } +} + #[derive(Debug, Default)] pub struct StringOptions { pub treat_null_as_empty_string: bool, diff --git a/moli-webidl/tests/signed_i64.rs b/moli-webidl/tests/signed_i64.rs new file mode 100644 index 0000000000..6403dd8fee --- /dev/null +++ b/moli-webidl/tests/signed_i64.rs @@ -0,0 +1,86 @@ +use moli_webidl::{WebIdlArgs, WebIdlDictionary}; +#[derive(WebIdlArgs)] +#[webidl(prefix = "Test.convert")] +struct RequiredArgs { + #[webidl(required, converter = "enforce_range_long_long")] + value: i64, +} +#[derive(WebIdlDictionary)] +#[webidl(prefix = "TestOptions")] +struct TestOptions { + #[webidl(default = -7, converter = "enforce_range_long_long")] + value: i64, +} +#[derive(WebIdlArgs)] +#[webidl(prefix = "Test.dictionary")] +struct DictionaryArgs { + #[webidl(dictionary)] + options: TestOptions, +} +fn required<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s>, +) { + if let Some(parsed) = moli_webidl::parse_args::(scope, &args) { + rv.set(v8::Number::new(scope, parsed.value as f64).into()); + } +} +fn dictionary<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s>, +) { + if let Some(parsed) = moli_webidl::parse_args::(scope, &args) { + rv.set(v8::Number::new(scope, parsed.options.value as f64).into()); + } +} +#[test] +fn numeric_conversion_preserves_boundaries_defaults_and_callee_realm_errors() { + moli_v8_test_util::ensure_v8(); + let mut isolate = v8::Isolate::new(v8::CreateParams::default()); + let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let callee = v8::Context::new(scope, Default::default()); + let values = { + let scope = &mut v8::ContextScope::new(scope, callee); + let required = v8::Function::new(scope, required).unwrap(); + let dictionary = v8::Function::new(scope, dictionary).unwrap(); + let key = v8::String::new(scope, "TypeError").unwrap(); + let error = callee.global(scope).get(scope, key.into()).unwrap(); + [ + ("required", required.into()), + ("dictionary", dictionary.into()), + ("CalleeTypeError", error), + ] + }; + let caller = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, caller); + for (name, value) in values { + let key = v8::String::new(scope, name).unwrap(); + assert_eq!( + caller.global(scope).set(scope, key.into(), value), + Some(true) + ); + } + let source=v8::String::new(scope,r#"(() => { + const assert=(value,message)=>{if(!value)throw Error(message);}; + for(const convert of [required,value=>dictionary({value})]) { + for(const [value,expected] of [[0,0],[-0,0],[3.9,3],[-3.9,-3],[9007199254740991,9007199254740991],[-9007199254740991,-9007199254740991],['-7.9',-7],[null,0],[true,1]])assert(Object.is(convert(value),expected),'boundary '+String(value)); + for(const value of [NaN,Infinity,-Infinity,9007199254740992,-9007199254740992,Symbol('number'),1n]) {let error;try{convert(value);}catch(caught){error=caught;}assert(error instanceof CalleeTypeError&&!(error instanceof TypeError),'callee TypeError');} + let reads=0;assert(convert({[Symbol.toPrimitive](hint){reads++;assert(hint==='number','numeric hint');return 4;}})===4&&reads===1,'one conversion'); + const sentinel={};let error;try{convert({valueOf(){throw sentinel;}});}catch(caught){error=caught;}assert(error===sentinel,'exception identity'); + } + assert(dictionary()===-7&&dictionary(null)===-7&&dictionary({})===-7,'dictionary defaults'); + const sentinel={};let error;try{dictionary({get value(){throw sentinel;}});}catch(caught){error=caught;}assert(error===sentinel,'getter exception'); + try{required();}catch(caught){error=caught;}assert(error instanceof CalleeTypeError,'required argument'); + return true; + })()"#).unwrap(); + let script = v8::Script::compile(scope, source, None).unwrap(); + assert!( + script + .run(scope) + .expect("numeric conversion regression") + .is_true() + ); +}