Add module loader "resolve" hook for local file system to test the loader in JSC...
authorutatane.tea@gmail.com <utatane.tea@gmail.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 27 Aug 2015 23:48:13 +0000 (23:48 +0000)
committerutatane.tea@gmail.com <utatane.tea@gmail.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 27 Aug 2015 23:48:13 +0000 (23:48 +0000)
https://bugs.webkit.org/show_bug.cgi?id=148543

Reviewed by Filip Pizlo.

Add the module loader "resolve" hook to the JSC shell.
It takes the module name and the referrer module key and resolves the name to the unique module key.

resolve(ModuleName moduleName, ModuleKey referrer) -> Promise<ModuleKey>

In the JSC shell, since we load the module from the local file system, we treat an absolute file path
as a module key. So, in this patch, we implement the "resolve" hook that resolves the module name to
the absolute file path.

This local file system "resolve" functionality makes JSC shell easy to test the module loader.

* jsc.cpp:
(GlobalObject::finishCreation):
(GlobalObject::moduleLoaderFetch):
(pathSeparator):
(extractDirectoryName):
(currentWorkingDirectory):
(resolvePath):
(GlobalObject::moduleLoaderResolve):
(functionDrainMicrotasks):
* runtime/JSInternalPromiseDeferred.cpp:
(JSC::JSInternalPromiseDeferred::resolve):
(JSC::JSInternalPromiseDeferred::reject):
* runtime/JSInternalPromiseDeferred.h:
* tests/stress/pathname-resolve.js: Added.
(shouldBe):
(shouldThrow):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@189071 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/jsc.cpp
Source/JavaScriptCore/runtime/JSInternalPromiseDeferred.cpp
Source/JavaScriptCore/runtime/JSInternalPromiseDeferred.h
Source/JavaScriptCore/tests/stress/pathname-resolve.js [new file with mode: 0644]

index bd4f469..41d78c7 100644 (file)
@@ -1,3 +1,38 @@
+2015-08-27  Yusuke Suzuki  <utatane.tea@gmail.com>
+
+        Add module loader "resolve" hook for local file system to test the loader in JSC shell
+        https://bugs.webkit.org/show_bug.cgi?id=148543
+
+        Reviewed by Filip Pizlo.
+
+        Add the module loader "resolve" hook to the JSC shell.
+        It takes the module name and the referrer module key and resolves the name to the unique module key.
+
+        resolve(ModuleName moduleName, ModuleKey referrer) -> Promise<ModuleKey>
+
+        In the JSC shell, since we load the module from the local file system, we treat an absolute file path
+        as a module key. So, in this patch, we implement the "resolve" hook that resolves the module name to
+        the absolute file path.
+
+        This local file system "resolve" functionality makes JSC shell easy to test the module loader.
+
+        * jsc.cpp:
+        (GlobalObject::finishCreation):
+        (GlobalObject::moduleLoaderFetch):
+        (pathSeparator):
+        (extractDirectoryName):
+        (currentWorkingDirectory):
+        (resolvePath):
+        (GlobalObject::moduleLoaderResolve):
+        (functionDrainMicrotasks):
+        * runtime/JSInternalPromiseDeferred.cpp:
+        (JSC::JSInternalPromiseDeferred::resolve):
+        (JSC::JSInternalPromiseDeferred::reject):
+        * runtime/JSInternalPromiseDeferred.h:
+        * tests/stress/pathname-resolve.js: Added.
+        (shouldBe):
+        (shouldThrow):
+
 2015-08-27  Filip Pizlo  <fpizlo@apple.com>
 
         Unreviewed, fix some FIXMEs and add some new ones, based on things we've learned from some
index 033e696..efaeb04 100644 (file)
@@ -64,7 +64,9 @@
 #include <wtf/StringPrintStream.h>
 #include <wtf/text/StringBuilder.h>
 
-#if !OS(WINDOWS)
+#if OS(WINDOWS)
+#include <direct.h>
+#else
 #include <unistd.h>
 #endif
 
@@ -491,6 +493,7 @@ static EncodedJSValue JSC_HOST_CALL functionReturnTypeFor(ExecState*);
 static EncodedJSValue JSC_HOST_CALL functionDumpBasicBlockExecutionRanges(ExecState*);
 static EncodedJSValue JSC_HOST_CALL functionHasBasicBlockExecuted(ExecState*);
 static EncodedJSValue JSC_HOST_CALL functionEnableExceptionFuzz(ExecState*);
+static EncodedJSValue JSC_HOST_CALL functionDrainMicrotasks(ExecState*);
 #if ENABLE(WEBASSEMBLY)
 static EncodedJSValue JSC_HOST_CALL functionLoadWebAssembly(ExecState*);
 #endif
