diff --git a/moli-webidl/src/buffer_source.rs b/moli-webidl/src/buffer_source.rs index 13be7b21ca..199a5d70c0 100644 --- a/moli-webidl/src/buffer_source.rs +++ b/moli-webidl/src/buffer_source.rs @@ -12,6 +12,56 @@ pub enum AllowSharedBufferSource<'s> { View(v8::Local<'s, v8::ArrayBufferView>), } +/// A fixed-length, non-shared `BufferSource`. Keep the native identity until +/// the consuming algorithm reads its bytes; conversion does not call author +/// getters or unwrap author Proxies. +pub struct NonSharedBufferSource<'s>(AllowSharedBufferSource<'s>); + +impl<'s> WebIdlConverter<'s> for NonSharedBufferSource<'s> { + type Options = (); + + fn convert( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + context: Context, + options: &Self::Options, + ) -> Result { + let source = AllowSharedBufferSource::convert(scope, value, context, options)?; + if matches!(&source, AllowSharedBufferSource::Shared(_)) + || source + .backing_store(scope) + .is_some_and(|backing| backing.is_shared()) + { + return Err(WebIdlError::cannot_convert( + context, + "non-shared BufferSource", + )); + } + // A detached resizable ArrayBuffer retains its resizable identity, + // even when its replacement empty backing store is fixed-length. + let resizable = match &source { + AllowSharedBufferSource::Buffer(buffer) => buffer.is_resizable_by_user_javascript(), + AllowSharedBufferSource::View(view) => view + .buffer(scope) + .is_some_and(|buffer| buffer.is_resizable_by_user_javascript()), + AllowSharedBufferSource::Shared(_) => unreachable!("shared sources were rejected"), + }; + if resizable { + return Err(WebIdlError::cannot_convert( + context, + "non-resizable BufferSource", + )); + } + Ok(Self(source)) + } +} + +impl NonSharedBufferSource<'_> { + pub fn to_vec(&self, scope: &mut v8::PinScope<'_, '_>) -> Vec { + self.0.to_vec(scope) + } +} + impl<'s> WebIdlConverter<'s> for AllowSharedBufferSource<'s> { type Options = (); diff --git a/moli-webidl/src/lib.rs b/moli-webidl/src/lib.rs index 963f786602..3d70fd7bcd 100644 --- a/moli-webidl/src/lib.rs +++ b/moli-webidl/src/lib.rs @@ -63,7 +63,7 @@ extern crate self as moli_webidl; mod buffer_source; mod convert; -pub use buffer_source::AllowSharedBufferSource; +pub use buffer_source::{AllowSharedBufferSource, NonSharedBufferSource}; mod error; mod helpers; mod traits; diff --git a/moli-webidl/tests/fixed_buffer_source.rs b/moli-webidl/tests/fixed_buffer_source.rs new file mode 100644 index 0000000000..5000f14117 --- /dev/null +++ b/moli-webidl/tests/fixed_buffer_source.rs @@ -0,0 +1,120 @@ +use moli_webidl::{Context, NonSharedBufferSource, convert}; + +fn eval<'s>(scope: &mut v8::PinScope<'s, '_>, source: &str) -> v8::Local<'s, v8::Value> { + let source = v8::String::new(scope, source).unwrap(); + v8::Script::compile(scope, source, None) + .unwrap() + .run(scope) + .unwrap() +} + +#[test] +fn fixed_buffer_sources_use_native_ranges_and_retain_live_identity() { + 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 [ + ("new Uint8Array([7,1,2,9]).buffer", vec![7, 1, 2, 9]), + ("new Uint8Array([7,1,2,9]).subarray(1,3)", vec![1, 2]), + ( + "new DataView(new Uint8Array([7,1,2,9]).buffer,1,2)", + vec![1, 2], + ), + ( + "Object.setPrototypeOf(new Uint8Array([1,2]),null)", + vec![1, 2], + ), + ( + "(() => {const view = new DataView(new Uint8Array([7,1,2,9]).buffer,1,2); for (const key of ['buffer','byteOffset','byteLength']) Object.defineProperty(view,key,{get(){throw Error('author getter')}}); return view;})()", + vec![1, 2], + ), + ] { + let value = eval(scope, source); + let native = + convert::(scope, value, Context::argument("Buffer", 1)).unwrap(); + assert_eq!(native.to_vec(scope), expected, "{source}"); + } + let value = eval( + scope, + "globalThis.bytes = new Uint8Array([1,2,3]); bytes.subarray(1)", + ); + let native = + convert::(scope, value, Context::argument("Buffer", 1)).unwrap(); + eval(scope, "bytes[1] = 8"); + assert_eq!(native.to_vec(scope), vec![8, 3]); +} + +#[test] +fn fixed_buffer_sources_reject_shared_resizable_and_author_proxies_without_traps() { + 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); + eval(scope, "globalThis.bufferTraps = 0"); + for source in [ + "null", + "undefined", + "({})", + "([])", + "1", + "'buffer'", + "Symbol()", + "new SharedArrayBuffer(0)", + "new SharedArrayBuffer(2)", + "new Uint8Array(new SharedArrayBuffer(0))", + "new DataView(new SharedArrayBuffer(2))", + "new ArrayBuffer(2,{maxByteLength:4})", + "new Uint8Array(new ArrayBuffer(2,{maxByteLength:4}))", + "new DataView(new ArrayBuffer(2,{maxByteLength:4}))", + "Object.create(new Uint8Array(2))", + "new Proxy(new Uint8Array(2),{get(){bufferTraps++;throw Error('proxy trap')}})", + "(() => {const pair = Proxy.revocable(new ArrayBuffer(2),{}); pair.revoke(); return pair.proxy;})()", + ] { + let value = eval(scope, source); + assert!( + convert::(scope, value, Context::argument("Buffer", 1)).is_err(), + "{source}" + ); + } + assert_eq!(eval(scope, "bufferTraps").int32_value(scope), Some(0)); +} + +#[test] +fn fixed_buffer_detachment_preserves_native_validation_and_empty_byte_reads() { + 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 resizable in [false, true] { + let value = eval( + scope, + if resizable { + "new ArrayBuffer(2,{maxByteLength:4})" + } else { + "new ArrayBuffer(2)" + }, + ); + let buffer = v8::Local::::try_from(value).unwrap(); + let view = v8::Uint8Array::new(scope, buffer, 0, 2).unwrap(); + let retained = + convert::(scope, view.into(), Context::argument("Buffer", 1)); + assert_eq!(retained.is_err(), resizable); + assert_eq!(buffer.detach(None), Some(true)); + if let Ok(retained) = retained { + assert!(retained.to_vec(scope).is_empty()); + } + for value in [buffer.into(), view.into()] { + let result = + convert::(scope, value, Context::argument("Buffer", 1)); + if resizable { + assert!(result.is_err()); + } else { + assert!(result.unwrap().to_vec(scope).is_empty()); + } + } + } +}