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