Skip to main content

script_bindings/
conversions.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::{ptr, slice};
6
7use js::context::JSContext;
8use js::conversions::{
9    ConversionResult, FromJSValConvertible, ToJSValConvertible, jsstr_to_string,
10};
11use js::error::throw_type_error;
12use js::glue::{
13    GetProxyHandlerExtra, GetProxyReservedSlot, IsProxyHandlerFamily, IsWrapper, JS_GetReservedSlot,
14};
15use js::jsapi::{Heap, IsWindowProxy, JS_DeprecatedStringHasLatin1Chars, JSObject};
16use js::jsval::{ObjectValue, StringValue, UndefinedValue};
17use js::rust::wrappers2::{
18    IsArrayObject, JS_GetLatin1StringCharsAndLength, JS_GetTwoByteStringCharsAndLength,
19    JS_NewStringCopyN, UnwrapObjectDynamic,
20};
21use js::rust::{
22    HandleId, HandleValue, MutableHandleValue, ToString, get_object_class, is_dom_class,
23    is_dom_object, maybe_wrap_value,
24};
25use keyboard_types::Modifiers;
26use num_traits::Float;
27
28use crate::JSTraceable;
29use crate::codegen::GenericBindings::EventModifierInitBinding::EventModifierInit;
30use crate::inheritance::Castable;
31use crate::num::Finite;
32use crate::reflector::{DomObject, Reflector};
33use crate::root::DomRoot;
34use crate::str::{ByteString, DOMString, USVString};
35use crate::trace::RootedTraceableBox;
36use crate::utils::{DOMClass, DOMJSClass};
37
38/// A trait to check whether a given `JSObject` implements an IDL interface.
39pub trait IDLInterface {
40    /// Returns whether the given DOM class derives that interface.
41    fn derives(_: &'static DOMClass) -> bool;
42
43    /// First prototype ID in the DFS-ordered range for this interface and its descendants.
44    const PROTO_FIRST: u16 = 0;
45    /// Last prototype ID in the DFS-ordered range for this interface and its descendants.
46    const PROTO_LAST: u16 = u16::MAX;
47}
48
49/// A trait to mark an IDL interface as deriving from another one.
50pub trait DerivedFrom<T: Castable>: Castable {}
51
52// http://heycam.github.io/webidl/#es-USVString
53impl ToJSValConvertible for USVString {
54    fn to_jsval(&self, cx: &mut JSContext, rval: MutableHandleValue) {
55        self.0.to_jsval(cx, rval);
56    }
57}
58
59/// Behavior for stringification of `JSVal`s.
60#[derive(Clone, PartialEq)]
61pub enum StringificationBehavior {
62    /// Convert `null` to the string `"null"`.
63    Default,
64    /// Convert `null` to the empty string.
65    Empty,
66}
67
68// https://heycam.github.io/webidl/#es-DOMString
69impl FromJSValConvertible for DOMString {
70    type Config = StringificationBehavior;
71
72    fn from_jsval(
73        cx: &mut JSContext,
74        value: HandleValue,
75        null_behavior: StringificationBehavior,
76    ) -> Result<ConversionResult<DOMString>, ()> {
77        if null_behavior == StringificationBehavior::Empty && value.get().is_null() {
78            Ok(ConversionResult::Success(DOMString::new()))
79        } else {
80            match DOMString::from_js_string(cx, value) {
81                Ok(domstring) => Ok(ConversionResult::Success(domstring)),
82                Err(_) => Err(()),
83            }
84        }
85    }
86}
87
88// http://heycam.github.io/webidl/#es-USVString
89impl FromJSValConvertible for USVString {
90    type Config = ();
91
92    fn from_jsval(
93        cx: &mut JSContext,
94        value: HandleValue,
95        _: (),
96    ) -> Result<ConversionResult<USVString>, ()> {
97        let Some(jsstr) = ptr::NonNull::new(unsafe { ToString(cx, value) }) else {
98            debug!("ToString failed");
99            return Err(());
100        };
101
102        // FIXME(ajeffrey): Convert directly from DOMString to USVString
103        Ok(ConversionResult::Success(USVString(unsafe {
104            jsstr_to_string(cx, jsstr)
105        })))
106    }
107}
108
109// http://heycam.github.io/webidl/#es-ByteString
110impl ToJSValConvertible for ByteString {
111    fn to_jsval(&self, cx: &mut JSContext, mut rval: MutableHandleValue) {
112        let jsstr = unsafe {
113            JS_NewStringCopyN(
114                cx,
115                self.as_ptr() as *const libc::c_char,
116                self.len() as libc::size_t,
117            )
118        };
119        if jsstr.is_null() {
120            panic!("JS_NewStringCopyN failed");
121        }
122        unsafe { rval.set(StringValue(&*jsstr)) };
123    }
124}
125
126// http://heycam.github.io/webidl/#es-ByteString
127impl FromJSValConvertible for ByteString {
128    type Config = ();
129
130    fn from_jsval(
131        cx: &mut JSContext,
132        value: HandleValue,
133        _option: (),
134    ) -> Result<ConversionResult<ByteString>, ()> {
135        unsafe {
136            let string = ToString(cx, value);
137            if string.is_null() {
138                debug!("ToString failed");
139                return Err(());
140            }
141
142            let latin1 = JS_DeprecatedStringHasLatin1Chars(string);
143            if latin1 {
144                let mut length = 0;
145                let chars = JS_GetLatin1StringCharsAndLength(cx, string, &mut length);
146                assert!(!chars.is_null());
147
148                let char_slice = slice::from_raw_parts(chars as *mut u8, length);
149                return Ok(ConversionResult::Success(ByteString::new(
150                    char_slice.to_vec(),
151                )));
152            }
153
154            let mut length = 0;
155            let chars = JS_GetTwoByteStringCharsAndLength(cx, string, &mut length);
156            let char_vec = slice::from_raw_parts(chars, length);
157
158            if char_vec.iter().any(|&c| c > 0xFF) {
159                throw_type_error(cx, c"Invalid ByteString");
160                Err(())
161            } else {
162                Ok(ConversionResult::Success(ByteString::new(
163                    char_vec.iter().map(|&c| c as u8).collect(),
164                )))
165            }
166        }
167    }
168}
169
170impl<T> ToJSValConvertible for Reflector<T> {
171    fn to_jsval(&self, cx: &mut JSContext, mut rval: MutableHandleValue) {
172        let obj = self.get_jsobject().get();
173        assert!(!obj.is_null());
174        rval.set(ObjectValue(obj));
175        maybe_wrap_value(cx, rval);
176    }
177}
178
179impl<T: DomObject + IDLInterface> FromJSValConvertible for DomRoot<T> {
180    type Config = ();
181
182    fn from_jsval(
183        cx: &mut JSContext,
184        value: HandleValue,
185        _config: Self::Config,
186    ) -> Result<ConversionResult<DomRoot<T>>, ()> {
187        Ok(match root_from_handlevalue(cx, value) {
188            Ok(result) => ConversionResult::Success(result),
189            Err(()) => ConversionResult::Failure(c"value is not an object".into()),
190        })
191    }
192}
193
194impl<T: DomObject> ToJSValConvertible for DomRoot<T> {
195    fn to_jsval(&self, cx: &mut JSContext, rval: MutableHandleValue) {
196        self.reflector().to_jsval(cx, rval);
197    }
198}
199
200/// Get the `DOMClass` from `obj`, or `Err(())` if `obj` is not a DOM object.
201///
202/// # Safety
203/// obj must point to a valid, non-null JS object.
204#[allow(clippy::result_unit_err)]
205pub unsafe fn get_dom_class(obj: *mut JSObject) -> Result<&'static DOMClass, ()> {
206    unsafe {
207        let clasp = get_object_class(obj);
208        if is_dom_class(&*clasp) {
209            trace!("plain old dom object");
210            let domjsclass: *const DOMJSClass = clasp as *const DOMJSClass;
211            return Ok(&(*domjsclass).dom_class);
212        }
213        if is_dom_proxy(obj) {
214            trace!("proxy dom object");
215            let dom_class: *const DOMClass = GetProxyHandlerExtra(obj) as *const DOMClass;
216            if dom_class.is_null() {
217                return Err(());
218            }
219            return Ok(&*dom_class);
220        }
221    }
222    trace!("not a dom object");
223    Err(())
224}
225
226/// Returns whether `obj` is a DOM object implemented as a proxy.
227///
228/// # Safety
229/// obj must point to a valid, non-null JS object.
230pub unsafe fn is_dom_proxy(obj: *mut JSObject) -> bool {
231    unsafe {
232        let clasp = get_object_class(obj);
233        ((*clasp).flags & js::JSCLASS_IS_PROXY) != 0 && IsProxyHandlerFamily(obj)
234    }
235}
236
237/// The index of the slot wherein a pointer to the reflected DOM object is
238/// stored for non-proxy bindings.
239// We use slot 0 for holding the raw object.  This is safe for both
240// globals and non-globals.
241pub const DOM_OBJECT_SLOT: u32 = 0;
242
243/// Get the private pointer of a DOM object from a given reflector.
244///
245/// # Safety
246/// obj must point to a valid non-null JS object.
247pub unsafe fn private_from_object(obj: *mut JSObject) -> *const libc::c_void {
248    let mut value = UndefinedValue();
249    unsafe {
250        if is_dom_object(obj) {
251            JS_GetReservedSlot(obj, DOM_OBJECT_SLOT, &mut value);
252        } else {
253            debug_assert!(is_dom_proxy(obj));
254            GetProxyReservedSlot(obj, 0, &mut value);
255        };
256    }
257    if value.is_undefined() {
258        ptr::null()
259    } else {
260        value.to_private()
261    }
262}
263
264pub enum PrototypeCheck {
265    Derive(fn(&'static DOMClass) -> bool),
266    Depth { depth: usize, proto_id: u16 },
267}
268
269/// Get a `*const libc::c_void` for the given DOM object, unwrapping any
270/// wrapper around it first, and checking if the object is of the correct type.
271///
272/// Returns Err(()) if `obj` is an opaque security wrapper or if the object is
273/// not an object for a DOM object of the given type (as defined by the
274/// proto_id and proto_depth).
275///
276/// # Safety
277/// obj must point to a valid, non-null JS object.
278/// cx must point to a valid, non-null JS context.
279#[inline]
280#[allow(clippy::result_unit_err)]
281pub unsafe fn private_from_proto_check(
282    cx: &mut JSContext,
283    mut obj: *mut JSObject,
284    proto_check: PrototypeCheck,
285) -> Result<*const libc::c_void, ()> {
286    let dom_class = unsafe {
287        get_dom_class(obj).or_else(|_| {
288            if IsWrapper(obj) {
289                trace!("found wrapper");
290                obj = UnwrapObjectDynamic(obj, cx, /* stopAtWindowProxy = */ false);
291                if obj.is_null() {
292                    trace!("unwrapping security wrapper failed");
293                    Err(())
294                } else {
295                    assert!(!IsWrapper(obj));
296                    trace!("unwrapped successfully");
297                    get_dom_class(obj)
298                }
299            } else {
300                trace!("not a dom wrapper");
301                Err(())
302            }
303        })?
304    };
305
306    let prototype_matches = match proto_check {
307        PrototypeCheck::Derive(f) => (f)(dom_class),
308        PrototypeCheck::Depth { depth, proto_id } => {
309            dom_class.interface_chain[depth] as u16 == proto_id
310        },
311    };
312
313    if prototype_matches {
314        trace!("good prototype");
315        Ok(unsafe { private_from_object(obj) })
316    } else {
317        trace!("bad prototype");
318        Err(())
319    }
320}
321
322/// Get a `*const T` for a DOM object accessible from a `JSObject`.
323///
324/// # Safety
325/// obj must point to a valid, non-null JS object.
326/// cx must point to a valid, non-null JS context.
327#[allow(clippy::result_unit_err)]
328pub unsafe fn native_from_object<T>(cx: &mut JSContext, obj: *mut JSObject) -> Result<*const T, ()>
329where
330    T: DomObject + IDLInterface,
331{
332    unsafe {
333        private_from_proto_check(cx, obj, PrototypeCheck::Derive(T::derives))
334            .map(|ptr| ptr as *const T)
335    }
336}
337
338/// Get a `DomRoot<T>` for the given DOM object, unwrapping any wrapper
339/// around it first, and checking if the object is of the correct type.
340///
341/// Returns Err(()) if `obj` is an opaque security wrapper or if the object is
342/// not a reflector for a DOM object of the given type (as defined by the
343/// proto_id and proto_depth).
344///
345/// # Safety
346/// obj must point to a valid, non-null JS object.
347/// cx must point to a valid, non-null JS context.
348#[allow(clippy::result_unit_err)]
349pub unsafe fn root_from_object<T>(cx: &mut JSContext, obj: *mut JSObject) -> Result<DomRoot<T>, ()>
350where
351    T: DomObject + IDLInterface,
352{
353    unsafe { native_from_object(cx, obj).map(|ptr| DomRoot::from_ref(&*ptr)) }
354}
355
356/// Get a `DomRoot<T>` for a DOM object accessible from a `HandleValue`.
357/// Caller is responsible for throwing a JS exception if needed in case of error.
358///
359/// # Safety
360/// cx must point to a valid, non-null JS context.
361#[allow(clippy::result_unit_err)]
362pub fn root_from_handlevalue<T>(cx: &mut JSContext, v: HandleValue) -> Result<DomRoot<T>, ()>
363where
364    T: DomObject + IDLInterface,
365{
366    if !v.get().is_object() {
367        return Err(());
368    }
369    #[expect(unsafe_code)]
370    unsafe {
371        root_from_object(cx, v.get().to_object())
372    }
373}
374
375/// Convert `id` to a `DOMString`. Returns `None` if `id` is not a string or
376/// integer.
377///
378/// Handling of invalid UTF-16 in strings depends on the relevant option.
379pub fn jsid_to_string(cx: &js::context::JSContext, id: HandleId) -> Option<DOMString> {
380    let id_raw = *id;
381    if id_raw.is_string() {
382        let jsstr = ptr::NonNull::new(id_raw.to_string()).unwrap();
383        return Some(unsafe { jsstr_to_string(cx, jsstr) }.into());
384    }
385
386    if id_raw.is_int() {
387        return Some(id_raw.to_int().to_string().into());
388    }
389
390    None
391}
392
393impl<T: Float + ToJSValConvertible> ToJSValConvertible for Finite<T> {
394    #[inline]
395    fn to_jsval(&self, cx: &mut JSContext, rval: MutableHandleValue) {
396        let value = **self;
397        value.to_jsval(cx, rval);
398    }
399}
400
401impl<T: Float + FromJSValConvertible<Config = ()>> FromJSValConvertible for Finite<T> {
402    type Config = ();
403
404    fn from_jsval(
405        cx: &mut JSContext,
406        value: HandleValue,
407        option: (),
408    ) -> Result<ConversionResult<Finite<T>>, ()> {
409        let result = match FromJSValConvertible::from_jsval(cx, value, option)? {
410            ConversionResult::Success(v) => v,
411            ConversionResult::Failure(error) => {
412                // FIXME(emilio): Why throwing instead of propagating the error?
413                throw_type_error(cx, &error);
414                return Err(());
415            },
416        };
417        match Finite::new(result) {
418            Some(v) => Ok(ConversionResult::Success(v)),
419            None => {
420                throw_type_error(cx, c"this argument is not a finite floating-point value");
421                Err(())
422            },
423        }
424    }
425}
426
427/// Get a `*const libc::c_void` for the given DOM object, unless it is a DOM
428/// wrapper, and checking if the object is of the correct type.
429///
430/// Returns Err(()) if `obj` is a wrapper or if the object is not an object
431/// for a DOM object of the given type (as defined by the proto_id and proto_depth).
432#[inline]
433#[allow(clippy::result_unit_err)]
434unsafe fn private_from_proto_check_static(
435    obj: *mut JSObject,
436    proto_check: fn(&'static DOMClass) -> bool,
437) -> Result<*const libc::c_void, ()> {
438    unsafe {
439        let dom_class = get_dom_class(obj).map_err(|_| ())?;
440        if proto_check(dom_class) {
441            trace!("good prototype");
442            Ok(private_from_object(obj))
443        } else {
444            trace!("bad prototype");
445            Err(())
446        }
447    }
448}
449
450/// Get a `*const T` for a DOM object accessible from a `JSObject`, where the DOM object
451/// is guaranteed not to be a wrapper.
452///
453/// # Safety
454/// `obj` must point to a valid, non-null JSObject.
455#[allow(clippy::result_unit_err)]
456pub unsafe fn native_from_object_static<T>(obj: *mut JSObject) -> Result<*const T, ()>
457where
458    T: DomObject + IDLInterface,
459{
460    unsafe { private_from_proto_check_static(obj, T::derives).map(|ptr| ptr as *const T) }
461}
462
463/// Get a `*const T` for a DOM object accessible from a `HandleValue`.
464/// Caller is responsible for throwing a JS exception if needed in case of error.
465///
466/// # Safety
467/// `cx` must point to a valid, non-null JSContext.
468#[allow(clippy::result_unit_err)]
469pub fn native_from_handlevalue<T>(cx: &mut JSContext, v: HandleValue) -> Result<*const T, ()>
470where
471    T: DomObject + IDLInterface,
472{
473    if !v.get().is_object() {
474        return Err(());
475    }
476
477    #[expect(unsafe_code)]
478    unsafe {
479        native_from_object(cx, v.get().to_object())
480    }
481}
482
483impl<T: ToJSValConvertible + JSTraceable> ToJSValConvertible for RootedTraceableBox<T> {
484    #[inline]
485    fn to_jsval(&self, cx: &mut JSContext, rval: MutableHandleValue) {
486        let value = &**self;
487        value.to_jsval(cx, rval);
488    }
489}
490
491impl<T> FromJSValConvertible for RootedTraceableBox<Heap<T>>
492where
493    T: FromJSValConvertible + js::rust::GCMethods + Copy,
494    Heap<T>: JSTraceable + Default,
495{
496    type Config = T::Config;
497
498    fn from_jsval(
499        cx: &mut JSContext,
500        value: HandleValue,
501        config: Self::Config,
502    ) -> Result<ConversionResult<Self>, ()> {
503        T::from_jsval(cx, value, config).map(|result| match result {
504            ConversionResult::Success(inner) => {
505                ConversionResult::Success(RootedTraceableBox::from_box(Heap::boxed(inner)))
506            },
507            ConversionResult::Failure(msg) => ConversionResult::Failure(msg),
508        })
509    }
510}
511
512/// Returns whether `value` is an array-like object (Array, FileList,
513/// HTMLCollection, HTMLFormControlsCollection, HTMLOptionsCollection,
514/// NodeList, DOMTokenList).
515pub fn is_array_like<D: crate::DomTypes>(cx: &mut JSContext, value: HandleValue) -> bool {
516    let mut is_array = false;
517    assert!(unsafe { IsArrayObject(cx, value, &mut is_array) });
518    if is_array {
519        return true;
520    }
521
522    let object: *mut JSObject = match FromJSValConvertible::from_jsval(cx, value, ()).unwrap() {
523        ConversionResult::Success(object) => object,
524        _ => return false,
525    };
526
527    unsafe {
528        // TODO: HTMLAllCollection
529        if root_from_object::<D::DOMTokenList>(cx, object).is_ok() {
530            return true;
531        }
532        if root_from_object::<D::FileList>(cx, object).is_ok() {
533            return true;
534        }
535        if root_from_object::<D::HTMLCollection>(cx, object).is_ok() {
536            return true;
537        }
538        if root_from_object::<D::HTMLFormControlsCollection>(cx, object).is_ok() {
539            return true;
540        }
541        if root_from_object::<D::HTMLOptionsCollection>(cx, object).is_ok() {
542            return true;
543        }
544        if root_from_object::<D::NodeList>(cx, object).is_ok() {
545            return true;
546        }
547    }
548
549    false
550}
551
552/// Get a `DomRoot<T>` for a WindowProxy accessible from a `HandleValue`.
553/// Caller is responsible for throwing a JS exception if needed in case of error.
554pub(crate) unsafe fn windowproxy_from_handlevalue<D: crate::DomTypes>(
555    v: HandleValue,
556) -> Result<DomRoot<D::WindowProxy>, ()> {
557    if !v.get().is_object() {
558        return Err(());
559    }
560    let object = v.get().to_object();
561    unsafe {
562        if !IsWindowProxy(object) {
563            return Err(());
564        }
565        let mut value = UndefinedValue();
566        GetProxyReservedSlot(object, 0, &mut value);
567        let ptr = value.to_private() as *const D::WindowProxy;
568        Ok(DomRoot::from_ref(&*ptr))
569    }
570}
571
572#[allow(deprecated)]
573impl<D: crate::DomTypes> EventModifierInit<D> {
574    pub fn modifiers(&self) -> Modifiers {
575        let mut modifiers = Modifiers::empty();
576        if self.altKey {
577            modifiers.insert(Modifiers::ALT);
578        }
579        if self.ctrlKey {
580            modifiers.insert(Modifiers::CONTROL);
581        }
582        if self.shiftKey {
583            modifiers.insert(Modifiers::SHIFT);
584        }
585        if self.metaKey {
586            modifiers.insert(Modifiers::META);
587        }
588        if self.keyModifierStateAltGraph {
589            modifiers.insert(Modifiers::ALT_GRAPH);
590        }
591        if self.keyModifierStateCapsLock {
592            modifiers.insert(Modifiers::CAPS_LOCK);
593        }
594        if self.keyModifierStateFn {
595            modifiers.insert(Modifiers::FN);
596        }
597        if self.keyModifierStateFnLock {
598            modifiers.insert(Modifiers::FN_LOCK);
599        }
600        if self.keyModifierStateHyper {
601            modifiers.insert(Modifiers::HYPER);
602        }
603        if self.keyModifierStateNumLock {
604            modifiers.insert(Modifiers::NUM_LOCK);
605        }
606        if self.keyModifierStateScrollLock {
607            modifiers.insert(Modifiers::SCROLL_LOCK);
608        }
609        if self.keyModifierStateSuper {
610            modifiers.insert(Modifiers::SUPER);
611        }
612        if self.keyModifierStateSymbol {
613            modifiers.insert(Modifiers::SYMBOL);
614        }
615        if self.keyModifierStateSymbolLock {
616            modifiers.insert(Modifiers::SYMBOL_LOCK);
617        }
618        modifiers
619    }
620}