@@ -666,7 +669,9 @@ protected:
         addFunction(vm, "hasBasicBlockExecuted", functionHasBasicBlockExecuted, 2);
 
         addFunction(vm, "enableExceptionFuzz", functionEnableExceptionFuzz, 0);
-        
+
+        addFunction(vm, "drainMicrotasks", functionDrainMicrotasks, 0);
+
 #if ENABLE(WEBASSEMBLY)
         addFunction(vm, "loadWebAssembly", functionLoadWebAssembly, 1);
 #endif
@@ -694,6 +699,8 @@ protected:
         putDirect(vm, identifier, JSFunction::create(vm, this, arguments, identifier.string(), function, NoIntrinsic, function));
     }
 
+    static JSInternalPromise* moduleLoaderResolve(JSGlobalObject*, ExecState*, JSValue, JSValue);
+
     static JSInternalPromise* moduleLoaderFetch(JSGlobalObject* globalObject, ExecState* exec, JSValue key)
     {
         JSInternalPromiseDeferred* deferred = JSInternalPromiseDeferred::create(exec, globalObject);
@@ -701,23 +708,20 @@ protected:
         if (exec->hadException()) {
             JSValue exception = exec->exception();
             exec->clearException();
-            deferred->reject(exec, exception);
-            return deferred->promise();
+            return deferred->reject(exec, exception);
         }
 
         // Here, now we consider moduleKey as the fileName.
         Vector<char> utf8;
-        if (!fillBufferWithContentsOfFile(moduleKey, utf8)) {
-            deferred->reject(exec, createError(exec, makeString("Could not open file '", moduleKey, "'.")));
-            return deferred->promise();
-        }
-        deferred->resolve(exec, jsString(exec, stringFromUTF(utf8.data())));
-        return deferred->promise();
+        if (!fillBufferWithContentsOfFile(moduleKey, utf8))
+            return deferred->reject(exec, createError(exec, makeString("Could not open file '", moduleKey, "'.")));
+
+        return deferred->resolve(exec, jsString(exec, stringFromUTF(utf8.data())));
     }
 };
 
 const ClassInfo GlobalObject::s_info = { "global", &JSGlobalObject::s_info, nullptr, CREATE_METHOD_TABLE(GlobalObject) };
-const GlobalObjectMethodTable GlobalObject::s_globalObjectMethodTable = { &allowsAccessFrom, &supportsProfiling, &supportsRichSourceInfo, &shouldInterruptScript, &javaScriptRuntimeFlags, 0, &shouldInterruptScriptBeforeTimeout, nullptr, &moduleLoaderFetch, nullptr, nullptr };
+const GlobalObjectMethodTable GlobalObject::s_globalObjectMethodTable = { &allowsAccessFrom, &supportsProfiling, &supportsRichSourceInfo, &shouldInterruptScript, &javaScriptRuntimeFlags, 0, &shouldInterruptScriptBeforeTimeout, &moduleLoaderResolve, &moduleLoaderFetch, nullptr, nullptr };
 
 
 GlobalObject::GlobalObject(VM& vm, Structure* structure)
@@ -725,6 +729,129 @@ GlobalObject::GlobalObject(VM& vm, Structure* structure)
 {
 }
 
