1 /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
2 // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
3 // SPDX-FileCopyrightText: 2008 litl, LLC
4 // SPDX-FileCopyrightText: 2012 Giovanni Campagna <scampa.giovanni@gmail.com>
5 
6 #include <config.h>
7 
8 #include <string.h>  // for strlen
9 
10 #include <glib.h>
11 
12 #include <js/CallArgs.h>  // for JSNative
13 #include <js/Class.h>
14 #include <js/ComparisonOperators.h>
15 #include <js/PropertyDescriptor.h>  // for JSPROP_GETTER
16 #include <js/Realm.h>  // for GetRealmObjectPrototype
17 #include <js/RootingAPI.h>
18 #include <js/TypeDecls.h>
19 #include <js/Value.h>
20 #include <js/ValueArray.h>
21 #include <jsapi.h>        // for JS_DefineFunctions, JS_DefineProp...
22 #include <jsfriendapi.h>  // for GetFunctionNativeReserved, NewFun...
23 #include <jspubtd.h>      // for JSProto_TypeError
24 
25 #include "gjs/atoms.h"
26 #include "gjs/context-private.h"
27 #include "gjs/jsapi-util.h"
28 #include "gjs/macros.h"
29 
30 struct JSFunctionSpec;
31 struct JSPropertySpec;
32 
33 /* Reserved slots of JSNative accessor wrappers */
34 enum {
35     DYNAMIC_PROPERTY_PRIVATE_SLOT,
36 };
37 
gjs_init_class_dynamic(JSContext * context,JS::HandleObject in_object,JS::HandleObject parent_proto,const char * ns_name,const char * class_name,const JSClass * clasp,JSNative constructor_native,unsigned nargs,JSPropertySpec * proto_ps,JSFunctionSpec * proto_fs,JSPropertySpec * static_ps,JSFunctionSpec * static_fs,JS::MutableHandleObject prototype,JS::MutableHandleObject constructor)38 bool gjs_init_class_dynamic(JSContext* context, JS::HandleObject in_object,
39                             JS::HandleObject parent_proto, const char* ns_name,
40                             const char* class_name, const JSClass* clasp,
41                             JSNative constructor_native, unsigned nargs,
42                             JSPropertySpec* proto_ps, JSFunctionSpec* proto_fs,
43                             JSPropertySpec* static_ps,
44                             JSFunctionSpec* static_fs,
45                             JS::MutableHandleObject prototype,
46                             JS::MutableHandleObject constructor) {
47     /* Without a name, JS_NewObject fails */
48     g_assert (clasp->name != NULL);
49 
50     /* gjs_init_class_dynamic only makes sense for instantiable classes,
51        use JS_InitClass for static classes like Math */
52     g_assert (constructor_native != NULL);
53 
54     /* Class initialization consists of five parts:
55        - building a prototype
56        - defining prototype properties and functions
57        - building a constructor and defining it on the right object
58        - defining constructor properties and functions
59        - linking the constructor and the prototype, so that
60          JS_NewObjectForConstructor can find it
61     */
62 
63     if (parent_proto) {
64         prototype.set(JS_NewObjectWithGivenProto(context, clasp, parent_proto));
65     } else {
66         /* JS_NewObject will use Object.prototype as the prototype if the
67          * clasp's constructor is not a built-in class.
68          */
69         prototype.set(JS_NewObject(context, clasp));
70     }
71     if (!prototype)
72         return false;
73 
74     if (proto_ps && !JS_DefineProperties(context, prototype, proto_ps))
75         return false;
76     if (proto_fs && !JS_DefineFunctions(context, prototype, proto_fs))
77         return false;
78 
79     GjsAutoChar full_function_name =
80         g_strdup_printf("%s_%s", ns_name, class_name);
81     JSFunction* constructor_fun =
82         JS_NewFunction(context, constructor_native, nargs, JSFUN_CONSTRUCTOR,
83                        full_function_name);
84     if (!constructor_fun)
85         return false;
86 
87     constructor.set(JS_GetFunctionObject(constructor_fun));
88 
89     if (static_ps && !JS_DefineProperties(context, constructor, static_ps))
90         return false;
91     if (static_fs && !JS_DefineFunctions(context, constructor, static_fs))
92         return false;
93 
94     if (!JS_LinkConstructorAndPrototype(context, constructor, prototype))
95         return false;
96 
97     /* The constructor defined by JS_InitClass has no property attributes, but this
98        is a more useful default for gjs */
99     return JS_DefineProperty(context, in_object, class_name, constructor,
100                              GJS_MODULE_PROP_FLAGS);
101 }
102 
format_dynamic_class_name(const char * name)103 [[nodiscard]] static const char* format_dynamic_class_name(const char* name) {
104     if (g_str_has_prefix(name, "_private_"))
105         return name + strlen("_private_");
106     else
107         return name;
108 }
109 
110 bool
gjs_typecheck_instance(JSContext * context,JS::HandleObject obj,const JSClass * static_clasp,bool throw_error)111 gjs_typecheck_instance(JSContext       *context,
112                        JS::HandleObject obj,
113                        const JSClass   *static_clasp,
114                        bool             throw_error)
115 {
116     if (!JS_InstanceOf(context, obj, static_clasp, NULL)) {
117         if (throw_error) {
118             const JSClass *obj_class = JS_GetClass(obj);
119 
120             gjs_throw_custom(context, JSProto_TypeError, nullptr,
121                              "Object %p is not a subclass of %s, it's a %s",
122                              obj.get(), static_clasp->name,
123                              format_dynamic_class_name(obj_class->name));
124         }
125 
126         return false;
127     }
128 
129     return true;
130 }
131 
132 JSObject*
gjs_construct_object_dynamic(JSContext * context,JS::HandleObject proto,const JS::HandleValueArray & args)133 gjs_construct_object_dynamic(JSContext                  *context,
134                              JS::HandleObject            proto,
135                              const JS::HandleValueArray& args)
136 {
137     const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
138     JS::RootedObject constructor(context);
139 
140     if (!gjs_object_require_property(context, proto, "prototype",
141                                      atoms.constructor(), &constructor))
142         return NULL;
143 
144     return JS_New(context, constructor, args);
145 }
146 
147 GJS_JSAPI_RETURN_CONVENTION
148 static JSObject *
define_native_accessor_wrapper(JSContext * cx,JSNative call,unsigned nargs,const char * func_name,JS::HandleValue private_slot)149 define_native_accessor_wrapper(JSContext      *cx,
150                                JSNative        call,
151                                unsigned        nargs,
152                                const char     *func_name,
153                                JS::HandleValue private_slot)
154 {
155     JSFunction *func = js::NewFunctionWithReserved(cx, call, nargs, 0, func_name);
156     if (!func)
157         return nullptr;
158 
159     JSObject *func_obj = JS_GetFunctionObject(func);
160     js::SetFunctionNativeReserved(func_obj, DYNAMIC_PROPERTY_PRIVATE_SLOT,
161                                   private_slot);
162     return func_obj;
163 }
164 
165 /**
166  * gjs_define_property_dynamic:
167  * @cx: the #JSContext
168  * @proto: the prototype of the object, on which to define the property
169  * @prop_name: name of the property or field in GObject, visible to JS code
170  * @func_namespace: string from which the internal names for the getter and
171  *   setter functions are built, not visible to JS code
172  * @getter: getter function
173  * @setter: setter function
174  * @private_slot: private data in the form of a #JS::Value that the getter and
175  *   setter will have access to
176  * @flags: additional flags to define the property with (other than the ones
177  *   required for a property with native getter/setter)
178  *
179  * When defining properties in a GBoxed or GObject, we can't have a separate
180  * getter and setter for each one, since the properties are defined dynamically.
181  * Therefore we must have one getter and setter for all the properties we define
182  * on all the types. In order to have that, we must provide the getter and
183  * setter with private data, e.g. the field index for GBoxed, in a "reserved
184  * slot" for which we must unfortunately use the jsfriendapi.
185  *
186  * Returns: %true on success, %false if an exception is pending on @cx.
187  */
188 bool
gjs_define_property_dynamic(JSContext * cx,JS::HandleObject proto,const char * prop_name,const char * func_namespace,JSNative getter,JSNative setter,JS::HandleValue private_slot,unsigned flags)189 gjs_define_property_dynamic(JSContext       *cx,
190                             JS::HandleObject proto,
191                             const char      *prop_name,
192                             const char      *func_namespace,
193                             JSNative         getter,
194                             JSNative         setter,
195                             JS::HandleValue  private_slot,
196                             unsigned         flags)
197 {
198     GjsAutoChar getter_name = g_strconcat(func_namespace, "_get::", prop_name, nullptr);
199     GjsAutoChar setter_name = g_strconcat(func_namespace, "_set::", prop_name, nullptr);
200 
201     JS::RootedObject getter_obj(cx,
202         define_native_accessor_wrapper(cx, getter, 0, getter_name, private_slot));
203     if (!getter_obj)
204         return false;
205 
206     JS::RootedObject setter_obj(cx,
207         define_native_accessor_wrapper(cx, setter, 1, setter_name, private_slot));
208     if (!setter_obj)
209         return false;
210 
211     flags |= JSPROP_GETTER | JSPROP_SETTER;
212 
213     return JS_DefineProperty(cx, proto, prop_name, getter_obj, setter_obj,
214                              flags);
215 }
216 
217 /**
218  * gjs_dynamic_property_private_slot:
219  * @accessor_obj: the getter or setter as a function object, i.e.
220  *   `&args.callee()` in the #JSNative function
221  *
222  * For use in dynamic property getters and setters (see
223  * gjs_define_property_dynamic()) to retrieve the private data passed there.
224  *
225  * Returns: the JS::Value that was passed to gjs_define_property_dynamic().
226  */
227 JS::Value
gjs_dynamic_property_private_slot(JSObject * accessor_obj)228 gjs_dynamic_property_private_slot(JSObject *accessor_obj)
229 {
230     return js::GetFunctionNativeReserved(accessor_obj,
231                                          DYNAMIC_PROPERTY_PRIVATE_SLOT);
232 }
233 
234 /**
235  * gjs_object_in_prototype_chain:
236  * @cx:
237  * @proto: The prototype which we are checking if @check_obj has in its chain
238  * @check_obj: The object to check
239  * @is_in_chain: (out): Whether @check_obj has @proto in its prototype chain
240  *
241  * Similar to JS_HasInstance() but takes into account abstract classes defined
242  * with JS_InitClass(), which JS_HasInstance() does not. Abstract classes don't
243  * have constructors, and JS_HasInstance() requires a constructor.
244  *
245  * Returns: false if an exception was thrown, true otherwise.
246  */
gjs_object_in_prototype_chain(JSContext * cx,JS::HandleObject proto,JS::HandleObject check_obj,bool * is_in_chain)247 bool gjs_object_in_prototype_chain(JSContext* cx, JS::HandleObject proto,
248                                    JS::HandleObject check_obj,
249                                    bool* is_in_chain) {
250     JS::RootedObject object_prototype(cx, JS::GetRealmObjectPrototype(cx));
251     if (!object_prototype)
252         return false;
253 
254     JS::RootedObject proto_iter(cx);
255     if (!JS_GetPrototype(cx, check_obj, &proto_iter))
256         return false;
257     while (proto_iter != object_prototype) {
258         if (proto_iter == proto) {
259             *is_in_chain = true;
260             return true;
261         }
262         if (!JS_GetPrototype(cx, proto_iter, &proto_iter))
263             return false;
264     }
265     *is_in_chain = false;
266     return true;
267 }
268