script/
window_named_properties.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 http://mozilla.org/MPL/2.0/. */
4
5use std::ptr;
6use std::ptr::NonNull;
7use std::sync::LazyLock;
8
9use js::conversions::jsstr_to_string;
10use js::glue::{AppendToIdVector, CreateProxyHandler, NewProxyObject, ProxyTraps};
11use js::jsapi::{
12    Handle, HandleId, HandleObject, JS_SetImmutablePrototype, JSCLASS_DELAY_METADATA_BUILDER,
13    JSCLASS_IS_PROXY, JSCLASS_RESERVED_SLOTS_MASK, JSCLASS_RESERVED_SLOTS_SHIFT, JSClass,
14    JSClass_NON_NATIVE, JSContext, JSErrNum, JSPROP_READONLY, MutableHandle, MutableHandleIdVector,
15    MutableHandleObject, ObjectOpResult, PropertyDescriptor, ProxyClassExtension, ProxyClassOps,
16    ProxyObjectOps, SymbolCode, UndefinedHandleValue,
17};
18use js::jsid::SymbolId;
19use js::jsval::UndefinedValue;
20use js::rust::wrappers2::GetWellKnownSymbol;
21use js::rust::{
22    Handle as RustHandle, HandleObject as RustHandleObject, IntoHandle,
23    MutableHandle as RustMutableHandle, MutableHandleObject as RustMutableHandleObject,
24};
25
26use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
27use crate::dom::bindings::proxyhandler::set_property_descriptor;
28use crate::dom::bindings::root::Root;
29use crate::dom::bindings::utils::has_property_on_prototype;
30use crate::dom::globalscope::GlobalScope;
31use crate::dom::window::Window;
32use crate::js::conversions::ToJSValConvertible;
33use crate::script_runtime::JSContext as SafeJSContext;
34
35struct SyncWrapper(*const libc::c_void);
36#[expect(unsafe_code)]
37unsafe impl Sync for SyncWrapper {}
38#[expect(unsafe_code)]
39unsafe impl Send for SyncWrapper {}
40
41static HANDLER: LazyLock<SyncWrapper> = LazyLock::new(|| {
42    let traps = ProxyTraps {
43        enter: None,
44        getOwnPropertyDescriptor: Some(get_own_property_descriptor),
45        defineProperty: Some(define_property),
46        ownPropertyKeys: Some(own_property_keys),
47        delete_: Some(delete),
48        enumerate: None,
49        getPrototypeIfOrdinary: Some(get_prototype_if_ordinary),
50        getPrototype: None,
51        setPrototype: None,
52        setImmutablePrototype: None,
53        preventExtensions: Some(prevent_extensions),
54        isExtensible: Some(is_extensible),
55        has: None,
56        get: None,
57        set: None,
58        call: None,
59        construct: None,
60        hasOwn: None,
61        getOwnEnumerablePropertyKeys: None,
62        nativeCall: None,
63        objectClassIs: None,
64        className: Some(class_name),
65        fun_toString: None,
66        boxedValue_unbox: None,
67        defaultValue: None,
68        trace: None,
69        finalize: None,
70        objectMoved: None,
71        isCallable: None,
72        isConstructor: None,
73    };
74
75    #[expect(unsafe_code)]
76    unsafe {
77        SyncWrapper(CreateProxyHandler(&traps, ptr::null()))
78    }
79});
80
81#[expect(unsafe_code)]
82unsafe extern "C" fn get_own_property_descriptor(
83    cx: *mut JSContext,
84    proxy: HandleObject,
85    id: HandleId,
86    desc: MutableHandle<PropertyDescriptor>,
87    is_none: *mut bool,
88) -> bool {
89    // SAFETY: it is safe to construct a JSContext from an engine callback.
90    let mut cx = unsafe { js::context::JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
91
92    if id.is_symbol() {
93        if id.get().asBits_ ==
94            SymbolId(unsafe { GetWellKnownSymbol(&cx, SymbolCode::toStringTag) }).asBits_
95        {
96            rooted!(&in(cx) let mut rval = UndefinedValue());
97            "WindowProperties".safe_to_jsval(&mut cx, rval.handle_mut());
98            set_property_descriptor(
99                unsafe { RustMutableHandle::from_raw(desc) },
100                rval.handle(),
101                JSPROP_READONLY.into(),
102                unsafe { &mut *is_none },
103            );
104        }
105        return true;
106    }
107
108    let mut found = false;
109    let lookup_succeeded = unsafe {
110        has_property_on_prototype(
111            cx.raw_cx(),
112            RustHandle::from_raw(proxy),
113            RustHandle::from_raw(id),
114            &mut found,
115        )
116    };
117    if !lookup_succeeded {
118        return false;
119    }
120    if found {
121        return true;
122    }
123
124    let s = if id.is_string() {
125        unsafe { jsstr_to_string(cx.raw_cx_no_gc(), NonNull::new(id.to_string()).unwrap()) }
126    } else if id.is_int() {
127        // If the property key is an integer index, convert it to a String too.
128        // For indexed access on the window object, which may shadow this, see
129        // the getOwnPropertyDescriptor trap in dom/windowproxy.rs.
130        id.to_int().to_string()
131    } else if id.is_symbol() {
132        // Symbol properties were already handled above.
133        unreachable!()
134    } else {
135        unimplemented!()
136    };
137    if s.is_empty() {
138        return true;
139    }
140
141    let window = Root::downcast::<Window>(unsafe { GlobalScope::from_object(proxy.get()) })
142        .expect("global is not a window");
143    if let Some(obj) = window.NamedGetter(&mut cx, s.into()) {
144        rooted!(&in(cx) let mut rval = UndefinedValue());
145        obj.safe_to_jsval(&mut cx, rval.handle_mut());
146        set_property_descriptor(
147            unsafe { RustMutableHandle::from_raw(desc) },
148            rval.handle(),
149            0,
150            unsafe { &mut *is_none },
151        );
152    }
153    true
154}
155
156#[expect(unsafe_code)]
157unsafe extern "C" fn own_property_keys(
158    cx: *mut JSContext,
159    _proxy: HandleObject,
160    props: MutableHandleIdVector,
161) -> bool {
162    // TODO is this all we need to return? compare with gecko:
163    // https://searchfox.org/mozilla-central/rev/af78418c4b5f2c8721d1a06486cf4cf0b33e1e8d/dom/base/WindowNamedPropertiesHandler.cpp#175-232
164    // see also https://github.com/whatwg/html/issues/9068
165    unsafe {
166        rooted!(in(cx) let mut rooted = SymbolId(js::jsapi::GetWellKnownSymbol(cx, SymbolCode::toStringTag)));
167        AppendToIdVector(props, rooted.handle().into());
168    }
169    true
170}
171
172#[expect(unsafe_code)]
173unsafe extern "C" fn define_property(
174    _cx: *mut JSContext,
175    _proxy: HandleObject,
176    _id: HandleId,
177    _desc: Handle<PropertyDescriptor>,
178    result: *mut ObjectOpResult,
179) -> bool {
180    unsafe {
181        (*result).code_ = JSErrNum::JSMSG_CANT_DEFINE_WINDOW_NAMED_PROPERTY as usize;
182    }
183    true
184}
185
186#[expect(unsafe_code)]
187unsafe extern "C" fn delete(
188    _cx: *mut JSContext,
189    _proxy: HandleObject,
190    _id: HandleId,
191    result: *mut ObjectOpResult,
192) -> bool {
193    unsafe {
194        (*result).code_ = JSErrNum::JSMSG_CANT_DELETE_WINDOW_NAMED_PROPERTY as usize;
195    }
196    true
197}
198
199#[expect(unsafe_code)]
200unsafe extern "C" fn get_prototype_if_ordinary(
201    _cx: *mut JSContext,
202    proxy: HandleObject,
203    is_ordinary: *mut bool,
204    proto: MutableHandleObject,
205) -> bool {
206    unsafe {
207        *is_ordinary = true;
208        proto.set(js::jsapi::GetStaticPrototype(proxy.get()));
209    }
210    true
211}
212
213#[expect(unsafe_code)]
214unsafe extern "C" fn prevent_extensions(
215    _cx: *mut JSContext,
216    _proxy: HandleObject,
217    result: *mut ObjectOpResult,
218) -> bool {
219    unsafe {
220        (*result).code_ = JSErrNum::JSMSG_CANT_PREVENT_EXTENSIONS as usize;
221    }
222    true
223}
224
225#[expect(unsafe_code)]
226unsafe extern "C" fn is_extensible(
227    _cx: *mut JSContext,
228    _proxy: HandleObject,
229    extensible: *mut bool,
230) -> bool {
231    unsafe {
232        *extensible = true;
233    }
234    true
235}
236
237#[expect(unsafe_code)]
238unsafe extern "C" fn class_name(_cx: *mut JSContext, _proxy: HandleObject) -> *const libc::c_char {
239    c"WindowProperties".as_ptr()
240}
241
242// Maybe this should be a DOMJSClass. See https://bugzilla.mozilla.org/show_bug.cgi?id=787070
243#[expect(unsafe_code)]
244static CLASS: JSClass = JSClass {
245    name: c"WindowProperties".as_ptr(),
246    flags: JSClass_NON_NATIVE |
247        JSCLASS_IS_PROXY |
248        JSCLASS_DELAY_METADATA_BUILDER |
249        ((1 & JSCLASS_RESERVED_SLOTS_MASK) << JSCLASS_RESERVED_SLOTS_SHIFT), /* JSCLASS_HAS_RESERVED_SLOTS(1) */
250    cOps: unsafe { &ProxyClassOps },
251    spec: ptr::null(),
252    ext: unsafe { &ProxyClassExtension },
253    oOps: unsafe { &ProxyObjectOps },
254};
255
256#[expect(unsafe_code)]
257pub(crate) fn create(
258    cx: SafeJSContext,
259    proto: RustHandleObject,
260    mut properties_obj: RustMutableHandleObject,
261) {
262    unsafe {
263        properties_obj.set(NewProxyObject(
264            *cx,
265            HANDLER.0,
266            UndefinedHandleValue,
267            proto.get(),
268            &CLASS,
269            false,
270        ));
271        assert!(!properties_obj.get().is_null());
272        let mut succeeded = false;
273        assert!(JS_SetImmutablePrototype(
274            *cx,
275            properties_obj.handle().into_handle(),
276            &mut succeeded
277        ));
278        assert!(succeeded);
279    }
280}