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: 2017 Philip Chimento <philip.chimento@gmail.com>
4 : :
5 : : #include <config.h>
6 : :
7 : : #include <stddef.h> // for size_t
8 : : #include <string.h>
9 : :
10 : : #include <vector> // for vector
11 : :
12 : : #include <gio/gio.h>
13 : : #include <glib-object.h>
14 : : #include <glib.h>
15 : :
16 : : #include <js/CallAndConstruct.h>
17 : : #include <js/CallArgs.h>
18 : : #include <js/CharacterEncoding.h> // for ConstUTF8CharsZ
19 : : #include <js/Class.h>
20 : : #include <js/CompilationAndEvaluation.h>
21 : : #include <js/CompileOptions.h>
22 : : #include <js/Conversions.h>
23 : : #include <js/ErrorReport.h> // for JS_ReportOutOfMemory
24 : : #include <js/Exception.h>
25 : : #include <js/GCVector.h> // for RootedVector
26 : : #include <js/GlobalObject.h> // for CurrentGlobalOrNull
27 : : #include <js/Id.h>
28 : : #include <js/Modules.h>
29 : : #include <js/Object.h>
30 : : #include <js/Promise.h>
31 : : #include <js/PropertyAndElement.h>
32 : : #include <js/PropertyDescriptor.h>
33 : : #include <js/RootingAPI.h>
34 : : #include <js/ScriptPrivate.h>
35 : : #include <js/SourceText.h>
36 : : #include <js/String.h>
37 : : #include <js/TypeDecls.h>
38 : : #include <js/Utility.h> // for UniqueChars
39 : : #include <js/Value.h>
40 : : #include <js/ValueArray.h>
41 : : #include <jsapi.h> // for JS_GetFunctionObject, JS_Ne...
42 : : #include <jsfriendapi.h> // for NewFunctionWithReserved
43 : : #include <mozilla/Maybe.h>
44 : :
45 : : #include "gjs/atoms.h"
46 : : #include "gjs/context-private.h"
47 : : #include "gjs/deprecation.h"
48 : : #include "gjs/global.h"
49 : : #include "gjs/jsapi-util-args.h"
50 : : #include "gjs/jsapi-util.h"
51 : : #include "gjs/macros.h"
52 : : #include "gjs/mem-private.h"
53 : : #include "gjs/module.h"
54 : : #include "gjs/native.h"
55 : : #include "util/log.h"
56 : : #include "util/misc.h"
57 : :
58 : : namespace mozilla {
59 : : union Utf8Unit;
60 : : }
61 : :
62 : : class GjsScriptModule {
63 : : GjsAutoChar m_name;
64 : :
65 : : // Reserved slots
66 : : static const size_t POINTER = 0;
67 : :
68 : 794 : GjsScriptModule(const char* name) : m_name(g_strdup(name)) {
69 : 397 : GJS_INC_COUNTER(module);
70 : 397 : }
71 : :
72 : 392 : ~GjsScriptModule() { GJS_DEC_COUNTER(module); }
73 : :
74 : : GjsScriptModule(GjsScriptModule&) = delete;
75 : : GjsScriptModule& operator=(GjsScriptModule&) = delete;
76 : :
77 : : /* Private data accessors */
78 : :
79 : 238421 : [[nodiscard]] static inline GjsScriptModule* priv(JSObject* module) {
80 : 238421 : return JS::GetMaybePtrFromReservedSlot<GjsScriptModule>(
81 : 238421 : module, GjsScriptModule::POINTER);
82 : : }
83 : :
84 : : /* Creates a JS module object. Use instead of the class's constructor */
85 : 397 : [[nodiscard]] static JSObject* create(JSContext* cx, const char* name) {
86 : 397 : JSObject* module = JS_NewObject(cx, &GjsScriptModule::klass);
87 : 397 : JS::SetReservedSlot(module, GjsScriptModule::POINTER,
88 : 397 : JS::PrivateValue(new GjsScriptModule(name)));
89 : 397 : return module;
90 : : }
91 : :
92 : : /* Defines the empty module as a property on the importer */
93 : : GJS_JSAPI_RETURN_CONVENTION
94 : : bool
95 : 397 : define_import(JSContext *cx,
96 : : JS::HandleObject module,
97 : : JS::HandleObject importer,
98 : : JS::HandleId name) const
99 : : {
100 [ - + ]: 397 : if (!JS_DefinePropertyById(cx, importer, name, module,
101 : : GJS_MODULE_PROP_FLAGS & ~JSPROP_PERMANENT)) {
102 : 0 : gjs_debug(GJS_DEBUG_IMPORTER, "Failed to define '%s' in importer",
103 : : m_name.get());
104 : 0 : return false;
105 : : }
106 : :
107 : 397 : return true;
108 : : }
109 : :
110 : : /* Carries out the actual execution of the module code */
111 : : GJS_JSAPI_RETURN_CONVENTION
112 : 397 : bool evaluate_import(JSContext* cx, JS::HandleObject module,
113 : : const char* source, size_t source_len,
114 : : const char* filename, const char* uri) {
115 : 397 : JS::SourceText<mozilla::Utf8Unit> buf;
116 [ - + ]: 397 : if (!buf.init(cx, source, source_len, JS::SourceOwnership::Borrowed))
117 : 0 : return false;
118 : :
119 : 397 : JS::RootedObjectVector scope_chain(cx);
120 [ - + ]: 397 : if (!scope_chain.append(module)) {
121 : 0 : JS_ReportOutOfMemory(cx);
122 : 0 : return false;
123 : : }
124 : :
125 : 397 : JS::CompileOptions options(cx);
126 : 397 : options.setFileAndLine(filename, 1).setNonSyntacticScope(true);
127 : :
128 : 397 : JS::RootedObject priv(cx, build_private(cx, uri));
129 [ - + ]: 397 : if (!priv)
130 : 0 : return false;
131 : :
132 : 397 : JS::RootedScript script(cx, JS::Compile(cx, options, buf));
133 [ + + ]: 397 : if (!script)
134 : 1 : return false;
135 : :
136 : 396 : JS::SetScriptPrivate(script, JS::ObjectValue(*priv));
137 : 396 : JS::RootedValue ignored_retval(cx);
138 [ + + ]: 396 : if (!JS_ExecuteScript(cx, scope_chain, script, &ignored_retval))
139 : 3 : return false;
140 : :
141 : 393 : GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx);
142 : 393 : gjs->schedule_gc_if_needed();
143 : :
144 : 393 : gjs_debug(GJS_DEBUG_IMPORTER, "Importing module %s succeeded",
145 : : m_name.get());
146 : :
147 : 393 : return true;
148 : 397 : }
149 : :
150 : : /* Loads JS code from a file and imports it */
151 : : GJS_JSAPI_RETURN_CONVENTION
152 : : bool
153 : 397 : import_file(JSContext *cx,
154 : : JS::HandleObject module,
155 : : GFile *file)
156 : : {
157 : 397 : GjsAutoError error;
158 : 397 : GjsAutoChar script;
159 : 397 : size_t script_len = 0;
160 : :
161 [ - + ]: 397 : if (!(g_file_load_contents(file, nullptr, script.out(), &script_len,
162 : : nullptr, &error)))
163 : 0 : return gjs_throw_gerror_message(cx, error);
164 : 397 : g_assert(script);
165 : :
166 : 397 : GjsAutoChar full_path = g_file_get_parse_name(file);
167 : 397 : GjsAutoChar uri = g_file_get_uri(file);
168 : 397 : return evaluate_import(cx, module, script, script_len, full_path, uri);
169 : 397 : }
170 : :
171 : : /* JSClass operations */
172 : :
173 : : GJS_JSAPI_RETURN_CONVENTION
174 : : bool
175 : 237235 : resolve_impl(JSContext *cx,
176 : : JS::HandleObject module,
177 : : JS::HandleId id,
178 : : bool *resolved)
179 : : {
180 : 237235 : JS::RootedObject lexical(cx, JS_ExtensibleLexicalEnvironment(module));
181 [ - + ]: 237235 : if (!lexical) {
182 : 0 : *resolved = false;
183 : 0 : return true; /* nothing imported yet */
184 : : }
185 : :
186 : 237235 : JS::Rooted<mozilla::Maybe<JS::PropertyDescriptor>> maybe_desc(cx);
187 : 237235 : JS::RootedObject holder(cx);
188 [ - + ]: 237235 : if (!JS_GetPropertyDescriptorById(cx, lexical, id, &maybe_desc,
189 : : &holder))
190 : 0 : return false;
191 [ + + ]: 237235 : if (maybe_desc.isNothing())
192 : 237233 : return true;
193 : :
194 : : /* The property is present in the lexical environment. This should not
195 : : * be supported according to ES6. For compatibility with earlier GJS,
196 : : * we treat it as if it were a real property, but warn about it. */
197 : :
198 : 4 : _gjs_warn_deprecated_once_per_callsite(
199 : : cx, GjsDeprecationMessageId::ModuleExportedLetOrConst,
200 : 4 : {gjs_debug_id(id).c_str(), m_name});
201 : :
202 : 2 : JS::Rooted<JS::PropertyDescriptor> desc(cx, maybe_desc.value());
203 : 2 : return JS_DefinePropertyById(cx, module, id, desc);
204 : 237235 : }
205 : :
206 : : GJS_JSAPI_RETURN_CONVENTION
207 : : static bool
208 : 237235 : resolve(JSContext *cx,
209 : : JS::HandleObject module,
210 : : JS::HandleId id,
211 : : bool *resolved)
212 : : {
213 : 237235 : return priv(module)->resolve_impl(cx, module, id, resolved);
214 : : }
215 : :
216 : 392 : static void finalize(JS::GCContext*, JSObject* module) {
217 [ + - ]: 392 : delete priv(module);
218 : 392 : }
219 : :
220 : : static constexpr JSClassOps class_ops = {
221 : : nullptr, // addProperty
222 : : nullptr, // deleteProperty
223 : : nullptr, // enumerate
224 : : nullptr, // newEnumerate
225 : : &GjsScriptModule::resolve,
226 : : nullptr, // mayResolve
227 : : &GjsScriptModule::finalize,
228 : : };
229 : :
230 : : static constexpr JSClass klass = {
231 : : "GjsScriptModule",
232 : : JSCLASS_HAS_RESERVED_SLOTS(1) | JSCLASS_BACKGROUND_FINALIZE,
233 : : &GjsScriptModule::class_ops,
234 : : };
235 : :
236 : : public:
237 : : /*
238 : : * Creates a JS object to pass to JS::SetScriptPrivate as a script's
239 : : * private.
240 : : */
241 : : GJS_JSAPI_RETURN_CONVENTION
242 : 641 : static JSObject* build_private(JSContext* cx, const char* script_uri) {
243 : 641 : JS::RootedObject priv(cx, JS_NewPlainObject(cx));
244 : 641 : const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
245 : :
246 : 641 : JS::RootedValue val(cx);
247 [ + - ]: 1282 : if (!gjs_string_from_utf8(cx, script_uri, &val) ||
248 [ - + - + ]: 1282 : !JS_SetPropertyById(cx, priv, atoms.uri(), val))
249 : 0 : return nullptr;
250 : :
251 : 641 : return priv;
252 : 641 : }
253 : :
254 : : /* Carries out the import operation */
255 : : GJS_JSAPI_RETURN_CONVENTION
256 : : static JSObject *
257 : 397 : import(JSContext *cx,
258 : : JS::HandleObject importer,
259 : : JS::HandleId id,
260 : : const char *name,
261 : : GFile *file)
262 : : {
263 : 397 : JS::RootedObject module(cx, GjsScriptModule::create(cx, name));
264 : 397 : if (!module ||
265 [ + - + - ]: 794 : !priv(module)->define_import(cx, module, importer, id) ||
266 [ + + + + ]: 794 : !priv(module)->import_file(cx, module, file))
267 : 4 : return nullptr;
268 : :
269 : 393 : return module;
270 : 397 : }
271 : : };
272 : :
273 : : /**
274 : : * gjs_script_module_build_private:
275 : : * @param cx the #JSContext
276 : : * @param uri the URI this script module is loaded from
277 : : *
278 : : * @brief To support dynamic imports from scripts, we need to provide private
279 : : * data when we compile scripts which is compatible with our module resolution
280 : : * hooks in modules/internal/loader.js
281 : : *
282 : : * @returns a JSObject which can be used for a JSScript's private data.
283 : : */
284 : 244 : JSObject* gjs_script_module_build_private(JSContext* cx, const char* uri) {
285 : 244 : return GjsScriptModule::build_private(cx, uri);
286 : : }
287 : :
288 : : /**
289 : : * gjs_module_import:
290 : : * @cx: the JS context
291 : : * @importer: the JS importer object, parent of the module to be imported
292 : : * @id: module name in the form of a jsid
293 : : * @name: module name, used for logging and identification
294 : : * @file: location of the file to import
295 : : *
296 : : * Carries out an import of a GJS module.
297 : : * Defines a property @name on @importer pointing to the module object, which
298 : : * is necessary in the case of cyclic imports.
299 : : * This property is not permanent; the caller is responsible for making it
300 : : * permanent if the import succeeds.
301 : : *
302 : : * Returns: the JS module object, or nullptr on failure.
303 : : */
304 : : JSObject *
305 : 397 : gjs_module_import(JSContext *cx,
306 : : JS::HandleObject importer,
307 : : JS::HandleId id,
308 : : const char *name,
309 : : GFile *file)
310 : : {
311 : 397 : return GjsScriptModule::import(cx, importer, id, name, file);
312 : : }
313 : :
314 : : decltype(GjsScriptModule::klass) constexpr GjsScriptModule::klass;
315 : : decltype(GjsScriptModule::class_ops) constexpr GjsScriptModule::class_ops;
316 : :
317 : : /**
318 : : * gjs_get_native_registry:
319 : : *
320 : : * @brief Retrieves a global's native registry from the NATIVE_REGISTRY slot.
321 : : * Registries are JS Map objects created with JS::NewMapObject instead
322 : : * of GCHashMaps (used elsewhere in GJS) because the objects need to be
323 : : * exposed to internal JS code and accessed from native C++ code.
324 : : *
325 : : * @param global a global #JSObject
326 : : *
327 : : * @returns the registry map as a #JSObject
328 : : */
329 : 24017 : JSObject* gjs_get_native_registry(JSObject* global) {
330 : : JS::Value native_registry =
331 : 24017 : gjs_get_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY);
332 : :
333 : 24017 : g_assert(native_registry.isObject());
334 : 24017 : return &native_registry.toObject();
335 : : }
336 : :
337 : : /**
338 : : * gjs_get_module_registry:
339 : : *
340 : : * @brief Retrieves a global's module registry from the MODULE_REGISTRY slot.
341 : : * Registries are JS Maps. See gjs_get_native_registry for more detail.
342 : : *
343 : : * @param cx the current #JSContext
344 : : * @param global a global #JSObject
345 : : *
346 : : * @returns the registry map as a #JSObject
347 : : */
348 : 5042 : JSObject* gjs_get_module_registry(JSObject* global) {
349 : : JS::Value esm_registry =
350 : 5042 : gjs_get_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY);
351 : :
352 : 5042 : g_assert(esm_registry.isObject());
353 : 5042 : return &esm_registry.toObject();
354 : : }
355 : :
356 : : /**
357 : : * gjs_module_load:
358 : : *
359 : : * Loads and registers a module given a specifier and
360 : : * URI.
361 : : *
362 : : * @returns whether an error occurred while resolving the specifier.
363 : : */
364 : 343 : JSObject* gjs_module_load(JSContext* cx, const char* identifier,
365 : : const char* file_uri) {
366 : 343 : g_assert((gjs_global_is_type(cx, GjsGlobalType::DEFAULT) ||
367 : : gjs_global_is_type(cx, GjsGlobalType::INTERNAL)) &&
368 : : "gjs_module_load can only be called from module-enabled "
369 : : "globals.");
370 : :
371 : 343 : JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
372 : : JS::RootedValue v_loader(
373 : 343 : cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_LOADER));
374 : 343 : g_assert(v_loader.isObject());
375 : 343 : JS::RootedObject loader(cx, &v_loader.toObject());
376 : :
377 : 343 : JS::ConstUTF8CharsZ id_chars(identifier, strlen(identifier));
378 : 343 : JS::ConstUTF8CharsZ uri_chars(file_uri, strlen(file_uri));
379 : 343 : JS::RootedString id(cx, JS_NewStringCopyUTF8Z(cx, id_chars));
380 [ - + ]: 343 : if (!id)
381 : 0 : return nullptr;
382 : 343 : JS::RootedString uri(cx, JS_NewStringCopyUTF8Z(cx, uri_chars));
383 [ - + ]: 343 : if (!uri)
384 : 0 : return nullptr;
385 : :
386 : 343 : JS::RootedValueArray<2> args(cx);
387 : 343 : args[0].setString(id);
388 : 343 : args[1].setString(uri);
389 : :
390 : 343 : gjs_debug(GJS_DEBUG_IMPORTER,
391 : : "Module resolve hook for module '%s' (%s), global %p", identifier,
392 : 343 : file_uri, global.get());
393 : :
394 : 343 : JS::RootedValue result(cx);
395 [ + + ]: 343 : if (!JS::Call(cx, loader, "moduleLoadHook", args, &result))
396 : 1 : return nullptr;
397 : :
398 : 342 : g_assert(result.isObject() && "Module hook failed to return an object!");
399 : 342 : return &result.toObject();
400 : 343 : }
401 : :
402 : : /**
403 : : * import_native_module_sync:
404 : : *
405 : : * @brief Synchronously imports native "modules" from the import global's
406 : : * native registry. This function does not do blocking I/O so it is
407 : : * safe to call it synchronously for accessing native "modules" within
408 : : * modules. This function is always called within the import global's
409 : : * realm.
410 : : *
411 : : * Compare gjs_import_native_module() for the legacy importer.
412 : : *
413 : : * @param cx the current JSContext
414 : : * @param argc
415 : : * @param vp
416 : : *
417 : : * @returns whether an error occurred while importing the native module.
418 : : */
419 : 6552 : static bool import_native_module_sync(JSContext* cx, unsigned argc,
420 : : JS::Value* vp) {
421 : 6552 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
422 : 6552 : JS::UniqueChars id;
423 [ - + ]: 6552 : if (!gjs_parse_call_args(cx, "importSync", args, "s", "identifier", &id))
424 : 0 : return false;
425 : :
426 : 6552 : Gjs::AutoMainRealm ar{cx};
427 : 6552 : JS::RootedObject global{cx, JS::CurrentGlobalOrNull(cx)};
428 : :
429 : 6552 : JS::AutoSaveExceptionState exc_state(cx);
430 : :
431 : 6552 : JS::RootedObject native_registry(cx, gjs_get_native_registry(global));
432 : 6552 : JS::RootedObject v_module(cx);
433 : :
434 : 6552 : JS::RootedId key(cx, gjs_intern_string_to_id(cx, id.get()));
435 [ - + ]: 6552 : if (!gjs_global_registry_get(cx, native_registry, key, &v_module))
436 : 0 : return false;
437 : :
438 [ + + ]: 6552 : if (v_module) {
439 : 6465 : args.rval().setObject(*v_module);
440 : 6465 : return true;
441 : : }
442 : :
443 : 87 : JS::RootedObject native_obj(cx);
444 [ - + ]: 87 : if (!Gjs::NativeModuleDefineFuncs::get().define(cx, id.get(),
445 : : &native_obj)) {
446 : 0 : gjs_throw(cx, "Failed to load native module: %s", id.get());
447 : 0 : return false;
448 : : }
449 : :
450 [ - + ]: 87 : if (!gjs_global_registry_set(cx, native_registry, key, native_obj))
451 : 0 : return false;
452 : :
453 : 87 : args.rval().setObject(*native_obj);
454 : 87 : return true;
455 : 6552 : }
456 : :
457 : : /**
458 : : * gjs_populate_module_meta:
459 : : *
460 : : * Hook SpiderMonkey calls to populate the import.meta object.
461 : : * Defines a property "import.meta.url", and additionally a method
462 : : * "import.meta.importSync" if this is an internal module.
463 : : *
464 : : * @param private_ref the private value for the #Module object
465 : : * @param meta the import.meta object
466 : : *
467 : : * @returns whether an error occurred while populating the module meta.
468 : : */
469 : 143 : bool gjs_populate_module_meta(JSContext* cx, JS::HandleValue private_ref,
470 : : JS::HandleObject meta) {
471 : 143 : g_assert(private_ref.isObject());
472 : 143 : JS::RootedObject module(cx, &private_ref.toObject());
473 : :
474 : 143 : gjs_debug(GJS_DEBUG_IMPORTER, "Module metadata hook for module %p",
475 : 143 : &private_ref.toObject());
476 : :
477 : 143 : const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
478 : 143 : JS::RootedValue specifier{cx};
479 [ + - ]: 286 : if (!JS_GetProperty(cx, module, "id", &specifier) ||
480 [ - + - + ]: 286 : !JS_DefinePropertyById(cx, meta, atoms.url(), specifier,
481 : : GJS_MODULE_PROP_FLAGS))
482 : 0 : return false;
483 : :
484 : 143 : JS::RootedValue v_internal(cx);
485 [ - + ]: 143 : if (!JS_GetPropertyById(cx, module, atoms.internal(), &v_internal))
486 : 0 : return false;
487 [ + + ]: 143 : if (JS::ToBoolean(v_internal)) {
488 : 138 : gjs_debug(GJS_DEBUG_IMPORTER, "Defining meta.importSync for module %p",
489 : 138 : &private_ref.toObject());
490 [ - + ]: 138 : if (!JS_DefineFunctionById(cx, meta, atoms.importSync(),
491 : : import_native_module_sync, 1,
492 : : GJS_MODULE_PROP_FLAGS))
493 : 0 : return false;
494 : : }
495 : :
496 : 143 : return true;
497 : 143 : }
498 : :
499 : : // Canonicalize specifier so that differently-spelled specifiers referring to
500 : : // the same module don't result in duplicate entries in the registry
501 : 3800 : static bool canonicalize_specifier(JSContext* cx,
502 : : JS::MutableHandleString specifier) {
503 : 3800 : JS::UniqueChars specifier_utf8 = JS_EncodeStringToUTF8(cx, specifier);
504 [ - + ]: 3800 : if (!specifier_utf8)
505 : 0 : return false;
506 : :
507 : 3800 : GjsAutoChar scheme, host, path, query;
508 [ - + ]: 3800 : if (!g_uri_split(specifier_utf8.get(), G_URI_FLAGS_NONE, scheme.out(),
509 : : nullptr, host.out(), nullptr, path.out(), query.out(),
510 : : nullptr, nullptr))
511 : 0 : return false;
512 : :
513 [ + + ]: 3800 : if (g_strcmp0(scheme, "gi")) {
514 : : // canonicalize without the query portion to avoid it being encoded
515 : : GjsAutoChar for_file_uri =
516 : 3507 : g_uri_join(G_URI_FLAGS_NONE, scheme.get(), nullptr, host.get(), -1,
517 : 7014 : path.get(), nullptr, nullptr);
518 : 3507 : GjsAutoUnref<GFile> file = g_file_new_for_uri(for_file_uri.get());
519 : 3507 : for_file_uri = g_file_get_uri(file);
520 : 3507 : host.reset();
521 : 3507 : path.reset();
522 [ - + ]: 3507 : if (!g_uri_split(for_file_uri.get(), G_URI_FLAGS_NONE, nullptr, nullptr,
523 : : host.out(), nullptr, path.out(), nullptr, nullptr,
524 : : nullptr))
525 : 0 : return false;
526 [ + - + - ]: 3507 : }
527 : :
528 : : GjsAutoChar canonical_specifier =
529 : 3800 : g_uri_join(G_URI_FLAGS_NONE, scheme.get(), nullptr, host.get(), -1,
530 : 7600 : path.get(), query.get(), nullptr);
531 : 3800 : JS::ConstUTF8CharsZ chars{canonical_specifier, strlen(canonical_specifier)};
532 : 3800 : JS::RootedString new_specifier{cx, JS_NewStringCopyUTF8Z(cx, chars)};
533 [ - + ]: 3800 : if (!new_specifier)
534 : 0 : return false;
535 : :
536 : 3800 : specifier.set(new_specifier);
537 : 3800 : return true;
538 : 3800 : }
539 : :
540 : : /**
541 : : * gjs_module_resolve:
542 : : *
543 : : * Hook SpiderMonkey calls to resolve import specifiers.
544 : : *
545 : : * @param importingModulePriv the private value of the #Module object initiating
546 : : * the import, or a JS null value
547 : : * @param specifier the import specifier to resolve
548 : : *
549 : : * @returns whether an error occurred while resolving the specifier.
550 : : */
551 : 3764 : JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importingModulePriv,
552 : : JS::HandleObject module_request) {
553 : 3764 : g_assert((gjs_global_is_type(cx, GjsGlobalType::DEFAULT) ||
554 : : gjs_global_is_type(cx, GjsGlobalType::INTERNAL)) &&
555 : : "gjs_module_resolve can only be called from module-enabled "
556 : : "globals.");
557 : : JS::RootedString specifier(
558 : 3764 : cx, JS::GetModuleRequestSpecifier(cx, module_request));
559 : :
560 : 3764 : JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
561 : : JS::RootedValue v_loader(
562 : 3764 : cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_LOADER));
563 : 3764 : g_assert(v_loader.isObject());
564 : 3764 : JS::RootedObject loader(cx, &v_loader.toObject());
565 : :
566 [ - + ]: 3764 : if (!canonicalize_specifier(cx, &specifier))
567 : 0 : return nullptr;
568 : :
569 : 3764 : JS::RootedValueArray<2> args(cx);
570 : 3764 : args[0].set(importingModulePriv);
571 : 3764 : args[1].setString(specifier);
572 : :
573 : 7528 : gjs_debug(GJS_DEBUG_IMPORTER,
574 : : "Module resolve hook for module %s (relative to %s), global %p",
575 : 7528 : gjs_debug_string(specifier).c_str(),
576 : 7528 : gjs_debug_value(importingModulePriv).c_str(), global.get());
577 : :
578 : 3764 : JS::RootedValue result(cx);
579 [ + + ]: 3764 : if (!JS::Call(cx, loader, "moduleResolveHook", args, &result))
580 : 2 : return nullptr;
581 : :
582 : 3762 : g_assert(result.isObject() && "resolve hook failed to return an object!");
583 : 3762 : return &result.toObject();
584 : 3764 : }
585 : :
586 : : // Call JS::FinishDynamicModuleImport() with the values stashed in the function.
587 : : // Can fail in JS::FinishDynamicModuleImport(), but will assert if anything
588 : : // fails in fetching the stashed values, since that would be a serious GJS bug.
589 : : GJS_JSAPI_RETURN_CONVENTION
590 : 36 : static bool finish_import(JSContext* cx, JS::HandleObject evaluation_promise,
591 : : const JS::CallArgs& args) {
592 : 36 : GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
593 : 36 : priv->main_loop_release();
594 : :
595 : 36 : JS::Value callback_priv = js::GetFunctionNativeReserved(&args.callee(), 0);
596 : 36 : g_assert(callback_priv.isObject() && "Wrong private value");
597 : 36 : JS::RootedObject callback_data(cx, &callback_priv.toObject());
598 : :
599 : 36 : JS::RootedValue importing_module_priv(cx);
600 : 36 : JS::RootedValue v_module_request(cx);
601 : 36 : JS::RootedValue v_internal_promise(cx);
602 : : bool ok GJS_USED_ASSERT =
603 [ + - ]: 72 : JS_GetProperty(cx, callback_data, "priv", &importing_module_priv) &&
604 [ + - + - ]: 72 : JS_GetProperty(cx, callback_data, "promise", &v_internal_promise) &&
605 : 36 : JS_GetProperty(cx, callback_data, "module_request", &v_module_request);
606 : 36 : g_assert(ok && "Wrong properties on private value");
607 : :
608 : 36 : g_assert(v_module_request.isObject() && "Wrong type for module request");
609 : 36 : g_assert(v_internal_promise.isObject() && "Wrong type for promise");
610 : :
611 : 36 : JS::RootedObject module_request(cx, &v_module_request.toObject());
612 : 36 : JS::RootedObject internal_promise(cx, &v_internal_promise.toObject());
613 : :
614 : 36 : args.rval().setUndefined();
615 : :
616 : 36 : return JS::FinishDynamicModuleImport(cx, evaluation_promise,
617 : : importing_module_priv, module_request,
618 : 36 : internal_promise);
619 : 36 : }
620 : :
621 : : // Failing a JSAPI function may result either in an exception pending on the
622 : : // context, in which case we must call JS::FinishDynamicModuleImport() to reject
623 : : // the internal promise; or in an uncatchable exception such as OOM, in which
624 : : // case we must not call JS::FinishDynamicModuleImport().
625 : : GJS_JSAPI_RETURN_CONVENTION
626 : 0 : static bool fail_import(JSContext* cx, const JS::CallArgs& args) {
627 [ # # ]: 0 : if (JS_IsExceptionPending(cx))
628 : 0 : return finish_import(cx, nullptr, args);
629 : 0 : return false;
630 : : }
631 : :
632 : : GJS_JSAPI_RETURN_CONVENTION
633 : 4 : static bool import_rejected(JSContext* cx, unsigned argc, JS::Value* vp) {
634 : 4 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
635 : :
636 : 4 : gjs_debug(GJS_DEBUG_IMPORTER, "Async import promise rejected");
637 : :
638 : : // Throw the value that the promise is rejected with, so that
639 : : // FinishDynamicModuleImport will reject the internal_promise with it.
640 : 4 : JS_SetPendingException(cx, args.get(0),
641 : : JS::ExceptionStackBehavior::DoNotCapture);
642 : :
643 : 4 : return finish_import(cx, nullptr, args);
644 : : }
645 : :
646 : : GJS_JSAPI_RETURN_CONVENTION
647 : 32 : static bool import_resolved(JSContext* cx, unsigned argc, JS::Value* vp) {
648 : 32 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
649 : :
650 : 32 : gjs_debug(GJS_DEBUG_IMPORTER, "Async import promise resolved");
651 : :
652 : 32 : Gjs::AutoMainRealm ar{cx};
653 : :
654 : 32 : g_assert(args[0].isObject());
655 : 32 : JS::RootedObject module(cx, &args[0].toObject());
656 : :
657 : 32 : JS::RootedValue evaluation_promise(cx);
658 [ + - ]: 64 : if (!JS::ModuleLink(cx, module) ||
659 [ - + - + ]: 64 : !JS::ModuleEvaluate(cx, module, &evaluation_promise))
660 : 0 : return fail_import(cx, args);
661 : :
662 : 32 : g_assert(evaluation_promise.isObject() &&
663 : : "got weird value from JS::ModuleEvaluate");
664 : : JS::RootedObject evaluation_promise_object(cx,
665 : 32 : &evaluation_promise.toObject());
666 : 32 : return finish_import(cx, evaluation_promise_object, args);
667 : 32 : }
668 : :
669 : 36 : bool gjs_dynamic_module_resolve(JSContext* cx,
670 : : JS::HandleValue importing_module_priv,
671 : : JS::HandleObject module_request,
672 : : JS::HandleObject internal_promise) {
673 : 36 : g_assert(gjs_global_is_type(cx, GjsGlobalType::DEFAULT) &&
674 : : "gjs_dynamic_module_resolve can only be called from the default "
675 : : "global.");
676 : :
677 : 36 : JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
678 : 36 : g_assert(global && "gjs_dynamic_module_resolve must be in a realm");
679 : :
680 : : JS::RootedValue v_loader(
681 : 36 : cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_LOADER));
682 : 36 : g_assert(v_loader.isObject());
683 : 36 : JS::RootedObject loader(cx, &v_loader.toObject());
684 : : JS::RootedString specifier(
685 : 36 : cx, JS::GetModuleRequestSpecifier(cx, module_request));
686 : :
687 [ - + ]: 36 : if (!canonicalize_specifier(cx, &specifier))
688 : 0 : return false;
689 : :
690 : 36 : JS::RootedObject callback_data(cx, JS_NewPlainObject(cx));
691 : 36 : if (!callback_data ||
692 [ + - ]: 36 : !JS_DefineProperty(cx, callback_data, "module_request", module_request,
693 : 36 : JSPROP_PERMANENT) ||
694 [ + - ]: 36 : !JS_DefineProperty(cx, callback_data, "promise", internal_promise,
695 [ + - ]: 72 : JSPROP_PERMANENT) ||
696 [ - + - + ]: 72 : !JS_DefineProperty(cx, callback_data, "priv", importing_module_priv,
697 : : JSPROP_PERMANENT))
698 : 0 : return false;
699 : :
700 [ + + ]: 36 : if (importing_module_priv.isObject()) {
701 : 33 : gjs_debug(GJS_DEBUG_IMPORTER,
702 : : "Async module resolve hook for module %s (relative to %p), "
703 : : "global %p",
704 : 66 : gjs_debug_string(specifier).c_str(),
705 : 33 : &importing_module_priv.toObject(), global.get());
706 : : } else {
707 : 3 : gjs_debug(GJS_DEBUG_IMPORTER,
708 : : "Async module resolve hook for module %s (unknown path), "
709 : : "global %p",
710 : 6 : gjs_debug_string(specifier).c_str(), global.get());
711 : : }
712 : :
713 : 36 : JS::RootedValueArray<2> args(cx);
714 : 36 : args[0].set(importing_module_priv);
715 : 36 : args[1].setString(specifier);
716 : :
717 : 36 : JS::RootedValue result(cx);
718 [ - + ]: 36 : if (!JS::Call(cx, loader, "moduleResolveAsyncHook", args, &result))
719 : 0 : return JS::FinishDynamicModuleImport(cx, nullptr, importing_module_priv,
720 : 0 : module_request, internal_promise);
721 : :
722 : : // Release in finish_import
723 : 36 : GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
724 : 36 : priv->main_loop_hold();
725 : :
726 : : JS::RootedObject resolved(
727 : 36 : cx, JS_GetFunctionObject(js::NewFunctionWithReserved(
728 : 36 : cx, import_resolved, 1, 0, "async import resolved")));
729 [ - + ]: 36 : if (!resolved)
730 : 0 : return false;
731 : : JS::RootedObject rejected(
732 : 36 : cx, JS_GetFunctionObject(js::NewFunctionWithReserved(
733 : 36 : cx, import_rejected, 1, 0, "async import rejected")));
734 [ - + ]: 36 : if (!rejected)
735 : 0 : return false;
736 : 36 : js::SetFunctionNativeReserved(resolved, 0, JS::ObjectValue(*callback_data));
737 : 36 : js::SetFunctionNativeReserved(rejected, 0, JS::ObjectValue(*callback_data));
738 : :
739 : 36 : JS::RootedObject promise(cx, &result.toObject());
740 : :
741 : : // Calling JS::FinishDynamicModuleImport() at the end of the resolve and
742 : : // reject handlers will also call the module resolve hook. The module will
743 : : // already have been resolved, but that is how SpiderMonkey obtains the
744 : : // module object.
745 : 36 : return JS::AddPromiseReactions(cx, promise, resolved, rejected);
746 : 36 : }
|