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