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