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