Bug 1340146 - Normalize paths for module registry in shell module loader. r=jonco
☠☠ backed out by cf4daf78f479 ☠ ☠
authorAndré Bargull <andre.bargull@gmail.com>
Mon, 27 Feb 2017 07:49:29 -0800
changeset 374161 c355a0ffcb10bdc459e56dde715f4b2c3335c90c
parent 374160 5a989a69ea61d7b945d8b454b49c5ae2ed545edb
child 374162 5556b171034ea8c0c458917bf4c4356c49ceda69
push id10863
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 23:02:23 +0000
treeherdermozilla-aurora@0931190cd725 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjonco
bugs1340146
milestone54.0a1
Bug 1340146 - 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;
@@ -4276,17 +4277,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;
 
@@ -7569,18 +7573,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();