Branch data Line data Source code
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-2010 litl, LLC
4 : :
5 : : #include <config.h>
6 : :
7 : : #include <string.h> // for size_t, strcmp, strlen
8 : :
9 : : #ifdef _WIN32
10 : : # include <windows.h>
11 : : #endif
12 : :
13 : : #include <string>
14 : : #include <vector> // for vector
15 : :
16 : : #include <gio/gio.h>
17 : : #include <glib-object.h>
18 : : #include <glib.h>
19 : :
20 : : #include <js/Array.h>
21 : : #include <js/CallArgs.h>
22 : : #include <js/CharacterEncoding.h>
23 : : #include <js/Class.h>
24 : : #include <js/ComparisonOperators.h>
25 : : #include <js/ErrorReport.h> // for JS_ReportOutOfMemory, JSEXN_ERR
26 : : #include <js/Exception.h>
27 : : #include <js/GlobalObject.h> // for CurrentGlobalOrNull
28 : : #include <js/Id.h> // for PropertyKey
29 : : #include <js/Object.h> // for GetClass
30 : : #include <js/PropertyAndElement.h>
31 : : #include <js/PropertyDescriptor.h>
32 : : #include <js/PropertySpec.h>
33 : : #include <js/RootingAPI.h>
34 : : #include <js/String.h>
35 : : #include <js/Symbol.h>
36 : : #include <js/TypeDecls.h>
37 : : #include <js/Utility.h> // for UniqueChars
38 : : #include <js/Value.h>
39 : : #include <jsapi.h> // for JS_NewPlainObject, IdVector, JS_...
40 : : #include <mozilla/Maybe.h>
41 : : #include <mozilla/UniquePtr.h>
42 : :
43 : : #include "gjs/atoms.h"
44 : : #include "gjs/context-private.h"
45 : : #include "gjs/global.h"
46 : : #include "gjs/importer.h"
47 : : #include "gjs/jsapi-util.h"
48 : : #include "gjs/macros.h"
49 : : #include "gjs/module.h"
50 : : #include "gjs/native.h"
51 : : #include "util/log.h"
52 : :
53 : : #define MODULE_INIT_FILENAME "__init__.js"
54 : :
55 : : extern const JSClass gjs_importer_class;
56 : :
57 : : GJS_JSAPI_RETURN_CONVENTION
58 : : static JSObject* gjs_define_importer(JSContext*, JS::HandleObject, const char*,
59 : : const std::vector<std::string>&, bool);
60 : :
61 : : GJS_JSAPI_RETURN_CONVENTION
62 : : static bool
63 : 14 : importer_to_string(JSContext *cx,
64 : : unsigned argc,
65 : : JS::Value *vp)
66 : : {
67 [ - + ]: 14 : GJS_GET_THIS(cx, argc, vp, args, importer);
68 : :
69 : 14 : GjsAutoChar output;
70 : :
71 : 14 : const JSClass* klass = JS::GetClass(importer);
72 : 14 : const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
73 : 14 : JS::RootedValue module_path(cx);
74 [ - + ]: 14 : if (!JS_GetPropertyById(cx, importer, atoms.module_path(), &module_path))
75 : 0 : return false;
76 : :
77 [ + + ]: 14 : if (module_path.isNull()) {
78 : 7 : output = g_strdup_printf("[%s root]", klass->name);
79 : : } else {
80 : 7 : g_assert(module_path.isString() && "Bad importer.__modulePath__");
81 : 7 : JS::UniqueChars path = gjs_string_to_utf8(cx, module_path);
82 [ - + ]: 7 : if (!path)
83 : 0 : return false;
84 : 7 : output = g_strdup_printf("[%s %s]", klass->name, path.get());
85 [ + - ]: 7 : }
86 : :
87 : 14 : args.rval().setString(JS_NewStringCopyZ(cx, output));
88 : 14 : return true;
89 : 14 : }
90 : :
91 : : GJS_JSAPI_RETURN_CONVENTION
92 : : static bool
93 : 1233 : define_meta_properties(JSContext *context,
94 : : JS::HandleObject module_obj,
95 : : const char *parse_name,
96 : : const char *module_name,
97 : : JS::HandleObject parent)
98 : : {
99 : : bool parent_is_module;
100 : 1233 : const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
101 : :
102 : : /* For these meta-properties, don't set ENUMERATE since we wouldn't want to
103 : : * copy these symbols to any other object for example. RESOLVING is used to
104 : : * make sure we don't try to invoke a "resolve" operation, since this
105 : : * function may be called from inside one. */
106 : 1233 : unsigned attrs = JSPROP_READONLY | JSPROP_PERMANENT | JSPROP_RESOLVING;
107 : :
108 : : /* We define both __moduleName__ and __parentModule__ to null
109 : : * on the root importer
110 : : */
111 [ + - ]: 1233 : parent_is_module = parent && JS_InstanceOf(context, parent,
112 [ + + ]: 1233 : &gjs_importer_class, nullptr);
113 : :
114 [ + - ]: 2466 : gjs_debug(GJS_DEBUG_IMPORTER, "Defining parent %p of %p '%s' is mod %d",
115 : 1233 : parent.get(), module_obj.get(),
116 : : module_name ? module_name : "<root>", parent_is_module);
117 : :
118 [ + + ]: 1233 : if (parse_name != nullptr) {
119 : 432 : JS::RootedValue file(context);
120 [ - + ]: 432 : if (!gjs_string_from_utf8(context, parse_name, &file))
121 : 0 : return false;
122 [ - + ]: 432 : if (!JS_DefinePropertyById(context, module_obj, atoms.file(), file,
123 : : attrs))
124 : 0 : return false;
125 [ + - ]: 432 : }
126 : :
127 : : /* Null is used instead of undefined for backwards compatibility with code
128 : : * that explicitly checks for null. */
129 : 1233 : JS::RootedValue module_name_val(context, JS::NullValue());
130 : 1233 : JS::RootedValue parent_module_val(context, JS::NullValue());
131 : 1233 : JS::RootedValue module_path(context, JS::NullValue());
132 : 1233 : JS::RootedValue to_string_tag(context);
133 [ + + ]: 1233 : if (parent_is_module) {
134 [ - + ]: 992 : if (!gjs_string_from_utf8(context, module_name, &module_name_val))
135 : 0 : return false;
136 : 992 : parent_module_val.setObject(*parent);
137 : :
138 : 992 : JS::RootedValue parent_module_path(context);
139 [ - + ]: 992 : if (!JS_GetPropertyById(context, parent, atoms.module_path(),
140 : : &parent_module_path))
141 : 0 : return false;
142 : :
143 : 992 : GjsAutoChar module_path_buf;
144 [ + + ]: 992 : if (parent_module_path.isNull()) {
145 : 825 : module_path_buf = g_strdup(module_name);
146 : : } else {
147 : : JS::UniqueChars parent_path =
148 : 167 : gjs_string_to_utf8(context, parent_module_path);
149 [ - + ]: 167 : if (!parent_path)
150 : 0 : return false;
151 : 167 : module_path_buf = g_strdup_printf("%s.%s", parent_path.get(), module_name);
152 [ + - ]: 167 : }
153 [ - + ]: 992 : if (!gjs_string_from_utf8(context, module_path_buf, &module_path))
154 : 0 : return false;
155 : :
156 : : GjsAutoChar to_string_tag_buf = g_strdup_printf("GjsModule %s",
157 : 992 : module_path_buf.get());
158 [ - + ]: 992 : if (!gjs_string_from_utf8(context, to_string_tag_buf, &to_string_tag))
159 : 0 : return false;
160 [ + - + - : 992 : } else {
+ - ]
161 : 241 : to_string_tag.setString(JS_AtomizeString(context, "GjsModule"));
162 : : }
163 : :
164 [ - + ]: 1233 : if (!JS_DefinePropertyById(context, module_obj, atoms.module_name(),
165 : : module_name_val, attrs))
166 : 0 : return false;
167 : :
168 [ - + ]: 1233 : if (!JS_DefinePropertyById(context, module_obj, atoms.parent_module(),
169 : : parent_module_val, attrs))
170 : 0 : return false;
171 : :
172 [ - + ]: 1233 : if (!JS_DefinePropertyById(context, module_obj, atoms.module_path(),
173 : : module_path, attrs))
174 : 0 : return false;
175 : :
176 : : JS::RootedId to_string_tag_name(
177 : 1233 : context, JS::PropertyKey::Symbol(JS::GetWellKnownSymbol(
178 : 1233 : context, JS::SymbolCode::toStringTag)));
179 : 1233 : return JS_DefinePropertyById(context, module_obj, to_string_tag_name,
180 : 1233 : to_string_tag, attrs);
181 : 1233 : }
182 : :
183 : : GJS_JSAPI_RETURN_CONVENTION
184 : 68 : static bool import_directory(JSContext* context, JS::HandleObject obj,
185 : : const char* name,
186 : : const std::vector<std::string>& full_paths) {
187 : 68 : gjs_debug(GJS_DEBUG_IMPORTER,
188 : : "Importing directory '%s'",
189 : : name);
190 : :
191 : : // We define a sub-importer that has only the given directories on its
192 : : // search path.
193 : 68 : return !!gjs_define_importer(context, obj, name, full_paths, false);
194 : : }
195 : :
196 : : /* Make the property we set in gjs_module_import() permanent;
197 : : * we do this after the import successfully completes.
198 : : */
199 : : GJS_JSAPI_RETURN_CONVENTION
200 : : static bool
201 : 432 : seal_import(JSContext *cx,
202 : : JS::HandleObject obj,
203 : : JS::HandleId id,
204 : : const char *name)
205 : : {
206 : 432 : JS::Rooted<mozilla::Maybe<JS::PropertyDescriptor>> maybe_descr(cx);
207 : :
208 [ + - - + : 864 : if (!JS_GetOwnPropertyDescriptorById(cx, obj, id, &maybe_descr) ||
- + ]
209 : 432 : maybe_descr.isNothing()) {
210 : 0 : gjs_debug(GJS_DEBUG_IMPORTER,
211 : : "Failed to get attributes to seal '%s' in importer",
212 : : name);
213 : 0 : return false;
214 : : }
215 : :
216 : 432 : JS::Rooted<JS::PropertyDescriptor> descr(cx, maybe_descr.value());
217 : :
218 : 432 : descr.setConfigurable(false);
219 : :
220 [ - + ]: 432 : if (!JS_DefinePropertyById(cx, obj, id, descr)) {
221 : 0 : gjs_debug(GJS_DEBUG_IMPORTER,
222 : : "Failed to redefine attributes to seal '%s' in importer",
223 : : name);
224 : 0 : return false;
225 : : }
226 : :
227 : 432 : return true;
228 : 432 : }
229 : :
230 : : /* An import failed. Delete the property pointing to the import
231 : : * from the parent namespace. In complicated situations this might
232 : : * not be sufficient to get us fully back to a sane state. If:
233 : : *
234 : : * - We import module A
235 : : * - module A imports module B
236 : : * - module B imports module A, storing a reference to the current
237 : : * module A module object
238 : : * - module A subsequently throws an exception
239 : : *
240 : : * Then module B is left imported, but the imported module B has
241 : : * a reference to the failed module A module object. To handle this
242 : : * we could could try to track the entire "import operation" and
243 : : * roll back *all* modifications made to the namespace objects.
244 : : * It's not clear that the complexity would be worth the small gain
245 : : * in robustness. (You can still come up with ways of defeating
246 : : * the attempt to clean up.)
247 : : */
248 : : static void
249 : 4 : cancel_import(JSContext *context,
250 : : JS::HandleObject obj,
251 : : const char *name)
252 : : {
253 : 4 : gjs_debug(GJS_DEBUG_IMPORTER,
254 : : "Cleaning up from failed import of '%s'",
255 : : name);
256 : :
257 [ - + ]: 4 : if (!JS_DeleteProperty(context, obj, name)) {
258 : 0 : gjs_debug(GJS_DEBUG_IMPORTER,
259 : : "Failed to delete '%s' in importer",
260 : : name);
261 : : }
262 : 4 : }
263 : :
264 : : /*
265 : : * gjs_import_native_module:
266 : : * @cx: the #JSContext
267 : : * @importer: the root importer
268 : : * @id_str: Name under which the module was registered with add()
269 : : *
270 : : * Imports a builtin native-code module so that it is available to JS code as
271 : : * `imports[id_str]`.
272 : : *
273 : : * Returns: true on success, false if an exception was thrown.
274 : : */
275 : 492 : bool gjs_import_native_module(JSContext* cx, JS::HandleObject importer,
276 : : const char* id_str) {
277 : 492 : gjs_debug(GJS_DEBUG_IMPORTER, "Importing '%s'", id_str);
278 : :
279 : : JS::RootedObject native_registry(
280 : 492 : cx, gjs_get_native_registry(JS::CurrentGlobalOrNull(cx)));
281 : :
282 : 492 : JS::RootedId id(cx, gjs_intern_string_to_id(cx, id_str));
283 [ - + ]: 492 : if (id.isVoid())
284 : 0 : return false;
285 : :
286 : 492 : JS::RootedObject module(cx);
287 [ - + ]: 492 : if (!gjs_global_registry_get(cx, native_registry, id, &module))
288 : 0 : return false;
289 : :
290 [ + + ]: 935 : if (!module &&
291 [ + - ]: 443 : (!Gjs::NativeModuleDefineFuncs::get().define(cx, id_str, &module) ||
292 [ - + - + ]: 935 : !gjs_global_registry_set(cx, native_registry, id, module)))
293 : 0 : return false;
294 : :
295 [ + - + - ]: 984 : return define_meta_properties(cx, module, nullptr, id_str, importer) &&
296 : 492 : JS_DefineProperty(cx, importer, id_str, module,
297 : 492 : GJS_MODULE_PROP_FLAGS);
298 : 492 : }
299 : :
300 : : GJS_JSAPI_RETURN_CONVENTION
301 : : static bool
302 : 133 : import_module_init(JSContext *context,
303 : : GFile *file,
304 : : JS::HandleObject module_obj)
305 : : {
306 : 133 : gsize script_len = 0;
307 : 133 : GjsAutoError error;
308 : :
309 : 133 : GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context);
310 : 133 : JS::RootedValue ignored(context);
311 : :
312 : 133 : GjsAutoChar script;
313 [ + + ]: 133 : if (!g_file_load_contents(file, nullptr, script.out(), &script_len, nullptr,
314 : : &error)) {
315 [ + - ]: 260 : if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY) &&
316 [ + - - + : 260 : !g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY) &&
- + ]
317 : 130 : !g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
318 : 0 : gjs_throw_gerror_message(context, error);
319 : 0 : return false;
320 : : }
321 : :
322 : 130 : return true;
323 : : }
324 : 3 : g_assert(script);
325 : :
326 : 3 : GjsAutoChar full_path = g_file_get_parse_name(file);
327 : :
328 : 3 : return gjs->eval_with_scope(module_obj, script, script_len, full_path,
329 : 3 : &ignored);
330 : 133 : }
331 : :
332 : : GJS_JSAPI_RETURN_CONVENTION
333 : 1161 : static JSObject* load_module_init(JSContext* cx, JS::HandleObject in_object,
334 : : GFile* file) {
335 : : bool found;
336 : 1161 : const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
337 : :
338 : : /* First we check if js module has already been loaded */
339 [ - + ]: 1161 : if (!JS_HasPropertyById(cx, in_object, atoms.module_init(), &found))
340 : 0 : return nullptr;
341 [ + + ]: 1161 : if (found) {
342 : 1028 : JS::RootedValue v_module(cx);
343 [ - + ]: 1028 : if (!JS_GetPropertyById(cx, in_object, atoms.module_init(),
344 : : &v_module))
345 : 0 : return nullptr;
346 [ + - ]: 1028 : if (v_module.isObject())
347 : 1028 : return &v_module.toObject();
348 : :
349 : 0 : GjsAutoChar full_path = g_file_get_parse_name(file);
350 : 0 : gjs_throw(cx, "Unexpected non-object module __init__ imported from %s",
351 : : full_path.get());
352 : 0 : return nullptr;
353 : 1028 : }
354 : :
355 : 133 : JS::RootedObject module_obj(cx, JS_NewPlainObject(cx));
356 [ - + ]: 133 : if (!module_obj)
357 : 0 : return nullptr;
358 : :
359 [ + + ]: 133 : if (!import_module_init(cx, file, module_obj))
360 : 2 : return nullptr;
361 : :
362 [ - + ]: 131 : if (!JS_DefinePropertyById(cx, in_object, atoms.module_init(), module_obj,
363 : : GJS_MODULE_PROP_FLAGS & ~JSPROP_PERMANENT))
364 : 0 : return nullptr;
365 : :
366 : 131 : return module_obj;
367 : 133 : }
368 : :
369 : : GJS_JSAPI_RETURN_CONVENTION
370 : 3 : static bool load_module_elements(JSContext* cx, JS::HandleObject in_object,
371 : : JS::MutableHandleIdVector prop_ids,
372 : : GFile* file) {
373 : 3 : JS::RootedObject module_obj(cx, load_module_init(cx, in_object, file));
374 [ - + ]: 3 : if (!module_obj)
375 : 0 : return false;
376 : :
377 : 3 : JS::Rooted<JS::IdVector> ids(cx, cx);
378 [ - + ]: 3 : if (!JS_Enumerate(cx, module_obj, &ids))
379 : 0 : return false;
380 : :
381 [ - + ]: 3 : if (!prop_ids.appendAll(ids)) {
382 : 0 : JS_ReportOutOfMemory(cx);
383 : 0 : return false;
384 : : }
385 : :
386 : 3 : return true;
387 : 3 : }
388 : :
389 : : /* If error, returns false. If not found, returns true but does not touch
390 : : * the value at *result. If found, returns true and sets *result = true.
391 : : */
392 : : GJS_JSAPI_RETURN_CONVENTION
393 : 1158 : static bool import_symbol_from_init_js(JSContext* cx, JS::HandleObject importer,
394 : : GFile* directory, const char* name,
395 : : bool* result) {
396 : : bool found;
397 : : GjsAutoUnref<GFile> file =
398 : 1158 : g_file_get_child(directory, MODULE_INIT_FILENAME);
399 : :
400 : 1158 : JS::RootedObject module_obj(cx, load_module_init(cx, importer, file));
401 [ + + - + : 1158 : if (!module_obj || !JS_AlreadyHasOwnProperty(cx, module_obj, name, &found))
+ + ]
402 : 2 : return false;
403 : :
404 [ + + ]: 1156 : if (!found)
405 : 1154 : return true;
406 : :
407 : 2 : JS::RootedValue obj_val(cx);
408 [ - + ]: 2 : if (!JS_GetProperty(cx, module_obj, name, &obj_val))
409 : 0 : return false;
410 : :
411 [ - + ]: 2 : if (obj_val.isUndefined())
412 : 0 : return true;
413 : :
414 [ - + ]: 2 : if (!JS_DefineProperty(cx, importer, name, obj_val,
415 : : GJS_MODULE_PROP_FLAGS & ~JSPROP_PERMANENT))
416 : 0 : return false;
417 : :
418 : 2 : *result = true;
419 : 2 : return true;
420 : 1158 : }
421 : :
422 : : GJS_JSAPI_RETURN_CONVENTION
423 : 436 : static bool attempt_import(JSContext* cx, JS::HandleObject obj,
424 : : JS::HandleId module_id, const char* module_name,
425 : : GFile* file) {
426 : : JS::RootedObject module_obj(
427 : 436 : cx, gjs_module_import(cx, obj, module_id, module_name, file));
428 [ + + ]: 436 : if (!module_obj)
429 : 4 : return false;
430 : :
431 : 432 : GjsAutoChar full_path = g_file_get_parse_name(file);
432 : :
433 : 432 : return define_meta_properties(cx, module_obj, full_path, module_name,
434 [ + - + - ]: 864 : obj) &&
435 : 432 : seal_import(cx, obj, module_id, module_name);
436 : 436 : }
437 : :
438 : : GJS_JSAPI_RETURN_CONVENTION
439 : : static bool
440 : 436 : import_file_on_module(JSContext *context,
441 : : JS::HandleObject obj,
442 : : JS::HandleId id,
443 : : const char *name,
444 : : GFile *file)
445 : : {
446 [ + + ]: 436 : if (!attempt_import(context, obj, id, name, file)) {
447 : 4 : cancel_import(context, obj, name);
448 : 4 : return false;
449 : : }
450 : :
451 : 432 : return true;
452 : : }
453 : :
454 : : GJS_JSAPI_RETURN_CONVENTION
455 : 1079 : static bool do_import(JSContext* context, JS::HandleObject obj,
456 : : JS::HandleId id) {
457 : 1079 : JS::RootedObject search_path(context);
458 : : guint32 search_path_len;
459 : : guint32 i;
460 : : bool exists, is_array;
461 : 1079 : const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
462 : :
463 [ - + ]: 1079 : if (!gjs_object_require_property(context, obj, "importer",
464 : : atoms.search_path(), &search_path))
465 : 0 : return false;
466 : :
467 [ - + ]: 1079 : if (!JS::IsArrayObject(context, search_path, &is_array))
468 : 0 : return false;
469 [ - + ]: 1079 : if (!is_array) {
470 : 0 : gjs_throw(context, "searchPath property on importer is not an array");
471 : 0 : return false;
472 : : }
473 : :
474 [ - + ]: 1079 : if (!JS::GetArrayLength(context, search_path, &search_path_len)) {
475 : 0 : gjs_throw(context, "searchPath array has no length");
476 : 0 : return false;
477 : : }
478 : :
479 : 1079 : JS::UniqueChars name;
480 [ - + ]: 1079 : if (!gjs_get_string_id(context, id, &name))
481 : 0 : return false;
482 [ - + ]: 1079 : if (!name) {
483 : 0 : gjs_throw(context, "Importing invalid module name");
484 : 0 : return false;
485 : : }
486 : :
487 : : // null if this is the root importer
488 : 1079 : JS::RootedValue parent(context);
489 [ - + ]: 1079 : if (!JS_GetPropertyById(context, obj, atoms.parent_module(), &parent))
490 : 0 : return false;
491 : :
492 : : /* First try importing an internal module like gi */
493 [ + + + + : 1907 : if (parent.isNull() &&
+ + ]
494 : 828 : Gjs::NativeModuleDefineFuncs::get().is_registered(name.get())) {
495 [ - + ]: 492 : if (!gjs_import_native_module(context, obj, name.get()))
496 : 0 : return false;
497 : :
498 : 492 : gjs_debug(GJS_DEBUG_IMPORTER, "successfully imported module '%s'",
499 : : name.get());
500 : 492 : return true;
501 : : }
502 : :
503 : 587 : GjsAutoChar filename = g_strdup_printf("%s.js", name.get());
504 : 587 : std::vector<std::string> directories;
505 : 587 : JS::RootedValue elem(context);
506 : 587 : JS::RootedString str(context);
507 : :
508 [ + + ]: 1305 : for (i = 0; i < search_path_len; ++i) {
509 : 1158 : elem.setUndefined();
510 [ - + ]: 1158 : if (!JS_GetElement(context, search_path, i, &elem)) {
511 : : /* this means there was an exception, while elem.isUndefined()
512 : : * means no element found
513 : : */
514 : 440 : return false;
515 : : }
516 : :
517 [ - + ]: 1158 : if (elem.isUndefined())
518 : 718 : continue;
519 : :
520 [ - + ]: 1158 : if (!elem.isString()) {
521 : 0 : gjs_throw(context, "importer searchPath contains non-string");
522 : 0 : return false;
523 : : }
524 : :
525 : 1158 : str = elem.toString();
526 : 1158 : JS::UniqueChars dirname(JS_EncodeStringToUTF8(context, str));
527 [ - + ]: 1158 : if (!dirname)
528 : 0 : return false;
529 : :
530 : : /* Ignore empty path elements */
531 [ - + ]: 1158 : if (dirname[0] == '\0')
532 : 0 : continue;
533 : :
534 : : GjsAutoUnref<GFile> directory =
535 : 1158 : g_file_new_for_commandline_arg(dirname.get());
536 : :
537 : : /* Try importing __init__.js and loading the symbol from it */
538 : 1158 : bool found = false;
539 [ + + ]: 1158 : if (!import_symbol_from_init_js(context, obj, directory, name.get(),
540 : : &found))
541 : 2 : return false;
542 [ + + ]: 1156 : if (found)
543 : 2 : return true;
544 : :
545 : : /* Second try importing a directory (a sub-importer) */
546 : 1154 : GjsAutoUnref<GFile> file = g_file_get_child(directory, name.get());
547 : :
548 [ + + ]: 1154 : if (g_file_query_file_type(file, GFileQueryInfoFlags(0), nullptr) ==
549 : : G_FILE_TYPE_DIRECTORY) {
550 : 68 : GjsAutoChar full_path = g_file_get_parse_name(file);
551 : 68 : gjs_debug(GJS_DEBUG_IMPORTER,
552 : : "Adding directory '%s' to child importer '%s'",
553 : : full_path.get(), name.get());
554 : 68 : directories.push_back(full_path.get());
555 : 68 : }
556 : :
557 : : /* If we just added to directories, we know we don't need to
558 : : * check for a file. If we added to directories on an earlier
559 : : * iteration, we want to ignore any files later in the
560 : : * path. So, always skip the rest of the loop block if we have
561 : : * directories.
562 : : */
563 [ + + ]: 1154 : if (!directories.empty())
564 : 258 : continue;
565 : :
566 : : /* Third, if it's not a directory, try importing a file */
567 : 896 : file = g_file_get_child(directory, filename.get());
568 : 896 : exists = g_file_query_exists(file, nullptr);
569 : :
570 [ + + ]: 896 : if (!exists) {
571 : 460 : GjsAutoChar full_path = g_file_get_parse_name(file);
572 : 460 : gjs_debug(GJS_DEBUG_IMPORTER,
573 : : "JS import '%s' not found in %s at %s", name.get(),
574 : : dirname.get(), full_path.get());
575 : 460 : continue;
576 : 460 : }
577 : :
578 [ + + ]: 436 : if (import_file_on_module(context, obj, id, name.get(), file)) {
579 : 432 : gjs_debug(GJS_DEBUG_IMPORTER, "successfully imported module '%s'",
580 : : name.get());
581 : 432 : return true;
582 : : }
583 : :
584 : : /* Don't keep searching path if we fail to load the file for
585 : : * reasons other than it doesn't exist... i.e. broken files
586 : : * block searching for nonbroken ones
587 : : */
588 : 4 : return false;
589 [ + + + + : 3470 : }
+ + ]
590 : :
591 [ + + ]: 147 : if (!directories.empty()) {
592 [ - + ]: 68 : if (!import_directory(context, obj, name.get(), directories))
593 : 0 : return false;
594 : :
595 : 68 : gjs_debug(GJS_DEBUG_IMPORTER, "successfully imported directory '%s'",
596 : : name.get());
597 : 68 : return true;
598 : : }
599 : :
600 : : /* If no exception occurred, the problem is just that we got to the
601 : : * end of the path. Be sure an exception is set. */
602 : 79 : g_assert(!JS_IsExceptionPending(context));
603 : 79 : gjs_throw_custom(context, JSEXN_ERR, "ImportError",
604 : : "No JS module '%s' found in search path", name.get());
605 : 79 : return false;
606 : 1079 : }
607 : :
608 : : GJS_JSAPI_RETURN_CONVENTION
609 : 3 : static bool importer_new_enumerate(JSContext* context, JS::HandleObject object,
610 : : JS::MutableHandleIdVector properties,
611 : : bool enumerable_only [[maybe_unused]]) {
612 : : guint32 search_path_len;
613 : : guint32 i;
614 : : bool is_array;
615 : 3 : const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
616 : :
617 : 3 : JS::RootedObject search_path(context);
618 [ - + ]: 3 : if (!gjs_object_require_property(context, object, "importer",
619 : : atoms.search_path(), &search_path))
620 : 0 : return false;
621 : :
622 [ - + ]: 3 : if (!JS::IsArrayObject(context, search_path, &is_array))
623 : 0 : return false;
624 [ - + ]: 3 : if (!is_array) {
625 : 0 : gjs_throw(context, "searchPath property on importer is not an array");
626 : 0 : return false;
627 : : }
628 : :
629 [ - + ]: 3 : if (!JS::GetArrayLength(context, search_path, &search_path_len)) {
630 : 0 : gjs_throw(context, "searchPath array has no length");
631 : 0 : return false;
632 : : }
633 : :
634 : 3 : JS::RootedValue elem(context);
635 : 3 : JS::RootedString str(context);
636 [ + + ]: 6 : for (i = 0; i < search_path_len; ++i) {
637 : 3 : elem.setUndefined();
638 [ - + ]: 3 : if (!JS_GetElement(context, search_path, i, &elem)) {
639 : : /* this means there was an exception, while elem.isUndefined()
640 : : * means no element found
641 : : */
642 : 0 : return false;
643 : : }
644 : :
645 [ - + ]: 3 : if (elem.isUndefined())
646 : 0 : continue;
647 : :
648 [ - + ]: 3 : if (!elem.isString()) {
649 : 0 : gjs_throw(context, "importer searchPath contains non-string");
650 : 0 : return false;
651 : : }
652 : :
653 : 3 : str = elem.toString();
654 : 3 : JS::UniqueChars dirname(JS_EncodeStringToUTF8(context, str));
655 [ - + ]: 3 : if (!dirname)
656 : 0 : return false;
657 : :
658 : : GjsAutoUnref<GFile> directory =
659 : 3 : g_file_new_for_commandline_arg(dirname.get());
660 : : GjsAutoUnref<GFile> file =
661 : 3 : g_file_get_child(directory, MODULE_INIT_FILENAME);
662 : :
663 [ - + ]: 3 : if (!load_module_elements(context, object, properties, file))
664 : 0 : return false;
665 : :
666 : : /* new_for_commandline_arg handles resource:/// paths */
667 : : GjsAutoUnref<GFileEnumerator> direnum = g_file_enumerate_children(
668 : : directory, "standard::name,standard::type", G_FILE_QUERY_INFO_NONE,
669 : 3 : nullptr, nullptr);
670 : :
671 : : while (true) {
672 : : GFileInfo *info;
673 : : GFile *file;
674 [ - + ]: 60 : if (!g_file_enumerator_iterate(direnum, &info, &file, NULL, NULL))
675 : 0 : break;
676 [ + + + - ]: 60 : if (info == NULL || file == NULL)
677 : : break;
678 : :
679 : 57 : GjsAutoChar filename = g_file_get_basename(file);
680 : :
681 : : /* skip hidden files and directories (.svn, .git, ...) */
682 [ - + ]: 57 : if (filename.get()[0] == '.')
683 : 0 : continue;
684 : :
685 : : /* skip module init file */
686 [ - + ]: 57 : if (strcmp(filename, MODULE_INIT_FILENAME) == 0)
687 : 0 : continue;
688 : :
689 [ + + ]: 57 : if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) {
690 : 21 : jsid id = gjs_intern_string_to_id(context, filename);
691 [ - + ]: 21 : if (id.isVoid())
692 : 0 : return false;
693 [ - + ]: 21 : if (!properties.append(id)) {
694 : 0 : JS_ReportOutOfMemory(context);
695 : 0 : return false;
696 : : }
697 [ - + + - : 36 : } else if (g_str_has_suffix(filename, ".js")) {
+ + ]
698 : : GjsAutoChar filename_noext =
699 : 30 : g_strndup(filename, strlen(filename) - 3);
700 : 30 : jsid id = gjs_intern_string_to_id(context, filename_noext);
701 [ - + ]: 30 : if (id.isVoid())
702 : 0 : return false;
703 [ - + ]: 30 : if (!properties.append(id)) {
704 : 0 : JS_ReportOutOfMemory(context);
705 : 0 : return false;
706 : : }
707 [ + - ]: 30 : }
708 [ + - - ]: 114 : }
709 [ + - + - : 3 : }
+ - + - ]
710 : 3 : return true;
711 : 3 : }
712 : :
713 : : /* The *resolved out parameter, on success, should be false to indicate that id
714 : : * was not resolved; and true if id was resolved. */
715 : : GJS_JSAPI_RETURN_CONVENTION
716 : : static bool
717 : 1362 : importer_resolve(JSContext *context,
718 : : JS::HandleObject obj,
719 : : JS::HandleId id,
720 : : bool *resolved)
721 : : {
722 [ + + ]: 1362 : if (!id.isString()) {
723 : 1 : *resolved = false;
724 : 1 : return true;
725 : : }
726 : :
727 : 1361 : const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
728 [ + + + + : 2440 : if (id == atoms.module_init() || id == atoms.to_string() ||
- + ]
729 [ + + ]: 2440 : id == atoms.value_of()) {
730 : 282 : *resolved = false;
731 : 282 : return true;
732 : : }
733 : :
734 : : gjs_debug_jsprop(GJS_DEBUG_IMPORTER, "Resolve prop '%s' hook, obj %s",
735 : : gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str());
736 : :
737 [ - + ]: 1079 : if (!id.isString()) {
738 : 0 : *resolved = false;
739 : 0 : return true;
740 : : }
741 : :
742 [ + + ]: 1079 : if (!do_import(context, obj, id))
743 : 85 : return false;
744 : :
745 : 994 : *resolved = true;
746 : 994 : return true;
747 : : }
748 : :
749 : : static const JSClassOps gjs_importer_class_ops = {
750 : : nullptr, // addProperty
751 : : nullptr, // deleteProperty
752 : : nullptr, // enumerate
753 : : importer_new_enumerate,
754 : : importer_resolve,
755 : : };
756 : :
757 : : const JSClass gjs_importer_class = {
758 : : "GjsFileImporter",
759 : : 0,
760 : : &gjs_importer_class_ops,
761 : : };
762 : :
763 : : static const JSPropertySpec gjs_importer_proto_props[] = {
764 : : JS_STRING_SYM_PS(toStringTag, "GjsFileImporter", JSPROP_READONLY),
765 : : JS_PS_END};
766 : :
767 : : JSFunctionSpec gjs_importer_proto_funcs[] = {
768 : : JS_FN("toString", importer_to_string, 0, 0),
769 : : JS_FS_END};
770 : :
771 : 241 : [[nodiscard]] static const std::vector<std::string>& gjs_get_search_path() {
772 [ + + + - ]: 241 : static std::vector<std::string> gjs_search_path;
773 : : static bool search_path_initialized = false;
774 : :
775 : : /* not thread safe */
776 : :
777 [ + + ]: 241 : if (!search_path_initialized) {
778 : : const char* const* system_data_dirs;
779 : : const char *envstr;
780 : : gsize i;
781 : :
782 : : /* in order of priority */
783 : :
784 : : /* $GJS_PATH */
785 : 102 : envstr = g_getenv("GJS_PATH");
786 [ + - ]: 102 : if (envstr) {
787 : : char **dirs, **d;
788 : 102 : dirs = g_strsplit(envstr, G_SEARCHPATH_SEPARATOR_S, 0);
789 [ - + ]: 102 : for (d = dirs; *d != NULL; d++)
790 : 0 : gjs_search_path.push_back(*d);
791 : : /* we assume the array and strings are allocated separately */
792 : 102 : g_free(dirs);
793 : : }
794 : :
795 : 204 : gjs_search_path.push_back("resource:///org/gnome/gjs/modules/script/");
796 : 102 : gjs_search_path.push_back("resource:///org/gnome/gjs/modules/core/");
797 : :
798 : : /* $XDG_DATA_DIRS /gjs-1.0 */
799 : 102 : system_data_dirs = g_get_system_data_dirs();
800 [ + + ]: 306 : for (i = 0; system_data_dirs[i] != NULL; ++i) {
801 : : GjsAutoChar s =
802 : 204 : g_build_filename(system_data_dirs[i], "gjs-1.0", nullptr);
803 : 204 : gjs_search_path.push_back(s.get());
804 : 204 : }
805 : :
806 : : /* ${datadir}/share/gjs-1.0 */
807 : : #ifdef G_OS_WIN32
808 : : extern HMODULE gjs_dll;
809 : : char *basedir = g_win32_get_package_installation_directory_of_module (gjs_dll);
810 : : GjsAutoChar gjs_data_dir =
811 : : g_build_filename(basedir, "share", "gjs-1.0", nullptr);
812 : : gjs_search_path.push_back(gjs_data_dir.get());
813 : : g_free (basedir);
814 : : #else
815 : 102 : gjs_search_path.push_back(GJS_JS_DIR);
816 : : #endif
817 : :
818 : 102 : search_path_initialized = true;
819 : : }
820 : :
821 : 241 : return gjs_search_path;
822 : : }
823 : :
824 : : GJS_JSAPI_RETURN_CONVENTION
825 : 1 : static bool no_construct(JSContext* cx, unsigned argc, JS::Value* vp) {
826 : 1 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
827 : 1 : gjs_throw_abstract_constructor_error(cx, args);
828 : 1 : return false;
829 : : }
830 : :
831 : : GJS_JSAPI_RETURN_CONVENTION
832 : 309 : static JSObject* gjs_importer_define_proto(JSContext* cx) {
833 : 309 : JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
834 : 309 : g_assert(global && "Must enter a realm before defining importer");
835 : :
836 : : // If we've been here more than once, we already have the proto
837 : : JS::Value v_proto =
838 : 309 : gjs_get_global_slot(global, GjsGlobalSlot::PROTOTYPE_importer);
839 [ + + ]: 309 : if (!v_proto.isUndefined()) {
840 : 68 : g_assert(v_proto.isObject() &&
841 : : "Someone stored some weird value in a global slot");
842 : 68 : return &v_proto.toObject();
843 : : }
844 : :
845 : 241 : JS::RootedObject proto(cx, JS_NewPlainObject(cx));
846 [ + - + - ]: 482 : if (!proto || !JS_DefineFunctions(cx, proto, gjs_importer_proto_funcs) ||
847 [ - + - + ]: 482 : !JS_DefineProperties(cx, proto, gjs_importer_proto_props))
848 : 0 : return nullptr;
849 : 241 : gjs_set_global_slot(global, GjsGlobalSlot::PROTOTYPE_importer,
850 : 241 : JS::ObjectValue(*proto));
851 : :
852 : : // For backwards compatibility
853 : 241 : JSFunction* constructor = JS_NewFunction(
854 : : cx, no_construct, 0, JSFUN_CONSTRUCTOR, "GjsFileImporter");
855 : 241 : JS::RootedObject ctor_obj(cx, JS_GetFunctionObject(constructor));
856 [ + - ]: 482 : if (!JS_LinkConstructorAndPrototype(cx, ctor_obj, proto) ||
857 [ - + - + ]: 482 : !JS_DefineProperty(cx, global, "GjsFileImporter", ctor_obj, 0))
858 : 0 : return nullptr;
859 : :
860 : 241 : gjs_debug(GJS_DEBUG_CONTEXT, "Initialized class %s prototype %p",
861 : 241 : gjs_importer_class.name, proto.get());
862 : 241 : return proto;
863 : 309 : }
864 : :
865 : : GJS_JSAPI_RETURN_CONVENTION
866 : 309 : static JSObject* gjs_create_importer(
867 : : JSContext* context, const char* importer_name,
868 : : const std::vector<std::string>& initial_search_path,
869 : : bool add_standard_search_path, JS::HandleObject in_object) {
870 : 309 : std::vector<std::string> search_paths = initial_search_path;
871 [ + + ]: 309 : if (add_standard_search_path) {
872 : : /* Stick the "standard" shared search path after the provided one. */
873 : 241 : const std::vector<std::string>& gjs_search_path = gjs_get_search_path();
874 : 241 : search_paths.insert(search_paths.end(), gjs_search_path.begin(),
875 : : gjs_search_path.end());
876 : : }
877 : :
878 : 309 : JS::RootedObject proto(context, gjs_importer_define_proto(context));
879 [ - + ]: 309 : if (!proto)
880 : 0 : return nullptr;
881 : :
882 : : JS::RootedObject importer(
883 : : context,
884 : 309 : JS_NewObjectWithGivenProto(context, &gjs_importer_class, proto));
885 [ - + ]: 309 : if (!importer)
886 : 0 : return nullptr;
887 : :
888 : : gjs_debug_lifecycle(GJS_DEBUG_IMPORTER, "importer constructor, obj %p",
889 : : importer.get());
890 : :
891 : : /* API users can replace this property from JS, is the idea */
892 [ - + ]: 309 : if (!gjs_define_string_array(
893 : : context, importer, "searchPath", search_paths,
894 : : // settable (no READONLY) but not deletable (PERMANENT)
895 : : JSPROP_PERMANENT | JSPROP_RESOLVING))
896 : 0 : return nullptr;
897 : :
898 [ - + ]: 309 : if (!define_meta_properties(context, importer, NULL, importer_name, in_object))
899 : 0 : return nullptr;
900 : :
901 : 309 : return importer;
902 : 309 : }
903 : :
904 : : GJS_JSAPI_RETURN_CONVENTION
905 : 68 : static JSObject* gjs_define_importer(
906 : : JSContext* context, JS::HandleObject in_object, const char* importer_name,
907 : : const std::vector<std::string>& initial_search_path,
908 : : bool add_standard_search_path) {
909 : : JS::RootedObject importer(
910 : : context,
911 : 68 : gjs_create_importer(context, importer_name, initial_search_path,
912 : 68 : add_standard_search_path, in_object));
913 : :
914 [ - + ]: 68 : if (!JS_DefineProperty(context, in_object, importer_name, importer,
915 : : GJS_MODULE_PROP_FLAGS))
916 : 0 : return nullptr;
917 : :
918 : 68 : gjs_debug(GJS_DEBUG_IMPORTER,
919 : 68 : "Defined importer '%s' %p in %p", importer_name, importer.get(),
920 : 68 : in_object.get());
921 : :
922 : 68 : return importer;
923 : 68 : }
924 : :
925 : 241 : JSObject* gjs_create_root_importer(
926 : : JSContext* cx, const std::vector<std::string>& search_path) {
927 : 241 : return gjs_create_importer(cx, "imports", search_path, true, nullptr);
928 : : }
|