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