From e09c889da1d91df32a9533329d44b081214382ef Mon Sep 17 00:00:00 2001 From: "utatane.tea@gmail.com" Date: Thu, 27 Aug 2015 23:48:13 +0000 Subject: [PATCH] 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 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 | 35 +++++ Source/JavaScriptCore/jsc.cpp | 155 +++++++++++++++++++-- .../runtime/JSInternalPromiseDeferred.cpp | 12 ++ .../runtime/JSInternalPromiseDeferred.h | 2 + .../tests/stress/pathname-resolve.js | 64 +++++++++ 5 files changed, 257 insertions(+), 11 deletions(-) create mode 100644 Source/JavaScriptCore/tests/stress/pathname-resolve.js diff --git a/Source/JavaScriptCore/ChangeLog b/Source/JavaScriptCore/ChangeLog index bd4f469..41d78c7 100644 --- a/Source/JavaScriptCore/ChangeLog +++ b/Source/JavaScriptCore/ChangeLog @@ -1,3 +1,38 @@ +2015-08-27 Yusuke Suzuki + + 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 + + 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 Unreviewed, fix some FIXMEs and add some new ones, based on things we've learned from some diff --git a/Source/JavaScriptCore/jsc.cpp b/Source/JavaScriptCore/jsc.cpp index 033e696..efaeb04 100644 --- a/Source/JavaScriptCore/jsc.cpp +++ b/Source/JavaScriptCore/jsc.cpp @@ -64,7 +64,9 @@ #include #include -#if !OS(WINDOWS) +#if OS(WINDOWS) +#include +#else #include #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 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(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 directoryPieces; + directoryName.queryName.split(pathSeparator(), false, directoryPieces); + + Vector 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) { diff --git a/Source/JavaScriptCore/runtime/JSInternalPromiseDeferred.cpp b/Source/JavaScriptCore/runtime/JSInternalPromiseDeferred.cpp index 9f88f19..4e67a8c 100644 --- a/Source/JavaScriptCore/runtime/JSInternalPromiseDeferred.cpp +++ b/Source/JavaScriptCore/runtime/JSInternalPromiseDeferred.cpp @@ -66,4 +66,16 @@ JSInternalPromise* JSInternalPromiseDeferred::promise() const return jsCast(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 diff --git a/Source/JavaScriptCore/runtime/JSInternalPromiseDeferred.h b/Source/JavaScriptCore/runtime/JSInternalPromiseDeferred.h index 540dade..662a277 100644 --- a/Source/JavaScriptCore/runtime/JSInternalPromiseDeferred.h +++ b/Source/JavaScriptCore/runtime/JSInternalPromiseDeferred.h @@ -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 index 0000000..5a2ca6b --- /dev/null +++ b/Source/JavaScriptCore/tests/stress/pathname-resolve.js @@ -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; -- 1.8.3.1