Bug 1340146 - Part 1: Normalize paths for module registry in shell module loader. r=jonco
authorAndré Bargull <andre.bargull@gmail.com>
Tue, 28 Feb 2017 03:31:25 -0800
changeset 394299 fb3ac97e6e79798e44b489465b4cb0b303a01d8b
parent 394298 39a67f6289c441fd3d1ae4726697c87dd412d8fb
child 394300 bf500219a0fc2686ef6c669cedf6b7b0d15fb0d4
push id1468
push userasasaki@mozilla.com
push dateMon, 05 Jun 2017 19:31:07 +0000
treeherdermozilla-release@0641fc6ee9d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjonco
bugs1340146
milestone54.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1340146 - Part 1: Normalize paths for module registry in shell module loader. r=jonco
js/src/shell/ModuleLoader.js
js/src/shell/OSObject.cpp
js/src/shell/OSObject.h
js/src/shell/js.cpp
--- a/js/src/shell/ModuleLoader.js
+++ b/js/src/shell/ModuleLoader.js
@@ -1,38 +1,129 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // A basic synchronous module loader for testing the shell.
+{
+// Save standard built-ins before scripts can modify them.
+const ArrayPrototypeJoin = Array.prototype.join;
+const MapPrototypeGet = Map.prototype.get;
+const MapPrototypeHas = Map.prototype.has;
+const MapPrototypeSet = Map.prototype.set;
+const ObjectDefineProperty = Object.defineProperty;
+const ReflectApply = Reflect.apply;
+const StringPrototypeIndexOf = String.prototype.indexOf;
+const StringPrototypeSubstring = String.prototype.substring;
 
-Reflect.Loader = new class {
+const ReflectLoader = new class {
     constructor() {
         this.registry = new Map();
         this.loadPath = getModuleLoadPath();
     }
 
     resolve(name) {
         if (os.path.isAbsolute(name))
             return name;
 
         return os.path.join(this.loadPath, name);
     }
 
+    normalize(path) {
+#ifdef XP_WIN
+        // Replace all forward slashes with backward slashes.
+        // NB: It may be tempting to replace this loop with a call to
+        // String.prototype.replace, but user scripts may have modified
+        // String.prototype or RegExp.prototype built-in functions, which makes
+        // it unsafe to call String.prototype.replace.
+        let newPath = "";
+        let lastSlash = 0;
+        while (true) {
+            let i = ReflectApply(StringPrototypeIndexOf, path, ["/", lastSlash]);
+            if (i < 0) {
+                newPath += ReflectApply(StringPrototypeSubstring, path, [lastSlash]);
+                break;
+            }
+            newPath += ReflectApply(StringPrototypeSubstring, path, [lastSlash, i]) + "\\";
+            lastSlash = i + 1;
+        }
+        path = newPath;
+
+        // Remove the drive letter, if present.
+        let isalpha = c => ("A" <= c && c <= "Z") || ("a" <= c && c <= "z");
+        let drive = "";
+        if (path.length > 2 && isalpha(path[0]) && path[1] === ":" && path[2] === "\\") {
+            drive = ReflectApply(StringPrototypeSubstring, path, [0, 2]);
+            path = ReflectApply(StringPrototypeSubstring, path, [2]);
+        }
+
+        const pathsep = "\\";
+#else
+        const pathsep = "/";
+#endif
+
+        let n = 0;
+        let components = [];
+
+        // Normalize the path by removing redundant path components.
+        // NB: See above for why we don't call String.prototype.split here.
+        let lastSep = 0;
+        while (lastSep < path.length) {
+            let i = ReflectApply(StringPrototypeIndexOf, path, [pathsep, lastSep]);
+            if (i < 0)
+                i = path.length;
+            let part = ReflectApply(StringPrototypeSubstring, path, [lastSep, i]);
+            lastSep = i + 1;
+
+            // Remove "." when preceded by a path component.
+            if (part === "." && n > 0)
+                continue;
+
+            if (part === ".." && n > 0) {
+                // Replace "./.." with "..".
+                if (components[n - 1] === ".") {
+                    components[n - 1] = "..";
+                    continue;
+                }
+
+                // When preceded by a non-empty path component, remove ".." and
+                // the preceding component, unless the preceding component is also
+                // "..".
+                if (components[n - 1] !== "" && components[n - 1] !== "..") {
+                    components.length = --n;
+                    continue;
+                }
+            }
+
+            ObjectDefineProperty(components, n++, {
+                __proto__: null,
+                value: part,
+                writable: true, enumerable: true, configurable: true
+            });
+        }
+
+        let normalized = ReflectApply(ArrayPrototypeJoin, components, [pathsep]);
+#ifdef XP_WIN
+        normalized = drive + normalized;
+#endif
+        return normalized;
+    }
+
     fetch(path) {
         return os.file.readFile(path);
     }
 
     loadAndParse(path) {
-        if (this.registry.has(path))
-            return this.registry.get(path);
+        let normalized = this.normalize(path);
+        if (ReflectApply(MapPrototypeHas, this.registry, [normalized]))
+            return ReflectApply(MapPrototypeGet, this.registry, [normalized]);
 
         let source = this.fetch(path);
         let module = parseModule(source, path);
-        this.registry.set(path, module);
+        ReflectApply(MapPrototypeSet, this.registry, [normalized, module]);
         return module;
     }
 
     loadAndExecute(path) {
         let module = this.loadAndParse(path);
         module.declarationInstantiation();
         return module.evaluation();
     }
@@ -43,11 +134,14 @@ Reflect.Loader = new class {
 
     ["import"](name, referrer) {
         let path = this.resolve(name);
         return this.loadAndExecute(path);
     }
 };
 
 setModuleResolveHook((module, requestName) => {
-    let path = Reflect.Loader.resolve(requestName);
-    return Reflect.Loader.loadAndParse(path)
+    let path = ReflectLoader.resolve(requestName);
+    return ReflectLoader.loadAndParse(path)
 });
+
+Reflect.Loader = ReflectLoader;
+}
--- a/js/src/shell/OSObject.cpp
+++ b/js/src/shell/OSObject.cpp
@@ -18,16 +18,17 @@
 #include <sys/wait.h>
 #include <unistd.h>
 #endif
 
 #include "jsapi.h"
 // For JSFunctionSpecWithHelp
 #include "jsfriendapi.h"
 #include "jsobj.h"
+#include "jsstr.h"
 #ifdef XP_WIN
 # include "jswin.h"
 #endif
 #include "jswrapper.h"
 
 #include "js/Conversions.h"
 #include "shell/jsshell.h"
 #include "vm/StringBuffer.h"
@@ -223,16 +224,29 @@ FileAsTypedArray(JSContext* cx, JS::Hand
                 }
                 obj = nullptr;
             }
         }
     }
     return obj;
 }
 
