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