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