0
0
mirror of https://github.com/mongodb/mongo.git synced 2024-11-25 00:58:53 +01:00

SERVER-70553 Add ES Modules support to mongo test runner

This commit is contained in:
Matt Broadstone 2022-11-10 15:57:44 +00:00 committed by Evergreen Agent
parent d5ff440817
commit 5f48de0b74
11 changed files with 721 additions and 21 deletions

View File

@ -2,6 +2,10 @@ env:
es6: true es6: true
mongo: true mongo: true
parserOptions:
ecmaVersion: 6
sourceType: "module"
rules: rules:
# Rules are documented at http://eslint.org/docs/rules/ # Rules are documented at http://eslint.org/docs/rules/
no-cond-assign: 2 no-cond-assign: 2

View File

@ -99,8 +99,8 @@ function SecondaryReadsTest(name = "secondary_reads_test") {
assert.gt(readers.length, 0, "no readers to stop"); assert.gt(readers.length, 0, "no readers to stop");
assert.commandWorked(primaryDB.getCollection(signalColl).insert({_id: testDoneId})); assert.commandWorked(primaryDB.getCollection(signalColl).insert({_id: testDoneId}));
for (let i = 0; i < readers.length; i++) { for (let i = 0; i < readers.length; i++) {
const await = readers[i]; const awaitReader = readers[i];
await (); awaitReader();
print("reader " + i + " done"); print("reader " + i + " done");
} }
readers = []; readers = [];

View File

@ -28,8 +28,7 @@ TestData.dbName = dbName;
TestData.collName = collName; TestData.collName = collName;
jsTestLog("1. Do a document write"); jsTestLog("1. Do a document write");
assert.commandWorked( assert.commandWorked(primaryColl.insert({_id: 0}, {"writeConcern": {"w": "majority"}}));
        primaryColl.insert({_id: 0}, {"writeConcern": {"w": "majority"}}));
rst.awaitReplication(); rst.awaitReplication();
// Open a cursor on primary. // Open a cursor on primary.

View File

@ -32,8 +32,7 @@ TestData.dbName = dbName;
TestData.collName = collName; TestData.collName = collName;
jsTestLog("1. Do a document write"); jsTestLog("1. Do a document write");
assert.commandWorked( assert.commandWorked(primaryColl.insert({_id: 0}, {"writeConcern": {"w": "majority"}}));
        primaryColl.insert({_id: 0}, {"writeConcern": {"w": "majority"}}));
rst.awaitReplication(); rst.awaitReplication();
// It's possible for notPrimaryUnacknowledgedWrites to be non-zero because of mirrored reads during // It's possible for notPrimaryUnacknowledgedWrites to be non-zero because of mirrored reads during

View File

@ -214,11 +214,6 @@ TenantMigrationConcurrentWriteUtil.kTestIndex = {
expireAfterSeconds: TenantMigrationConcurrentWriteUtil.kExpireAfterSeconds 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) { function insertTestDoc(primaryDB, collName) {
assert.commandWorked(primaryDB.runCommand( assert.commandWorked(primaryDB.runCommand(
{insert: collName, documents: [TenantMigrationConcurrentWriteUtil.kTestDoc]})); {insert: collName, documents: [TenantMigrationConcurrentWriteUtil.kTestDoc]}));

View File

@ -92,6 +92,7 @@ if jsEngine:
'mozjs/jsthread.cpp', 'mozjs/jsthread.cpp',
'mozjs/maxkey.cpp', 'mozjs/maxkey.cpp',
'mozjs/minkey.cpp', 'mozjs/minkey.cpp',
'mozjs/module_loader.cpp',
'mozjs/mongo.cpp', 'mozjs/mongo.cpp',
'mozjs/mongohelpers.cpp', 'mozjs/mongohelpers.cpp',
'mozjs/mongohelpers_js.cpp', 'mozjs/mongohelpers_js.cpp',
@ -123,6 +124,15 @@ if jsEngine:
'$BUILD_DIR/third_party/mozjs/mozjs', '$BUILD_DIR/third_party/mozjs/mozjs',
], ],
) )
scriptingEnv.CppUnitTest(
target='scripting_mozjs_test',
source=['mozjs/module_loader_test.cpp'],
LIBDEPS=[
'bson_template_evaluator',
'scripting',
],
)
else: else:
env.Library( env.Library(
target='scripting', target='scripting',

View File

@ -39,13 +39,17 @@
#include <js/CompilationAndEvaluation.h> #include <js/CompilationAndEvaluation.h>
#include <js/ContextOptions.h> #include <js/ContextOptions.h>
#include <js/Initialization.h> #include <js/Initialization.h>
#include <js/Modules.h>
#include <js/Object.h> #include <js/Object.h>
#include <js/SourceText.h> #include <js/SourceText.h>
#include <js/TypeDecls.h> #include <js/TypeDecls.h>
#include <js/friend/ErrorMessages.h>
#include <jsapi.h> #include <jsapi.h>
#include <jscustomallocator.h> #include <jscustomallocator.h>
#include <jsfriendapi.h> #include <jsfriendapi.h>
#include <boost/filesystem.hpp>
#include "mongo/base/error_codes.h" #include "mongo/base/error_codes.h"
#include "mongo/config.h" #include "mongo/config.h"
#include "mongo/db/operation_context.h" #include "mongo/db/operation_context.h"
@ -77,6 +81,7 @@ extern const JSFile assert;
namespace mozjs { namespace mozjs {
const char* const MozJSImplScope::kInteractiveShellName = "(shell)";
const char* const MozJSImplScope::kExecResult = "__lastres__"; const char* const MozJSImplScope::kExecResult = "__lastres__";
const char* const MozJSImplScope::kInvokeResult = "__returnValue"; const char* const MozJSImplScope::kInvokeResult = "__returnValue";
@ -462,6 +467,12 @@ MozJSImplScope::MozJSImplScope(MozJSScriptEngine* engine, boost::optional<int> j
_timestampProto(_context), _timestampProto(_context),
_uriProto(_context) { _uriProto(_context) {
_moduleLoader = std::make_unique<ModuleLoader>();
uassert(ErrorCodes::JSInterpreterFailure, "Failed to create ModuleLoader", _moduleLoader);
uassert(ErrorCodes::JSInterpreterFailure,
"Failed to initialize ModuleLoader",
_moduleLoader->init(_context, boost::filesystem::current_path()));
try { try {
kCurrentScope = this; 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, bool MozJSImplScope::exec(StringData code,
const std::string& name, const std::string& name,
bool printResult, bool printResult,
@ -804,19 +844,24 @@ bool MozJSImplScope::exec(StringData code,
co.setFileAndLine(name.c_str(), 1); co.setFileAndLine(name.c_str(), 1);
JS::SourceText<mozilla::Utf8Unit> srcBuf; JS::SourceText<mozilla::Utf8Unit> srcBuf;
JSScript* scriptPtr;
bool success = bool success =
srcBuf.init(_context, code.rawData(), code.size(), JS::SourceOwnership::Borrowed); srcBuf.init(_context, code.rawData(), code.size(), JS::SourceOwnership::Borrowed);
if (_checkErrorState(success, reportError, assertOnError)) if (_checkErrorState(success, reportError, assertOnError)) {
return false; return false;
}
scriptPtr = JS::Compile(_context, co, srcBuf); JSScript* scriptPtr = JS::Compile(_context, co, srcBuf);
success = scriptPtr != nullptr; 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) { if (timeoutMs) {
_engine->getDeadlineMonitor().startDeadline(this, timeoutMs); _engine->getDeadlineMonitor().startDeadline(this, timeoutMs);
@ -828,10 +873,27 @@ bool MozJSImplScope::exec(StringData code,
{ {
ScopeGuard guard([&] { _engine->getDeadlineMonitor().stopDeadline(this); }); 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<JS::Value> 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; return false;
}
// Run all of the async JS functions // Run all of the async JS functions
js::RunJobs(_context); js::RunJobs(_context);
} }
@ -1078,5 +1140,9 @@ std::string MozJSImplScope::buildStackString() {
} }
} }
ModuleLoader* MozJSImplScope::getModuleLoader() const {
return _moduleLoader.get();
}
} // namespace mozjs } // namespace mozjs
} // namespace mongo } // namespace mongo

View File

@ -30,9 +30,9 @@
#pragma once #pragma once
#include <jsapi.h> #include <jsapi.h>
#include <jsfriendapi.h>
#include <vm/PosixNSPR.h> #include <vm/PosixNSPR.h>
#include "mongo/client/dbclient_cursor.h" #include "mongo/client/dbclient_cursor.h"
#include "mongo/scripting/mozjs/bindata.h" #include "mongo/scripting/mozjs/bindata.h"
#include "mongo/scripting/mozjs/bson.h" #include "mongo/scripting/mozjs/bson.h"
@ -53,6 +53,7 @@
#include "mongo/scripting/mozjs/jsthread.h" #include "mongo/scripting/mozjs/jsthread.h"
#include "mongo/scripting/mozjs/maxkey.h" #include "mongo/scripting/mozjs/maxkey.h"
#include "mongo/scripting/mozjs/minkey.h" #include "mongo/scripting/mozjs/minkey.h"
#include "mongo/scripting/mozjs/module_loader.h"
#include "mongo/scripting/mozjs/mongo.h" #include "mongo/scripting/mozjs/mongo.h"
#include "mongo/scripting/mozjs/mongohelpers.h" #include "mongo/scripting/mozjs/mongohelpers.h"
#include "mongo/scripting/mozjs/nativefunction.h" #include "mongo/scripting/mozjs/nativefunction.h"
@ -320,6 +321,7 @@ public:
return _globalProto; return _globalProto;
} }
static const char* const kInteractiveShellName;
static const char* const kExecResult; static const char* const kExecResult;
static const char* const kInvokeResult; static const char* const kInvokeResult;
@ -369,6 +371,8 @@ public:
void setStatus(Status status); void setStatus(Status status);
ModuleLoader* getModuleLoader() const;
private: private:
template <typename ImplScopeFunction> template <typename ImplScopeFunction>
auto _runSafely(ImplScopeFunction&& functionToRun) -> decltype(functionToRun()); auto _runSafely(ImplScopeFunction&& functionToRun) -> decltype(functionToRun());
@ -437,6 +441,8 @@ private:
bool _inReportError; bool _inReportError;
std::unique_ptr<ModuleLoader> _moduleLoader;
WrapType<BinDataInfo> _binDataProto; WrapType<BinDataInfo> _binDataProto;
WrapType<BSONInfo> _bsonProto; WrapType<BSONInfo> _bsonProto;
WrapType<CodeInfo> _codeProto; WrapType<CodeInfo> _codeProto;

View File

@ -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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*
* 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 <boost/filesystem.hpp>
#include "mongo/scripting/mozjs/implscope.h"
#include "mongo/scripting/mozjs/module_loader.h"
#include "mongo/util/file.h"
#include <js/Modules.h>
#include <js/SourceText.h>
#include <js/StableStringChars.h>
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<StringData> 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<JSString*> 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<JSString*> 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<JSString*> 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<char16_t> 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<size_t>(fo);
if (len < 0) {
JS_ReportErrorUTF8(cx, "can't read length of %s", pathname.get());
return nullptr;
}
JS::UniqueChars buf(js_pod_malloc<char>(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<const char*>(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<JSString*> path,
JS::Handle<JSString*> source) {
JS::Rooted<JSObject*> info(cx, JS_NewPlainObject(cx));
if (!info) {
return nullptr;
}
if (path) {
JS::Rooted<JS::Value> pathValue(cx, JS::StringValue(path));
if (!JS_DefineProperty(cx, info, "path", pathValue, JSPROP_ENUMERATE)) {
return nullptr;
}
}
if (source) {
JS::Rooted<JS::Value> sourceValue(cx, JS::StringValue(source));
if (!JS_DefineProperty(cx, info, "source", sourceValue, JSPROP_ENUMERATE)) {
return nullptr;
}
}
return info;
}
} // namespace mozjs
} // namespace mongo

View File

@ -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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*
* 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 <boost/filesystem.hpp>
#include <boost/optional.hpp>
#include <jsapi.h>
#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<StringData> 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<JSString*> path,
JS::Handle<JSString*> source);
std::string _loadPath;
};
} // namespace mozjs
} // namespace mongo

View File

@ -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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*
* 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 <boost/filesystem.hpp>
#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<mongo::Scope> 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<mongo::Scope> 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<mongo::Scope> 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