+static UChar pathSeparator()
+{
+#if OS(WINDOWS)
+    return '\\';
+#else
+    return '/';
+#endif
+}
+
+struct DirectoryName {
+    // In unix, it is "/". In Windows, it becomes a drive letter like "C:\"
+    String rootName;
+
+    // If the directory name is "/home/WebKit", this becomes "home/WebKit". If the directory name is "/", this becomes "".
+    String queryName;
+};
+
+static bool extractDirectoryName(const String& absolutePathToFile, DirectoryName& directoryName)
+{
+    size_t firstSeparatorPosition = absolutePathToFile.find(pathSeparator());
+    if (firstSeparatorPosition == notFound)
+        return false;
+    directoryName.rootName = absolutePathToFile.substring(0, firstSeparatorPosition + 1); // Include the separator.
+    size_t lastSeparatorPosition = absolutePathToFile.reverseFind(pathSeparator());
+    ASSERT_WITH_MESSAGE(lastSeparatorPosition != notFound, "If the separator is not found, this function already returns when performing the forward search.");
+    if (firstSeparatorPosition == lastSeparatorPosition)
+        directoryName.queryName = StringImpl::empty();
+    else
+        directoryName.queryName = absolutePathToFile.substring(firstSeparatorPosition + 1, lastSeparatorPosition); // Not include the separator.
+    return true;
+}
+
+static bool currentWorkingDirectory(DirectoryName& directoryName)
+{
+#if OS(WINDOWS)
+    // https://msdn.microsoft.com/en-us/library/sf98bd4y.aspx
+    char* buffer = nullptr;
+    if (!(buffer = _getcwd(nullptr, 0)))
+        return false;
+    String directoryString = String::fromUTF8(buffer);
+    free(buffer);
+#else
+    auto buffer = std::make_unique<char[]>(PATH_MAX);
+    if (!getcwd(buffer.get(), PATH_MAX))
+        return false;
+    String directoryString = String::fromUTF8(buffer.get());
+#endif
+    if (directoryString.isEmpty())
+        return false;
+
+    if (directoryString[directoryString.length() - 1] == pathSeparator())
+        return extractDirectoryName(directoryString, directoryName);
+    // Append the seperator to represents the file name. extractDirectoryName only accepts the absolute file name.
+    return extractDirectoryName(makeString(directoryString, pathSeparator()), directoryName);
+}
+
+static String resolvePath(const DirectoryName& directoryName, const String& query)
+{
+    Vector<String> directoryPieces;
+    directoryName.queryName.split(pathSeparator(), false, directoryPieces);
+
+    Vector<String> queryPieces;
+    query.split(pathSeparator(), true, queryPieces);
+
+    // Only first '/' is recognized as the path from the root.
+    if (!queryPieces.isEmpty() && queryPieces[0].isEmpty())
+        directoryPieces.clear();
+
+    for (const auto& query : queryPieces) {
+        if (query == String(ASCIILiteral(".."))) {
+            if (!directoryPieces.isEmpty())
+                directoryPieces.removeLast();
+        } else if (!query.isEmpty() && query != String(ASCIILiteral(".")))
+            directoryPieces.append(query);
+    }
+
+    StringBuilder builder;
+    builder.append(directoryName.rootName);
+    for (size_t i = 0; i < directoryPieces.size(); ++i) {
+        builder.append(directoryPieces[i]);
+        if (i + 1 != directoryPieces.size())
+            builder.append(pathSeparator());
+    }
+    return builder.toString();
+}
+
+JSInternalPromise* GlobalObject::moduleLoaderResolve(JSGlobalObject* globalObject, ExecState* exec, JSValue keyValue, JSValue referrerValue)
+{
+    JSInternalPromiseDeferred* deferred = JSInternalPromiseDeferred::create(exec, globalObject);
+    const Identifier key = keyValue.toPropertyKey(exec);
+    if (exec->hadException()) {
+        JSValue exception = exec->exception();
+        exec->clearException();
+        return deferred->reject(exec, exception);
+    }
+
+    if (key.isSymbol())
+        return deferred->resolve(exec, keyValue);
+
+    DirectoryName directoryName;
+    if (referrerValue.isUndefined()) {
+        if (!currentWorkingDirectory(directoryName))
+            return deferred->reject(exec, createError(exec, ASCIILiteral("Could not resolve the current working directory.")));
+    } else {
+        const Identifier referrer = referrerValue.toPropertyKey(exec);
+        if (exec->hadException()) {
+            JSValue exception = exec->exception();
+            exec->clearException();
+            return deferred->reject(exec, exception);
+        }
+        if (referrer.isSymbol()) {
+            if (!currentWorkingDirectory(directoryName))
+                return deferred->reject(exec, createError(exec, ASCIILiteral("Could not resolve the current working directory.")));
+        } else {
+            // If the referrer exists, we assume that the referrer is the correct absolute path.
+            if (!extractDirectoryName(referrer.impl(), directoryName))
+                return deferred->reject(exec, createError(exec, makeString("Could not resolve the referrer name '", String(referrer.impl()), "'.")));
+        }
+    }
+
+    return deferred->resolve(exec, jsString(exec, resolvePath(directoryName, key.impl())));
+}
+
 EncodedJSValue JSC_HOST_CALL functionPrint(ExecState* exec)
 {
     for (unsigned i = 0; i < exec->argumentCount(); ++i) {
@@ -1202,6 +1329,12 @@ EncodedJSValue JSC_HOST_CALL functionEnableExceptionFuzz(ExecState*)
     return JSValue::encode(jsUndefined());
 }
 
+EncodedJSValue JSC_HOST_CALL functionDrainMicrotasks(ExecState* exec)
+{
+    exec->vm().drainMicrotasks();
+    return JSValue::encode(jsUndefined());
+}
+
 #if ENABLE(WEBASSEMBLY)
 EncodedJSValue JSC_HOST_CALL functionLoadWebAssembly(ExecState* exec)
 {
index 9f88f19..4e67a8c 100644 (file)
@@ -66,4 +66,16 @@ JSInternalPromise* JSInternalPromiseDeferred::promise() const
     return jsCast<JSInternalPromise*>(Base::promise());
 }
 
+JSInternalPromise* JSInternalPromiseDeferred::resolve(ExecState* exec, JSValue value)
+{
+    Base::resolve(exec, value);
+    return promise();
+}
+
+JSInternalPromise* JSInternalPromiseDeferred::reject(ExecState* exec, JSValue reason)
+{
+    Base::reject(exec, reason);
+    return promise();
+}
+
 } // namespace JSC
