1 /* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4
2 * -*- */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "shell/ModuleLoader.h"
8
9 #include "mozilla/DebugOnly.h"
10 #include "mozilla/TextUtils.h"
11
12 #include "NamespaceImports.h"
13
14 #include "js/Modules.h"
15 #include "js/SourceText.h"
16 #include "js/StableStringChars.h"
17 #include "shell/jsshell.h"
18 #include "shell/OSObject.h"
19 #include "shell/StringUtils.h"
20 #include "util/Text.h"
21 #include "vm/JSAtom.h"
22 #include "vm/JSContext.h"
23 #include "vm/StringType.h"
24
25 using namespace js;
26 using namespace js::shell;
27
28 static constexpr char16_t JavaScriptScheme[] = u"javascript:";
29
IsJavaScriptURL(HandleLinearString path)30 static bool IsJavaScriptURL(HandleLinearString path) {
31 return StringStartsWith(path, JavaScriptScheme);
32 }
33
ExtractJavaScriptURLSource(JSContext * cx,HandleLinearString path)34 static JSString* ExtractJavaScriptURLSource(JSContext* cx,
35 HandleLinearString path) {
36 MOZ_ASSERT(IsJavaScriptURL(path));
37
38 const size_t schemeLength = js_strlen(JavaScriptScheme);
39 return SubString(cx, path, schemeLength);
40 }
41
init(JSContext * cx,HandleString loadPath)42 bool ModuleLoader::init(JSContext* cx, HandleString loadPath) {
43 loadPathStr = AtomizeString(cx, loadPath, PinAtom);
44 if (!loadPathStr) {
45 return false;
46 }
47
48 MOZ_ASSERT(IsAbsolutePath(loadPathStr));
49
50 char16_t sep = PathSeparator;
51 pathSeparatorStr = AtomizeChars(cx, &sep, 1);
52 if (!pathSeparatorStr) {
53 return false;
54 }
55
56 JSRuntime* rt = cx->runtime();
57 JS::SetModuleResolveHook(rt, ModuleLoader::ResolveImportedModule);
58 JS::SetModuleMetadataHook(rt, ModuleLoader::GetImportMetaProperties);
59 JS::SetModuleDynamicImportHook(rt, ModuleLoader::ImportModuleDynamically);
60
61 return true;
62 }
63
64 // static
ResolveImportedModule(JSContext * cx,JS::HandleValue referencingPrivate,JS::HandleObject moduleRequest)65 JSObject* ModuleLoader::ResolveImportedModule(
66 JSContext* cx, JS::HandleValue referencingPrivate,
67 JS::HandleObject moduleRequest) {
68 ShellContext* scx = GetShellContext(cx);
69 return scx->moduleLoader->resolveImportedModule(cx, referencingPrivate,
70 moduleRequest);
71 }
72
73 // static
GetImportMetaProperties(JSContext * cx,JS::HandleValue privateValue,JS::HandleObject metaObject)74 bool ModuleLoader::GetImportMetaProperties(JSContext* cx,
75 JS::HandleValue privateValue,
76 JS::HandleObject metaObject) {
77 ShellContext* scx = GetShellContext(cx);
78 return scx->moduleLoader->populateImportMeta(cx, privateValue, metaObject);
79 }
80
81 // static
ImportModuleDynamically(JSContext * cx,JS::HandleValue referencingPrivate,JS::HandleObject moduleRequest,JS::HandleObject promise)82 bool ModuleLoader::ImportModuleDynamically(JSContext* cx,
83 JS::HandleValue referencingPrivate,
84 JS::HandleObject moduleRequest,
85 JS::HandleObject promise) {
86 ShellContext* scx = GetShellContext(cx);
87 return scx->moduleLoader->dynamicImport(cx, referencingPrivate, moduleRequest,
88 promise);
89 }
90
loadRootModule(JSContext * cx,HandleString path)91 bool ModuleLoader::loadRootModule(JSContext* cx, HandleString path) {
92 RootedValue rval(cx);
93 if (!loadAndExecute(cx, path, &rval)) {
94 return false;
95 }
96
97 if (cx->options().topLevelAwait()) {
98 RootedObject evaluationPromise(cx, &rval.toObject());
99 if (evaluationPromise == nullptr) {
100 return false;
101 }
102
103 return JS::ThrowOnModuleEvaluationFailure(cx, evaluationPromise);
104 }
105 return true;
106 }
107
registerTestModule(JSContext * cx,HandleObject moduleRequest,HandleModuleObject module)108 bool ModuleLoader::registerTestModule(JSContext* cx, HandleObject moduleRequest,
109 HandleModuleObject module) {
110 RootedLinearString path(cx, resolve(cx, moduleRequest, UndefinedHandleValue));
111 if (!path) {
112 return false;
113 }
114
115 path = normalizePath(cx, path);
116 if (!path) {
117 return false;
118 }
119
120 return addModuleToRegistry(cx, path, module);
121 }
122
loadAndExecute(JSContext * cx,HandleString path,MutableHandleValue rval)123 bool ModuleLoader::loadAndExecute(JSContext* cx, HandleString path,
124 MutableHandleValue rval) {
125 RootedObject module(cx, loadAndParse(cx, path));
126 if (!module) {
127 return false;
128 }
129
130 if (!JS::ModuleInstantiate(cx, module)) {
131 return false;
132 }
133
134 return JS::ModuleEvaluate(cx, module, rval);
135 }
136
resolveImportedModule(JSContext * cx,JS::HandleValue referencingPrivate,JS::HandleObject moduleRequest)137 JSObject* ModuleLoader::resolveImportedModule(
138 JSContext* cx, JS::HandleValue referencingPrivate,
139 JS::HandleObject moduleRequest) {
140 RootedLinearString path(cx, resolve(cx, moduleRequest, referencingPrivate));
141 if (!path) {
142 return nullptr;
143 }
144
145 return loadAndParse(cx, path);
146 }
147
populateImportMeta(JSContext * cx,JS::HandleValue privateValue,JS::HandleObject metaObject)148 bool ModuleLoader::populateImportMeta(JSContext* cx,
149 JS::HandleValue privateValue,
150 JS::HandleObject metaObject) {
151 RootedLinearString path(cx);
152 if (!privateValue.isUndefined()) {
153 if (!getScriptPath(cx, privateValue, &path)) {
154 return false;
155 }
156 }
157
158 if (!path) {
159 path = NewStringCopyZ<CanGC>(cx, "(unknown)");
160 if (!path) {
161 return false;
162 }
163 }
164
165 RootedValue pathValue(cx, StringValue(path));
166 return JS_DefineProperty(cx, metaObject, "url", pathValue, JSPROP_ENUMERATE);
167 }
168
dynamicImport(JSContext * cx,JS::HandleValue referencingPrivate,JS::HandleObject moduleRequest,JS::HandleObject promise)169 bool ModuleLoader::dynamicImport(JSContext* cx,
170 JS::HandleValue referencingPrivate,
171 JS::HandleObject moduleRequest,
172 JS::HandleObject promise) {
173 // To make this more realistic, use a promise to delay the import and make it
174 // happen asynchronously. This method packages up the arguments and creates a
175 // resolved promise, which on fullfillment calls doDynamicImport with the
176 // original arguments.
177
178 MOZ_ASSERT(promise);
179 RootedValue moduleRequestValue(cx, ObjectValue(*moduleRequest));
180 RootedValue promiseValue(cx, ObjectValue(*promise));
181 RootedObject closure(cx, JS_NewObjectWithGivenProto(cx, nullptr, nullptr));
182 if (!closure ||
183 !JS_DefineProperty(cx, closure, "referencingPrivate", referencingPrivate,
184 JSPROP_ENUMERATE) ||
185 !JS_DefineProperty(cx, closure, "moduleRequest", moduleRequestValue,
186 JSPROP_ENUMERATE) ||
187 !JS_DefineProperty(cx, closure, "promise", promiseValue,
188 JSPROP_ENUMERATE)) {
189 return false;
190 }
191
192 RootedFunction onResolved(
193 cx, NewNativeFunction(cx, DynamicImportDelayFulfilled, 1, nullptr));
194 if (!onResolved) {
195 return false;
196 }
197
198 RootedFunction onRejected(
199 cx, NewNativeFunction(cx, DynamicImportDelayRejected, 1, nullptr));
200 if (!onRejected) {
201 return false;
202 }
203
204 RootedObject delayPromise(cx);
205 RootedValue closureValue(cx, ObjectValue(*closure));
206 delayPromise = PromiseObject::unforgeableResolve(cx, closureValue);
207 if (!delayPromise) {
208 return false;
209 }
210
211 return JS::AddPromiseReactions(cx, delayPromise, onResolved, onRejected);
212 }
213
DynamicImportDelayFulfilled(JSContext * cx,unsigned argc,Value * vp)214 bool ModuleLoader::DynamicImportDelayFulfilled(JSContext* cx, unsigned argc,
215 Value* vp) {
216 CallArgs args = CallArgsFromVp(argc, vp);
217 RootedObject closure(cx, &args[0].toObject());
218
219 RootedValue referencingPrivate(cx);
220 RootedValue moduleRequestValue(cx);
221 RootedValue promiseValue(cx);
222 if (!JS_GetProperty(cx, closure, "referencingPrivate", &referencingPrivate) ||
223 !JS_GetProperty(cx, closure, "moduleRequest", &moduleRequestValue) ||
224 !JS_GetProperty(cx, closure, "promise", &promiseValue)) {
225 return false;
226 }
227
228 RootedObject moduleRequest(cx, &moduleRequestValue.toObject());
229 RootedObject promise(cx, &promiseValue.toObject());
230
231 ShellContext* scx = GetShellContext(cx);
232 return scx->moduleLoader->doDynamicImport(cx, referencingPrivate,
233 moduleRequest, promise);
234 }
235
DynamicImportDelayRejected(JSContext * cx,unsigned argc,Value * vp)236 bool ModuleLoader::DynamicImportDelayRejected(JSContext* cx, unsigned argc,
237 Value* vp) {
238 MOZ_CRASH("This promise should never be rejected");
239 }
240
doDynamicImport(JSContext * cx,JS::HandleValue referencingPrivate,JS::HandleObject moduleRequest,JS::HandleObject promise)241 bool ModuleLoader::doDynamicImport(JSContext* cx,
242 JS::HandleValue referencingPrivate,
243 JS::HandleObject moduleRequest,
244 JS::HandleObject promise) {
245 // Exceptions during dynamic import are handled by calling
246 // FinishDynamicModuleImport with a pending exception on the context.
247 RootedValue rval(cx);
248 bool ok =
249 tryDynamicImport(cx, referencingPrivate, moduleRequest, promise, &rval);
250 if (cx->options().topLevelAwait()) {
251 JSObject* evaluationObject = ok ? &rval.toObject() : nullptr;
252 RootedObject evaluationPromise(cx, evaluationObject);
253 return JS::FinishDynamicModuleImport(
254 cx, evaluationPromise, referencingPrivate, moduleRequest, promise);
255 }
256 JS::DynamicImportStatus status =
257 ok ? JS::DynamicImportStatus::Ok : JS::DynamicImportStatus::Failed;
258 return JS::FinishDynamicModuleImport_NoTLA(cx, status, referencingPrivate,
259 moduleRequest, promise);
260 }
261
tryDynamicImport(JSContext * cx,JS::HandleValue referencingPrivate,JS::HandleObject moduleRequest,JS::HandleObject promise,JS::MutableHandleValue rval)262 bool ModuleLoader::tryDynamicImport(JSContext* cx,
263 JS::HandleValue referencingPrivate,
264 JS::HandleObject moduleRequest,
265 JS::HandleObject promise,
266 JS::MutableHandleValue rval) {
267 RootedLinearString path(cx, resolve(cx, moduleRequest, referencingPrivate));
268 if (!path) {
269 return false;
270 }
271
272 return loadAndExecute(cx, path, rval);
273 }
274
resolve(JSContext * cx,HandleObject moduleRequestArg,HandleValue referencingInfo)275 JSLinearString* ModuleLoader::resolve(JSContext* cx,
276 HandleObject moduleRequestArg,
277 HandleValue referencingInfo) {
278 ModuleRequestObject* moduleRequest =
279 &moduleRequestArg->as<ModuleRequestObject>();
280 if (moduleRequest->specifier()->length() == 0) {
281 JS_ReportErrorASCII(cx, "Invalid module specifier");
282 return nullptr;
283 }
284
285 RootedLinearString name(
286 cx, JS_EnsureLinearString(cx, moduleRequest->specifier()));
287 if (!name) {
288 return nullptr;
289 }
290
291 if (IsJavaScriptURL(name) || IsAbsolutePath(name)) {
292 return name;
293 }
294
295 // Treat |name| as a relative path if it starts with either "./" or "../".
296 bool isRelative =
297 StringStartsWith(name, u"./") || StringStartsWith(name, u"../")
298 #ifdef XP_WIN
299 || StringStartsWith(name, u".\\") || StringStartsWith(name, u"..\\")
300 #endif
301 ;
302
303 RootedString path(cx, loadPathStr);
304
305 if (isRelative) {
306 if (referencingInfo.isUndefined()) {
307 JS_ReportErrorASCII(cx, "No referencing module for relative import");
308 return nullptr;
309 }
310
311 RootedLinearString refPath(cx);
312 if (!getScriptPath(cx, referencingInfo, &refPath)) {
313 return nullptr;
314 }
315
316 if (!refPath) {
317 JS_ReportErrorASCII(cx, "No path set for referencing module");
318 return nullptr;
319 }
320
321 int32_t sepIndex = LastIndexOf(refPath, u'/');
322 #ifdef XP_WIN
323 sepIndex = std::max(sepIndex, LastIndexOf(refPath, u'\\'));
324 #endif
325 if (sepIndex >= 0) {
326 path = SubString(cx, refPath, 0, sepIndex);
327 if (!path) {
328 return nullptr;
329 }
330 }
331 }
332
333 RootedString result(cx);
334 RootedString pathSep(cx, pathSeparatorStr);
335 result = JS_ConcatStrings(cx, path, pathSep);
336 if (!result) {
337 return nullptr;
338 }
339
340 result = JS_ConcatStrings(cx, result, name);
341 if (!result) {
342 return nullptr;
343 }
344
345 return JS_EnsureLinearString(cx, result);
346 }
347
loadAndParse(JSContext * cx,HandleString pathArg)348 JSObject* ModuleLoader::loadAndParse(JSContext* cx, HandleString pathArg) {
349 RootedLinearString path(cx, JS_EnsureLinearString(cx, pathArg));
350 if (!path) {
351 return nullptr;
352 }
353
354 path = normalizePath(cx, path);
355 if (!path) {
356 return nullptr;
357 }
358
359 RootedObject module(cx);
360 if (!lookupModuleInRegistry(cx, path, &module)) {
361 return nullptr;
362 }
363
364 if (module) {
365 return module;
366 }
367
368 UniqueChars filename = JS_EncodeStringToLatin1(cx, path);
369 if (!filename) {
370 return nullptr;
371 }
372
373 JS::CompileOptions options(cx);
374 options.setFileAndLine(filename.get(), 1);
375
376 RootedString source(cx, fetchSource(cx, path));
377 if (!source) {
378 return nullptr;
379 }
380
381 JS::AutoStableStringChars stableChars(cx);
382 if (!stableChars.initTwoByte(cx, source)) {
383 return nullptr;
384 }
385
386 const char16_t* chars = stableChars.twoByteRange().begin().get();
387 JS::SourceText<char16_t> srcBuf;
388 if (!srcBuf.init(cx, chars, source->length(),
389 JS::SourceOwnership::Borrowed)) {
390 return nullptr;
391 }
392
393 module = JS::CompileModule(cx, options, srcBuf);
394 if (!module) {
395 return nullptr;
396 }
397
398 RootedObject info(cx, CreateScriptPrivate(cx, path));
399 if (!info) {
400 return nullptr;
401 }
402
403 JS::SetModulePrivate(module, ObjectValue(*info));
404
405 if (!addModuleToRegistry(cx, path, module)) {
406 return nullptr;
407 }
408
409 return module;
410 }
411
lookupModuleInRegistry(JSContext * cx,HandleString path,MutableHandleObject moduleOut)412 bool ModuleLoader::lookupModuleInRegistry(JSContext* cx, HandleString path,
413 MutableHandleObject moduleOut) {
414 moduleOut.set(nullptr);
415
416 RootedObject registry(cx, getOrCreateModuleRegistry(cx));
417 if (!registry) {
418 return false;
419 }
420
421 RootedValue pathValue(cx, StringValue(path));
422 RootedValue moduleValue(cx);
423 if (!JS::MapGet(cx, registry, pathValue, &moduleValue)) {
424 return false;
425 }
426
427 if (!moduleValue.isUndefined()) {
428 moduleOut.set(&moduleValue.toObject());
429 }
430
431 return true;
432 }
433
addModuleToRegistry(JSContext * cx,HandleString path,HandleObject module)434 bool ModuleLoader::addModuleToRegistry(JSContext* cx, HandleString path,
435 HandleObject module) {
436 RootedObject registry(cx, getOrCreateModuleRegistry(cx));
437 if (!registry) {
438 return false;
439 }
440
441 RootedValue pathValue(cx, StringValue(path));
442 RootedValue moduleValue(cx, ObjectValue(*module));
443 return JS::MapSet(cx, registry, pathValue, moduleValue);
444 }
445
getOrCreateModuleRegistry(JSContext * cx)446 JSObject* ModuleLoader::getOrCreateModuleRegistry(JSContext* cx) {
447 Handle<GlobalObject*> global = cx->global();
448 RootedValue value(cx, global->getReservedSlot(GlobalAppSlotModuleRegistry));
449 if (!value.isUndefined()) {
450 return &value.toObject();
451 }
452
453 JSObject* registry = JS::NewMapObject(cx);
454 if (!registry) {
455 return nullptr;
456 }
457
458 global->setReservedSlot(GlobalAppSlotModuleRegistry, ObjectValue(*registry));
459 return registry;
460 }
461
getScriptPath(JSContext * cx,HandleValue privateValue,MutableHandle<JSLinearString * > pathOut)462 bool ModuleLoader::getScriptPath(JSContext* cx, HandleValue privateValue,
463 MutableHandle<JSLinearString*> pathOut) {
464 pathOut.set(nullptr);
465
466 RootedObject infoObj(cx, &privateValue.toObject());
467 RootedValue pathValue(cx);
468 if (!JS_GetProperty(cx, infoObj, "path", &pathValue)) {
469 return false;
470 }
471
472 if (pathValue.isUndefined()) {
473 return true;
474 }
475
476 RootedString path(cx, pathValue.toString());
477 pathOut.set(JS_EnsureLinearString(cx, path));
478 return pathOut;
479 }
480
normalizePath(JSContext * cx,HandleLinearString pathArg)481 JSLinearString* ModuleLoader::normalizePath(JSContext* cx,
482 HandleLinearString pathArg) {
483 RootedLinearString path(cx, pathArg);
484
485 if (IsJavaScriptURL(path)) {
486 return path;
487 }
488
489 #ifdef XP_WIN
490 // Replace all forward slashes with backward slashes.
491 path = ReplaceCharGlobally(cx, path, u'/', PathSeparator);
492 if (!path) {
493 return nullptr;
494 }
495
496 // Remove the drive letter, if present.
497 RootedLinearString drive(cx);
498 if (path->length() > 2 && mozilla::IsAsciiAlpha(CharAt(path, 0)) &&
499 CharAt(path, 1) == u':' && CharAt(path, 2) == u'\\') {
500 drive = SubString(cx, path, 0, 2);
501 path = SubString(cx, path, 2);
502 if (!drive || !path) {
503 return nullptr;
504 }
505 }
506 #endif // XP_WIN
507
508 // Normalize the path by removing redundant path components.
509 Rooted<GCVector<JSLinearString*>> components(cx);
510 size_t lastSep = 0;
511 while (lastSep < path->length()) {
512 int32_t i = IndexOf(path, PathSeparator, lastSep);
513 if (i < 0) {
514 i = path->length();
515 }
516
517 RootedLinearString part(cx, SubString(cx, path, lastSep, i));
518 if (!part) {
519 return nullptr;
520 }
521
522 lastSep = i + 1;
523
524 // Remove "." when preceded by a path component.
525 if (StringEquals(part, u".") && !components.empty()) {
526 continue;
527 }
528
529 if (StringEquals(part, u"..") && !components.empty()) {
530 // Replace "./.." with "..".
531 if (StringEquals(components.back(), u".")) {
532 components.back() = part;
533 continue;
534 }
535
536 // When preceded by a non-empty path component, remove ".." and the
537 // preceding component, unless the preceding component is also "..".
538 if (!StringEquals(components.back(), u"") &&
539 !StringEquals(components.back(), u"..")) {
540 components.popBack();
541 continue;
542 }
543 }
544
545 if (!components.append(part)) {
546 return nullptr;
547 }
548 }
549
550 RootedLinearString pathSep(cx, pathSeparatorStr);
551 RootedString normalized(cx, JoinStrings(cx, components, pathSep));
552 if (!normalized) {
553 return nullptr;
554 }
555
556 #ifdef XP_WIN
557 if (drive) {
558 normalized = JS_ConcatStrings(cx, drive, normalized);
559 if (!normalized) {
560 return nullptr;
561 }
562 }
563 #endif
564
565 return JS_EnsureLinearString(cx, normalized);
566 }
567
fetchSource(JSContext * cx,HandleLinearString path)568 JSString* ModuleLoader::fetchSource(JSContext* cx, HandleLinearString path) {
569 if (IsJavaScriptURL(path)) {
570 return ExtractJavaScriptURLSource(cx, path);
571 }
572
573 RootedString resolvedPath(cx, ResolvePath(cx, path, RootRelative));
574 if (!resolvedPath) {
575 return nullptr;
576 }
577
578 return FileAsString(cx, resolvedPath);
579 }
580