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