+/**
+ * Return the current working directory or |null| on failure.
+ */
+UniqueChars
+GetCWD()
+{
+    static char buffer[PATH_MAX + 1];
+    const char* cwd = getcwd(buffer, PATH_MAX);
+    if (!cwd)
+        return UniqueChars();
+    return js::DuplicateString(buffer);
+}
+
 static bool
 ReadFile(JSContext* cx, unsigned argc, Value* vp, bool scriptRelative)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     if (args.length() < 1 || args.length() > 2) {
         JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr,
                                   args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS,
--- a/js/src/shell/OSObject.h
+++ b/js/src/shell/OSObject.h
@@ -28,12 +28,15 @@ enum PathResolutionMode {
 };
 
 JSString*
 ResolvePath(JSContext* cx, JS::HandleString filenameStr, PathResolutionMode resolveMode);
 
 JSObject*
 FileAsTypedArray(JSContext* cx, JS::HandleString pathnameStr);
 
+JS::UniqueChars
+GetCWD();
+
 } // namespace shell
 } // namespace js
 
 #endif /* shell_OSObject_h */
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -322,16 +322,18 @@ struct ShellContext
     UniqueChars readLineBuf;
     size_t readLineBufPos;
 
     static const uint32_t GeckoProfilingMaxStackSize = 1000;
     ProfileEntry geckoProfilingStack[GeckoProfilingMaxStackSize];
     uint32_t geckoProfilingStackSize;
 
     OffThreadState offThreadState;
+
+    UniqueChars moduleLoadPath;
 };
 
 struct MOZ_STACK_CLASS EnvironmentPreparer : public js::ScriptEnvironmentPreparer {
     JSContext* cx;
     explicit EnvironmentPreparer(JSContext* cx)
       : cx(cx)
     {
         js::SetScriptEnvironmentPreparer(cx, this);
@@ -359,17 +361,16 @@ static bool printTiming = false;
 static const char* jsCacheDir = nullptr;
 static const char* jsCacheAsmJSPath = nullptr;
 static RCFile* gErrFile = nullptr;
 static RCFile* gOutFile = nullptr;
 static bool reportWarnings = true;
 static bool compileOnly = false;
 static bool fuzzingSafe = false;
 static bool disableOOMFunctions = false;
-static const char* moduleLoadPath = ".";
 
 #ifdef DEBUG
 static bool dumpEntrainedVariables = false;
 static bool OOM_printAllocationCount = false;
 #endif
 
 // Shell state this is only accessed on the main thread.
 bool jsCachingEnabled = false;
@@ -4318,17 +4319,20 @@ SetModuleResolveHook(JSContext* cx, unsi
     args.rval().setUndefined();
     return true;
 }
 
 static bool
 GetModuleLoadPath(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
-    args.rval().setString(JS_NewStringCopyZ(cx, moduleLoadPath));
+
+    ShellContext* sc = GetShellContext(cx);
+    MOZ_ASSERT(sc->moduleLoadPath);
+    args.rval().setString(JS_NewStringCopyZ(cx, sc->moduleLoadPath.get()));
     return true;
 }
 
 static bool
 Parse(JSContext* cx, unsigned argc, Value* vp)
 {
     using namespace js::frontend;
 
@@ -7611,18 +7615,32 @@ ProcessArgs(JSContext* cx, OptionParser*
     if (filePaths.empty() &&
         codeChunks.empty() &&
         modulePaths.empty() &&
         !op->getStringArg("script"))
     {
         return Process(cx, nullptr, true); /* Interactive. */
     }
 
-    if (const char* path = op->getStringOption("module-load-path"))
-        moduleLoadPath = path;
+    if (const char* path = op->getStringOption("module-load-path")) {
+        RootedString jspath(cx, JS_NewStringCopyZ(cx, path));
+        if (!jspath)
+            return false;
+
+        JSString* absolutePath = js::shell::ResolvePath(cx, jspath, RootRelative);
+        if (!absolutePath)
+            return false;
+
+        sc->moduleLoadPath = UniqueChars(JS_EncodeString(cx, absolutePath));
+    } else {
+        sc->moduleLoadPath = js::shell::GetCWD();
+    }
+
+    if (!sc->moduleLoadPath)
+        return false;
 
     if (!modulePaths.empty() && !InitModuleLoader(cx))
         return false;
 
     while (!filePaths.empty() || !codeChunks.empty() || !modulePaths.empty()) {
         size_t fpArgno = filePaths.empty() ? SIZE_MAX : filePaths.argno();
         size_t ccArgno = codeChunks.empty() ? SIZE_MAX : codeChunks.argno();
         size_t mpArgno = modulePaths.empty() ? SIZE_MAX : modulePaths.argno();