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:
parent
d5ff440817
commit
5f48de0b74
@ -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
|
||||
|
@ -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 = [];
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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]}));
|
||||
|
@ -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',
|
||||
|
@ -39,13 +39,17 @@
|
||||
#include <js/CompilationAndEvaluation.h>
|
||||
#include <js/ContextOptions.h>
|
||||
#include <js/Initialization.h>
|
||||
#include <js/Modules.h>
|
||||
#include <js/Object.h>
|
||||
#include <js/SourceText.h>
|
||||
#include <js/TypeDecls.h>
|
||||
#include <js/friend/ErrorMessages.h>
|
||||
#include <jsapi.h>
|
||||
#include <jscustomallocator.h>
|
||||
#include <jsfriendapi.h>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#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<int> j
|
||||
_timestampProto(_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 {
|
||||
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<mozilla::Utf8Unit> 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); });
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -30,9 +30,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <jsapi.h>
|
||||
#include <jsfriendapi.h>
|
||||
#include <vm/PosixNSPR.h>
|
||||
|
||||
|
||||
#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 <typename ImplScopeFunction>
|
||||
auto _runSafely(ImplScopeFunction&& functionToRun) -> decltype(functionToRun());
|
||||
@ -437,6 +441,8 @@ private:
|
||||
|
||||
bool _inReportError;
|
||||
|
||||
std::unique_ptr<ModuleLoader> _moduleLoader;
|
||||
|
||||
WrapType<BinDataInfo> _binDataProto;
|
||||
WrapType<BSONInfo> _bsonProto;
|
||||
WrapType<CodeInfo> _codeProto;
|
||||
|
446
src/mongo/scripting/mozjs/module_loader.cpp
Normal file
446
src/mongo/scripting/mozjs/module_loader.cpp
Normal 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
|
84
src/mongo/scripting/mozjs/module_loader.h
Normal file
84
src/mongo/scripting/mozjs/module_loader.h
Normal 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
|
91
src/mongo/scripting/mozjs/module_loader_test.cpp
Normal file
91
src/mongo/scripting/mozjs/module_loader_test.cpp
Normal 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
|
Loading…
Reference in New Issue
Block a user