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