diff --git a/.eslintrc.yml b/.eslintrc.yml index de2e6f3ba06..3bdb7d66a05 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -2,6 +2,10 @@ env: es6: true mongo: true +parserOptions: + ecmaVersion: 6 + sourceType: "module" + rules: # Rules are documented at http://eslint.org/docs/rules/ no-cond-assign: 2 diff --git a/jstests/replsets/libs/secondary_reads_test.js b/jstests/replsets/libs/secondary_reads_test.js index 4840708dba2..9c91c871b84 100644 --- a/jstests/replsets/libs/secondary_reads_test.js +++ b/jstests/replsets/libs/secondary_reads_test.js @@ -99,8 +99,8 @@ function SecondaryReadsTest(name = "secondary_reads_test") { assert.gt(readers.length, 0, "no readers to stop"); assert.commandWorked(primaryDB.getCollection(signalColl).insert({_id: testDoneId})); for (let i = 0; i < readers.length; i++) { - const await = readers[i]; - await (); + const awaitReader = readers[i]; + awaitReader(); print("reader " + i + " done"); } readers = []; diff --git a/jstests/replsets/read_operations_during_step_down.js b/jstests/replsets/read_operations_during_step_down.js index 1c277d24501..6b80a80d888 100644 --- a/jstests/replsets/read_operations_during_step_down.js +++ b/jstests/replsets/read_operations_during_step_down.js @@ -28,8 +28,7 @@ TestData.dbName = dbName; TestData.collName = collName; jsTestLog("1. Do a document write"); -assert.commandWorked( -        primaryColl.insert({_id: 0}, {"writeConcern": {"w": "majority"}})); +assert.commandWorked(primaryColl.insert({_id: 0}, {"writeConcern": {"w": "majority"}})); rst.awaitReplication(); // Open a cursor on primary. diff --git a/jstests/replsets/read_operations_during_step_up.js b/jstests/replsets/read_operations_during_step_up.js index f3a7bb96008..643059e837b 100644 --- a/jstests/replsets/read_operations_during_step_up.js +++ b/jstests/replsets/read_operations_during_step_up.js @@ -32,8 +32,7 @@ TestData.dbName = dbName; TestData.collName = collName; jsTestLog("1. Do a document write"); -assert.commandWorked( -        primaryColl.insert({_id: 0}, {"writeConcern": {"w": "majority"}})); +assert.commandWorked(primaryColl.insert({_id: 0}, {"writeConcern": {"w": "majority"}})); rst.awaitReplication(); // It's possible for notPrimaryUnacknowledgedWrites to be non-zero because of mirrored reads during diff --git a/jstests/replsets/tenant_migration_concurrent_writes_on_donor_util.js b/jstests/replsets/tenant_migration_concurrent_writes_on_donor_util.js index 851c3bad41d..dc6752874ab 100644 --- a/jstests/replsets/tenant_migration_concurrent_writes_on_donor_util.js +++ b/jstests/replsets/tenant_migration_concurrent_writes_on_donor_util.js @@ -214,11 +214,6 @@ TenantMigrationConcurrentWriteUtil.kTestIndex = { expireAfterSeconds: TenantMigrationConcurrentWriteUtil.kExpireAfterSeconds }; -function collectionExists(db, collName) { - const res = assert.commandWorked(db.runCommand({listCollections: 1, filter: {name: collName}})); - return res.cursor.firstBatch.length == 1; -} - function insertTestDoc(primaryDB, collName) { assert.commandWorked(primaryDB.runCommand( {insert: collName, documents: [TenantMigrationConcurrentWriteUtil.kTestDoc]})); diff --git a/src/mongo/scripting/SConscript b/src/mongo/scripting/SConscript index 92578718aef..fb5b8ccc41e 100644 --- a/src/mongo/scripting/SConscript +++ b/src/mongo/scripting/SConscript @@ -92,6 +92,7 @@ if jsEngine: 'mozjs/jsthread.cpp', 'mozjs/maxkey.cpp', 'mozjs/minkey.cpp', + 'mozjs/module_loader.cpp', 'mozjs/mongo.cpp', 'mozjs/mongohelpers.cpp', 'mozjs/mongohelpers_js.cpp', @@ -123,6 +124,15 @@ if jsEngine: '$BUILD_DIR/third_party/mozjs/mozjs', ], ) + + scriptingEnv.CppUnitTest( + target='scripting_mozjs_test', + source=['mozjs/module_loader_test.cpp'], + LIBDEPS=[ + 'bson_template_evaluator', + 'scripting', + ], + ) else: env.Library( target='scripting', diff --git a/src/mongo/scripting/mozjs/implscope.cpp b/src/mongo/scripting/mozjs/implscope.cpp index a02d7aa0688..94b1d8ed096 100644 --- a/src/mongo/scripting/mozjs/implscope.cpp +++ b/src/mongo/scripting/mozjs/implscope.cpp @@ -39,13 +39,17 @@ #include #include #include +#include #include #include #include +#include #include #include #include +#include + #include "mongo/base/error_codes.h" #include "mongo/config.h" #include "mongo/db/operation_context.h" @@ -77,6 +81,7 @@ extern const JSFile assert; namespace mozjs { +const char* const MozJSImplScope::kInteractiveShellName = "(shell)"; const char* const MozJSImplScope::kExecResult = "__lastres__"; const char* const MozJSImplScope::kInvokeResult = "__returnValue"; @@ -462,6 +467,12 @@ MozJSImplScope::MozJSImplScope(MozJSScriptEngine* engine, boost::optional j _timestampProto(_context), _uriProto(_context) { + _moduleLoader = std::make_unique(); + uassert(ErrorCodes::JSInterpreterFailure, "Failed to create ModuleLoader", _moduleLoader); + uassert(ErrorCodes::JSInterpreterFailure, + "Failed to initialize ModuleLoader", + _moduleLoader->init(_context, boost::filesystem::current_path())); + try { kCurrentScope = this; @@ -792,6 +803,35 @@ int MozJSImplScope::invoke(ScriptingFunction func, }); } +bool shouldTryExecAsModule(JSContext* cx, const std::string& name, bool success) { + if (name == MozJSImplScope::kInteractiveShellName) { + return false; + } + + if (success) { + return false; + } + + JS::RootedValue ex(cx); + if (!JS_GetPendingException(cx, &ex)) { + return false; + } + + JS::RootedObject obj(cx, ex.toObjectOrNull()); + const JSClass* syntaxError = js::ProtoKeyToClass(JSProto_SyntaxError); + if (!JS_InstanceOf(cx, obj, syntaxError, nullptr)) { + return false; + } + + JSErrorReport* report = JS_ErrorFromException(cx, obj); + if (!report) { + return false; + } + + return report->errorNumber == JSMSG_IMPORT_DECL_AT_TOP_LEVEL || + report->errorNumber == JSMSG_EXPORT_DECL_AT_TOP_LEVEL; +} + bool MozJSImplScope::exec(StringData code, const std::string& name, bool printResult, @@ -804,19 +844,24 @@ bool MozJSImplScope::exec(StringData code, co.setFileAndLine(name.c_str(), 1); JS::SourceText srcBuf; - JSScript* scriptPtr; - bool success = srcBuf.init(_context, code.rawData(), code.size(), JS::SourceOwnership::Borrowed); - if (_checkErrorState(success, reportError, assertOnError)) + if (_checkErrorState(success, reportError, assertOnError)) { return false; + } - scriptPtr = JS::Compile(_context, co, srcBuf); + JSScript* scriptPtr = JS::Compile(_context, co, srcBuf); success = scriptPtr != nullptr; - if (_checkErrorState(success, reportError, assertOnError)) - return false; - JS::RootedScript script(_context, scriptPtr); + JSObject* modulePtr = nullptr; + if (shouldTryExecAsModule(_context, name, success)) { + modulePtr = _moduleLoader->loadRootModuleFromSource(_context, name, code); + success = modulePtr != nullptr; + } + + if (_checkErrorState(success, reportError, assertOnError)) { + return false; + } if (timeoutMs) { _engine->getDeadlineMonitor().startDeadline(this, timeoutMs); @@ -828,10 +873,27 @@ bool MozJSImplScope::exec(StringData code, { ScopeGuard guard([&] { _engine->getDeadlineMonitor().stopDeadline(this); }); - success = JS_ExecuteScript(_context, script, &out); + if (scriptPtr) { + JS::RootedScript script(_context, scriptPtr); + success = JS_ExecuteScript(_context, script, &out); + } else { + JS::Rooted returnValue(_context); + JS::RootedObject module(_context, modulePtr); - if (_checkErrorState(success, reportError, assertOnError)) + success = JS::ModuleInstantiate(_context, module); + if (success) { + success = JS::ModuleEvaluate(_context, module, &returnValue); + if (success) { + JS::RootedObject evaluationPromise(_context, &returnValue.toObject()); + success = JS::ThrowOnModuleEvaluationFailure(_context, evaluationPromise); + } + } + } + + if (_checkErrorState(success, reportError, assertOnError)) { return false; + } + // Run all of the async JS functions js::RunJobs(_context); } @@ -1078,5 +1140,9 @@ std::string MozJSImplScope::buildStackString() { } } +ModuleLoader* MozJSImplScope::getModuleLoader() const { + return _moduleLoader.get(); +} + } // namespace mozjs } // namespace mongo diff --git a/src/mongo/scripting/mozjs/implscope.h b/src/mongo/scripting/mozjs/implscope.h index 27bb26925de..de516ee1525 100644 --- a/src/mongo/scripting/mozjs/implscope.h +++ b/src/mongo/scripting/mozjs/implscope.h @@ -30,9 +30,9 @@ #pragma once #include +#include #include - #include "mongo/client/dbclient_cursor.h" #include "mongo/scripting/mozjs/bindata.h" #include "mongo/scripting/mozjs/bson.h" @@ -53,6 +53,7 @@ #include "mongo/scripting/mozjs/jsthread.h" #include "mongo/scripting/mozjs/maxkey.h" #include "mongo/scripting/mozjs/minkey.h" +#include "mongo/scripting/mozjs/module_loader.h" #include "mongo/scripting/mozjs/mongo.h" #include "mongo/scripting/mozjs/mongohelpers.h" #include "mongo/scripting/mozjs/nativefunction.h" @@ -320,6 +321,7 @@ public: return _globalProto; } + static const char* const kInteractiveShellName; static const char* const kExecResult; static const char* const kInvokeResult; @@ -369,6 +371,8 @@ public: void setStatus(Status status); + ModuleLoader* getModuleLoader() const; + private: template auto _runSafely(ImplScopeFunction&& functionToRun) -> decltype(functionToRun()); @@ -437,6 +441,8 @@ private: bool _inReportError; + std::unique_ptr _moduleLoader; + WrapType _binDataProto; WrapType _bsonProto; WrapType _codeProto; diff --git a/src/mongo/scripting/mozjs/module_loader.cpp b/src/mongo/scripting/mozjs/module_loader.cpp new file mode 100644 index 00000000000..370a56cf1b5 --- /dev/null +++ b/src/mongo/scripting/mozjs/module_loader.cpp @@ -0,0 +1,446 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/module_loader.h" +#include "mongo/util/file.h" + +#include +#include +#include + +namespace mongo { +namespace mozjs { + +bool ModuleLoader::init(JSContext* cx, const boost::filesystem::path& loadPath) { + invariant(loadPath.is_absolute()); + _loadPath = loadPath.string(); + + JS::SetModuleResolveHook(JS_GetRuntime(cx), ModuleLoader::moduleResolveHook); + return true; +} + +JSObject* ModuleLoader::loadRootModuleFromPath(JSContext* cx, const std::string& path) { + return loadRootModule(cx, path, boost::none); +} + +JSObject* ModuleLoader::loadRootModuleFromSource(JSContext* cx, + const std::string& path, + StringData source) { + return loadRootModule(cx, path, source); +} + +JSObject* ModuleLoader::loadRootModule(JSContext* cx, + const std::string& path, + boost::optional source) { + JS::RootedString loadPath(cx, JS_NewStringCopyN(cx, _loadPath.c_str(), _loadPath.size())); + JS::RootedObject info(cx, [&]() { + if (source) { + JS::RootedString src(cx, JS_NewStringCopyN(cx, source->rawData(), source->size())); + return createScriptPrivateInfo(cx, loadPath, src); + } + + return createScriptPrivateInfo(cx, loadPath, nullptr); + }()); + + if (!info) { + return nullptr; + } + + JS::RootedValue referencingPrivate(cx, JS::ObjectValue(*info)); + JS::RootedString specifier(cx, JS_NewStringCopyN(cx, path.c_str(), path.size())); + JS::RootedObject moduleRequest(cx, JS::CreateModuleRequest(cx, specifier)); + if (!moduleRequest) { + return nullptr; + } + + return resolveImportedModule(cx, referencingPrivate, moduleRequest); +} + +// static +JSObject* ModuleLoader::moduleResolveHook(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest) { + + auto scope = getScope(cx); + return scope->getModuleLoader()->resolveImportedModule(cx, referencingPrivate, moduleRequest); +} + +JSObject* ModuleLoader::resolveImportedModule(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest) { + + JS::Rooted path(cx, resolveAndNormalize(cx, moduleRequest, referencingPrivate)); + if (!path) { + return nullptr; + } + + return loadAndParse(cx, path, referencingPrivate); +} + +/** + * A few things to note about module resolution: + * - A "specifier" refers to the name of the imported module (e.g. `import {x} from 'specifier'`) + * - Specifiers with relative paths are always relative to their referencing module. The + * referencing module for the root module is the file `mongo` was run on, or the mongo binary + * itself when run as a REPL. In practice this means relative paths in scripts behave as you + * would expect relative paths to work on the command line. + * - If we already have source for a specifier we are trying to load (this is only the case when + * executing the root module), we will skip normalization and reading the source again. + */ +JSString* ModuleLoader::resolveAndNormalize(JSContext* cx, + JS::HandleObject moduleRequestArg, + JS::HandleValue referencingInfo) { + JS::Rooted specifierString(cx, JS::GetModuleRequestSpecifier(cx, moduleRequestArg)); + if (!specifierString) { + return nullptr; + } + + if (referencingInfo.isUndefined()) { + JS_ReportErrorASCII(cx, "No referencing module for relative import"); + return nullptr; + } + + bool hasSource; + JS::RootedObject referencingInfoObject(cx, &referencingInfo.toObject()); + if (!JS_HasProperty(cx, referencingInfoObject, "source", &hasSource)) { + return nullptr; + } + + if (hasSource) { + return specifierString; + } + + JS::RootedString refPath(cx); + if (!getScriptPath(cx, referencingInfo, &refPath)) { + return nullptr; + } + + if (!refPath) { + JS_ReportErrorASCII(cx, "No path set for referencing module"); + return nullptr; + } + + boost::filesystem::path specifierPath(JS_EncodeStringToUTF8(cx, specifierString).get()); + auto refAbsPath = boost::filesystem::path(JS_EncodeStringToUTF8(cx, refPath).get()); + if (is_directory(specifierPath)) { + JS_ReportErrorUTF8(cx, + "Directory import '%s' is not supported, imported from %s", + specifierPath.c_str(), + refAbsPath.c_str()); + return nullptr; + } + + if (!specifierPath.is_relative()) { + return specifierString; + } + + boost::system::error_code ec; + auto fullPath = [&]() { + if (!boost::filesystem::is_directory(refAbsPath)) { + return boost::filesystem::canonical(specifierPath, refAbsPath.parent_path(), ec) + .lexically_normal() + .string(); + } + + return boost::filesystem::canonical(specifierPath, refAbsPath, ec) + .lexically_normal() + .string(); + }(); + + if (ec) { + if (ec.value() == boost::system::errc::no_such_file_or_directory) { + JS_ReportErrorUTF8(cx, + "Cannot find module '%s' imported from %s", + specifierPath.c_str(), + refAbsPath.c_str()); + } else { + JS_ReportErrorUTF8(cx, "%s", ec.message().c_str()); + } + + return nullptr; + } + + return JS_NewStringCopyN(cx, fullPath.c_str(), fullPath.size()); +} + +bool ModuleLoader::getScriptPath(JSContext* cx, + JS::HandleValue privateValue, + JS::MutableHandleString pathOut) { + pathOut.set(nullptr); + + JS::RootedObject infoObj(cx, &privateValue.toObject()); + JS::RootedValue pathValue(cx); + if (!JS_GetProperty(cx, infoObj, "path", &pathValue)) { + return false; + } + + if (pathValue.isUndefined()) { + return true; + } + + JS::RootedString path(cx, pathValue.toString()); + pathOut.set(path); + return pathOut; +} + +JSObject* ModuleLoader::loadAndParse(JSContext* cx, + JS::HandleString pathArg, + JS::HandleValue referencingPrivate) { + JS::Rooted path(cx, pathArg); + if (!path) { + return nullptr; + } + + JS::RootedObject module(cx); + if (!lookUpModuleInRegistry(cx, path, &module)) { + return nullptr; + } + + if (module) { + return module; + } + + JS::UniqueChars filename = JS_EncodeStringToLatin1(cx, path); + if (!filename) { + return nullptr; + } + + JS::CompileOptions options(cx); + options.setFileAndLine(filename.get(), 1); + + JS::RootedString source(cx, fetchSource(cx, path, referencingPrivate)); + if (!source) { + return nullptr; + } + + JS::AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, source)) { + return nullptr; + } + + const char16_t* chars = stableChars.twoByteRange().begin().get(); + JS::SourceText srcBuf; + if (!srcBuf.init(cx, chars, JS::GetStringLength(source), JS::SourceOwnership::Borrowed)) { + return nullptr; + } + + module = JS::CompileModule(cx, options, srcBuf); + if (!module) { + return nullptr; + } + + JS::RootedObject info(cx, createScriptPrivateInfo(cx, path, nullptr)); + if (!info) { + return nullptr; + } + + JS::SetModulePrivate(module, JS::ObjectValue(*info)); + + if (!addModuleToRegistry(cx, path, module)) { + return nullptr; + } + + return module; +} + +JSString* ModuleLoader::fetchSource(JSContext* cx, + JS::HandleString pathArg, + JS::HandleValue referencingPrivate) { + JS::RootedObject infoObj(cx, &referencingPrivate.toObject()); + JS::RootedValue sourceValue(cx); + if (!JS_GetProperty(cx, infoObj, "source", &sourceValue)) { + return nullptr; + } + + if (!sourceValue.isUndefined()) { + return sourceValue.toString(); + } + + JS::RootedString resolvedPath(cx, pathArg); + if (!resolvedPath) { + return nullptr; + } + + return fileAsString(cx, resolvedPath); +} + +enum GlobalAppSlot { GlobalAppSlotModuleRegistry, GlobalAppSlotCount }; +JSObject* ModuleLoader::getOrCreateModuleRegistry(JSContext* cx) { + JSObject* global = JS::CurrentGlobalOrNull(cx); + if (!global) { + return nullptr; + } + + JS::RootedValue value(cx, JS::GetReservedSlot(global, GlobalAppSlotModuleRegistry)); + if (!value.isUndefined()) { + return &value.toObject(); + } + + JSObject* registry = JS::NewMapObject(cx); + if (!registry) { + return nullptr; + } + + JS::SetReservedSlot(global, GlobalAppSlotModuleRegistry, JS::ObjectValue(*registry)); + return registry; +} + +bool ModuleLoader::lookUpModuleInRegistry(JSContext* cx, + JS::HandleString path, + JS::MutableHandleObject moduleOut) { + moduleOut.set(nullptr); + + JS::RootedObject registry(cx, getOrCreateModuleRegistry(cx)); + if (!registry) { + return false; + } + + JS::RootedValue pathValue(cx, StringValue(path)); + JS::RootedValue moduleValue(cx); + if (!JS::MapGet(cx, registry, pathValue, &moduleValue)) { + return false; + } + + if (!moduleValue.isUndefined()) { + moduleOut.set(&moduleValue.toObject()); + } + + return true; +} + +bool ModuleLoader::addModuleToRegistry(JSContext* cx, + JS::HandleString path, + JS::HandleObject module) { + JS::RootedObject registry(cx, getOrCreateModuleRegistry(cx)); + if (!registry) { + return false; + } + + JS::RootedValue pathValue(cx, StringValue(path)); + JS::RootedValue moduleValue(cx, JS::ObjectValue(*module)); + return JS::MapSet(cx, registry, pathValue, moduleValue); +} + +// 2 GB is the largest support Javascript file size. +const fileofs kMaxJsFileLength = fileofs(2) * 1024 * 1024 * 1024; +JSString* ModuleLoader::fileAsString(JSContext* cx, JS::HandleString pathnameStr) { + JS::UniqueChars pathname = JS_EncodeStringToLatin1(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + + File file; + file.open(pathname.get(), true); + if (!file.is_open() || file.bad()) { + JS_ReportErrorUTF8(cx, "can't open for reading %s", pathname.get()); + return nullptr; + } + + fileofs fo = file.len(); + if (fo > kMaxJsFileLength) { + JS_ReportErrorUTF8(cx, "file contents too large reading %s", pathname.get()); + return nullptr; + } + + size_t len = static_cast(fo); + if (len < 0) { + JS_ReportErrorUTF8(cx, "can't read length of %s", pathname.get()); + return nullptr; + } + + JS::UniqueChars buf(js_pod_malloc(len)); + if (!buf) { + JS_ReportOutOfMemory(cx); + return nullptr; + } + + file.read(0, buf.get(), len); + if (file.bad()) { + JS_ReportErrorUTF8(cx, "failed to read file %s", pathname.get()); + return nullptr; + } + + int offset = 0; + if (len > 2 && buf[0] == '#' && buf[1] == '!') { + const char* newline = reinterpret_cast(memchr(buf.get(), '\n', len)); + if (newline) { + offset = newline - buf.get(); + } else { + // file of just shebang treated same as empty file + offset = len; + } + } + + JS::UniqueTwoByteChars ucbuf( + JS::LossyUTF8CharsToNewTwoByteCharsZ( + cx, JS::UTF8Chars(buf.get() + offset, len), &len, js::MallocArena) + .get()); + if (!ucbuf) { + pathname = JS_EncodeStringToUTF8(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + + JS_ReportErrorUTF8(cx, "invalid UTF-8 in file '%s'", pathname.get()); + return nullptr; + } + + return JS_NewUCStringCopyN(cx, ucbuf.get(), len); +} + +JSObject* ModuleLoader::createScriptPrivateInfo(JSContext* cx, + JS::Handle path, + JS::Handle source) { + JS::Rooted info(cx, JS_NewPlainObject(cx)); + if (!info) { + return nullptr; + } + + if (path) { + JS::Rooted pathValue(cx, JS::StringValue(path)); + if (!JS_DefineProperty(cx, info, "path", pathValue, JSPROP_ENUMERATE)) { + return nullptr; + } + } + + if (source) { + JS::Rooted sourceValue(cx, JS::StringValue(source)); + if (!JS_DefineProperty(cx, info, "source", sourceValue, JSPROP_ENUMERATE)) { + return nullptr; + } + } + + return info; +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/module_loader.h b/src/mongo/scripting/mozjs/module_loader.h new file mode 100644 index 00000000000..7ce1da55228 --- /dev/null +++ b/src/mongo/scripting/mozjs/module_loader.h @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include +#include + +#include + +#include "mongo/base/string_data.h" + +namespace mongo { +namespace mozjs { + +class ModuleLoader { +public: + bool init(JSContext* ctx, const boost::filesystem::path& loadPath); + JSObject* loadRootModuleFromPath(JSContext* cx, const std::string& path); + JSObject* loadRootModuleFromSource(JSContext* cx, const std::string& path, StringData source); + +private: + static JSObject* moduleResolveHook(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest); + + JSObject* loadRootModule(JSContext* cx, + const std::string& path, + boost::optional source); + JSObject* resolveImportedModule(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest); + JSObject* loadAndParse(JSContext* cx, + JS::HandleString path, + JS::HandleValue referencingPrivate); + bool lookUpModuleInRegistry(JSContext* cx, + JS::HandleString path, + JS::MutableHandleObject moduleOut); + bool addModuleToRegistry(JSContext* cx, JS::HandleString path, JS::HandleObject module); + JSString* resolveAndNormalize(JSContext* cx, + JS::HandleObject moduleRequestArg, + JS::HandleValue referencingInfo); + JSObject* getOrCreateModuleRegistry(JSContext* cx); + JSString* fetchSource(JSContext* cx, JS::HandleString path, JS::HandleValue referencingPrivate); + bool getScriptPath(JSContext* cx, + JS::HandleValue privateValue, + JS::MutableHandleString pathOut); + + JSString* fileAsString(JSContext* cx, JS::HandleString pathnameStr); + JSObject* createScriptPrivateInfo(JSContext* cx, + JS::Handle path, + JS::Handle source); + + std::string _loadPath; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/module_loader_test.cpp b/src/mongo/scripting/mozjs/module_loader_test.cpp new file mode 100644 index 00000000000..db934a72995 --- /dev/null +++ b/src/mongo/scripting/mozjs/module_loader_test.cpp @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include + +#include "mongo/scripting/engine.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace mozjs { + +TEST(ModuleLoaderTest, ImportBaseSpecifierFails) { + mongo::ScriptEngine::setup(); + std::unique_ptr scope(mongo::getGlobalScriptEngine()->newScope()); + + auto code = "import * as test from \"base_specifier\""_sd; + ASSERT_THROWS_WITH_CHECK( + scope->exec(code, + "root_module", + true /* printResult */, + true /* reportError */, + true /* assertOnError , timeout*/), + DBException, + [&](const auto& ex) { ASSERT_STRING_CONTAINS(ex.what(), "Cannot find module"); }); +} + +#if !defined(_WIN32) +TEST(ModuleLoaderTest, ImportDirectoryFails) { + mongo::ScriptEngine::setup(); + std::unique_ptr scope(mongo::getGlobalScriptEngine()->newScope()); + + auto code = fmt::format("import * as test from \"{}\"", + boost::filesystem::temp_directory_path().string()); + ASSERT_THROWS_WITH_CHECK( + scope->exec(code, + "root_module", + true /* printResult */, + true /* reportError */, + true /* assertOnError , timeout*/), + DBException, + [&](const auto& ex) { ASSERT_STRING_CONTAINS(ex.what(), "Directory import"); }); +} +#endif + +TEST(ModuleLoaderTest, ImportInInteractiveFails) { + mongo::ScriptEngine::setup(); + std::unique_ptr scope(mongo::getGlobalScriptEngine()->newScope()); + + auto code = "import * as test from \"some_module\""_sd; + ASSERT_THROWS_WITH_CHECK( + scope->exec(code, + MozJSImplScope::kInteractiveShellName, + true /* printResult */, + true /* reportError */, + true /* assertOnError , timeout*/), + DBException, + [&](const auto& ex) { + ASSERT_STRING_CONTAINS(ex.what(), + "import declarations may only appear at top level of a module"); + }); +} + +} // namespace mozjs +} // namespace mongo