index 540dade..662a277 100644 (file)
@@ -47,6 +47,8 @@ public:
     DECLARE_EXPORT_INFO;
 
     JS_EXPORT_PRIVATE JSInternalPromise* promise() const;
+    JS_EXPORT_PRIVATE JSInternalPromise* resolve(ExecState*, JSValue);
+    JS_EXPORT_PRIVATE JSInternalPromise* reject(ExecState*, JSValue);
 
 private:
     JSInternalPromiseDeferred(VM&);
diff --git a/Source/JavaScriptCore/tests/stress/pathname-resolve.js b/Source/JavaScriptCore/tests/stress/pathname-resolve.js
new file mode 100644 (file)
index 0000000..5a2ca6b
--- /dev/null
@@ -0,0 +1,64 @@
+//@ skip
+// To execute this test, need to specify the JSC_exposeInternalModuleLoader environment variable and execute it on non Windows platform.
+function shouldBe(actual, expected) {
+    if (actual !== expected)
+        throw new Error(`bad value: ${String(actual)}`);
+}
+
+function shouldResolve(name, referrer, expected)
+{
+    var promise = Loader.resolve(name, referrer);
+    return promise.then(function (actual) {
+        shouldBe(actual, expected);
+    });
+}
+
+function shouldThrow(name, referrer, errorMessage)
+{
+    var notThrown = false;
+    return Loader.resolve(name, referrer).then(function (error) {
+        notThrown = true;
+    }).catch(function (error) {
+        shouldBe(String(error), errorMessage);
+    }).then(function () {
+        if (notThrown)
+            throw new Error("not thrown");
+    });
+}
+
+var error = null;
+
+// On windows platform, all "/" becomes "\".
+Promise.all([
+    shouldResolve('tmp.js', '/home/WebKit/', '/home/WebKit/tmp.js'),
+    shouldResolve('tmp.js', '/home/', '/home/tmp.js'),
+    shouldResolve('/tmp.js', '/home/WebKit/', '/tmp.js'),
+    shouldResolve('///tmp.js', '/home/WebKit/', '/tmp.js'),
+    shouldResolve('.///tmp.js', '/home/WebKit/', '/home/WebKit/tmp.js'),
+    shouldResolve('./../tmp.js', '/home/WebKit/', '/home/tmp.js'),
+    shouldResolve('./../../tmp.js', '/home/WebKit/', '/tmp.js'),
+    shouldResolve('./../../../tmp.js', '/home/WebKit/', '/tmp.js'),
+    shouldResolve('./../../home/../tmp.js', '/home/WebKit/', '/tmp.js'),
+    shouldResolve('./../../../home/WebKit/../tmp.js', '/home/WebKit/', '/home/tmp.js'),
+    shouldResolve('../home/WebKit/tmp.js', '/home/WebKit/', '/home/home/WebKit/tmp.js'),
+    shouldResolve('../home/WebKit/../tmp.js', '/home/WebKit/', '/home/home/tmp.js'),
+    shouldResolve('./tmp.js', '/home/WebKit/hello.js', '/home/WebKit/tmp.js'),
+
+    shouldResolve('./tmp.js', 'C:/', 'C:/tmp.js'),
+    shouldResolve('./tmp.js', 'C:/home/', 'C:/home/tmp.js'),
+    shouldResolve('../tmp.js', 'C:/home/', 'C:/tmp.js'),
+    shouldResolve('../../tmp.js', 'C:/home/', 'C:/tmp.js'),
+    shouldResolve('./hello/tmp.js', 'C:/home/', 'C:/home/hello/tmp.js'),
+    shouldResolve('/tmp.js', 'C:/home/', 'C:/tmp.js'),
+
+    shouldThrow('/tmp.js', '', `Error: Could not resolve the referrer name ''.`),
+    shouldThrow('/tmp.js', 'hello', `Error: Could not resolve the referrer name 'hello'.`),
+    shouldThrow('tmp.js', 'hello', `Error: Could not resolve the referrer name 'hello'.`),
+]).catch(function (e) {
+    error = e;
+});
+
+// Force to run all pending tasks.
+drainMicrotasks();
+if (error)
+    throw error;