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