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