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