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;
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(cx.raw_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                unsafe { throw_type_error(cx.raw_cx(), &error) };
408                return Err(());
409            },
410        };
411        match Finite::new(result) {
412            Some(v) => Ok(ConversionResult::Success(v)),
413            None => {
414                unsafe {
415                    throw_type_error(
416                        cx.raw_cx(),
417                        c"this argument is not a finite floating-point value",
418                    )
419                };
420                Err(())
421            },
422        }
423    }
424}
425
426/// Get a `*const libc::c_void` for the given DOM object, unless it is a DOM
427/// wrapper, and checking if the object is of the correct type.
428///
429/// Returns Err(()) if `obj` is a wrapper or if the object is not an object
430/// for a DOM object of the given type (as defined by the proto_id and proto_depth).
431#[inline]
432#[allow(clippy::result_unit_err)]
433unsafe fn private_from_proto_check_static(
434    obj: *mut JSObject,
435    proto_check: fn(&'static DOMClass) -> bool,
436) -> Result<*const libc::c_void, ()> {
437    let dom_class = get_dom_class(obj).map_err(|_| ())?;
438    if proto_check(dom_class) {
439        trace!("good prototype");
440        Ok(private_from_object(obj))
441    } else {
442        trace!("bad prototype");
443        Err(())
444    }
445}
446
447/// Get a `*const T` for a DOM object accessible from a `JSObject`, where the DOM object
448/// is guaranteed not to be a wrapper.
449///
450/// # Safety
451/// `obj` must point to a valid, non-null JSObject.
452#[allow(clippy::result_unit_err)]
453pub unsafe fn native_from_object_static<T>(obj: *mut JSObject) -> Result<*const T, ()>
454where
455    T: DomObject + IDLInterface,
456{
457    private_from_proto_check_static(obj, T::derives).map(|ptr| ptr as *const T)
458}
459
460/// Get a `*const T` for a DOM object accessible from a `HandleValue`.
461/// Caller is responsible for throwing a JS exception if needed in case of error.
462///
463/// # Safety
464/// `cx` must point to a valid, non-null JSContext.
465#[allow(clippy::result_unit_err)]
466pub fn native_from_handlevalue<T>(cx: &mut JSContext, v: HandleValue) -> Result<*const T, ()>
467where
468    T: DomObject + IDLInterface,
469{
470    if !v.get().is_object() {
471        return Err(());
472    }
473
474    #[expect(unsafe_code)]
475    unsafe {
476        native_from_object(cx, v.get().to_object())
477    }
478}
479
480impl<T: ToJSValConvertible + JSTraceable> ToJSValConvertible for RootedTraceableBox<T> {
481    #[inline]
482    fn safe_to_jsval(&self, cx: &mut JSContext, rval: MutableHandleValue) {
483        let value = &**self;
484        value.safe_to_jsval(cx, rval);
485    }
486}
487
488impl<T> FromJSValConvertible for RootedTraceableBox<Heap<T>>
489where
490    T: FromJSValConvertible + js::rust::GCMethods + Copy,
491    Heap<T>: JSTraceable + Default,
492{
493    type Config = T::Config;
494
495    fn safe_from_jsval(
496        cx: &mut JSContext,
497        value: HandleValue,
498        config: Self::Config,
499    ) -> Result<ConversionResult<Self>, ()> {
500        T::safe_from_jsval(cx, value, config).map(|result| match result {
501            ConversionResult::Success(inner) => {
502                ConversionResult::Success(RootedTraceableBox::from_box(Heap::boxed(inner)))
503            },
504            ConversionResult::Failure(msg) => ConversionResult::Failure(msg),
505        })
506    }
507}
508
509/// Returns whether `value` is an array-like object (Array, FileList,
510/// HTMLCollection, HTMLFormControlsCollection, HTMLOptionsCollection,
511/// NodeList, DOMTokenList).
512pub fn is_array_like<D: crate::DomTypes>(cx: &mut JSContext, value: HandleValue) -> bool {
513    let mut is_array = false;
514    assert!(unsafe { IsArrayObject(cx, value, &mut is_array) });
515    if is_array {
516        return true;
517    }
518
519    let object: *mut JSObject = match FromJSValConvertible::safe_from_jsval(cx, value, ()).unwrap()
520    {
521        ConversionResult::Success(object) => object,
522        _ => return false,
523    };
524
525    unsafe {
526        // TODO: HTMLAllCollection
527        if root_from_object::<D::DOMTokenList>(cx, object).is_ok() {
528            return true;
529        }
530        if root_from_object::<D::FileList>(cx, object).is_ok() {
531            return true;
532        }
533        if root_from_object::<D::HTMLCollection>(cx, object).is_ok() {
534            return true;
535        }
536        if root_from_object::<D::HTMLFormControlsCollection>(cx, object).is_ok() {
537            return true;
538        }
539        if root_from_object::<D::HTMLOptionsCollection>(cx, object).is_ok() {
540            return true;
541        }
542        if root_from_object::<D::NodeList>(cx, object).is_ok() {
543            return true;
544        }
545    }
546
547    false
548}
549
550/// Get a `DomRoot<T>` for a WindowProxy accessible from a `HandleValue`.
551/// Caller is responsible for throwing a JS exception if needed in case of error.
552pub(crate) unsafe fn windowproxy_from_handlevalue<D: crate::DomTypes>(
553    v: HandleValue,
554) -> Result<DomRoot<D::WindowProxy>, ()> {
555    if !v.get().is_object() {
556        return Err(());
557    }
558    let object = v.get().to_object();
559    if !IsWindowProxy(object) {
560        return Err(());
561    }
562    let mut value = UndefinedValue();
563    GetProxyReservedSlot(object, 0, &mut value);
564    let ptr = value.to_private() as *const D::WindowProxy;
565    Ok(DomRoot::from_ref(&*ptr))
566}
567
568#[allow(deprecated)]
569impl<D: crate::DomTypes> EventModifierInit<D> {
570    pub fn modifiers(&self) -> Modifiers {
571        let mut modifiers = Modifiers::empty();
572        if self.altKey {
573            modifiers.insert(Modifiers::ALT);
574        }
575        if self.ctrlKey {
576            modifiers.insert(Modifiers::CONTROL);
577        }
578        if self.shiftKey {
579            modifiers.insert(Modifiers::SHIFT);
580        }
581        if self.metaKey {
582            modifiers.insert(Modifiers::META);
583        }
584        if self.keyModifierStateAltGraph {
585            modifiers.insert(Modifiers::ALT_GRAPH);
586        }
587        if self.keyModifierStateCapsLock {
588            modifiers.insert(Modifiers::CAPS_LOCK);
589        }
590        if self.keyModifierStateFn {
591            modifiers.insert(Modifiers::FN);
592        }
593        if self.keyModifierStateFnLock {
594            modifiers.insert(Modifiers::FN_LOCK);
595        }
596        if self.keyModifierStateHyper {
597            modifiers.insert(Modifiers::HYPER);
598        }
599        if self.keyModifierStateNumLock {
600            modifiers.insert(Modifiers::NUM_LOCK);
601        }
602        if self.keyModifierStateScrollLock {
603            modifiers.insert(Modifiers::SCROLL_LOCK);
604        }
605        if self.keyModifierStateSuper {
606            modifiers.insert(Modifiers::SUPER);
607        }
608        if self.keyModifierStateSymbol {
609            modifiers.insert(Modifiers::SYMBOL);
610        }
611        if self.keyModifierStateSymbolLock {
612            modifiers.insert(Modifiers::SYMBOL_LOCK);
613        }
614        modifiers
615    }
616}