diff options
author | Jason Carey <jcarey@argv.me> | 2015-07-09 14:05:20 -0400 |
---|---|---|
committer | Jason Carey <jcarey@argv.me> | 2015-07-14 16:15:54 -0400 |
commit | e749ffad9b4fe3110d97f366ebe39e7c9a4edd9d (patch) | |
tree | 927e727645da9bfc80095bb124860e31e58e9d77 /src/mongo/scripting | |
parent | 1af5f44f9ba2b7cff8e0457798b7a25b64e9fe69 (diff) | |
download | mongo-e749ffad9b4fe3110d97f366ebe39e7c9a4edd9d.tar.gz |
SERVER-18531 Integrate SpiderMonkey
Provides SpiderMonkey 38.0.1esr as a JS engine for mongo and mongod.
Diffstat (limited to 'src/mongo/scripting')
70 files changed, 9066 insertions, 5 deletions
diff --git a/src/mongo/scripting/SConscript b/src/mongo/scripting/SConscript index f05572b98c8..f3c3d9a05fd 100644 --- a/src/mongo/scripting/SConscript +++ b/src/mongo/scripting/SConscript @@ -4,6 +4,7 @@ Import([ 'env', 'serverJs', 'usev8', + 'usemozjs', 'v8suffix', ]) @@ -68,6 +69,79 @@ if usev8: '$BUILD_DIR/mongo/db/service_context', ], ) +elif usemozjs: + scriptingEnv = env.Clone() + scriptingEnv.InjectThirdPartyIncludePaths(libraries=['mozjs']) + + # TODO: get rid of all of this /FI and -include stuff and migrate to a shim + # header we include in all of our files. + if env.TargetOSIs('windows'): + scriptingEnv.Append(CCFLAGS=[ + '/FI', 'js-config.h', + '/FI', 'js/RequiredDefines.h', + ]) + else: + scriptingEnv.Append( + CCFLAGS=[ + '-include', 'js-config.h', + '-include', 'js/RequiredDefines.h', + '-Wno-invalid-offsetof', + ], + CXXFLAGS=[ + '-Wno-non-virtual-dtor', + ], + ) + + scriptingEnv.Prepend(CPPDEFINES=[ + 'JS_USE_CUSTOM_ALLOCATOR', + 'STATIC_JS_API=1', + ]) + + scriptingEnv.Library( + target='scripting', + source=[ + 'mozjs/base.cpp', + 'mozjs/bindata.cpp', + 'mozjs/bson.cpp', + 'mozjs/countdownlatch.cpp', + 'mozjs/cursor.cpp', + 'mozjs/dbcollection.cpp', + 'mozjs/db.cpp', + 'mozjs/dbpointer.cpp', + 'mozjs/dbquery.cpp', + 'mozjs/dbref.cpp', + 'mozjs/engine.cpp', + 'mozjs/exception.cpp', + 'mozjs/global.cpp', + 'mozjs/idwrapper.cpp', + 'mozjs/implscope.cpp', + 'mozjs/jscustomallocator.cpp', + 'mozjs/jsstringwrapper.cpp', + 'mozjs/jsthread.cpp', + 'mozjs/maxkey.cpp', + 'mozjs/minkey.cpp', + 'mozjs/mongo.cpp', + 'mozjs/nativefunction.cpp', + 'mozjs/numberint.cpp', + 'mozjs/numberlong.cpp', + 'mozjs/object.cpp', + 'mozjs/objectwrapper.cpp', + 'mozjs/oid.cpp', + 'mozjs/PosixNSPR.cpp', + 'mozjs/proxyscope.cpp', + 'mozjs/regexp.cpp', + 'mozjs/timestamp.cpp', + 'mozjs/valuereader.cpp', + 'mozjs/valuewriter.cpp', + ], + LIBDEPS=[ + 'bson_template_evaluator', + 'scripting_common', + '$BUILD_DIR/third_party/shim_mozjs', + '$BUILD_DIR/mongo/shell/mongojs', + '$BUILD_DIR/mongo/db/service_context', + ], + ) else: env.Library( target='scripting', @@ -88,9 +162,9 @@ env.Library( ) env.CppUnitTest( - target='v8_deadline_monitor_test', + target='deadline_monitor_test', source=[ - 'v8_deadline_monitor_test.cpp', + 'deadline_monitor_test.cpp', ], LIBDEPS=[ ], diff --git a/src/mongo/scripting/v8_deadline_monitor.h b/src/mongo/scripting/deadline_monitor.h index 89f7e9d1b84..89f7e9d1b84 100644 --- a/src/mongo/scripting/v8_deadline_monitor.h +++ b/src/mongo/scripting/deadline_monitor.h diff --git a/src/mongo/scripting/v8_deadline_monitor_test.cpp b/src/mongo/scripting/deadline_monitor_test.cpp index 702160cd1ed..98f4008b234 100644 --- a/src/mongo/scripting/v8_deadline_monitor_test.cpp +++ b/src/mongo/scripting/deadline_monitor_test.cpp @@ -30,7 +30,7 @@ #include "mongo/platform/basic.h" -#include "mongo/scripting/v8_deadline_monitor.h" +#include "mongo/scripting/deadline_monitor.h" #include "mongo/unittest/unittest.h" diff --git a/src/mongo/scripting/engine_v8-3.25.h b/src/mongo/scripting/engine_v8-3.25.h index f3e594c33ee..0865ab59ac3 100644 --- a/src/mongo/scripting/engine_v8-3.25.h +++ b/src/mongo/scripting/engine_v8-3.25.h @@ -40,7 +40,7 @@ #include "mongo/client/dbclientcursor.h" #include "mongo/platform/unordered_map.h" #include "mongo/scripting/engine.h" -#include "mongo/scripting/v8_deadline_monitor.h" +#include "mongo/scripting/deadline_monitor.h" #include "mongo/scripting/v8-3.25_profiler.h" /** diff --git a/src/mongo/scripting/engine_v8.h b/src/mongo/scripting/engine_v8.h index 8825580c987..d70439c45ff 100644 --- a/src/mongo/scripting/engine_v8.h +++ b/src/mongo/scripting/engine_v8.h @@ -38,7 +38,7 @@ #include "mongo/client/dbclientcursor.h" #include "mongo/platform/unordered_map.h" #include "mongo/scripting/engine.h" -#include "mongo/scripting/v8_deadline_monitor.h" +#include "mongo/scripting/deadline_monitor.h" #include "mongo/scripting/v8_profiler.h" /** diff --git a/src/mongo/scripting/mozjs/PosixNSPR.cpp b/src/mongo/scripting/mozjs/PosixNSPR.cpp new file mode 100644 index 00000000000..2e6bbe93fe0 --- /dev/null +++ b/src/mongo/scripting/mozjs/PosixNSPR.cpp @@ -0,0 +1,259 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file was copied out of the firefox 38.0.1esr source tree from + * js/src/vm/PosixNSPR.cpp and modified to use the MongoDB threading + * primitives. + * + * The point of this file is to shim the posix emulation of nspr that Mozilla + * ships with firefox. We force configuration such that the SpiderMonkey build + * looks for these symbols and we provide them from within our object code + * rather than attempting to build it in there's so we can take advantage of + * the cross platform abstractions that we rely upon. + */ + +#include "mongo/platform/basic.h" + +#include <array> +#include <js/Utility.h> +#include <vm/PosixNSPR.h> + +#include "mongo/stdx/chrono.h" +#include "mongo/stdx/condition_variable.h" +#include "mongo/stdx/mutex.h" +#include "mongo/stdx/thread.h" +#include "mongo/util/concurrency/thread_name.h" +#include "mongo/util/concurrency/threadlocal.h" + +class nspr::Thread { + mongo::stdx::thread thread_; + void (*start)(void* arg); + void* arg; + bool joinable; + +public: + Thread(void (*start)(void* arg), void* arg, bool joinable) + : start(start), arg(arg), joinable(joinable) {} + + static void* ThreadRoutine(void* arg); + + mongo::stdx::thread& thread() { + return thread_; + } +}; + +namespace { +MONGO_TRIVIALLY_CONSTRUCTIBLE_THREAD_LOCAL nspr::Thread* kCurrentThread; +} // namespace + +void* nspr::Thread::ThreadRoutine(void* arg) { + Thread* self = static_cast<Thread*>(arg); + kCurrentThread = self; + self->start(self->arg); + if (!self->joinable) + js_delete(self); + return nullptr; +} + +PRThread* PR_CreateThread(PRThreadType type, + void (*start)(void* arg), + void* arg, + PRThreadPriority priority, + PRThreadScope scope, + PRThreadState state, + uint32_t stackSize) { + MOZ_ASSERT(type == PR_USER_THREAD); + MOZ_ASSERT(priority == PR_PRIORITY_NORMAL); + + try { + std::unique_ptr<nspr::Thread, void (*)(nspr::Thread*)> t( + js_new<nspr::Thread>(start, arg, state != PR_UNJOINABLE_THREAD), + js_delete<nspr::Thread>); + + t->thread() = mongo::stdx::thread(&nspr::Thread::ThreadRoutine, t.get()); + + if (state == PR_UNJOINABLE_THREAD) { + t->thread().detach(); + } + + return t.release(); + } catch (...) { + return nullptr; + } +} + +PRStatus PR_JoinThread(PRThread* thread) { + try { + thread->thread().join(); + + js_delete(thread); + + return PR_SUCCESS; + } catch (...) { + return PR_FAILURE; + } +} + +PRThread* PR_GetCurrentThread() { + return kCurrentThread; +} + +PRStatus PR_SetCurrentThreadName(const char* name) { + mongo::setThreadName(name); + + return PR_SUCCESS; +} + +static const size_t MaxTLSKeyCount = 32; +static size_t gTLSKeyCount; +namespace { +MONGO_TRIVIALLY_CONSTRUCTIBLE_THREAD_LOCAL std::array<void*, MaxTLSKeyCount> gTLSArray; + +} // namespace + +PRStatus PR_NewThreadPrivateIndex(unsigned* newIndex, PRThreadPrivateDTOR destructor) { + /* + * We only call PR_NewThreadPrivateIndex from the main thread, so there's no + * need to lock the table of TLS keys. + */ + MOZ_ASSERT(gTLSKeyCount + 1 < MaxTLSKeyCount); + + *newIndex = gTLSKeyCount; + gTLSKeyCount++; + + return PR_SUCCESS; +} + +PRStatus PR_SetThreadPrivate(unsigned index, void* priv) { + if (index >= gTLSKeyCount) + return PR_FAILURE; + + gTLSArray[index] = priv; + + return PR_SUCCESS; +} + +void* PR_GetThreadPrivate(unsigned index) { + if (index >= gTLSKeyCount) + return nullptr; + + return gTLSArray[index]; +} + +PRStatus PR_CallOnce(PRCallOnceType* once, PRCallOnceFN func) { + MOZ_CRASH("PR_CallOnce unimplemented"); +} + +PRStatus PR_CallOnceWithArg(PRCallOnceType* once, PRCallOnceWithArgFN func, void* arg) { + MOZ_CRASH("PR_CallOnceWithArg unimplemented"); +} + +class nspr::Lock { + mongo::stdx::mutex mutex_; + +public: + Lock() {} + mongo::stdx::mutex& mutex() { + return mutex_; + } +}; + +PRLock* PR_NewLock() { + return js_new<nspr::Lock>(); +} + +void PR_DestroyLock(PRLock* lock) { + js_delete(lock); +} + +void PR_Lock(PRLock* lock) { + lock->mutex().lock(); +} + +PRStatus PR_Unlock(PRLock* lock) { + lock->mutex().unlock(); + + return PR_SUCCESS; +} + +class nspr::CondVar { + mongo::stdx::condition_variable cond_; + nspr::Lock* lock_; + +public: + CondVar(nspr::Lock* lock) : lock_(lock) {} + mongo::stdx::condition_variable& cond() { + return cond_; + } + nspr::Lock* lock() { + return lock_; + } +}; + +PRCondVar* PR_NewCondVar(PRLock* lock) { + return js_new<nspr::CondVar>(lock); +} + +void PR_DestroyCondVar(PRCondVar* cvar) { + js_delete(cvar); +} + +PRStatus PR_NotifyCondVar(PRCondVar* cvar) { + cvar->cond().notify_one(); + + return PR_SUCCESS; +} + +PRStatus PR_NotifyAllCondVar(PRCondVar* cvar) { + cvar->cond().notify_all(); + + return PR_SUCCESS; +} + +uint32_t PR_MillisecondsToInterval(uint32_t milli) { + return milli; +} + +uint32_t PR_MicrosecondsToInterval(uint32_t micro) { + return (micro + 999) / 1000; +} + +static const uint64_t TicksPerSecond = 1000; +static const uint64_t NanoSecondsInSeconds = 1000000000; +static const uint64_t MicroSecondsInSeconds = 1000000; + +uint32_t PR_TicksPerSecond() { + return TicksPerSecond; +} + +PRStatus PR_WaitCondVar(PRCondVar* cvar, uint32_t timeout) { + if (timeout == PR_INTERVAL_NO_TIMEOUT) { + try { + mongo::stdx::unique_lock<mongo::stdx::mutex> lk(cvar->lock()->mutex(), + mongo::stdx::adopt_lock_t()); + + cvar->cond().wait(lk); + lk.release(); + + return PR_SUCCESS; + } catch (...) { + return PR_FAILURE; + } + } else { + try { + mongo::stdx::unique_lock<mongo::stdx::mutex> lk(cvar->lock()->mutex(), + mongo::stdx::adopt_lock_t()); + + cvar->cond().wait_for(lk, mongo::stdx::chrono::microseconds(timeout)); + lk.release(); + + return PR_SUCCESS; + } catch (...) { + return PR_FAILURE; + } + } +} diff --git a/src/mongo/scripting/mozjs/base.cpp b/src/mongo/scripting/mozjs/base.cpp new file mode 100644 index 00000000000..b85229ae354 --- /dev/null +++ b/src/mongo/scripting/mozjs/base.cpp @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/base.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec* BaseInfo::freeFunctions = nullptr; +const JSFunctionSpec* BaseInfo::methods = nullptr; + +const char* const BaseInfo::inheritFrom = nullptr; + +void BaseInfo::addProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue v) {} +void BaseInfo::call(JSContext* cx, JS::CallArgs args) {} +void BaseInfo::construct(JSContext* cx, JS::CallArgs args) {} +void BaseInfo::convert(JSContext* cx, + JS::HandleObject obj, + JSType type, + JS::MutableHandleValue vp) {} +void BaseInfo::delProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* succeeded) {} +void BaseInfo::enumerate(JSContext* cx, JS::HandleObject obj, JS::AutoIdVector& properties) {} +void BaseInfo::finalize(JSFreeOp* fop, JSObject* obj) {} +void BaseInfo::getProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue vp) {} +void BaseInfo::hasInstance(JSContext* cx, + JS::HandleObject obj, + JS::MutableHandleValue vp, + bool* bp) {} +void BaseInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto) {} +void BaseInfo::resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* resolvedp) {} +void BaseInfo::setProperty( + JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool strict, JS::MutableHandleValue vp) {} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/base.h b/src/mongo/scripting/mozjs/base.h new file mode 100644 index 00000000000..fe90ea65b97 --- /dev/null +++ b/src/mongo/scripting/mozjs/base.h @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <jsapi.h> + +namespace mongo { +namespace mozjs { + +/** + * InstallType represents how we want this type overlayed in the JS world. + * + * Global is for regular types that get installed into the global scope + * Private gives us the type, but doesn't make the prototype publicly available + * OverNative is used to attach functionality to prototypes that are already there. + */ +enum class InstallType : char { + Global = 0, + Private, + OverNative, +}; + +/** + * The Base object for all info types + * + * It's difficult to access the array types correctly in a non constexpr world, + * so we just stash some nullptrs that are universally available. + */ +struct BaseInfo { + static const char* const inheritFrom; + static const InstallType installType = InstallType::Global; + static const JSFunctionSpec* freeFunctions; + static const JSFunctionSpec* methods; + static const unsigned classFlags = 0; + static void addProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue v); + static void call(JSContext* cx, JS::CallArgs args); + static void construct(JSContext* cx, JS::CallArgs args); + static void convert(JSContext* cx, + JS::HandleObject obj, + JSType type, + JS::MutableHandleValue vp); + static void delProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* succeeded); + static void enumerate(JSContext* cx, JS::HandleObject obj, JS::AutoIdVector& properties); + static void finalize(JSFreeOp* fop, JSObject* obj); + static void getProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue vp); + static void hasInstance(JSContext* cx, + JS::HandleObject obj, + JS::MutableHandleValue vp, + bool* bp); + static void postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto); + static void resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* resolvedp); + static void setProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + bool strict, + JS::MutableHandleValue vp); +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/bindata.cpp b/src/mongo/scripting/mozjs/bindata.cpp new file mode 100644 index 00000000000..10a3045f103 --- /dev/null +++ b/src/mongo/scripting/mozjs/bindata.cpp @@ -0,0 +1,216 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/bindata.h" + +#include <iomanip> + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/util/base64.h" +#include "mongo/util/hex.h" +#include "mongo/util/mongoutils/str.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec BinDataInfo::methods[4] = { + MONGO_ATTACH_JS_FUNCTION(base64), + MONGO_ATTACH_JS_FUNCTION(hex), + MONGO_ATTACH_JS_FUNCTION(toString), + JS_FS_END, +}; + +const JSFunctionSpec BinDataInfo::freeFunctions[4] = { + MONGO_ATTACH_JS_FUNCTION_WITH_FLAGS(HexData, JSFUN_CONSTRUCTOR), + MONGO_ATTACH_JS_FUNCTION_WITH_FLAGS(MD5, JSFUN_CONSTRUCTOR), + MONGO_ATTACH_JS_FUNCTION_WITH_FLAGS(UUID, JSFUN_CONSTRUCTOR), + JS_FS_END, +}; + +const char* const BinDataInfo::className = "BinData"; + +namespace { + +void hexToBinData(JSContext* cx, int type, StringData hexstr, JS::MutableHandleValue out) { + auto scope = getScope(cx); + + // SERVER-9686: This function does not correctly check to make sure hexstr is actually made + // up of valid hex digits, and fails in the hex utility functions + + int len = hexstr.size() / 2; + std::unique_ptr<char[]> data(new char[len]); + const char* src = hexstr.rawData(); + for (int i = 0; i < len; i++) { + data[i] = fromHex(src + i * 2); + } + + std::string encoded = base64::encode(data.get(), len); + JS::AutoValueArray<2> args(cx); + + args[0].setInt32(type); + ValueReader(cx, args[1]).fromStringData(encoded); + return scope->getBinDataProto().newInstance(args, out); +} + +std::string* getEncoded(JS::HandleValue thisv) { + return static_cast<std::string*>(JS_GetPrivate(thisv.toObjectOrNull())); +} + +std::string* getEncoded(JSObject* thisv) { + return static_cast<std::string*>(JS_GetPrivate(thisv)); +} + +} // namespace + +void BinDataInfo::finalize(JSFreeOp* fop, JSObject* obj) { + auto str = getEncoded(obj); + + if (str) { + delete str; + } +} + +void BinDataInfo::Functions::UUID(JSContext* cx, JS::CallArgs args) { + if (args.length() != 1) + uasserted(ErrorCodes::BadValue, "UUID needs 1 argument"); + + auto str = ValueWriter(cx, args.get(0)).toString(); + + if (str.length() != 32) + uasserted(ErrorCodes::BadValue, "UUID string must have 32 characters"); + + hexToBinData(cx, bdtUUID, str, args.rval()); +} + +void BinDataInfo::Functions::MD5(JSContext* cx, JS::CallArgs args) { + if (args.length() != 1) + uasserted(ErrorCodes::BadValue, "MD5 needs 1 argument"); + + auto str = ValueWriter(cx, args.get(0)).toString(); + + if (str.length() != 32) + uasserted(ErrorCodes::BadValue, "MD5 string must have 32 characters"); + + hexToBinData(cx, MD5Type, str, args.rval()); +} + +void BinDataInfo::Functions::HexData(JSContext* cx, JS::CallArgs args) { + if (args.length() != 2) + uasserted(ErrorCodes::BadValue, "HexData needs 2 arguments"); + + JS::RootedValue type(cx, args.get(0)); + + if (!type.isNumber() || type.toInt32() < 0 || type.toInt32() > 255) + uasserted(ErrorCodes::BadValue, + "HexData subtype must be a Number between 0 and 255 inclusive"); + + auto str = ValueWriter(cx, args.get(1)).toString(); + + hexToBinData(cx, type.toInt32(), str, args.rval()); +} + +void BinDataInfo::Functions::toString(JSContext* cx, JS::CallArgs args) { + ObjectWrapper o(cx, args.thisv()); + + auto str = getEncoded(args.thisv()); + + str::stream ss; + + ss << "BinData(" << o.getNumber("type") << ",\"" << *str << "\")"; + + ValueReader(cx, args.rval()).fromStringData(ss.operator std::string()); +} + +void BinDataInfo::Functions::base64(JSContext* cx, JS::CallArgs args) { + auto str = getEncoded(args.thisv()); + + ValueReader(cx, args.rval()).fromStringData(*str); +} + +void BinDataInfo::Functions::hex(JSContext* cx, JS::CallArgs args) { + auto str = getEncoded(args.thisv()); + + std::string data = base64::decode(*str); + std::stringstream ss; + ss.setf(std::ios_base::hex, std::ios_base::basefield); + ss.fill('0'); + ss.setf(std::ios_base::right, std::ios_base::adjustfield); + for (auto it = data.begin(); it != data.end(); ++it) { + unsigned v = (unsigned char)*it; + ss << std::setw(2) << v; + } + + ValueReader(cx, args.rval()).fromStringData(ss.str()); +} + +void BinDataInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + if (args.length() != 2) { + uasserted(ErrorCodes::BadValue, "BinData takes 2 arguments -- BinData(subtype,data)"); + } + + auto type = args.get(0); + + if (!type.isNumber() || type.toInt32() < 0 || type.toInt32() > 255) { + uasserted(ErrorCodes::BadValue, + "BinData subtype must be a Number between 0 and 255 inclusive"); + } + + auto utf = args.get(1); + + if (!utf.isString()) { + uasserted(ErrorCodes::BadValue, "BinData data must be a String"); + } + + auto str = ValueWriter(cx, utf).toString(); + + auto tmpBase64 = base64::decode(str); + + JS::RootedObject thisv(cx); + scope->getBinDataProto().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + JS::RootedValue len(cx); + len.setInt32(tmpBase64.length()); + + o.defineProperty("len", len, JSPROP_READONLY); + o.defineProperty("type", type, JSPROP_READONLY); + + JS_SetPrivate(thisv, new std::string(std::move(str))); + + args.rval().setObjectOrNull(thisv); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/bindata.h b/src/mongo/scripting/mozjs/bindata.h new file mode 100644 index 00000000000..85b504230ff --- /dev/null +++ b/src/mongo/scripting/mozjs/bindata.h @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * Wrapper for the BinData bson type + * + * It offers some simple methods and a handful of specialized constructors + */ +struct BinDataInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + static void finalize(JSFreeOp* fop, JSObject* obj); + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(base64); + MONGO_DEFINE_JS_FUNCTION(hex); + MONGO_DEFINE_JS_FUNCTION(toString); + + MONGO_DEFINE_JS_FUNCTION(HexData); + MONGO_DEFINE_JS_FUNCTION(MD5); + MONGO_DEFINE_JS_FUNCTION(UUID); + }; + + static const JSFunctionSpec methods[4]; + static const JSFunctionSpec freeFunctions[4]; + + static const char* const className; + static const unsigned classFlags = JSCLASS_HAS_PRIVATE; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/bson.cpp b/src/mongo/scripting/mozjs/bson.cpp new file mode 100644 index 00000000000..308e29d90f3 --- /dev/null +++ b/src/mongo/scripting/mozjs/bson.cpp @@ -0,0 +1,235 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/bson.h" + +#include <set> + +#include "mongo/scripting/mozjs/idwrapper.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" + +namespace mongo { +namespace mozjs { + +const char* const BSONInfo::className = "BSON"; + +const JSFunctionSpec BSONInfo::freeFunctions[2] = { + MONGO_ATTACH_JS_FUNCTION(bsonWoCompare), JS_FS_END, +}; + +namespace { + +/** + * Holder for bson objects which tracks state for the js wrapper + * + * Basically, we have read only and read/write variants, and a need to manage + * the appearance of mutable state on the read/write versions. + */ +struct BSONHolder { + BSONHolder(const BSONObj& obj, bool ro) + : _obj(obj.getOwned()), _resolved(false), _readOnly(ro), _altered(false) {} + + BSONObj _obj; + bool _resolved; + bool _readOnly; + bool _altered; + std::set<std::string> _removed; +}; + +BSONHolder* getHolder(JSObject* obj) { + return static_cast<BSONHolder*>(JS_GetPrivate(obj)); +} + +} // namespace + +void BSONInfo::make(JSContext* cx, JS::MutableHandleObject obj, BSONObj bson, bool ro) { + auto scope = getScope(cx); + + scope->getBsonProto().newInstance(obj); + JS_SetPrivate(obj, new BSONHolder(bson, ro)); +} + +void BSONInfo::finalize(JSFreeOp* fop, JSObject* obj) { + auto holder = getHolder(obj); + + if (!holder) + return; + + delete holder; +} + +void BSONInfo::enumerate(JSContext* cx, JS::HandleObject obj, JS::AutoIdVector& properties) { + auto holder = getHolder(obj); + + if (!holder) + return; + + BSONObjIterator i(holder->_obj); + + ObjectWrapper o(cx, obj); + JS::RootedValue val(cx); + JS::RootedId id(cx); + + while (i.more()) { + BSONElement e = i.next(); + + // TODO: when we get heterogenous set lookup, switch to StringData + // rather than involving the temporary string + if (holder->_removed.count(e.fieldName())) + continue; + + ValueReader(cx, &val).fromStringData(e.fieldNameStringData()); + + if (!JS_ValueToId(cx, val, &id)) + uasserted(ErrorCodes::JSInterpreterFailure, "Failed to invoke JS_ValueToId"); + + properties.append(id); + } +} + +void BSONInfo::setProperty( + JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool strict, JS::MutableHandleValue vp) { + auto holder = getHolder(obj); + + if (holder) { + if (holder->_readOnly) { + uasserted(ErrorCodes::BadValue, "Read only object"); + } + + auto iter = holder->_removed.find(IdWrapper(cx, id).toString()); + + if (iter != holder->_removed.end()) { + holder->_removed.erase(iter); + } + + holder->_altered = true; + } + + ObjectWrapper(cx, obj).defineProperty(id, vp, JSPROP_ENUMERATE); +} + +void BSONInfo::delProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* succeeded) { + auto holder = getHolder(obj); + + if (holder) { + if (holder->_readOnly) { + uasserted(ErrorCodes::BadValue, "Read only object"); + } + + holder->_altered = true; + + holder->_removed.insert(IdWrapper(cx, id).toString()); + } + + *succeeded = true; +} + +void BSONInfo::resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* resolvedp) { + auto holder = getHolder(obj); + + *resolvedp = false; + + if (!holder) { + return; + } + + IdWrapper idw(cx, id); + + if (!holder->_readOnly && holder->_removed.count(idw.toString())) { + return; + } + + ObjectWrapper o(cx, obj); + + std::string sname = IdWrapper(cx, id).toString(); + + if (holder->_obj.hasField(sname)) { + auto elem = holder->_obj[sname]; + + JS::RootedValue vp(cx); + + ValueReader(cx, &vp).fromBSONElement(elem, holder->_readOnly); + + o.defineProperty(id, vp, JSPROP_ENUMERATE); + + if (!holder->_readOnly && (elem.type() == mongo::Object || elem.type() == mongo::Array)) { + // if accessing a subobject, we have no way to know if + // modifications are being made on writable objects + + holder->_altered = true; + } + + *resolvedp = true; + } +} + +void BSONInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + scope->getBsonProto().newObject(args.rval()); +} + +std::tuple<BSONObj*, bool> BSONInfo::originalBSON(JSContext* cx, JS::HandleObject obj) { + std::tuple<BSONObj*, bool> out(nullptr, false); + + if (auto holder = getHolder(obj)) + out = std::make_tuple(&holder->_obj, holder->_altered); + + return out; +} + +void BSONInfo::Functions::bsonWoCompare(JSContext* cx, JS::CallArgs args) { + if (args.length() != 2) + uasserted(ErrorCodes::BadValue, "bsonWoCompare needs 2 argument"); + + if (!args.get(0).isObject()) + uasserted(ErrorCodes::BadValue, "first argument to bsonWoCompare must be an object"); + + if (!args.get(1).isObject()) + uasserted(ErrorCodes::BadValue, "second argument to bsonWoCompare must be an object"); + + BSONObj firstObject = ValueWriter(cx, args.get(0)).toBSON(); + BSONObj secondObject = ValueWriter(cx, args.get(1)).toBSON(); + + args.rval().setInt32(firstObject.woCompare(secondObject)); +} + +void BSONInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto) { + JS::RootedValue value(cx); + value.setBoolean(true); + + ObjectWrapper(cx, proto).defineProperty("_bson", value, 0); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/bson.h b/src/mongo/scripting/mozjs/bson.h new file mode 100644 index 00000000000..bcf57b5eda4 --- /dev/null +++ b/src/mongo/scripting/mozjs/bson.h @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <tuple> + +#include "mongo/db/jsobj.h" +#include "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * Provides a wrapper for BSONObj's in JS. The main idea here is that BSONObj's + * can be read only, or read write when we shim them in, and in all cases we + * lazily load their member elements into JS. So a bunch of these lifecycle + * methods are set up to wrap field access (enumerate, resolve and + * del/setProperty). + * + * Note that installType is private. So you can only get BSON types in JS via + * ::make() from C++. + */ +struct BSONInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + static void delProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* succeeded); + static void enumerate(JSContext* cx, JS::HandleObject obj, JS::AutoIdVector& properties); + static void finalize(JSFreeOp* fop, JSObject* obj); + static void resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* resolvedp); + static void setProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + bool strict, + JS::MutableHandleValue vp); + + static const char* const className; + static const unsigned classFlags = JSCLASS_HAS_PRIVATE; + static const InstallType installType = InstallType::Private; + static void postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto); + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(bsonWoCompare); + }; + + static const JSFunctionSpec freeFunctions[2]; + + static std::tuple<BSONObj*, bool> originalBSON(JSContext* cx, JS::HandleObject obj); + static void make(JSContext* cx, JS::MutableHandleObject obj, BSONObj bson, bool ro); +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/countdownlatch.cpp b/src/mongo/scripting/mozjs/countdownlatch.cpp new file mode 100644 index 00000000000..bf7ae4ff551 --- /dev/null +++ b/src/mongo/scripting/mozjs/countdownlatch.cpp @@ -0,0 +1,198 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/countdownlatch.h" + +#include <unordered_map> + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/stdx/condition_variable.h" +#include "mongo/stdx/mutex.h" + +namespace mongo { +namespace mozjs { + +const char* const CountDownLatchInfo::className = "CountDownLatch"; + +const JSFunctionSpec CountDownLatchInfo::methods[5] = { + MONGO_ATTACH_JS_FUNCTION(_new), + MONGO_ATTACH_JS_FUNCTION(_await), + MONGO_ATTACH_JS_FUNCTION(_countDown), + MONGO_ATTACH_JS_FUNCTION(_getCount), + JS_FS_END, +}; + +/** + * The global CountDownLatch holder. + * + * Provides an interface for communicating between JSThread's + */ +class CountDownLatchHolder { +public: + CountDownLatchHolder() : _counter(0) {} + + int32_t make(int32_t count) { + uassert(ErrorCodes::JSInterpreterFailure, "argument must be >= 0", count >= 0); + stdx::lock_guard<stdx::mutex> lock(_mutex); + + int32_t desc = ++_counter; + _latches.insert(std::make_pair(desc, std::make_shared<Latch>(count))); + + return desc; + } + + void await(int32_t desc) { + std::shared_ptr<Latch> latch = get(desc); + stdx::unique_lock<stdx::mutex> lock(latch->mutex); + + while (latch->count != 0) { + latch->cv.wait(lock); + } + } + + void countDown(int32_t desc) { + std::shared_ptr<Latch> latch = get(desc); + stdx::unique_lock<stdx::mutex> lock(latch->mutex); + + if (latch->count > 0) + latch->count--; + + if (latch->count == 0) + latch->cv.notify_all(); + } + + int32_t getCount(int32_t desc) { + std::shared_ptr<Latch> latch = get(desc); + stdx::unique_lock<stdx::mutex> lock(latch->mutex); + + return latch->count; + } + +private: + /** + * Latches for communication between threads + */ + struct Latch { + Latch(int32_t count) : count(count) {} + + stdx::mutex mutex; + stdx::condition_variable cv; + int32_t count; + }; + + std::shared_ptr<Latch> get(int32_t desc) { + stdx::lock_guard<stdx::mutex> lock(_mutex); + + auto iter = _latches.find(desc); + uassert(ErrorCodes::JSInterpreterFailure, + "not a valid CountDownLatch descriptor", + iter != _latches.end()); + + return iter->second; + } + + using Map = std::unordered_map<int32_t, std::shared_ptr<Latch>>; + + stdx::mutex _mutex; + Map _latches; + int32_t _counter; +}; + +namespace { +CountDownLatchHolder globalCountDownLatchHolder; +} // namespace + +void CountDownLatchInfo::Functions::_new(JSContext* cx, JS::CallArgs args) { + uassert(ErrorCodes::JSInterpreterFailure, "need exactly one argument", args.length() == 1); + uassert( + ErrorCodes::JSInterpreterFailure, "argument must be an integer", args.get(0).isNumber()); + + args.rval().setInt32(globalCountDownLatchHolder.make(args.get(0).toNumber())); +} + +void CountDownLatchInfo::Functions::_await(JSContext* cx, JS::CallArgs args) { + uassert(ErrorCodes::JSInterpreterFailure, "need exactly one argument", args.length() == 1); + uassert( + ErrorCodes::JSInterpreterFailure, "argument must be an integer", args.get(0).isNumber()); + + globalCountDownLatchHolder.await(args.get(0).toNumber()); + + args.rval().setUndefined(); +} + +void CountDownLatchInfo::Functions::_countDown(JSContext* cx, JS::CallArgs args) { + uassert(ErrorCodes::JSInterpreterFailure, "need exactly one argument", args.length() == 1); + uassert( + ErrorCodes::JSInterpreterFailure, "argument must be an integer", args.get(0).isNumber()); + + globalCountDownLatchHolder.countDown(args.get(0).toNumber()); + + args.rval().setUndefined(); +} + +void CountDownLatchInfo::Functions::_getCount(JSContext* cx, JS::CallArgs args) { + uassert(ErrorCodes::JSInterpreterFailure, "need exactly one argument", args.length() == 1); + uassert( + ErrorCodes::JSInterpreterFailure, "argument must be an integer", args.get(0).isNumber()); + + args.rval().setInt32(globalCountDownLatchHolder.getCount(args.get(0).toNumber())); +} + +/** + * We have to do this odd dance here because we need the methods from + * CountDownLatch to be installed in a plain object as enumerable properties. + * This is due to the way CountDownLatch is invoked, specifically after being + * transmitted across our js fork(). So we can't inherit and can't rely on the + * type. Practically, we also end up wrapping up all of these functions in pure + * js variants that call down, which makes them bson <-> js safe. + */ +void CountDownLatchInfo::postInstall(JSContext* cx, + JS::HandleObject global, + JS::HandleObject proto) { + auto objPtr = JS_NewPlainObject(cx); + uassert(ErrorCodes::JSInterpreterFailure, "Failed to JS_NewPlainObject", objPtr); + + JS::RootedObject obj(cx, objPtr); + ObjectWrapper objWrapper(cx, obj); + ObjectWrapper protoWrapper(cx, proto); + + JS::RootedValue val(cx); + for (auto iter = methods; iter->name; ++iter) { + protoWrapper.getValue(iter->name, &val); + objWrapper.setValue(iter->name, val); + } + + val.setObjectOrNull(obj); + ObjectWrapper(cx, global).setValue("CountDownLatch", val); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/countdownlatch.h b/src/mongo/scripting/mozjs/countdownlatch.h new file mode 100644 index 00000000000..07001ebf473 --- /dev/null +++ b/src/mongo/scripting/mozjs/countdownlatch.h @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "CountDownLatch" javascript object. + * + * Installs a global "CountDownLatch" object with associated methods. + * + * Note that there is only one instance of this class and it is used to + * communicate between different C++ threads. + */ +struct CountDownLatchInfo : public BaseInfo { + struct Functions { + MONGO_DEFINE_JS_FUNCTION(_new); + MONGO_DEFINE_JS_FUNCTION(_await); + MONGO_DEFINE_JS_FUNCTION(_countDown); + MONGO_DEFINE_JS_FUNCTION(_getCount); + }; + + static const JSFunctionSpec methods[5]; + + static const char* const className; + static const InstallType installType = InstallType::Private; + + static void postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto); +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/cursor.cpp b/src/mongo/scripting/mozjs/cursor.cpp new file mode 100644 index 00000000000..8f6bb7b540c --- /dev/null +++ b/src/mongo/scripting/mozjs/cursor.cpp @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/cursor.h" + +#include "mongo/scripting/mozjs/bson.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec CursorInfo::methods[5] = { + MONGO_ATTACH_JS_FUNCTION(hasNext), + MONGO_ATTACH_JS_FUNCTION(next), + MONGO_ATTACH_JS_FUNCTION(objsLeftInBatch), + MONGO_ATTACH_JS_FUNCTION(readOnly), + JS_FS_END, +}; + +const char* const CursorInfo::className = "Cursor"; + +namespace { + +DBClientCursor* getCursor(JSObject* thisv) { + return static_cast<CursorInfo::CursorHolder*>(JS_GetPrivate(thisv))->cursor.get(); +} + +DBClientCursor* getCursor(JS::CallArgs& args) { + return getCursor(args.thisv().toObjectOrNull()); +} + +} // namespace + +void CursorInfo::finalize(JSFreeOp* fop, JSObject* obj) { + auto cursor = static_cast<CursorInfo::CursorHolder*>(JS_GetPrivate(obj)); + + if (cursor) { + delete cursor; + } +} + +void CursorInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + scope->getCursorProto().newObject(args.rval()); +} + +void CursorInfo::Functions::next(JSContext* cx, JS::CallArgs args) { + auto cursor = getCursor(args); + + if (!cursor) { + args.rval().setUndefined(); + return; + } + + ObjectWrapper o(cx, args.thisv()); + + BSONObj bson = cursor->next(); + bool ro = o.hasField("_ro") ? o.getBoolean("_ro") : false; + + ValueReader(cx, args.rval()).fromBSON(bson, ro); +} + +void CursorInfo::Functions::hasNext(JSContext* cx, JS::CallArgs args) { + auto cursor = getCursor(args); + + if (!cursor) { + args.rval().setBoolean(false); + return; + } + + args.rval().setBoolean(cursor->more()); +} + +void CursorInfo::Functions::objsLeftInBatch(JSContext* cx, JS::CallArgs args) { + auto cursor = getCursor(args); + + if (!cursor) { + args.rval().setInt32(0); + return; + } + + args.rval().setInt32(cursor->objsLeftInBatch()); +} + +void CursorInfo::Functions::readOnly(JSContext* cx, JS::CallArgs args) { + ObjectWrapper(cx, args.thisv()).setBoolean("_ro", true); + + args.rval().set(args.thisv()); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/cursor.h b/src/mongo/scripting/mozjs/cursor.h new file mode 100644 index 00000000000..39c8ba56295 --- /dev/null +++ b/src/mongo/scripting/mozjs/cursor.h @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/client/dbclientcursor.h" +#include "mongo/client/dbclientinterface.h" +#include "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * Wraps a DBClientCursor in javascript + * + * Note that the install is private, so this class should only be constructible + * from C++. Current callers are all via the Mongo object. + */ +struct CursorInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + static void finalize(JSFreeOp* fop, JSObject* obj); + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(hasNext); + MONGO_DEFINE_JS_FUNCTION(next); + MONGO_DEFINE_JS_FUNCTION(objsLeftInBatch); + MONGO_DEFINE_JS_FUNCTION(readOnly); + }; + + static const JSFunctionSpec methods[5]; + + static const char* const className; + static const unsigned classFlags = JSCLASS_HAS_PRIVATE; + static const InstallType installType = InstallType::Private; + + /** + * We need this because the DBClientBase can go out of scope before all of + * its children (as in global shutdown). So we have to manage object + * lifetimes in C++ land. + */ + struct CursorHolder { + CursorHolder(std::unique_ptr<DBClientCursor> cursor, std::shared_ptr<DBClientBase> client) + : client(std::move(client)), cursor(std::move(cursor)) {} + + std::shared_ptr<DBClientBase> client; + std::unique_ptr<DBClientCursor> cursor; + }; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/db.cpp b/src/mongo/scripting/mozjs/db.cpp new file mode 100644 index 00000000000..da0197cc293 --- /dev/null +++ b/src/mongo/scripting/mozjs/db.cpp @@ -0,0 +1,139 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/db.h" + +#include "mongo/db/namespace_string.h" +#include "mongo/db/operation_context.h" +#include "mongo/scripting/mozjs/idwrapper.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/s/d_state.h" + +namespace mongo { +namespace mozjs { + +const char* const DBInfo::className = "DB"; + +void DBInfo::getProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue vp) { + JS::RootedObject parent(cx); + if (!JS_GetPrototype(cx, obj, &parent)) + uasserted(ErrorCodes::JSInterpreterFailure, "Couldn't get prototype"); + + auto scope = getScope(cx); + + ObjectWrapper parentWrapper(cx, parent); + + std::string sname = IdWrapper(cx, id).toString(); + + // 2nd look into real values, may be cached collection object + if (!vp.isUndefined()) { + if (vp.isObject()) { + ObjectWrapper o(cx, vp); + + if (o.hasField("_fullName")) { + auto opContext = scope->getOpContext(); + + // need to check every time that the collection did not get sharded + if (opContext && + haveLocalShardingInfo(opContext->getClient(), o.getString("_fullName"))) + uasserted(ErrorCodes::BadValue, "can't use sharded collection from db.eval"); + } + } + + return; + } else if (parentWrapper.hasField(id)) { + parentWrapper.getValue(id, vp); + return; + } else if (sname.length() == 0 || sname[0] == '_') { + // if starts with '_' we dont return collection, one must use getCollection() + return; + } + + // no hit, create new collection + JS::RootedValue getCollection(cx); + parentWrapper.getValue("getCollection", &getCollection); + + if (!(getCollection.isObject() && JS_ObjectIsFunction(cx, getCollection.toObjectOrNull()))) { + uasserted(ErrorCodes::BadValue, "getCollection is not a function"); + } + + JS::AutoValueArray<1> args(cx); + + ValueReader(cx, args[0]).fromStringData(sname); + + JS::RootedValue coll(cx); + ObjectWrapper(cx, obj).callMethod(getCollection, args, &coll); + + uassert(16861, + "getCollection returned something other than a collection", + scope->getDbCollectionProto().instanceOf(coll)); + + // cache collection for reuse, don't enumerate + ObjectWrapper(cx, obj).defineProperty(sname.c_str(), coll, 0); + + vp.set(coll); +} + +void DBInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + if (args.length() != 2) + uasserted(ErrorCodes::BadValue, "db constructor requires 2 arguments"); + + for (unsigned i = 0; i < args.length(); ++i) { + uassert(ErrorCodes::BadValue, + "db initializer called with undefined argument", + !args.get(i).isUndefined()); + } + + JS::RootedObject thisv(cx); + scope->getDbProto().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + o.setValue("_mongo", args.get(0)); + o.setValue("_name", args.get(1)); + + std::string dbName = ValueWriter(cx, args.get(1)).toString(); + + if (!NamespaceString::validDBName(dbName)) + uasserted(ErrorCodes::BadValue, + str::stream() << "[" << dbName << "] is not a valid database name"); + + args.rval().setObjectOrNull(thisv); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/db.h b/src/mongo/scripting/mozjs/db.h new file mode 100644 index 00000000000..c752953236f --- /dev/null +++ b/src/mongo/scripting/mozjs/db.h @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "DB" Javascript object. + * + * This maps to the 'db' global variable you can call db.COLLECTION_NAME.X() on + * in the shell. + * + * Its major magic is in its getProperty() callback, which threads through to + * a getCollection method installed in js + */ +struct DBInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + static void getProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue vp); + + static const char* const className; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/dbcollection.cpp b/src/mongo/scripting/mozjs/dbcollection.cpp new file mode 100644 index 00000000000..fd8bc086676 --- /dev/null +++ b/src/mongo/scripting/mozjs/dbcollection.cpp @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/dbcollection.h" + +#include "mongo/db/namespace_string.h" +#include "mongo/db/operation_context.h" +#include "mongo/scripting/mozjs/bson.h" +#include "mongo/scripting/mozjs/db.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/s/d_state.h" + +namespace mongo { +namespace mozjs { + +const char* const DBCollectionInfo::className = "DBCollection"; + +void DBCollectionInfo::getProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue vp) { + DBInfo::getProperty(cx, obj, id, vp); +} + +void DBCollectionInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + if (args.length() != 4) + uasserted(ErrorCodes::BadValue, "collection constructor requires 4 arguments"); + + for (unsigned i = 0; i < args.length(); ++i) { + uassert(ErrorCodes::BadValue, + "collection constructor called with undefined argument", + !args.get(i).isUndefined()); + } + + JS::RootedObject thisv(cx); + scope->getDbCollectionProto().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + o.setValue("_mongo", args.get(0)); + o.setValue("_db", args.get(1)); + o.setValue("_shortName", args.get(2)); + o.setValue("_fullName", args.get(3)); + + std::string fullName = ValueWriter(cx, args.get(3)).toString(); + + auto context = scope->getOpContext(); + if (context && haveLocalShardingInfo(context->getClient(), fullName)) + uasserted(ErrorCodes::BadValue, "can't use sharded collection from db.eval"); + + args.rval().setObjectOrNull(thisv); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/dbcollection.h b/src/mongo/scripting/mozjs/dbcollection.h new file mode 100644 index 00000000000..34be422dfd4 --- /dev/null +++ b/src/mongo/scripting/mozjs/dbcollection.h @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "DBCollection" Javascript object. + * + * This maps to the object you get after calling db.COLLECTION_NAME on the + * global 'db' object in the shell. + * + * Its major magic is in its getProperty() callback, which threads through to + * a getCollection method installed in js + */ +struct DBCollectionInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + static void getProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue vp); + + static const char* const className; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/dbpointer.cpp b/src/mongo/scripting/mozjs/dbpointer.cpp new file mode 100644 index 00000000000..fbfb8282649 --- /dev/null +++ b/src/mongo/scripting/mozjs/dbpointer.cpp @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/dbpointer.h" + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/util/mongoutils/str.h" + +namespace mongo { +namespace mozjs { + +const char* const DBPointerInfo::className = "DBPointer"; + +void DBPointerInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + if (args.length() != 2) + uasserted(ErrorCodes::BadValue, "DBPointer needs 2 arguments"); + + if (!args.get(0).isString()) + uasserted(ErrorCodes::BadValue, "DBPointer 1st parameter must be a string"); + + if (!scope->getOidProto().instanceOf(args.get(1))) + uasserted(ErrorCodes::BadValue, "DBPointer 2nd parameter must be an ObjectId"); + + JS::RootedObject thisv(cx); + scope->getDbPointerProto().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + o.setValue("ns", args.get(0)); + o.setValue("id", args.get(1)); + + args.rval().setObjectOrNull(thisv); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/dbpointer.h b/src/mongo/scripting/mozjs/dbpointer.h new file mode 100644 index 00000000000..d521eda4681 --- /dev/null +++ b/src/mongo/scripting/mozjs/dbpointer.h @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "DBPointer" Javascript Object + * + * These look like: + * { + * id : OID(), + * ns : String(), + * } + */ +struct DBPointerInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + + static const char* const className; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/dbquery.cpp b/src/mongo/scripting/mozjs/dbquery.cpp new file mode 100644 index 00000000000..e9bddf4b30e --- /dev/null +++ b/src/mongo/scripting/mozjs/dbquery.cpp @@ -0,0 +1,143 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/dbquery.h" + +#include "mongo/scripting/mozjs/idwrapper.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" + +namespace mongo { +namespace mozjs { + +const char* const DBQueryInfo::className = "DBQuery"; + +void DBQueryInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + if (args.length() < 4) + uasserted(ErrorCodes::BadValue, "dbQuery constructor requires at least 4 arguments"); + + JS::RootedObject thisv(cx); + scope->getDbQueryProto().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + o.setValue("_mongo", args.get(0)); + o.setValue("_db", args.get(1)); + o.setValue("_collection", args.get(2)); + o.setValue("_ns", args.get(3)); + + JS::RootedObject emptyObj(cx); + JS::RootedValue emptyObjVal(cx); + emptyObjVal.setObjectOrNull(emptyObj); + + JS::RootedValue nullVal(cx); + nullVal.setNull(); + + if (args.length() > 4 && args.get(4).isObject()) { + o.setValue("_query", args.get(4)); + } else { + o.setValue("_query", emptyObjVal); + } + + if (args.length() > 5 && args.get(5).isObject()) { + o.setValue("_fields", args.get(5)); + } else { + o.setValue("_fields", nullVal); + } + + if (args.length() > 6 && args.get(6).isNumber()) { + o.setValue("_limit", args.get(6)); + } else { + o.setNumber("_limit", 0); + } + + if (args.length() > 7 && args.get(7).isNumber()) { + o.setValue("_skip", args.get(7)); + } else { + o.setNumber("_skip", 0); + } + + if (args.length() > 8 && args.get(8).isNumber()) { + o.setValue("_batchSize", args.get(8)); + } else { + o.setNumber("_batchSize", 0); + } + + if (args.length() > 9 && args.get(9).isNumber()) { + o.setValue("_options", args.get(9)); + } else { + o.setNumber("_options", 0); + } + + o.setValue("_cursor", nullVal); + o.setNumber("_numReturned", 0); + o.setBoolean("_special", false); + + args.rval().setObjectOrNull(thisv); +} + +void DBQueryInfo::getProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue vp) { + if (!vp.isUndefined()) { + return; + } + + IdWrapper wid(cx, id); + + // We only use this for index access + if (!wid.isInt()) { + return; + } + + JS::RootedObject parent(cx); + if (!JS_GetPrototype(cx, obj, &parent)) + uasserted(ErrorCodes::InternalError, "Couldn't get prototype"); + + ObjectWrapper parentWrapper(cx, parent); + + JS::RootedValue arrayAccess(cx); + parentWrapper.getValue("arrayAccess", &arrayAccess); + + if (arrayAccess.isObject() && JS_ObjectIsFunction(cx, arrayAccess.toObjectOrNull())) { + JS::AutoValueArray<1> args(cx); + + args[0].setInt32(wid.toInt32()); + + ObjectWrapper(cx, obj).callMethod(arrayAccess, args, vp); + } else { + uasserted(ErrorCodes::BadValue, "arrayAccess is not a function"); + } +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/dbquery.h b/src/mongo/scripting/mozjs/dbquery.h new file mode 100644 index 00000000000..dc844c3084c --- /dev/null +++ b/src/mongo/scripting/mozjs/dbquery.h @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "DBQuery" Javascript object. + * + * This represents the result of a find() and uses its getProperty() callback + * to shim operator[]. I.e. db.test.find()[4] + */ +struct DBQueryInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + static void getProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue vp); + + static const char* const className; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/dbref.cpp b/src/mongo/scripting/mozjs/dbref.cpp new file mode 100644 index 00000000000..16ad0058084 --- /dev/null +++ b/src/mongo/scripting/mozjs/dbref.cpp @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/dbref.h" + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" + +namespace mongo { +namespace mozjs { + +const char* const DBRefInfo::className = "DBRef"; + +void DBRefInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + if (!(args.length() == 2 || args.length() == 3)) + uasserted(ErrorCodes::BadValue, "DBRef needs 2 or 3 arguments"); + + if (!args.get(0).isString()) + uasserted(ErrorCodes::BadValue, "DBRef 1st parameter must be a string"); + + JS::RootedObject thisv(cx); + scope->getDbRefProto().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + o.setValue("$ref", args.get(0)); + o.setValue("$id", args.get(1)); + + if (args.length() == 3) { + if (!args.get(2).isString()) + uasserted(ErrorCodes::BadValue, "DBRef 3rd parameter must be a string"); + + o.setValue("$db", args.get(2)); + } + + args.rval().setObjectOrNull(thisv); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/dbref.h b/src/mongo/scripting/mozjs/dbref.h new file mode 100644 index 00000000000..e556a61f765 --- /dev/null +++ b/src/mongo/scripting/mozjs/dbref.h @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "DBRef" Javascript object. + * + * These look like: + * { + * $ref : String(), + * $id : Any, + * $db : String(), + * } + */ +struct DBRefInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + + static const char* const className; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/engine.cpp b/src/mongo/scripting/mozjs/engine.cpp new file mode 100644 index 00000000000..15875da16e1 --- /dev/null +++ b/src/mongo/scripting/mozjs/engine.cpp @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kQuery + +#include "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/engine.h" + +#include "mongo/db/operation_context.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/proxyscope.h" +#include "mongo/util/log.h" + +namespace mongo { + +void ScriptEngine::setup() { + if (!globalScriptEngine) { + globalScriptEngine = new mozjs::MozJSScriptEngine(); + + if (hasGlobalServiceContext()) { + getGlobalServiceContext()->registerKillOpListener(globalScriptEngine); + } + } +} + +std::string ScriptEngine::getInterpreterVersionString() { + return "MozJS-38"; +} + +namespace mozjs { + +MozJSScriptEngine::MozJSScriptEngine() { + uassert(ErrorCodes::JSInterpreterFailure, "Failed to JS_Init()", JS_Init()); +} + +MozJSScriptEngine::~MozJSScriptEngine() { + JS_ShutDown(); +} + +mongo::Scope* MozJSScriptEngine::createScope() { + return new MozJSProxyScope(this); +} + +void MozJSScriptEngine::interrupt(unsigned opId) { + stdx::lock_guard<stdx::mutex> intLock(_globalInterruptLock); + OpIdToScopeMap::iterator iScope = _opToScopeMap.find(opId); + if (iScope == _opToScopeMap.end()) { + // got interrupt request for a scope that no longer exists + LOG(1) << "received interrupt request for unknown op: " << opId << printKnownOps_inlock(); + return; + } + + LOG(1) << "interrupting op: " << opId << printKnownOps_inlock(); + iScope->second->kill(); +} + +std::string MozJSScriptEngine::printKnownOps_inlock() { + str::stream out; + + if (shouldLog(logger::LogSeverity::Debug(2))) { + out << " known ops: \n"; + + for (auto&& iSc : _opToScopeMap) { + out << " " << iSc.first << "\n"; + } + } + + return out; +} + +void MozJSScriptEngine::interruptAll() { + stdx::lock_guard<stdx::mutex> interruptLock(_globalInterruptLock); + + for (auto&& iScope : _opToScopeMap) { + iScope.second->kill(); + } +} + +void MozJSScriptEngine::registerOperation(OperationContext* txn, MozJSImplScope* scope) { + stdx::lock_guard<stdx::mutex> giLock(_globalInterruptLock); + + auto opId = txn->getOpID(); + + _opToScopeMap[opId] = scope; + + LOG(2) << "SMScope " << static_cast<const void*>(scope) << " registered for op " << opId; + Status status = txn->checkForInterruptNoAssert(); + if (!status.isOK()) { + scope->kill(); + } +} + +void MozJSScriptEngine::unregisterOperation(unsigned int opId) { + stdx::lock_guard<stdx::mutex> giLock(_globalInterruptLock); + + LOG(2) << "ImplScope " << static_cast<const void*>(this) << " unregistered for op " << opId; + + if (opId != 0) { + // scope is currently associated with an operation id + auto it = _opToScopeMap.find(opId); + if (it != _opToScopeMap.end()) + _opToScopeMap.erase(it); + } +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/engine.h b/src/mongo/scripting/mozjs/engine.h new file mode 100644 index 00000000000..a337ef0bcdc --- /dev/null +++ b/src/mongo/scripting/mozjs/engine.h @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <jsapi.h> +#include <unordered_map> + +#include "mongo/scripting/deadline_monitor.h" +#include "mongo/scripting/engine.h" +#include "mongo/stdx/mutex.h" +#include "mongo/util/concurrency/mutex.h" + +namespace mongo { +namespace mozjs { + +class MozJSImplScope; + +/** + * Implements the global ScriptEngine interface for MozJS. The associated TU + * pulls this in for the polymorphic globalScriptEngine. + */ +class MozJSScriptEngine final : public mongo::ScriptEngine { +public: + MozJSScriptEngine(); + ~MozJSScriptEngine() override; + + mongo::Scope* createScope() override; + + void runTest() override {} + + bool utf8Ok() const override { + return true; + } + + void interrupt(unsigned opId) override; + + void interruptAll() override; + + void registerOperation(OperationContext* ctx, MozJSImplScope* scope); + void unregisterOperation(unsigned int opId); + + using ScopeCallback = void (*)(Scope&); + ScopeCallback getScopeInitCallback() { + return _scopeInitCallback; + }; + + DeadlineMonitor<MozJSImplScope>& getDeadlineMonitor() { + return _deadlineMonitor; + } + +private: + std::string printKnownOps_inlock(); + + /** + * This mutex protects _opToScopeMap + */ + stdx::mutex _globalInterruptLock; + + using OpIdToScopeMap = std::unordered_map<unsigned, MozJSImplScope*>; + OpIdToScopeMap _opToScopeMap; // map of mongo op ids to scopes (protected by + // _globalInterruptLock). + + DeadlineMonitor<MozJSImplScope> _deadlineMonitor; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/exception.cpp b/src/mongo/scripting/mozjs/exception.cpp new file mode 100644 index 00000000000..7f7349acee1 --- /dev/null +++ b/src/mongo/scripting/mozjs/exception.cpp @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/exception.h" + +#include <jsfriendapi.h> + +#include "mongo/scripting/mozjs/jsstringwrapper.h" +#include "mongo/util/assert_util.h" + +namespace mongo { +namespace mozjs { + +namespace { + +JSErrorFormatString kFormatString = {"{0}", 1, JSEXN_ERR}; +const JSErrorFormatString* errorCallback(void* data, const unsigned code) { + return &kFormatString; +} + +} // namespace + +void mongoToJSException(JSContext* cx) { + auto status = exceptionToStatus(); + + JS_ReportErrorNumber(cx, errorCallback, nullptr, status.code(), status.reason().c_str()); +} + +void setJSException(JSContext* cx, ErrorCodes::Error code, StringData sd) { + JS_ReportErrorNumber(cx, errorCallback, nullptr, code, sd.rawData()); +} + +Status currentJSExceptionToStatus(JSContext* cx, ErrorCodes::Error altCode, StringData altReason) { + JS::RootedValue vp(cx); + if (!JS_GetPendingException(cx, &vp)) + return Status(altCode, altReason.rawData()); + + JS::RootedObject obj(cx, vp.toObjectOrNull()); + JSErrorReport* report = JS_ErrorFromException(cx, obj); + if (!report) + return Status(altCode, altReason.rawData()); + + JSStringWrapper jsstr(cx, js::ErrorReportToString(cx, report)); + if (!jsstr) + return Status(altCode, altReason.rawData()); + + /** + * errorNumber is only set by library consumers of MozJS, and then only via + * JS_ReportErrorNumber, so all of the codes we see here are ours. + */ + return Status(report->errorNumber ? static_cast<ErrorCodes::Error>(report->errorNumber) + : altCode, + jsstr.toStringData().rawData()); +} + +void throwCurrentJSException(JSContext* cx, ErrorCodes::Error altCode, StringData altReason) { + auto status = currentJSExceptionToStatus(cx, altCode, altReason); + uasserted(status.code(), status.reason()); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/exception.h b/src/mongo/scripting/mozjs/exception.h new file mode 100644 index 00000000000..9ef48664db6 --- /dev/null +++ b/src/mongo/scripting/mozjs/exception.h @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <jsapi.h> + +#include "mongo/base/error_codes.h" +#include "mongo/base/string_data.h" +#include "mongo/util/assert_util.h" + +namespace mongo { +namespace mozjs { + +/** + * Turns a current C++ exception into a JS exception + */ +void mongoToJSException(JSContext* cx); + +/** + * Sets an exception for javascript + */ +void setJSException(JSContext* cx, ErrorCodes::Error code, StringData sd); + +/** + * Converts the current pending js expection into a status + * + * The altCode and altReason are used if no JS exception is pending + */ +Status currentJSExceptionToStatus(JSContext* cx, ErrorCodes::Error altCode, StringData altReason); + +/** + * Turns the current JS exception into a C++ exception + * + * The altCode and altReason are used if no JS exception is pending + */ +MONGO_COMPILER_NORETURN void throwCurrentJSException(JSContext* cx, + ErrorCodes::Error altCode, + StringData altReason); + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/global.cpp b/src/mongo/scripting/mozjs/global.cpp new file mode 100644 index 00000000000..d8c6da94e35 --- /dev/null +++ b/src/mongo/scripting/mozjs/global.cpp @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/global.h" + +#include <js/Conversions.h> + +#include "mongo/base/init.h" +#include "mongo/logger/logstream_builder.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/jsstringwrapper.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec GlobalInfo::freeFunctions[4] = { + MONGO_ATTACH_JS_FUNCTION(gc), + MONGO_ATTACH_JS_FUNCTION(print), + MONGO_ATTACH_JS_FUNCTION(version), + JS_FS_END, +}; + +const char* const GlobalInfo::className = "Global"; + +namespace { + +logger::MessageLogDomain* jsPrintLogDomain; + +} // namespace + +void GlobalInfo::Functions::print(JSContext* cx, JS::CallArgs args) { + logger::LogstreamBuilder builder(jsPrintLogDomain, getThreadName(), logger::LogSeverity::Log()); + std::ostream& ss = builder.stream(); + + bool first = true; + for (size_t i = 0; i < args.length(); i++) { + if (first) + first = false; + else + ss << " "; + + if (args.get(i).isNullOrUndefined()) { + // failed to get object to convert + ss << "[unknown type]"; + continue; + } + + JSStringWrapper jsstr(cx, JS::ToString(cx, args.get(i))); + ss << jsstr.toStringData(); + } + ss << std::endl; + + args.rval().setUndefined(); +} + +void GlobalInfo::Functions::version(JSContext* cx, JS::CallArgs args) { + ValueReader(cx, args.rval()).fromStringData(JS_VersionToString(JS_GetVersion(cx))); +} + +void GlobalInfo::Functions::gc(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + scope->gc(); + + args.rval().setUndefined(); +} + +MONGO_INITIALIZER(JavascriptPrintDomain)(InitializerContext*) { + jsPrintLogDomain = logger::globalLogManager()->getNamedDomain("javascriptOutput"); + return Status::OK(); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/global.h b/src/mongo/scripting/mozjs/global.h new file mode 100644 index 00000000000..1da83350137 --- /dev/null +++ b/src/mongo/scripting/mozjs/global.h @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The global object for all of our JS. + * + * This function is super special and it's properties are the globally visible + * symbol for JS execution. + */ +struct GlobalInfo : public BaseInfo { + struct Functions { + MONGO_DEFINE_JS_FUNCTION(gc); + MONGO_DEFINE_JS_FUNCTION(print); + MONGO_DEFINE_JS_FUNCTION(version); + }; + + static const JSFunctionSpec freeFunctions[4]; + + static const char* const className; + static const unsigned classFlags = JSCLASS_GLOBAL_FLAGS; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/idwrapper.cpp b/src/mongo/scripting/mozjs/idwrapper.cpp new file mode 100644 index 00000000000..2c701037a22 --- /dev/null +++ b/src/mongo/scripting/mozjs/idwrapper.cpp @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/idwrapper.h" + +#include "mongo/base/error_codes.h" +#include "mongo/scripting/mozjs/exception.h" +#include "mongo/scripting/mozjs/jsstringwrapper.h" +#include "mongo/util/assert_util.h" + +namespace mongo { +namespace mozjs { + +IdWrapper::IdWrapper(JSContext* cx, JS::HandleId value) : _context(cx), _value(cx, value) {} + +std::string IdWrapper::toString() const { + if (JSID_IS_STRING(_value)) { + return JSStringWrapper(_context, JSID_TO_STRING(_value)).toString(); + } else if (JSID_IS_INT(_value)) { + return std::to_string(JSID_TO_INT(_value)); + } else { + throwCurrentJSException(_context, + ErrorCodes::TypeMismatch, + "Cannot toString() non-string and non-integer jsid"); + } +} + +uint32_t IdWrapper::toInt32() const { + uassert(ErrorCodes::TypeMismatch, "Cannot toInt32() non-integer jsid", JSID_IS_INT(_value)); + + return JSID_TO_INT(_value); +} + +bool IdWrapper::equals(StringData sd) const { + return sd.compare(toString()) == 0; +} + +bool IdWrapper::isInt() const { + return JSID_IS_INT(_value); +} + +bool IdWrapper::isString() const { + return JSID_IS_STRING(_value); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/idwrapper.h b/src/mongo/scripting/mozjs/idwrapper.h new file mode 100644 index 00000000000..4865e5e0aec --- /dev/null +++ b/src/mongo/scripting/mozjs/idwrapper.h @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <jsapi.h> +#include <string> + +#include "mongo/base/string_data.h" + +namespace mongo { +namespace mozjs { + +/** + * Wraps jsid's to make them slightly easier to use + * + * As these own a JS::RootedId they're not movable or copyable + * + * IdWrapper should only be used on the stack, never in a heap allocation + */ +class IdWrapper { +public: + IdWrapper(JSContext* cx, JS::HandleId id); + + /** + * Converts to a string. This coerces for integers + */ + std::string toString() const; + + /** + * Converts to an int. This throws if the id is not an integer + */ + uint32_t toInt32() const; + + bool isString() const; + bool isInt() const; + + bool equals(StringData sd) const; + +private: + JSContext* _context; + JS::RootedId _value; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/implscope.cpp b/src/mongo/scripting/mozjs/implscope.cpp new file mode 100644 index 00000000000..0d5d3675603 --- /dev/null +++ b/src/mongo/scripting/mozjs/implscope.cpp @@ -0,0 +1,728 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kQuery + +#include "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/implscope.h" + +#include <jscustomallocator.h> +#include <jsfriendapi.h> + +#include "mongo/base/error_codes.h" +#include "mongo/db/operation_context.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/stdx/mutex.h" +#include "mongo/util/concurrency/threadlocal.h" +#include "mongo/util/log.h" + +using namespace mongoutils; + +namespace mongo { + +// Generated symbols for JS files +namespace JSFiles { +extern const JSFile types; +extern const JSFile assert; +} // namespace + +namespace mozjs { + +const char* const MozJSImplScope::kExecResult = "__lastres__"; +const char* const MozJSImplScope::kInvokeResult = "__returnValue"; + +namespace { + +/** + * The maximum amount of memory to be given out per thread to mozilla. We + * manage this by trapping all calls to malloc, free, etc. and keeping track of + * counts in some thread locals + */ +const size_t kMallocMemoryLimit = 1024ul * 1024 * 1024 * 1.1; + +/** + * The number of bytes to allocate after which garbage collection is run + */ +const int kMaxBytesBeforeGC = 8 * 1024 * 1024; + +/** + * The size, in bytes, of each "stack chunk". 8192 is the recommended amount + * from mozilla + */ +const int kStackChunkSize = 8192; + +/** + * Runtime's can race on first creation (on some function statics), so we just + * serialize the initial Runtime creation. + */ +stdx::mutex gRuntimeCreationMutex; +bool gFirstRuntimeCreated = false; + +} // namespace + +MONGO_TRIVIALLY_CONSTRUCTIBLE_THREAD_LOCAL MozJSImplScope* kCurrentScope; + +struct MozJSImplScope::MozJSEntry { + MozJSEntry(MozJSImplScope* scope) : ar(scope->_context), ac(scope->_context, scope->_global) {} + + JSAutoRequest ar; + JSAutoCompartment ac; +}; + +void MozJSImplScope::_reportError(JSContext* cx, const char* message, JSErrorReport* report) { + auto scope = getScope(cx); + + if (!JSREPORT_IS_WARNING(report->flags)) { + scope->_status = + Status(report->errorNumber ? static_cast<ErrorCodes::Error>(report->errorNumber) + : ErrorCodes::JSInterpreterFailure, + str::stream() << message << ":\n" + << JS::FormatStackDump(cx, nullptr, true, true, false) << "\n"); + } +} + +std::string MozJSImplScope::getError() { + return ""; +} + +void MozJSImplScope::registerOperation(OperationContext* txn) { + invariant(_opId == 0); + _opId = txn->getOpID(); + + _engine->registerOperation(txn, this); +} + +void MozJSImplScope::unregisterOperation() { + if (_opId != 0) { + _engine->unregisterOperation(_opId); + + _opId = 0; + } +} + +void MozJSImplScope::kill() { + _pendingKill.store(true); + JS_RequestInterruptCallback(_runtime); +} + +bool MozJSImplScope::isKillPending() const { + return _pendingKill.load(); +} + +OperationContext* MozJSImplScope::getOpContext() const { + return _opCtx; +} + +bool MozJSImplScope::_interruptCallback(JSContext* cx) { + auto scope = getScope(cx); + + if (scope->_pendingGC.load()) { + JS_GC(scope->_runtime); + } + + bool kill = scope->isKillPending(); + + if (kill) { + scope->_engine->getDeadlineMonitor().stopDeadline(scope); + scope->unregisterOperation(); + } + + return !kill; +} + +void MozJSImplScope::_gcCallback(JSRuntime* rt, JSGCStatus status, void* data) { + if (!shouldLog(logger::LogSeverity::Debug(1))) { + // don't collect stats unless verbose + return; + } + + log() << "MozJS GC " << (status == JSGC_BEGIN ? "prologue" : "epilogue") << " heap stats - " + << " total: " << mongo::sm::get_total_bytes() << " limit: " << mongo::sm::get_max_bytes() + << std::endl; +} + +MozJSImplScope::MozRuntime::MozRuntime() { + mongo::sm::reset(kMallocMemoryLimit); + + { + stdx::unique_lock<stdx::mutex> lk(gRuntimeCreationMutex); + + if (gFirstRuntimeCreated) { + // If we've already made a runtime, just proceed + lk.unlock(); + } else { + // If this is the first one, hold the lock until after the first + // one's done + gFirstRuntimeCreated = true; + } + + _runtime = JS_NewRuntime(kMaxBytesBeforeGC); + } + + uassert(ErrorCodes::JSInterpreterFailure, "Failed to initialize JSRuntime", _runtime); + + _context = JS_NewContext(_runtime, kStackChunkSize); + uassert(ErrorCodes::JSInterpreterFailure, "Failed to initialize JSContext", _context); +} + +MozJSImplScope::MozRuntime::~MozRuntime() { + JS_DestroyContext(_context); + JS_DestroyRuntime(_runtime); +} + +MozJSImplScope::MozJSImplScope(MozJSScriptEngine* engine) + : _engine(engine), + _mr(), + _runtime(_mr._runtime), + _context(_mr._context), + _globalProto(_context), + _global(_globalProto.getProto()), + _funcs(), + _pendingKill(false), + _opId(0), + _opCtx(nullptr), + _pendingGC(false), + _connectState(ConnectState::Not), + _status(Status::OK()), + _binDataProto(_context), + _bsonProto(_context), + _countDownLatchProto(_context), + _cursorProto(_context), + _dbCollectionProto(_context), + _dbPointerProto(_context), + _dbQueryProto(_context), + _dbProto(_context), + _dbRefProto(_context), + _jsThreadProto(_context), + _maxKeyProto(_context), + _minKeyProto(_context), + _mongoExternalProto(_context), + _mongoLocalProto(_context), + _nativeFunctionProto(_context), + _numberIntProto(_context), + _numberLongProto(_context), + _objectProto(_context), + _oidProto(_context), + _regExpProto(_context), + _timestampProto(_context) { + kCurrentScope = this; + + // The default is quite low and doesn't seem to directly correlate with + // malloc'd bytes. Set it to MAX_INT here and catching things in the + // jscustomallocator.cpp + JS_SetGCParameter(_runtime, JSGC_MAX_BYTES, 0xffffffff); + + JS_SetInterruptCallback(_runtime, _interruptCallback); + JS_SetGCCallback(_runtime, _gcCallback, this); + JS_SetContextPrivate(_context, this); + JSAutoRequest ar(_context); + + JS_SetErrorReporter(_runtime, _reportError); + + JSAutoCompartment ac(_context, _global); + + _checkErrorState(JS_InitStandardClasses(_context, _global)); + + installBSONTypes(); + execSetup(JSFiles::assert); + execSetup(JSFiles::types); + + // install process-specific utilities in the global scope (dependancy: types.js, assert.js) + if (_engine->getScopeInitCallback()) + _engine->getScopeInitCallback()(*this); + + // install global utility functions + installGlobalUtils(*this); +} + +MozJSImplScope::~MozJSImplScope() { + for (auto&& x : _funcs) { + x.reset(); + } + + unregisterOperation(); +} + +bool MozJSImplScope::hasOutOfMemoryException() { + return false; +} + +void MozJSImplScope::init(const BSONObj* data) { + if (!data) + return; + + BSONObjIterator i(*data); + while (i.more()) { + BSONElement e = i.next(); + setElement(e.fieldName(), e); + } +} + +void MozJSImplScope::setNumber(const char* field, double val) { + MozJSEntry entry(this); + + ObjectWrapper(_context, _global).setNumber(field, val); +} + +void MozJSImplScope::setString(const char* field, StringData val) { + MozJSEntry entry(this); + + ObjectWrapper(_context, _global).setString(field, val); +} + +void MozJSImplScope::setBoolean(const char* field, bool val) { + MozJSEntry entry(this); + + ObjectWrapper(_context, _global).setBoolean(field, val); +} + +void MozJSImplScope::setElement(const char* field, const BSONElement& e) { + MozJSEntry entry(this); + + ObjectWrapper(_context, _global).setBSONElement(field, e, false); +} + +void MozJSImplScope::setObject(const char* field, const BSONObj& obj, bool readOnly) { + MozJSEntry entry(this); + + ObjectWrapper(_context, _global).setBSON(field, obj, readOnly); +} + +int MozJSImplScope::type(const char* field) { + MozJSEntry entry(this); + + return ObjectWrapper(_context, _global).type(field); +} + +double MozJSImplScope::getNumber(const char* field) { + MozJSEntry entry(this); + + return ObjectWrapper(_context, _global).getNumber(field); +} + +int MozJSImplScope::getNumberInt(const char* field) { + MozJSEntry entry(this); + + return ObjectWrapper(_context, _global).getNumberInt(field); +} + +long long MozJSImplScope::getNumberLongLong(const char* field) { + MozJSEntry entry(this); + + return ObjectWrapper(_context, _global).getNumberLongLong(field); +} + +std::string MozJSImplScope::getString(const char* field) { + MozJSEntry entry(this); + + return ObjectWrapper(_context, _global).getString(field); +} + +bool MozJSImplScope::getBoolean(const char* field) { + MozJSEntry entry(this); + + return ObjectWrapper(_context, _global).getBoolean(field); +} + +BSONObj MozJSImplScope::getObject(const char* field) { + MozJSEntry entry(this); + + return ObjectWrapper(_context, _global).getObject(field); +} + +void MozJSImplScope::newFunction(StringData raw, JS::MutableHandleValue out) { + MozJSEntry entry(this); + + std::string code = str::stream() << "____MongoToSM_newFunction_temp = " << raw; + + JS::CompileOptions co(_context); + setCompileOptions(&co); + _checkErrorState(JS::Evaluate(_context, _global, co, code.c_str(), code.length(), out)); +} + +BSONObj MozJSImplScope::callThreadArgs(const BSONObj& args) { + MozJSEntry entry(this); + + JS::RootedValue function(_context); + ValueReader(_context, &function).fromBSONElement(args.firstElement(), true); + + int argc = args.nFields() - 1; + + JS::AutoValueVector argv(_context); + BSONObjIterator it(args); + it.next(); + JS::RootedValue value(_context); + + for (int i = 0; i < argc; ++i) { + ValueReader(_context, &value).fromBSONElement(*it, true); + argv.append(value); + it.next(); + } + + JS::RootedValue out(_context); + JS::RootedObject thisv(_context); + + bool success = JS::Call(_context, thisv, function, argv, &out); + + if (!success) { + auto status = currentJSExceptionToStatus( + _context, ErrorCodes::JSInterpreterFailure, "Unknown callThread failure"); + + log() << "js thread raised js exception: " << status; + + uasserted(status.code(), status.reason()); + } + + BSONObjBuilder b; + ValueWriter(_context, out).writeThis(&b, "ret"); + + return b.obj(); +} + +bool hasFunctionIdentifier(StringData code) { + if (code.size() < 9 || code.find("function") != 0) + return false; + + return code[8] == ' ' || code[8] == '('; +} + +// TODO: This function identification code is broken. Fix it up to be more robust +// +// See: SERVER-16703 for more info +void MozJSImplScope::_MozJSCreateFunction(const char* raw, + ScriptingFunction functionNumber, + JS::MutableHandleValue fun) { + std::string code = jsSkipWhiteSpace(raw); + if (!hasFunctionIdentifier(code)) { + if (code.find('\n') == std::string::npos && !hasJSReturn(code) && + (code.find(';') == std::string::npos || code.find(';') == code.size() - 1)) { + code = "return " + code; + } + code = "function(){ " + code + "}"; + } + + code = str::stream() << "_funcs" << functionNumber << " = " << code; + + JS::CompileOptions co(_context); + setCompileOptions(&co); + + _checkErrorState(JS::Evaluate(_context, _global, co, code.c_str(), code.length(), fun)); + uassert(10232, + "not a function", + fun.isObject() && JS_ObjectIsFunction(_context, fun.toObjectOrNull())); +} + +ScriptingFunction MozJSImplScope::_createFunction(const char* raw, + ScriptingFunction functionNumber) { + MozJSEntry entry(this); + + JS::RootedValue fun(_context); + _MozJSCreateFunction(raw, functionNumber, &fun); + _funcs.emplace_back(_context, fun.get()); + + return functionNumber; +} + +void MozJSImplScope::setFunction(const char* field, const char* code) { + MozJSEntry entry(this); + + JS::RootedValue fun(_context); + + _MozJSCreateFunction(code, getFunctionCache().size() + 1, &fun); + + ObjectWrapper(_context, _global).setValue(field, fun); +} + +void MozJSImplScope::rename(const char* from, const char* to) { + MozJSEntry entry(this); + + ObjectWrapper(_context, _global).rename(from, to); +} + +int MozJSImplScope::invoke(ScriptingFunction func, + const BSONObj* argsObject, + const BSONObj* recv, + int timeoutMs, + bool ignoreReturn, + bool readOnlyArgs, + bool readOnlyRecv) { + MozJSEntry entry(this); + + auto funcValue = _funcs[func - 1]; + JS::RootedValue result(_context); + + const int nargs = argsObject ? argsObject->nFields() : 0; + + JS::AutoValueVector args(_context); + + if (nargs) { + BSONObjIterator it(*argsObject); + for (int i = 0; i < nargs; i++) { + BSONElement next = it.next(); + + JS::RootedValue value(_context); + ValueReader(_context, &value).fromBSONElement(next, readOnlyArgs); + + args.append(value); + } + } + + JS::RootedValue smrecv(_context); + if (recv) + ValueReader(_context, &smrecv).fromBSON(*recv, readOnlyRecv); + else + smrecv.setObjectOrNull(_global); + + if (timeoutMs) + _engine->getDeadlineMonitor().startDeadline(this, timeoutMs); + + JS::RootedValue out(_context); + JS::RootedObject obj(_context, smrecv.toObjectOrNull()); + + bool success = JS::Call(_context, obj, funcValue, args, &out); + + if (timeoutMs) + _engine->getDeadlineMonitor().stopDeadline(this); + + _checkErrorState(success); + + if (!ignoreReturn) { + // must validate the handle because TerminateExecution may have + // been thrown after the above checks + if (out.isObject() && _nativeFunctionProto.instanceOf(out)) { + warning() << "storing native function as return value"; + _lastRetIsNativeCode = true; + } else { + _lastRetIsNativeCode = false; + } + + ObjectWrapper(_context, _global).setValue(kInvokeResult, out); + } + + return 0; +} + +bool MozJSImplScope::exec(StringData code, + const std::string& name, + bool printResult, + bool reportError, + bool assertOnError, + int timeoutMs) { + MozJSEntry entry(this); + + JS::CompileOptions co(_context); + setCompileOptions(&co); + JS::RootedScript script(_context); + + bool success = JS::Compile(_context, _global, co, code.rawData(), code.size(), &script); + + if (_checkErrorState(success, reportError, assertOnError)) + return false; + + if (timeoutMs) + _engine->getDeadlineMonitor().startDeadline(this, timeoutMs); + + JS::RootedValue out(_context); + + success = JS_ExecuteScript(_context, _global, script, &out); + + if (timeoutMs) + _engine->getDeadlineMonitor().stopDeadline(this); + + if (_checkErrorState(success, reportError, assertOnError)) + return false; + + ObjectWrapper(_context, _global).setValue(kExecResult, out); + + if (printResult && !out.isUndefined()) { + // TODO: We seem to use this productively in v8, but it seems + // unecessary under sm. That probably means somethings off + // + // appears to only be used by shell + // std::cout << ValueWriter(_context, out).toString() << std::endl; + } + + return true; +} + +void MozJSImplScope::injectNative(const char* field, NativeFunction func, void* data) { + MozJSEntry entry(this); + + JS::RootedObject obj(_context); + + NativeFunctionInfo::make(_context, &obj, func, data); + + JS::RootedValue value(_context); + value.setObjectOrNull(obj); + ObjectWrapper(_context, _global).setValue(field, value); +} + +void MozJSImplScope::gc() { + _pendingGC.store(true); + JS_RequestInterruptCallback(_runtime); +} + +void MozJSImplScope::localConnectForDbEval(OperationContext* txn, const char* dbName) { + MozJSEntry entry(this); + + invariant(_opCtx == NULL); + _opCtx = txn; + + if (_connectState == ConnectState::External) + uasserted(12510, "externalSetup already called, can't call localConnect"); + if (_connectState == ConnectState::Local) { + if (_localDBName == dbName) + return; + uasserted(12511, + str::stream() << "localConnect previously called with name " << _localDBName); + } + + // NOTE: order is important here. the following methods must be called after + // the above conditional statements. + + // install db access functions in the global object + installDBAccess(); + + // install the Mongo function object and instantiate the 'db' global + _mongoLocalProto.install(_global); + execCoreFiles(); + + const char* const makeMongo = "_mongo = new Mongo()"; + exec(makeMongo, "local connect 2", false, true, true, 0); + + std::string makeDB = str::stream() << "db = _mongo.getDB(\"" << dbName << "\");"; + exec(makeDB, "local connect 3", false, true, true, 0); + + _connectState = ConnectState::Local; + _localDBName = dbName; + + loadStored(txn); +} + +void MozJSImplScope::externalSetup() { + MozJSEntry entry(this); + + if (_connectState == ConnectState::External) + return; + if (_connectState == ConnectState::Local) + uasserted(12512, "localConnect already called, can't call externalSetup"); + + mongo::sm::reset(0); + + // install db access functions in the global object + installDBAccess(); + + // install thread-related functions (e.g. _threadInject) + installFork(); + + // install the Mongo function object + _mongoExternalProto.install(_global); + execCoreFiles(); + _connectState = ConnectState::External; +} + +void MozJSImplScope::reset() { + unregisterOperation(); + _pendingKill.store(false); + _pendingGC.store(false); +} + +void MozJSImplScope::installBSONTypes() { + _binDataProto.install(_global); + _bsonProto.install(_global); + _dbPointerProto.install(_global); + _dbRefProto.install(_global); + _maxKeyProto.install(_global); + _minKeyProto.install(_global); + _nativeFunctionProto.install(_global); + _numberIntProto.install(_global); + _numberLongProto.install(_global); + _objectProto.install(_global); + _oidProto.install(_global); + _regExpProto.install(_global); + _timestampProto.install(_global); + + // This builtin map is a javascript 6 thing. We want our version. so + // take theirs out + ObjectWrapper(_context, _global).deleteProperty("Map"); +} + +void MozJSImplScope::installDBAccess() { + _cursorProto.install(_global); + _dbProto.install(_global); + _dbQueryProto.install(_global); + _dbCollectionProto.install(_global); +} + +void MozJSImplScope::installFork() { + _countDownLatchProto.install(_global); + _jsThreadProto.install(_global); +} + +bool MozJSImplScope::_checkErrorState(bool success, bool reportError, bool assertOnError) { + if (success) + return false; + + if (_status.isOK()) { + _status = Status(ErrorCodes::UnknownError, "Unknown Failure from JSInterpreter"); + } + + _error = _status.reason(); + + if (reportError) + error() << _error << std::endl; + + // Clear the status state + auto status = std::move(_status); + + if (assertOnError) { + // Throw if necessary + uassertStatusOK(status); + } + + return true; +} + + +void MozJSImplScope::setCompileOptions(JS::CompileOptions* co) { + co->setUTF8(true); +} + +MozJSImplScope* MozJSImplScope::getThreadScope() { + return kCurrentScope; +} + +void MozJSImplScope::setOOM() { + _status = Status(ErrorCodes::JSInterpreterFailure, "Out of memory"); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/implscope.h b/src/mongo/scripting/mozjs/implscope.h new file mode 100644 index 00000000000..4bd29e76676 --- /dev/null +++ b/src/mongo/scripting/mozjs/implscope.h @@ -0,0 +1,323 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <jsapi.h> + +#include "mongo/client/dbclientcursor.h" +#include "mongo/scripting/mozjs/bindata.h" +#include "mongo/scripting/mozjs/bson.h" +#include "mongo/scripting/mozjs/countdownlatch.h" +#include "mongo/scripting/mozjs/cursor.h" +#include "mongo/scripting/mozjs/db.h" +#include "mongo/scripting/mozjs/dbcollection.h" +#include "mongo/scripting/mozjs/dbpointer.h" +#include "mongo/scripting/mozjs/dbquery.h" +#include "mongo/scripting/mozjs/dbref.h" +#include "mongo/scripting/mozjs/engine.h" +#include "mongo/scripting/mozjs/global.h" +#include "mongo/scripting/mozjs/jsthread.h" +#include "mongo/scripting/mozjs/maxkey.h" +#include "mongo/scripting/mozjs/minkey.h" +#include "mongo/scripting/mozjs/mongo.h" +#include "mongo/scripting/mozjs/nativefunction.h" +#include "mongo/scripting/mozjs/numberint.h" +#include "mongo/scripting/mozjs/numberlong.h" +#include "mongo/scripting/mozjs/object.h" +#include "mongo/scripting/mozjs/oid.h" +#include "mongo/scripting/mozjs/regexp.h" +#include "mongo/scripting/mozjs/timestamp.h" + +namespace mongo { +namespace mozjs { + +/** + * Implementation Scope for MozJS + * + * The Implementation scope holds the actual mozjs runtime and context objects, + * along with a number of global prototypes for mongoDB specific types. Each + * ImplScope requires it's own thread and cannot be accessed from any thread + * other than the one it was created on (this is a detail inherited from the + * JSRuntime). If you need a scope that can be accessed by different threads + * over the course of it's lifetime, see MozJSProxyScope + * + * For more information about overriden fields, see mongo::Scope + */ +class MozJSImplScope final : public Scope { + MONGO_DISALLOW_COPYING(MozJSImplScope); + +public: + explicit MozJSImplScope(MozJSScriptEngine* engine); + ~MozJSImplScope(); + + void init(const BSONObj* data) override; + + void reset() override; + + void kill(); + + bool isKillPending() const override; + + OperationContext* getOpContext() const; + + void registerOperation(OperationContext* txn) override; + + void unregisterOperation() override; + + void localConnectForDbEval(OperationContext* txn, const char* dbName) override; + + void externalSetup() override; + + std::string getError() override; + + bool hasOutOfMemoryException() override; + + void gc() override; + + double getNumber(const char* field) override; + int getNumberInt(const char* field) override; + long long getNumberLongLong(const char* field) override; + std::string getString(const char* field) override; + bool getBoolean(const char* field) override; + BSONObj getObject(const char* field) override; + + void setNumber(const char* field, double val) override; + void setString(const char* field, StringData val) override; + void setBoolean(const char* field, bool val) override; + void setElement(const char* field, const BSONElement& e) override; + void setObject(const char* field, const BSONObj& obj, bool readOnly) override; + void setFunction(const char* field, const char* code) override; + + int type(const char* field) override; + + void rename(const char* from, const char* to) override; + + int invoke(ScriptingFunction func, + const BSONObj* args, + const BSONObj* recv, + int timeoutMs = 0, + bool ignoreReturn = false, + bool readOnlyArgs = false, + bool readOnlyRecv = false) override; + + bool exec(StringData code, + const std::string& name, + bool printResult, + bool reportError, + bool assertOnError, + int timeoutMs) override; + + void injectNative(const char* field, NativeFunction func, void* data = 0) override; + + ScriptingFunction _createFunction(const char* code, + ScriptingFunction functionNumber = 0) override; + + void newFunction(StringData code, JS::MutableHandleValue out); + + BSONObj callThreadArgs(const BSONObj& obj); + + WrapType<BinDataInfo>& getBinDataProto() { + return _binDataProto; + } + + WrapType<BSONInfo>& getBsonProto() { + return _bsonProto; + } + + WrapType<CountDownLatchInfo>& getCountDownLatchProto() { + return _countDownLatchProto; + } + + WrapType<CursorInfo>& getCursorProto() { + return _cursorProto; + } + + WrapType<DBCollectionInfo>& getDbCollectionProto() { + return _dbCollectionProto; + } + + WrapType<DBPointerInfo>& getDbPointerProto() { + return _dbPointerProto; + } + + WrapType<DBQueryInfo>& getDbQueryProto() { + return _dbQueryProto; + } + + WrapType<DBInfo>& getDbProto() { + return _dbProto; + } + + WrapType<DBRefInfo>& getDbRefProto() { + return _dbRefProto; + } + + WrapType<JSThreadInfo>& getJSThreadProto() { + return _jsThreadProto; + } + + WrapType<MaxKeyInfo>& getMaxKeyProto() { + return _maxKeyProto; + } + + WrapType<MinKeyInfo>& getMinKeyProto() { + return _minKeyProto; + } + + WrapType<MongoExternalInfo>& getMongoExternalProto() { + return _mongoExternalProto; + } + + WrapType<MongoLocalInfo>& getMongoLocalProto() { + return _mongoLocalProto; + } + + WrapType<NativeFunctionInfo>& getNativeFunctionProto() { + return _nativeFunctionProto; + } + + WrapType<NumberIntInfo>& getNumberIntProto() { + return _numberIntProto; + } + + WrapType<NumberLongInfo>& getNumberLongProto() { + return _numberLongProto; + } + + WrapType<ObjectInfo>& getObjectProto() { + return _objectProto; + } + + WrapType<OIDInfo>& getOidProto() { + return _oidProto; + } + + WrapType<RegExpInfo>& getRegExpProto() { + return _regExpProto; + } + + WrapType<TimestampInfo>& getTimestampProto() { + return _timestampProto; + } + + static const char* const kExecResult; + static const char* const kInvokeResult; + + static MozJSImplScope* getThreadScope(); + void setOOM(); + +private: + void _MozJSCreateFunction(const char* raw, + ScriptingFunction functionNumber, + JS::MutableHandleValue fun); + + /** + * This structure exists exclusively to construct the runtime and context + * ahead of the various global prototypes in the ImplScope construction. + * Basically, we have to call some c apis on the way up and down and this + * takes care of that + */ + struct MozRuntime { + public: + MozRuntime(); + ~MozRuntime(); + + JSRuntime* _runtime; + JSContext* _context; + }; + + /** + * The connection state of the scope. + * + * This is for dbeval and the shell + */ + enum class ConnectState : char { + Not, + Local, + External, + }; + + struct MozJSEntry; + friend struct MozJSEntry; + + static void _reportError(JSContext* cx, const char* message, JSErrorReport* report); + static bool _interruptCallback(JSContext* cx); + static void _gcCallback(JSRuntime* rt, JSGCStatus status, void* data); + bool _checkErrorState(bool success, bool reportError = true, bool assertOnError = true); + + void installDBAccess(); + void installBSONTypes(); + void installFork(); + + void setCompileOptions(JS::CompileOptions* co); + + MozJSScriptEngine* _engine; + MozRuntime _mr; + JSRuntime* _runtime; + JSContext* _context; + WrapType<GlobalInfo> _globalProto; + JS::HandleObject _global; + std::vector<JS::PersistentRootedValue> _funcs; + std::atomic<bool> _pendingKill; + std::string _error; + unsigned int _opId; // op id for this scope + OperationContext* _opCtx; // Op context for DbEval + std::atomic<bool> _pendingGC; + ConnectState _connectState; + Status _status; + + WrapType<BinDataInfo> _binDataProto; + WrapType<BSONInfo> _bsonProto; + WrapType<CountDownLatchInfo> _countDownLatchProto; + WrapType<CursorInfo> _cursorProto; + WrapType<DBCollectionInfo> _dbCollectionProto; + WrapType<DBPointerInfo> _dbPointerProto; + WrapType<DBQueryInfo> _dbQueryProto; + WrapType<DBInfo> _dbProto; + WrapType<DBRefInfo> _dbRefProto; + WrapType<JSThreadInfo> _jsThreadProto; + WrapType<MaxKeyInfo> _maxKeyProto; + WrapType<MinKeyInfo> _minKeyProto; + WrapType<MongoExternalInfo> _mongoExternalProto; + WrapType<MongoLocalInfo> _mongoLocalProto; + WrapType<NativeFunctionInfo> _nativeFunctionProto; + WrapType<NumberIntInfo> _numberIntProto; + WrapType<NumberLongInfo> _numberLongProto; + WrapType<ObjectInfo> _objectProto; + WrapType<OIDInfo> _oidProto; + WrapType<RegExpInfo> _regExpProto; + WrapType<TimestampInfo> _timestampProto; +}; + +inline MozJSImplScope* getScope(JSContext* cx) { + return static_cast<MozJSImplScope*>(JS_GetContextPrivate(cx)); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/jscustomallocator.cpp b/src/mongo/scripting/mozjs/jscustomallocator.cpp new file mode 100644 index 00000000000..adba220129d --- /dev/null +++ b/src/mongo/scripting/mozjs/jscustomallocator.cpp @@ -0,0 +1,234 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include <cstddef> +#include <type_traits> +#include <jscustomallocator.h> + +#include "mongo/config.h" +#include "mongo/util/concurrency/threadlocal.h" +#include "mongo/scripting/mozjs/implscope.h" + +#ifdef __linux__ +#include <malloc.h> +#elif defined(__APPLE__) +#include <malloc/malloc.h> +#elif defined(_WIN32) +#include <malloc.h> +#else +#define MONGO_NO_MALLOC_USABLE_SIZE +#endif + +/** + * This shim interface (which controls dynamic allocation within SpiderMonkey), + * consciously uses std::malloc and friends over mongoMalloc. It does this + * because SpiderMonkey has some plausible options in the event of OOM, + * specifically it can begin aggressive garbage collection. It would also be + * reasonable to go the other route and fail, but for the moment I erred on the + * side of maintaining the contract that SpiderMonkey expects. + * + * The overall strategy here is to keep track of allocations in a thread local, + * offering us the chance to enforce soft limits on memory use rather than + * waiting for the OS to OOM us. + */ + +namespace mongo { +namespace sm { + +namespace { +/** + * These two variables track the total number of bytes handed out, and the + * maximum number of bytes we will consider handing out. They are set by + * MozJSImplScope on start up. + */ +MONGO_TRIVIALLY_CONSTRUCTIBLE_THREAD_LOCAL size_t total_bytes; +MONGO_TRIVIALLY_CONSTRUCTIBLE_THREAD_LOCAL size_t max_bytes; + +/** + * When we don't have malloc_usable_size, we manage by adjusting our pointer by + * kMaxAlign bytes and storing the size of the allocation kMaxAlign bytes + * behind the pointer we hand back. That let's us get to the value at runtime. + * We know kMaxAlign is enough (generally 8 or 16 bytes), because that's + * literally the contract between malloc and std::max_align_t. + * + * This is commented out right now because std::max_align_t didn't seem to be + * available on our solaris builder. TODO: revisit in the future to see if that + * still holds. + */ +// const size_t kMaxAlign = std::alignment_of<std::max_align_t>::value; +const size_t kMaxAlign = 16; +} // namespace + +size_t get_total_bytes() { + return total_bytes; +} + +void reset(size_t bytes) { + total_bytes = 0; + max_bytes = bytes; +} + +size_t get_max_bytes() { + return max_bytes; +} + +/** + * Wraps std::Xalloc functions + * + * The idea here is to abstract soft limits on allocations, as well as possibly + * necessary pointer adjustment (if we don't have a malloc_usable_size + * replacement). + * + */ +template <typename T> +void* wrap_alloc(T&& func, void* ptr, size_t bytes) { + size_t mb = get_max_bytes(); + size_t tb = get_total_bytes(); + + if (mb && (tb + bytes > mb)) { + auto scope = mongo::mozjs::MozJSImplScope::getThreadScope(); + invariant(scope); + + scope->setOOM(); + + return nullptr; + } + +#ifdef MONGO_NO_MALLOC_USABLE_SIZE + void* p = func(ptr ? static_cast<char*>(ptr) - kMaxAlign : nullptr, bytes + kMaxAlign); +#else + void* p = func(ptr, bytes); +#endif + + if (!p) { + return nullptr; + } + +#ifdef MONGO_NO_MALLOC_USABLE_SIZE + *reinterpret_cast<size_t*>(p) = bytes; + p = static_cast<char*>(p) + kMaxAlign; +#endif + + total_bytes = tb + bytes; + + return p; +} + +size_t get_current(void* ptr) { +#ifdef MONGO_NO_MALLOC_USABLE_SIZE + if (!ptr) + return 0; + + return *reinterpret_cast<size_t*>(static_cast<char*>(ptr) - kMaxAlign); +#elif defined(__linux__) + return malloc_usable_size(ptr); +#elif defined(__APPLE__) + return malloc_size(ptr); +#elif defined(_WIN32) + return _msize(ptr); +#else +#error "Should be unreachable" +#endif +} + +} // namespace sm +} // namespace mongo + +void* js_malloc(size_t bytes) { + return mongo::sm::wrap_alloc( + [](void* ptr, size_t b) { return std::malloc(b); }, nullptr, bytes); +} + +void* js_calloc(size_t bytes) { + return mongo::sm::wrap_alloc( + [](void* ptr, size_t b) { return std::calloc(b, 1); }, nullptr, bytes); +} + +void* js_calloc(size_t nmemb, size_t size) { + return mongo::sm::wrap_alloc( + [](void* ptr, size_t b) { return std::calloc(b, 1); }, nullptr, nmemb * size); +} + +void js_free(void* p) { + if (!p) + return; + + size_t current = mongo::sm::get_current(p); + size_t tb = mongo::sm::get_total_bytes(); + + if (tb >= current) { + mongo::sm::total_bytes = tb - current; + } + + mongo::sm::wrap_alloc([](void* ptr, size_t b) { + std::free(ptr); + return nullptr; + }, p, 0); +} + +void* js_realloc(void* p, size_t bytes) { + if (!p) { + return js_malloc(bytes); + } + + if (!bytes) { + js_free(p); + return nullptr; + } + + size_t current = mongo::sm::get_current(p); + + if (current >= bytes) { + return p; + } + + size_t tb = mongo::sm::total_bytes; + + if (tb >= current) { + mongo::sm::total_bytes = tb - current; + } + + return mongo::sm::wrap_alloc( + [](void* ptr, size_t b) { return std::realloc(ptr, b); }, p, bytes); +} + +char* js_strdup(const char* s) { + size_t bytes = std::strlen(s) + 1; + + char* new_s = static_cast<char*>(js_malloc(bytes)); + + if (!new_s) { + return nullptr; + } + + std::memcpy(new_s, s, bytes); + + return new_s; +} diff --git a/src/mongo/scripting/mozjs/jsstringwrapper.cpp b/src/mongo/scripting/mozjs/jsstringwrapper.cpp new file mode 100644 index 00000000000..8261b44a2fe --- /dev/null +++ b/src/mongo/scripting/mozjs/jsstringwrapper.cpp @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/jsstringwrapper.h" + +#include <js/CharacterEncoding.h> +#include <jsapi.h> +#include <utility> + +#include "mongo/base/error_codes.h" +#include "mongo/scripting/mozjs/exception.h" +#include "mongo/util/assert_util.h" + +namespace mongo { +namespace mozjs { + +JSStringWrapper::JSStringWrapper(JSContext* cx, JSString* str) : _context(cx) { + if (!str) + throwCurrentJSException(cx, ErrorCodes::InternalError, "Cannot encode null JSString"); + + // We have to do this flatstring thing because no public api tells us + // how long the utf8 strings we get out are. + // + // Well, at least js/CharacterEncoding's GetDeflatedUTF8StringLength + // and JS_flattenString are all in the public headers... + JSFlatString* flat = JS_FlattenString(cx, str); + if (!flat) + throwCurrentJSException(cx, ErrorCodes::InternalError, "Failed to flatten JSString"); + + _length = JS::GetDeflatedUTF8StringLength(flat); + + JS::RootedString rstr(cx, str); + + JSAutoByteString abs; + abs.encodeUtf8(cx, rstr); + + if (!abs) + throwCurrentJSException(cx, ErrorCodes::InternalError, "Failed to encode JSString"); + + _str.reset(new char[_length]); + std::memcpy(_str.get(), abs.ptr(), _length); +} + +StringData JSStringWrapper::toStringData() const { + return StringData(_str.get(), _length); +} + +std::string JSStringWrapper::toString() const { + return toStringData().toString(); +} + +JSStringWrapper::operator bool() const { + return _str.get(); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/jsstringwrapper.h b/src/mongo/scripting/mozjs/jsstringwrapper.h new file mode 100644 index 00000000000..9a0ee21e900 --- /dev/null +++ b/src/mongo/scripting/mozjs/jsstringwrapper.h @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <jsapi.h> +#include <memory> +#include <string> + +#include "mongo/base/string_data.h" + +namespace mongo { +namespace mozjs { + +/** + * Wraps JSStrings to simplify coercing them to and from C++ style StringData + * and std::strings. + */ +class JSStringWrapper { +public: + JSStringWrapper() = default; + JSStringWrapper(JSContext* cx, JSString* str); + + StringData toStringData() const; + std::string toString() const; + + explicit operator bool() const; + +private: + JSContext* _context = nullptr; + std::unique_ptr<char[]> _str; + size_t _length = 0; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/jsthread.cpp b/src/mongo/scripting/mozjs/jsthread.cpp new file mode 100644 index 00000000000..24b4c5d88af --- /dev/null +++ b/src/mongo/scripting/mozjs/jsthread.cpp @@ -0,0 +1,274 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kQuery + +#include "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/jsthread.h" + +#include <cstdio> + +#include "mongo/db/jsobj.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/stdx/condition_variable.h" +#include "mongo/stdx/memory.h" +#include "mongo/stdx/mutex.h" +#include "mongo/stdx/thread.h" +#include "mongo/util/log.h" +#include "mongo/util/stacktrace.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec JSThreadInfo::threadMethods[6] = { + MONGO_ATTACH_JS_FUNCTION(init), + MONGO_ATTACH_JS_FUNCTION(start), + MONGO_ATTACH_JS_FUNCTION(join), + MONGO_ATTACH_JS_FUNCTION(hasFailed), + MONGO_ATTACH_JS_FUNCTION(returnData), + JS_FS_END, +}; + +const JSFunctionSpec JSThreadInfo::freeFunctions[3] = { + MONGO_ATTACH_JS_FUNCTION(_threadInject), + MONGO_ATTACH_JS_FUNCTION(_scopedThreadInject), + JS_FS_END, +}; + +const char* const JSThreadInfo::className = "JSThread"; + +/** + * Holder for JSThreads as exposed by fork() in the shell. + * + * The idea here is that we create a jsthread by taking a js function and its + * parameters and encoding them into a single bson object. Then we spawn a + * thread, have that thread do the work and join() it before checking it's + * result (serialized through bson). We can check errors at any time by + * checking a mutex guarded hasError(). + */ +class JSThreadConfig { +public: + JSThreadConfig(JSContext* cx, JS::CallArgs args) + : _started(false), _done(false), _sharedData(new SharedData()) { + uassert(ErrorCodes::JSInterpreterFailure, "need at least one argument", args.length() > 0); + uassert(ErrorCodes::JSInterpreterFailure, + "first argument must be a function", + args.get(0).isObject() && JS_ObjectIsFunction(cx, args.get(0).toObjectOrNull())); + + BSONObjBuilder b; + for (unsigned i = 0; i < args.length(); ++i) { + // 10 decimal digits for a 32 bit unsigned, then 1 for the null + char buf[11]; + std::sprintf(buf, "%i", i); + + ValueWriter(cx, args.get(i)).writeThis(&b, buf); + } + + _sharedData->_args = b.obj(); + } + + void start() { + uassert(ErrorCodes::JSInterpreterFailure, "Thread already started", !_started); + + _thread = stdx::thread(JSThread(*this)); + _started = true; + } + + void join() { + uassert(ErrorCodes::JSInterpreterFailure, "Thread not running", _started && !_done); + + _thread.join(); + _done = true; + } + + /** + * Returns true if the JSThread terminated as a result of an error + * during its execution, and false otherwise. This operation does + * not block, nor does it require join() to have been called. + */ + bool hasFailed() const { + uassert(ErrorCodes::JSInterpreterFailure, "Thread not started", _started); + + return _sharedData->getErrored(); + } + + BSONObj returnData() { + if (!_done) + join(); + + return _sharedData->_returnData; + } + +private: + /** + * SharedData between the calling thread and the callee + * + * JSThreadConfig doesn't always outlive its JSThread (for example, if the parent thread + * garbage collects the JSThreadConfig before the JSThread has finished running), so any + * data shared between them has to go in a shared_ptr. + */ + class SharedData { + public: + SharedData() : _errored(false) {} + + void setErrored(bool value) { + stdx::lock_guard<stdx::mutex> lck(_erroredMutex); + _errored = value; + } + + bool getErrored() { + stdx::lock_guard<stdx::mutex> lck(_erroredMutex); + return _errored; + } + + /** + * These two members aren't protected in any way, so you have to be + * mindful about how they're used. I.e. _args needs to be set before + * start() and _returnData can't be touched until after join(). + */ + BSONObj _args; + BSONObj _returnData; + + private: + stdx::mutex _erroredMutex; + bool _errored; + }; + + /** + * The callable object used by stdx::thread + */ + class JSThread { + public: + JSThread(JSThreadConfig& config) : _sharedData(config._sharedData) {} + + void operator()() { + try { + MozJSImplScope scope(static_cast<MozJSScriptEngine*>(globalScriptEngine)); + + _sharedData->_returnData = scope.callThreadArgs(_sharedData->_args); + } catch (...) { + auto status = exceptionToStatus(); + + log() << "js thread threw c++ exception: " << status; + _sharedData->setErrored(true); + _sharedData->_returnData = BSON("ret" << BSONUndefined); + } + } + + private: + std::shared_ptr<SharedData> _sharedData; + }; + + bool _started; + bool _done; + stdx::thread _thread; + std::shared_ptr<SharedData> _sharedData; +}; + +namespace { + +JSThreadConfig* getConfig(JSContext* cx, JS::CallArgs args) { + JS::RootedValue value(cx); + ObjectWrapper(cx, args.thisv()).getValue("_JSThreadConfig", &value); + + if (!value.isObject()) + uasserted(ErrorCodes::InternalError, "_JSThreadConfig not an object"); + + return static_cast<JSThreadConfig*>(JS_GetPrivate(value.toObjectOrNull())); +} + +} // namespace + +void JSThreadInfo::finalize(JSFreeOp* fop, JSObject* obj) { + auto config = static_cast<JSThreadConfig*>(JS_GetPrivate(obj)); + + if (!config) + return; + + delete config; +} + +void JSThreadInfo::Functions::init(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + JS::RootedObject obj(cx); + scope->getJSThreadProto().newObject(&obj); + JSThreadConfig* config = new JSThreadConfig(cx, args); + JS_SetPrivate(obj, config); + + ObjectWrapper(cx, args.thisv()).setObject("_JSThreadConfig", obj); + + args.rval().setUndefined(); +} + +void JSThreadInfo::Functions::start(JSContext* cx, JS::CallArgs args) { + getConfig(cx, args)->start(); + + args.rval().setUndefined(); +} + +void JSThreadInfo::Functions::join(JSContext* cx, JS::CallArgs args) { + getConfig(cx, args)->join(); + + args.rval().setUndefined(); +} + +void JSThreadInfo::Functions::hasFailed(JSContext* cx, JS::CallArgs args) { + args.rval().setBoolean(getConfig(cx, args)->hasFailed()); +} + +void JSThreadInfo::Functions::returnData(JSContext* cx, JS::CallArgs args) { + ValueReader(cx, args.rval()) + .fromBSONElement(getConfig(cx, args)->returnData().firstElement(), true); +} + +void JSThreadInfo::Functions::_threadInject(JSContext* cx, JS::CallArgs args) { + uassert(ErrorCodes::JSInterpreterFailure, + "threadInject takes exactly 1 argument", + args.length() == 1); + uassert(ErrorCodes::JSInterpreterFailure, + "threadInject needs to be passed a prototype", + args.get(0).isObject()); + + JS::RootedObject o(cx, args.get(0).toObjectOrNull()); + + if (!JS_DefineFunctions(cx, o, JSThreadInfo::threadMethods)) + throwCurrentJSException(cx, ErrorCodes::JSInterpreterFailure, "Failed to define functions"); + + args.rval().setUndefined(); +} + +void JSThreadInfo::Functions::_scopedThreadInject(JSContext* cx, JS::CallArgs args) { + _threadInject(cx, args); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/jsthread.h b/src/mongo/scripting/mozjs/jsthread.h new file mode 100644 index 00000000000..ff468d987ab --- /dev/null +++ b/src/mongo/scripting/mozjs/jsthread.h @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * Helper for the JSThread javascript object + * + * The workflow is strange because we have a thing in javascript called a + * JSThread, but we don't actually get to construct it. Instead, we have to + * inject methods into that thing (via _threadInject) and hang our C++ thread + * separately (via init() on that type). + * + * To manage lifetime, we just add a field into the injected object that's our + * JSThread and add our holder in as our JSThread's private member. + */ +struct JSThreadInfo : public BaseInfo { + static void finalize(JSFreeOp* fop, JSObject* obj); + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(init); + MONGO_DEFINE_JS_FUNCTION(start); + MONGO_DEFINE_JS_FUNCTION(join); + MONGO_DEFINE_JS_FUNCTION(hasFailed); + MONGO_DEFINE_JS_FUNCTION(returnData); + + MONGO_DEFINE_JS_FUNCTION(_threadInject); + MONGO_DEFINE_JS_FUNCTION(_scopedThreadInject); + }; + + /** + * Note that this isn't meant to supply methods for JSThread, it's just + * there to work with _threadInject. So the name isn't a mistake + */ + static const JSFunctionSpec threadMethods[6]; + static const JSFunctionSpec freeFunctions[3]; + + static const char* const className; + static const unsigned classFlags = JSCLASS_HAS_PRIVATE; + static const InstallType installType = InstallType::Private; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/maxkey.cpp b/src/mongo/scripting/mozjs/maxkey.cpp new file mode 100644 index 00000000000..022ce347773 --- /dev/null +++ b/src/mongo/scripting/mozjs/maxkey.cpp @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/maxkey.h" + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec MaxKeyInfo::methods[2] = { + MONGO_ATTACH_JS_FUNCTION(tojson), JS_FS_END, +}; + +const char* const MaxKeyInfo::className = "MaxKey"; + +namespace { +const char* const kSingleton = "singleton"; +} // namespace + +void MaxKeyInfo::construct(JSContext* cx, JS::CallArgs args) { + call(cx, args); +} + +/** + * The idea here is that MinKey and MaxKey are singleton callable objects that + * return the singleton when called. This enables all instances to compare + * == and === to MinKey even if created by "new MinKey()" in JS. + */ +void MaxKeyInfo::call(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + ObjectWrapper o(cx, scope->getMaxKeyProto().getProto()); + + JS::RootedValue val(cx); + + if (!o.hasField(kSingleton)) { + JS::RootedObject thisv(cx); + scope->getMaxKeyProto().newObject(&thisv); + + val.setObjectOrNull(thisv); + o.setValue(kSingleton, val); + } else { + o.getValue(kSingleton, &val); + } + + args.rval().setObjectOrNull(val.toObjectOrNull()); +} + +void MaxKeyInfo::Functions::tojson(JSContext* cx, JS::CallArgs args) { + ValueReader(cx, args.rval()).fromStringData("{ \"$maxKey\" : 1 }"); +} + +void MaxKeyInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto) { + ObjectWrapper protoWrapper(cx, proto); + + JS::RootedValue value(cx); + getScope(cx)->getMaxKeyProto().newObject(&value); + + ObjectWrapper(cx, global).setValue("MaxKey", value); + protoWrapper.setValue(kSingleton, value); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/maxkey.h b/src/mongo/scripting/mozjs/maxkey.h new file mode 100644 index 00000000000..ac5d937f157 --- /dev/null +++ b/src/mongo/scripting/mozjs/maxkey.h @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "MaxKey" Javascript object. + * + * These are slightly special, in that there is only one MaxKey object and + * whenever you call the constructor to make a new one you just get the + * "singleton" MaxKey from the prototype. See the postInstall for details. + */ +struct MaxKeyInfo : public BaseInfo { + static void call(JSContext* cx, JS::CallArgs args); + static void construct(JSContext* cx, JS::CallArgs args); + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(tojson); + }; + + static const JSFunctionSpec methods[2]; + + static void postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto); + + static const char* const className; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/minkey.cpp b/src/mongo/scripting/mozjs/minkey.cpp new file mode 100644 index 00000000000..fe06fa6bb49 --- /dev/null +++ b/src/mongo/scripting/mozjs/minkey.cpp @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/minkey.h" + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec MinKeyInfo::methods[2] = { + MONGO_ATTACH_JS_FUNCTION(tojson), JS_FS_END, +}; + +const char* const MinKeyInfo::className = "MinKey"; + +namespace { +const char* const kSingleton = "singleton"; +} // namespace + +void MinKeyInfo::construct(JSContext* cx, JS::CallArgs args) { + call(cx, args); +} + +/** + * The idea here is that MinKey and MaxKey are singleton callable objects that + * return the singleton when called. This enables all instances to compare + * == and === to MinKey even if created by "new MinKey()" in JS. + */ +void MinKeyInfo::call(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + ObjectWrapper o(cx, scope->getMinKeyProto().getProto()); + + JS::RootedValue val(cx); + + if (!o.hasField(kSingleton)) { + JS::RootedObject thisv(cx); + scope->getMinKeyProto().newObject(&thisv); + + val.setObjectOrNull(thisv); + o.setValue(kSingleton, val); + } else { + o.getValue(kSingleton, &val); + } + + args.rval().setObjectOrNull(val.toObjectOrNull()); +} + +void MinKeyInfo::Functions::tojson(JSContext* cx, JS::CallArgs args) { + ValueReader(cx, args.rval()).fromStringData("{ \"$minKey\" : 1 }"); +} + +void MinKeyInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto) { + ObjectWrapper protoWrapper(cx, proto); + + JS::RootedValue value(cx); + getScope(cx)->getMinKeyProto().newObject(&value); + + ObjectWrapper(cx, global).setValue("MinKey", value); + protoWrapper.setValue(kSingleton, value); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/minkey.h b/src/mongo/scripting/mozjs/minkey.h new file mode 100644 index 00000000000..caea91d54ac --- /dev/null +++ b/src/mongo/scripting/mozjs/minkey.h @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "MinKey" Javascript object. + * + * These are slightly special, in that there is only one MinKey object and + * whenever you call the constructor to make a new one you just get the + * "singleton" MinKey from the prototype. See the postInstall for details. + */ +struct MinKeyInfo : public BaseInfo { + static void call(JSContext* cx, JS::CallArgs args); + static void construct(JSContext* cx, JS::CallArgs args); + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(tojson); + }; + + static const JSFunctionSpec methods[2]; + + static void postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto); + + static const char* const className; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/mongo.cpp b/src/mongo/scripting/mozjs/mongo.cpp new file mode 100644 index 00000000000..f1f69d0d39c --- /dev/null +++ b/src/mongo/scripting/mozjs/mongo.cpp @@ -0,0 +1,565 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/mongo.h" + +#include "mongo/client/dbclientinterface.h" +#include "mongo/client/native_sasl_client_session.h" +#include "mongo/client/sasl_client_authenticate.h" +#include "mongo/client/sasl_client_session.h" +#include "mongo/db/namespace_string.h" +#include "mongo/scripting/mozjs/cursor.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/stdx/memory.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec MongoBase::methods[13] = { + MONGO_ATTACH_JS_FUNCTION(auth), + MONGO_ATTACH_JS_FUNCTION(copyDatabaseWithSCRAM), + MONGO_ATTACH_JS_FUNCTION(cursorFromId), + MONGO_ATTACH_JS_FUNCTION(find), + MONGO_ATTACH_JS_FUNCTION(getClientRPCProtocols), + MONGO_ATTACH_JS_FUNCTION(getServerRPCProtocols), + MONGO_ATTACH_JS_FUNCTION(insert), + MONGO_ATTACH_JS_FUNCTION(logout), + MONGO_ATTACH_JS_FUNCTION(remove), + MONGO_ATTACH_JS_FUNCTION(runCommand), + MONGO_ATTACH_JS_FUNCTION(setClientRPCProtocols), + MONGO_ATTACH_JS_FUNCTION(update), + JS_FS_END, +}; + +const char* const MongoBase::className = "Mongo"; + +const JSFunctionSpec MongoExternalInfo::freeFunctions[2] = { + MONGO_ATTACH_JS_FUNCTION(load), JS_FS_END, +}; + +namespace { +DBClientBase* getConnection(JS::CallArgs& args) { + return static_cast<std::shared_ptr<DBClientBase>*>(JS_GetPrivate(args.thisv().toObjectOrNull())) + ->get(); +} + +void setCursor(JS::HandleObject target, + std::unique_ptr<DBClientCursor> cursor, + JS::CallArgs& args) { + auto client = + static_cast<std::shared_ptr<DBClientBase>*>(JS_GetPrivate(args.thisv().toObjectOrNull())); + + // Copy the client shared pointer to up the refcount + JS_SetPrivate(target, new CursorInfo::CursorHolder(std::move(cursor), *client)); +} +} // namespace + +void MongoBase::finalize(JSFreeOp* fop, JSObject* obj) { + auto conn = static_cast<std::shared_ptr<DBClientBase>*>(JS_GetPrivate(obj)); + + if (conn) { + delete conn; + } +} + +void MongoBase::Functions::runCommand(JSContext* cx, JS::CallArgs args) { + if (args.length() != 3) + uasserted(ErrorCodes::BadValue, "runCommand needs 3 args"); + + if (!args.get(0).isString()) + uasserted(ErrorCodes::BadValue, "the database parameter to runCommand must be a string"); + + if (!args.get(1).isObject()) + uasserted(ErrorCodes::BadValue, "the cmdObj parameter to runCommand must be an object"); + + if (!args.get(2).isNumber()) + uasserted(ErrorCodes::BadValue, "the options parameter to runCommand must be a number"); + + auto conn = getConnection(args); + + std::string database = ValueWriter(cx, args.get(0)).toString(); + + BSONObj cmdObj = ValueWriter(cx, args.get(1)).toBSON(); + + int queryOptions = ValueWriter(cx, args.get(2)).toInt32(); + BSONObj cmdRes; + conn->runCommand(database, cmdObj, cmdRes, queryOptions); + + // the returned object is not read only as some of our tests depend on modifying it. + ValueReader(cx, args.rval()).fromBSON(cmdRes, false /* read only */); +} + +void MongoBase::Functions::find(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + if (args.length() != 7) + uasserted(ErrorCodes::BadValue, "find needs 7 args"); + + if (!args.get(1).isObject()) + uasserted(ErrorCodes::BadValue, "needs to be an object"); + + auto conn = getConnection(args); + + std::string ns = ValueWriter(cx, args.get(0)).toString(); + + BSONObj fields; + BSONObj q = ValueWriter(cx, args.get(1)).toBSON(); + + bool haveFields = false; + + if (args.get(2).isObject()) { + size_t i = 0; + + JS::RootedObject obj(cx, args.get(2).toObjectOrNull()); + + ObjectWrapper(cx, obj).enumerate([&i](jsid) { ++i; }); + + if (i > 0) + haveFields = true; + } + + if (haveFields) + fields = ValueWriter(cx, args.get(2)).toBSON(); + + int nToReturn = ValueWriter(cx, args.get(3)).toInt32(); + int nToSkip = ValueWriter(cx, args.get(4)).toInt32(); + int batchSize = ValueWriter(cx, args.get(5)).toInt32(); + int options = ValueWriter(cx, args.get(6)).toInt32(); + + std::unique_ptr<DBClientCursor> cursor( + conn->query(ns, q, nToReturn, nToSkip, haveFields ? &fields : NULL, options, batchSize)); + if (!cursor.get()) { + uasserted(ErrorCodes::InternalError, "error doing query: failed"); + } + + JS::RootedObject c(cx); + scope->getCursorProto().newInstance(&c); + + setCursor(c, std::move(cursor), args); + + args.rval().setObjectOrNull(c); +} + +void MongoBase::Functions::insert(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + if (args.length() != 3) + uasserted(ErrorCodes::BadValue, "insert needs 3 args"); + + if (!args.get(1).isObject()) + uasserted(ErrorCodes::BadValue, "attempted to insert a non-object"); + + ObjectWrapper o(cx, args.thisv()); + + if (o.hasField("readOnly") && o.getBoolean("readOnly")) + uasserted(ErrorCodes::BadValue, "js db in read only mode"); + + auto conn = getConnection(args); + + std::string ns = ValueWriter(cx, args.get(0)).toString(); + + int flags = ValueWriter(cx, args.get(2)).toInt32(); + + auto addId = [cx, scope](JS::HandleValue value) { + if (!value.isObject()) + uasserted(ErrorCodes::BadValue, "attempted to insert a non-object type"); + + JS::RootedObject elementObj(cx, value.toObjectOrNull()); + + ObjectWrapper ele(cx, elementObj); + + if (!ele.hasField("_id")) { + JS::RootedValue value(cx); + scope->getOidProto().newInstance(&value); + ele.setValue("_id", value); + } + + return ValueWriter(cx, value).toBSON(); + }; + + if (args.get(1).isObject() && JS_IsArrayObject(cx, args.get(1))) { + JS::RootedObject obj(cx, args.get(1).toObjectOrNull()); + ObjectWrapper array(cx, obj); + + std::vector<BSONObj> bos; + + bool foundElement = false; + + array.enumerate([&](JS::HandleId id) { + foundElement = true; + + JS::RootedValue value(cx); + array.getValue(id, &value); + + bos.push_back(addId(value)); + }); + + if (!foundElement) + uasserted(ErrorCodes::BadValue, "attempted to insert an empty array"); + + conn->insert(ns, bos, flags); + } else { + conn->insert(ns, addId(args.get(1))); + } + + args.rval().setUndefined(); +} + +void MongoBase::Functions::remove(JSContext* cx, JS::CallArgs args) { + if (!(args.length() == 2 || args.length() == 3)) + uasserted(ErrorCodes::BadValue, "remove needs 2 or 3 args"); + + if (!(args.get(1).isObject())) + uasserted(ErrorCodes::BadValue, "attempted to remove a non-object"); + + ObjectWrapper o(cx, args.thisv()); + + if (o.hasField("readOnly") && o.getBoolean("readOnly")) + uasserted(ErrorCodes::BadValue, "js db in read only mode"); + + auto conn = getConnection(args); + std::string ns = ValueWriter(cx, args.get(0)).toString(); + + BSONObj bson = ValueWriter(cx, args.get(1)).toBSON(); + + bool justOne = false; + if (args.length() > 2) { + justOne = args.get(2).toBoolean(); + } + + conn->remove(ns, bson, justOne); + args.rval().setUndefined(); +} + +void MongoBase::Functions::update(JSContext* cx, JS::CallArgs args) { + if (args.length() < 3) + uasserted(ErrorCodes::BadValue, "update needs at least 3 args"); + + if (!args.get(1).isObject()) + uasserted(ErrorCodes::BadValue, "1st param to update has to be an object"); + + if (!args.get(2).isObject()) + uasserted(ErrorCodes::BadValue, "2nd param to update has to be an object"); + + ObjectWrapper o(cx, args.thisv()); + + if (o.hasField("readOnly") && o.getBoolean("readOnly")) + uasserted(ErrorCodes::BadValue, "js db in read only mode"); + + auto conn = getConnection(args); + std::string ns = ValueWriter(cx, args.get(0)).toString(); + + BSONObj q1 = ValueWriter(cx, args.get(1)).toBSON(); + BSONObj o1 = ValueWriter(cx, args.get(2)).toBSON(); + + bool upsert = args.length() > 3 && args.get(3).isBoolean() && args.get(3).toBoolean(); + bool multi = args.length() > 4 && args.get(4).isBoolean() && args.get(4).toBoolean(); + + conn->update(ns, q1, o1, upsert, multi); + args.rval().setUndefined(); +} + +void MongoBase::Functions::auth(JSContext* cx, JS::CallArgs args) { + auto conn = getConnection(args); + if (!conn) + uasserted(ErrorCodes::BadValue, "no connection"); + + BSONObj params; + switch (args.length()) { + case 1: + params = ValueWriter(cx, args.get(0)).toBSON(); + break; + case 3: + params = BSON(saslCommandMechanismFieldName + << "MONGODB-CR" << saslCommandUserDBFieldName + << ValueWriter(cx, args[0]).toString() << saslCommandUserFieldName + << ValueWriter(cx, args[1]).toString() << saslCommandPasswordFieldName + << ValueWriter(cx, args[2]).toString()); + break; + default: + uasserted(ErrorCodes::BadValue, "mongoAuth takes 1 object or 3 string arguments"); + } + + conn->auth(params); + + args.rval().setBoolean(true); +} + +void MongoBase::Functions::logout(JSContext* cx, JS::CallArgs args) { + if (args.length() != 1) + uasserted(ErrorCodes::BadValue, "logout needs 1 arg"); + + BSONObj ret; + + std::string db = ValueWriter(cx, args.get(0)).toString(); + + auto conn = getConnection(args); + if (conn) { + conn->logout(db, ret); + } + + ValueReader(cx, args.rval()).fromBSON(ret, false); +} + +void MongoBase::Functions::cursorFromId(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + if (!(args.length() == 2 || args.length() == 3)) + uasserted(ErrorCodes::BadValue, "cursorFromId needs 2 or 3 args"); + + if (!scope->getNumberLongProto().instanceOf(args.get(1))) + uasserted(ErrorCodes::BadValue, "2nd arg must be a NumberLong"); + + if (!(args.get(2).isNumber() || args.get(2).isUndefined())) + uasserted(ErrorCodes::BadValue, "3rd arg must be a js Number"); + + auto conn = getConnection(args); + + std::string ns = ValueWriter(cx, args.get(0)).toString(); + + long long cursorId = NumberLongInfo::ToNumberLong(cx, args.get(1)); + + auto cursor = stdx::make_unique<DBClientCursor>(conn, ns, cursorId, 0, 0); + + if (args.get(2).isNumber()) + cursor->setBatchSize(ValueWriter(cx, args.get(2)).toInt32()); + + JS::RootedObject c(cx); + scope->getCursorProto().newInstance(&c); + + setCursor(c, std::move(cursor), args); + + args.rval().setObjectOrNull(c); +} + +void MongoBase::Functions::copyDatabaseWithSCRAM(JSContext* cx, JS::CallArgs args) { + auto conn = getConnection(args); + + if (!conn) + uasserted(ErrorCodes::BadValue, "no connection"); + + if (args.length() != 5) + uasserted(ErrorCodes::BadValue, "copyDatabase needs 5 arg"); + + // copyDatabase(fromdb, todb, fromhost, username, password); + std::string fromDb = ValueWriter(cx, args.get(0)).toString(); + std::string toDb = ValueWriter(cx, args.get(1)).toString(); + std::string fromHost = ValueWriter(cx, args.get(2)).toString(); + std::string user = ValueWriter(cx, args.get(3)).toString(); + std::string password = ValueWriter(cx, args.get(4)).toString(); + + std::string hashedPwd = DBClientWithCommands::createPasswordDigest(user, password); + + std::unique_ptr<SaslClientSession> session(new NativeSaslClientSession()); + + session->setParameter(SaslClientSession::parameterMechanism, "SCRAM-SHA-1"); + session->setParameter(SaslClientSession::parameterUser, user); + session->setParameter(SaslClientSession::parameterPassword, hashedPwd); + session->initialize(); + + BSONObj saslFirstCommandPrefix = + BSON("copydbsaslstart" << 1 << "fromhost" << fromHost << "fromdb" << fromDb + << saslCommandMechanismFieldName << "SCRAM-SHA-1"); + + BSONObj saslFollowupCommandPrefix = + BSON("copydb" << 1 << "fromhost" << fromHost << "fromdb" << fromDb << "todb" << toDb); + + BSONObj saslCommandPrefix = saslFirstCommandPrefix; + BSONObj inputObj = BSON(saslCommandPayloadFieldName << ""); + bool isServerDone = false; + + while (!session->isDone()) { + std::string payload; + BSONType type; + + Status status = saslExtractPayload(inputObj, &payload, &type); + uassertStatusOK(status); + + std::string responsePayload; + status = session->step(payload, &responsePayload); + uassertStatusOK(status); + + BSONObjBuilder commandBuilder; + + commandBuilder.appendElements(saslCommandPrefix); + commandBuilder.appendBinData(saslCommandPayloadFieldName, + static_cast<int>(responsePayload.size()), + BinDataGeneral, + responsePayload.c_str()); + BSONElement conversationId = inputObj[saslCommandConversationIdFieldName]; + if (!conversationId.eoo()) + commandBuilder.append(conversationId); + + BSONObj command = commandBuilder.obj(); + + bool ok = conn->runCommand("admin", command, inputObj); + + ErrorCodes::Error code = + ErrorCodes::fromInt(inputObj[saslCommandCodeFieldName].numberInt()); + + if (!ok || code != ErrorCodes::OK) { + if (code == ErrorCodes::OK) + code = ErrorCodes::UnknownError; + + ValueReader(cx, args.rval()).fromBSON(inputObj, true); + return; + } + + isServerDone = inputObj[saslCommandDoneFieldName].trueValue(); + saslCommandPrefix = saslFollowupCommandPrefix; + } + + if (!isServerDone) { + uasserted(ErrorCodes::InternalError, "copydb client finished before server."); + } + + ValueReader(cx, args.rval()).fromBSON(inputObj, true); +} + +void MongoBase::Functions::getClientRPCProtocols(JSContext* cx, JS::CallArgs args) { + auto conn = getConnection(args); + + if (args.length() != 0) + uasserted(ErrorCodes::BadValue, "getClientRPCProtocols takes no args"); + + auto clientRPCProtocols = rpc::toString(conn->getClientRPCProtocols()); + uassertStatusOK(clientRPCProtocols); + + auto protoStr = clientRPCProtocols.getValue().toString(); + + ValueReader(cx, args.rval()).fromStringData(protoStr); +} + +void MongoBase::Functions::setClientRPCProtocols(JSContext* cx, JS::CallArgs args) { + auto conn = getConnection(args); + + if (args.length() != 1) + uasserted(ErrorCodes::BadValue, "setClientRPCProtocols needs 1 arg"); + if (!args.get(0).isString()) + uasserted(ErrorCodes::BadValue, "first argument to setClientRPCProtocols must be a string"); + + std::string rpcProtosStr = ValueWriter(cx, args.get(0)).toString(); + + auto clientRPCProtocols = rpc::parseProtocolSet(rpcProtosStr); + uassertStatusOK(clientRPCProtocols); + + conn->setClientRPCProtocols(clientRPCProtocols.getValue()); + + args.rval().setUndefined(); +} + +void MongoBase::Functions::getServerRPCProtocols(JSContext* cx, JS::CallArgs args) { + auto conn = getConnection(args); + + if (args.length() != 0) + uasserted(ErrorCodes::BadValue, "getServerRPCProtocols takes no args"); + + auto serverRPCProtocols = rpc::toString(conn->getServerRPCProtocols()); + uassertStatusOK(serverRPCProtocols); + + auto protoStr = serverRPCProtocols.getValue().toString(); + + ValueReader(cx, args.rval()).fromStringData(protoStr); +} + +void MongoLocalInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + if (args.length() != 0) + uasserted(ErrorCodes::BadValue, "local Mongo constructor takes no args"); + + std::unique_ptr<DBClientBase> conn; + + conn.reset(createDirectClient(scope->getOpContext())); + + JS::RootedObject thisv(cx); + scope->getMongoLocalProto().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + JS_SetPrivate(thisv, new std::shared_ptr<DBClientBase>(conn.release())); + + o.setBoolean("slaveOk", false); + o.setString("host", "EMBEDDED"); + + args.rval().setObjectOrNull(thisv); +} + +void MongoExternalInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + std::string host("127.0.0.1"); + + if (args.length() > 0 && args.get(0).isString()) { + host = ValueWriter(cx, args.get(0)).toString(); + } + + auto statusWithHost = ConnectionString::parse(host); + uassertStatusOK(statusWithHost); + + const ConnectionString cs(statusWithHost.getValue()); + + std::string errmsg; + std::unique_ptr<DBClientBase> conn(cs.connect(errmsg)); + + if (!conn.get()) { + uasserted(ErrorCodes::InternalError, errmsg); + } + + JS::RootedObject thisv(cx); + scope->getMongoExternalProto().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + JS_SetPrivate(thisv, new std::shared_ptr<DBClientBase>(conn.release())); + + o.setBoolean("slaveOk", false); + o.setString("host", host); + + args.rval().setObjectOrNull(thisv); +} + +void MongoExternalInfo::Functions::load(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + for (unsigned i = 0; i < args.length(); ++i) { + std::string filename = ValueWriter(cx, args.get(i)).toString(); + + if (!scope->execFile(filename, false, true)) { + uasserted(ErrorCodes::BadValue, std::string("error loading js file: ") + filename); + } + } + + args.rval().setBoolean(true); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/mongo.h b/src/mongo/scripting/mozjs/mongo.h new file mode 100644 index 00000000000..35815da5455 --- /dev/null +++ b/src/mongo/scripting/mozjs/mongo.h @@ -0,0 +1,88 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * Shared code for the "Mongo" javascript object. + * + * The idea here is that there is a lot of shared functionality between the + * "Mongo" we see in the shell and the "Mongo" in dbeval. So we provide one + * info type with common code and differentiate with varying constructors. + */ +struct MongoBase : public BaseInfo { + static void finalize(JSFreeOp* fop, JSObject* obj); + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(auth); + MONGO_DEFINE_JS_FUNCTION(copyDatabaseWithSCRAM); + MONGO_DEFINE_JS_FUNCTION(cursorFromId); + MONGO_DEFINE_JS_FUNCTION(find); + MONGO_DEFINE_JS_FUNCTION(getClientRPCProtocols); + MONGO_DEFINE_JS_FUNCTION(getServerRPCProtocols); + MONGO_DEFINE_JS_FUNCTION(insert); + MONGO_DEFINE_JS_FUNCTION(logout); + MONGO_DEFINE_JS_FUNCTION(remove); + MONGO_DEFINE_JS_FUNCTION(runCommand); + MONGO_DEFINE_JS_FUNCTION(setClientRPCProtocols); + MONGO_DEFINE_JS_FUNCTION(update); + }; + + static const JSFunctionSpec methods[13]; + + static const char* const className; + static const unsigned classFlags = JSCLASS_HAS_PRIVATE; +}; + +/** + * The dbeval variant of "Mongo" + */ +struct MongoLocalInfo : public MongoBase { + static void construct(JSContext* cx, JS::CallArgs args); +}; + +/** + * The shell variant of "Mongo" + */ +struct MongoExternalInfo : public MongoBase { + static void construct(JSContext* cx, JS::CallArgs args); + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(load); + }; + + static const JSFunctionSpec freeFunctions[2]; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/nativefunction.cpp b/src/mongo/scripting/mozjs/nativefunction.cpp new file mode 100644 index 00000000000..010b6a13587 --- /dev/null +++ b/src/mongo/scripting/mozjs/nativefunction.cpp @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/nativefunction.h" + +#include <cstdio> + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/util/mongoutils/str.h" + +namespace mongo { +namespace mozjs { + +const char* const NativeFunctionInfo::inheritFrom = "Function"; +const char* const NativeFunctionInfo::className = "NativeFunction"; + +const JSFunctionSpec NativeFunctionInfo::methods[2] = { + MONGO_ATTACH_JS_FUNCTION(toString), JS_FS_END, +}; + +namespace { + +/** + * Holder for the caller of ::make()'s callback function and context pointer + */ +class NativeHolder { +public: + NativeHolder(NativeFunction func, void* ctx) : _func(func), _ctx(ctx) {} + + NativeFunction _func; + void* _ctx; +}; + +NativeHolder* getHolder(JS::CallArgs args) { + return static_cast<NativeHolder*>(JS_GetPrivate(&args.callee())); +} + +} // namespace + +void NativeFunctionInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + scope->getNativeFunctionProto().newObject(args.rval()); +} + +void NativeFunctionInfo::call(JSContext* cx, JS::CallArgs args) { + auto holder = getHolder(args); + + BSONObjBuilder bob; + + for (unsigned i = 0; i < args.length(); i++) { + // 11 is enough here. unsigned's are only 32 bits, and 1 << 32 is only + // 10 decimal digits. +1 for the null and we're only at 11. + char buf[11]; + std::sprintf(buf, "%i", i); + + ValueWriter(cx, args.get(i)).writeThis(&bob, buf); + } + + BSONObj out = holder->_func(bob.obj(), holder->_ctx); + + ValueReader(cx, args.rval()).fromBSONElement(out.firstElement(), false); +} + +void NativeFunctionInfo::finalize(JSFreeOp* fop, JSObject* obj) { + auto holder = static_cast<NativeHolder*>(JS_GetPrivate(obj)); + + if (holder) + delete holder; +} + +void NativeFunctionInfo::Functions::toString(JSContext* cx, JS::CallArgs args) { + ObjectWrapper o(cx, args.thisv()); + + str::stream ss; + ss << "[native code]"; + + ValueReader(cx, args.rval()).fromStringData(ss.operator std::string()); +} + + +void NativeFunctionInfo::make(JSContext* cx, + JS::MutableHandleObject obj, + NativeFunction function, + void* data) { + auto scope = getScope(cx); + + scope->getNativeFunctionProto().newInstance(obj); + + JS_SetPrivate(obj, new NativeHolder(function, data)); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/nativefunction.h b/src/mongo/scripting/mozjs/nativefunction.h new file mode 100644 index 00000000000..0467b2c4743 --- /dev/null +++ b/src/mongo/scripting/mozjs/nativefunction.h @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/engine.h" +#include "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * Wrapper for JS Interpreter agnostic functions. Think mapReduce, or any use + * case that can tolerate automatic json <-> bson translation. + * + * The business end of the shim methods comes via ::call(). These types are + * invokable as js functions, with a little bit of automatic translation for + * arguments. + * + * This inherits from the global Function type. + * + * Also note that installType is private. So you can only get NativeFunctions + * in JS via ::make() from C++. + */ +struct NativeFunctionInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + static void call(JSContext* cx, JS::CallArgs args); + static void finalize(JSFreeOp* fop, JSObject* obj); + + static const char* const inheritFrom; + static const char* const className; + static const unsigned classFlags = JSCLASS_HAS_PRIVATE; + static const InstallType installType = InstallType::Private; + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(toString); + }; + + static const JSFunctionSpec methods[2]; + + static void make(JSContext* cx, + JS::MutableHandleObject obj, + NativeFunction function, + void* data); +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/numberint.cpp b/src/mongo/scripting/mozjs/numberint.cpp new file mode 100644 index 00000000000..1704154ac74 --- /dev/null +++ b/src/mongo/scripting/mozjs/numberint.cpp @@ -0,0 +1,112 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/numberint.h" + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/util/mongoutils/str.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec NumberIntInfo::methods[4] = { + MONGO_ATTACH_JS_FUNCTION(toNumber), + MONGO_ATTACH_JS_FUNCTION(toString), + MONGO_ATTACH_JS_FUNCTION(valueOf), + JS_FS_END, +}; + +const char* const NumberIntInfo::className = "NumberInt"; + +void NumberIntInfo::finalize(JSFreeOp* fop, JSObject* obj) { + auto x = static_cast<int*>(JS_GetPrivate(obj)); + + if (x) + delete x; +} + +int NumberIntInfo::ToNumberInt(JSContext* cx, JS::HandleValue thisv) { + auto x = static_cast<int*>(JS_GetPrivate(thisv.toObjectOrNull())); + + return x ? *x : 0; +} + +int NumberIntInfo::ToNumberInt(JSContext* cx, JS::HandleObject thisv) { + auto x = static_cast<int*>(JS_GetPrivate(thisv)); + + return x ? *x : 0; +} + +void NumberIntInfo::Functions::valueOf(JSContext* cx, JS::CallArgs args) { + int out = NumberIntInfo::ToNumberInt(cx, args.thisv()); + + args.rval().setInt32(out); +} + +void NumberIntInfo::Functions::toNumber(JSContext* cx, JS::CallArgs args) { + valueOf(cx, args); +} + +void NumberIntInfo::Functions::toString(JSContext* cx, JS::CallArgs args) { + int val = NumberIntInfo::ToNumberInt(cx, args.thisv()); + + str::stream ss; + ss << "NumberInt(" << val << ")"; + + ValueReader(cx, args.rval()).fromStringData(ss.operator std::string()); +} + +void NumberIntInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + JS::RootedObject thisv(cx); + + scope->getNumberIntProto().newObject(&thisv); + + int32_t x = 0; + + if (args.length() == 0) { + // Do nothing + } else if (args.length() == 1) { + x = ValueWriter(cx, args.get(0)).toInt32(); + } else { + uasserted(ErrorCodes::BadValue, "NumberInt takes 0 or 1 arguments"); + } + + JS_SetPrivate(thisv, new int(x)); + + args.rval().setObjectOrNull(thisv); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/numberint.h b/src/mongo/scripting/mozjs/numberint.h new file mode 100644 index 00000000000..378c3a0d57e --- /dev/null +++ b/src/mongo/scripting/mozjs/numberint.h @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "NumberInt" Javascript object. + * + * Wraps an actual c++ 'int' as its private member + */ +struct NumberIntInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + static void finalize(JSFreeOp* fop, JSObject* obj); + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(toNumber); + MONGO_DEFINE_JS_FUNCTION(toString); + MONGO_DEFINE_JS_FUNCTION(valueOf); + }; + + static const JSFunctionSpec methods[4]; + + static const char* const className; + static const unsigned classFlags = JSCLASS_HAS_PRIVATE; + + static int ToNumberInt(JSContext* cx, JS::HandleObject object); + static int ToNumberInt(JSContext* cx, JS::HandleValue value); +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/numberlong.cpp b/src/mongo/scripting/mozjs/numberlong.cpp new file mode 100644 index 00000000000..f8955689c9e --- /dev/null +++ b/src/mongo/scripting/mozjs/numberlong.cpp @@ -0,0 +1,164 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/numberlong.h" + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/util/mongoutils/str.h" +#include "mongo/util/text.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec NumberLongInfo::methods[4] = { + MONGO_ATTACH_JS_FUNCTION(toNumber), + MONGO_ATTACH_JS_FUNCTION(toString), + MONGO_ATTACH_JS_FUNCTION(valueOf), + JS_FS_END, +}; + +const char* const NumberLongInfo::className = "NumberLong"; + +namespace { +const char* const kTop = "top"; +const char* const kBottom = "bottom"; +const char* const kFloatApprox = "floatApprox"; +} // namespace + +long long NumberLongInfo::ToNumberLong(JSContext* cx, JS::HandleValue thisv) { + JS::RootedObject obj(cx, thisv.toObjectOrNull()); + return ToNumberLong(cx, obj); +} + +long long NumberLongInfo::ToNumberLong(JSContext* cx, JS::HandleObject thisv) { + ObjectWrapper o(cx, thisv); + + if (!o.hasField(kTop)) { + if (!o.hasField(kFloatApprox)) + uasserted(ErrorCodes::InternalError, "No top and no floatApprox fields"); + + return o.getNumber(kFloatApprox); + } + + if (!o.hasField(kBottom)) + uasserted(ErrorCodes::InternalError, "top but no bottom field"); + + return ((unsigned long long)((long long)o.getNumber(kTop) << 32) + + (unsigned)(o.getNumber(kBottom))); +} + +void NumberLongInfo::Functions::valueOf(JSContext* cx, JS::CallArgs args) { + long long out = NumberLongInfo::ToNumberLong(cx, args.thisv()); + + args.rval().setDouble(out); +} + +void NumberLongInfo::Functions::toNumber(JSContext* cx, JS::CallArgs args) { + valueOf(cx, args); +} + +void NumberLongInfo::Functions::toString(JSContext* cx, JS::CallArgs args) { + str::stream ss; + + long long val = NumberLongInfo::ToNumberLong(cx, args.thisv()); + + const long long limit = 2LL << 30; + + if (val <= -limit || limit <= val) + ss << "NumberLong(\"" << val << "\")"; + else + ss << "NumberLong(" << val << ")"; + + ValueReader(cx, args.rval()).fromStringData(ss.operator std::string()); +} + +void NumberLongInfo::construct(JSContext* cx, JS::CallArgs args) { + uassert(ErrorCodes::BadValue, + "NumberLong needs 0, 1 or 3 arguments", + args.length() == 0 || args.length() == 1 || args.length() == 3); + + auto scope = getScope(cx); + + JS::RootedObject thisv(cx); + + scope->getNumberLongProto().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + JS::RootedValue floatApprox(cx); + JS::RootedValue top(cx); + JS::RootedValue bottom(cx); + + if (args.length() == 0) { + o.setNumber(kFloatApprox, 0); + } else if (args.length() == 1) { + if (args.get(0).isNumber()) { + o.setValue(kFloatApprox, args.get(0)); + } else { + std::string str = ValueWriter(cx, args.get(0)).toString(); + + unsigned long long val = parseLL(str.c_str()); + + // values above 2^53 are not accurately represented in JS + if ((long long)val == (long long)(double)(long long)(val) && + val < 9007199254740992ULL) { + o.setNumber(kFloatApprox, val); + } else { + o.setNumber(kFloatApprox, val); + o.setNumber(kTop, val >> 32); + o.setNumber(kBottom, val & 0x00000000ffffffff); + } + } + } else { + if (!args.get(0).isNumber()) + uasserted(ErrorCodes::BadValue, "floatApprox must be a number"); + + if (!args.get(1).isNumber() || + args.get(1).toNumber() != + static_cast<double>(static_cast<uint32_t>(args.get(1).toNumber()))) + uasserted(ErrorCodes::BadValue, "top must be a 32 bit unsigned number"); + + if (!args.get(2).isNumber() || + args.get(2).toNumber() != + static_cast<double>(static_cast<uint32_t>(args.get(2).toNumber()))) + uasserted(ErrorCodes::BadValue, "bottom must be a 32 bit unsigned number"); + + o.setValue(kFloatApprox, args.get(0)); + o.setValue(kTop, args.get(1)); + o.setValue(kBottom, args.get(2)); + } + + args.rval().setObjectOrNull(thisv); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/numberlong.h b/src/mongo/scripting/mozjs/numberlong.h new file mode 100644 index 00000000000..bedfa558b2b --- /dev/null +++ b/src/mongo/scripting/mozjs/numberlong.h @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "NumberLong" Javascript object. + * + * Represents a 64 integer with a JS representation like: + * + * { + * top : Double, + * bottom : Double, + * floatApprox : Double, + * } + * + * Where top is the high 32 bits, bottom the low 32 bits and floatApprox a + * floating point approximation. + */ +struct NumberLongInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(toNumber); + MONGO_DEFINE_JS_FUNCTION(toString); + MONGO_DEFINE_JS_FUNCTION(valueOf); + }; + + static const JSFunctionSpec methods[4]; + + static const char* const className; + + static long long ToNumberLong(JSContext* cx, JS::HandleObject object); + static long long ToNumberLong(JSContext* cx, JS::HandleValue value); +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/object.cpp b/src/mongo/scripting/mozjs/object.cpp new file mode 100644 index 00000000000..a90bec36a72 --- /dev/null +++ b/src/mongo/scripting/mozjs/object.cpp @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/object.h" + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec ObjectInfo::methods[3] = { + MONGO_ATTACH_JS_FUNCTION(bsonsize), MONGO_ATTACH_JS_FUNCTION(invalidForStorage), JS_FS_END, +}; + +const char* const ObjectInfo::className = "Object"; + +void ObjectInfo::Functions::bsonsize(JSContext* cx, JS::CallArgs args) { + if (args.length() != 1) + uasserted(ErrorCodes::BadValue, "bsonsize needs 1 argument"); + + if (args.get(0).isNull()) { + args.rval().setInt32(0); + return; + } + + if (!args.get(0).isObject()) + uasserted(ErrorCodes::BadValue, "argument to bsonsize has to be an object"); + + args.rval().setInt32(ValueWriter(cx, args.get(0)).toBSON().objsize()); +} + +void ObjectInfo::Functions::invalidForStorage(JSContext* cx, JS::CallArgs args) { + if (args.length() != 1) + uasserted(ErrorCodes::BadValue, "invalidForStorage needs 1 argument"); + + if (args.get(0).isNull()) { + args.rval().setNull(); + return; + } + + if (!args.get(0).isObject()) + uasserted(ErrorCodes::BadValue, "argument to invalidForStorage has to be an object"); + + Status validForStorage = ValueWriter(cx, args.get(0)).toBSON().storageValid(true); + if (validForStorage.isOK()) { + args.rval().setNull(); + return; + } + + std::string errmsg = str::stream() << validForStorage.codeString() << ": " + << validForStorage.reason(); + + ValueReader(cx, args.rval()).fromStringData(errmsg); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/object.h b/src/mongo/scripting/mozjs/object.h new file mode 100644 index 00000000000..70f475c3f0e --- /dev/null +++ b/src/mongo/scripting/mozjs/object.h @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * Adds some methods onto the JS type "Object" + * + * Note that this installs "overNative", so we don't actually do anything other + * than layer a couple of our own functions on top of the existing prototype. + */ +struct ObjectInfo : public BaseInfo { + struct Functions { + MONGO_DEFINE_JS_FUNCTION(bsonsize); + MONGO_DEFINE_JS_FUNCTION(invalidForStorage); + }; + + static const JSFunctionSpec methods[3]; + + static const char* const className; + + static const InstallType installType = InstallType::OverNative; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/objectwrapper.cpp b/src/mongo/scripting/mozjs/objectwrapper.cpp new file mode 100644 index 00000000000..0450eeee42b --- /dev/null +++ b/src/mongo/scripting/mozjs/objectwrapper.cpp @@ -0,0 +1,385 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/objectwrapper.h" + +#include "mongo/base/error_codes.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/scripting/mozjs/idwrapper.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" + +namespace mongo { +namespace mozjs { + +void ObjectWrapper::Key::get(JSContext* cx, JS::HandleObject o, JS::MutableHandleValue value) { + switch (_type) { + case Type::Field: + if (JS_GetProperty(cx, o, _field, value)) + return; + break; + case Type::Index: + if (JS_GetElement(cx, o, _idx, value)) + return; + break; + case Type::Id: { + JS::RootedId id(cx, _id); + + if (JS_GetPropertyById(cx, o, id, value)) + return; + break; + } + } + + throwCurrentJSException(cx, ErrorCodes::InternalError, "Failed to get value on a JSObject"); +} + +void ObjectWrapper::Key::set(JSContext* cx, JS::HandleObject o, JS::HandleValue value) { + switch (_type) { + case Type::Field: + if (JS_SetProperty(cx, o, _field, value)) + return; + break; + case Type::Index: + if (JS_SetElement(cx, o, _idx, value)) + return; + break; + case Type::Id: { + JS::RootedId id(cx, _id); + + if (JS_SetPropertyById(cx, o, id, value)) + return; + break; + } + } + + throwCurrentJSException(cx, ErrorCodes::InternalError, "Failed to set value on a JSObject"); +} + +void ObjectWrapper::Key::define(JSContext* cx, + JS::HandleObject o, + JS::HandleValue value, + unsigned attrs) { + switch (_type) { + case Type::Field: + if (JS_DefineProperty(cx, o, _field, value, attrs)) + return; + break; + case Type::Index: + if (JS_DefineElement(cx, o, _idx, value, attrs)) + return; + break; + case Type::Id: { + JS::RootedId id(cx, _id); + + if (JS_DefinePropertyById(cx, o, id, value, attrs)) + return; + break; + } + } + + throwCurrentJSException(cx, ErrorCodes::InternalError, "Failed to define value on a JSObject"); +} + +bool ObjectWrapper::Key::has(JSContext* cx, JS::HandleObject o) { + bool has; + + switch (_type) { + case Type::Field: + if (JS_HasProperty(cx, o, _field, &has)) + return has; + break; + case Type::Index: + if (JS_HasElement(cx, o, _idx, &has)) + return has; + break; + case Type::Id: { + JS::RootedId id(cx, _id); + + if (JS_HasPropertyById(cx, o, id, &has)) + return has; + break; + } + } + + throwCurrentJSException(cx, ErrorCodes::InternalError, "Failed to has value on a JSObject"); +} + +void ObjectWrapper::Key::del(JSContext* cx, JS::HandleObject o) { + switch (_type) { + case Type::Field: + if (JS_DeleteProperty(cx, o, _field)) + return; + break; + case Type::Index: + if (JS_DeleteElement(cx, o, _idx)) + return; + break; + case Type::Id: { + JS::RootedId id(cx, _id); + + // For some reason JS_DeletePropertyById doesn't link + if (JS_DeleteProperty(cx, o, IdWrapper(cx, id).toString().c_str())) + return; + break; + } + } + + throwCurrentJSException(cx, ErrorCodes::InternalError, "Failed to delete value on a JSObject"); +} + +std::string ObjectWrapper::Key::toString(JSContext* cx) { + switch (_type) { + case Type::Field: + return _field; + case Type::Index: + return std::to_string(_idx); + case Type::Id: { + JS::RootedId id(cx, _id); + return IdWrapper(cx, id).toString(); + } + } + + throwCurrentJSException( + cx, ErrorCodes::InternalError, "Failed to toString a ObjectWrapper::Key"); +} + +ObjectWrapper::ObjectWrapper(JSContext* cx, JS::HandleObject obj, int depth) + : _context(cx), _object(cx, obj), _depth(depth) {} + +ObjectWrapper::ObjectWrapper(JSContext* cx, JS::HandleValue value, int depth) + : _context(cx), _object(cx, value.toObjectOrNull()), _depth(depth) {} + +double ObjectWrapper::getNumber(Key key) { + JS::RootedValue x(_context); + getValue(key, &x); + + return ValueWriter(_context, x).toNumber(); +} + +int ObjectWrapper::getNumberInt(Key key) { + JS::RootedValue x(_context); + getValue(key, &x); + + return ValueWriter(_context, x).toInt32(); +} + +long long ObjectWrapper::getNumberLongLong(Key key) { + JS::RootedValue x(_context); + getValue(key, &x); + + return ValueWriter(_context, x).toInt64(); +} + +std::string ObjectWrapper::getString(Key key) { + JS::RootedValue x(_context); + getValue(key, &x); + + return ValueWriter(_context, x).toString(); +} + +bool ObjectWrapper::getBoolean(Key key) { + JS::RootedValue x(_context); + getValue(key, &x); + + return ValueWriter(_context, x).toBoolean(); +} + +BSONObj ObjectWrapper::getObject(Key key) { + JS::RootedValue x(_context); + getValue(key, &x); + + return ValueWriter(_context, x, _depth).toBSON(); +} + +void ObjectWrapper::getValue(Key key, JS::MutableHandleValue value) { + key.get(_context, _object, value); +} + +void ObjectWrapper::setNumber(Key key, double val) { + JS::RootedValue jsValue(_context); + jsValue.setDouble(val); + + setValue(key, jsValue); +} + +void ObjectWrapper::setString(Key key, StringData val) { + JS::RootedValue jsValue(_context); + ValueReader(_context, &jsValue).fromStringData(val); + + setValue(key, jsValue); +} + +void ObjectWrapper::setBoolean(Key key, bool val) { + JS::RootedValue jsValue(_context); + jsValue.setBoolean(val); + + setValue(key, jsValue); +} + +void ObjectWrapper::setBSONElement(Key key, const BSONElement& elem, bool readOnly) { + JS::RootedValue value(_context); + ValueReader(_context, &value, _depth).fromBSONElement(elem, readOnly); + + setValue(key, value); +} + +void ObjectWrapper::setBSON(Key key, const BSONObj& obj, bool readOnly) { + JS::RootedValue value(_context); + ValueReader(_context, &value, _depth).fromBSON(obj, readOnly); + + setValue(key, value); +} + +void ObjectWrapper::setValue(Key key, JS::HandleValue val) { + key.set(_context, _object, val); +} + +void ObjectWrapper::setObject(Key key, JS::HandleObject object) { + JS::RootedValue value(_context); + value.setObjectOrNull(object); + + setValue(key, value); +} + +void ObjectWrapper::defineProperty(Key key, JS::HandleValue val, unsigned attrs) { + key.define(_context, _object, val, attrs); +} + +void ObjectWrapper::deleteProperty(Key key) { + key.del(_context, _object); +} + +int ObjectWrapper::type(Key key) { + JS::RootedValue x(_context); + getValue(key, &x); + + return ValueWriter(_context, x).type(); +} + +void ObjectWrapper::rename(Key from, const char* to) { + JS::RootedValue value(_context); + + JS::RootedValue undefValue(_context); + undefValue.setUndefined(); + + getValue(from, &value); + + setValue(to, value); + setValue(from, undefValue); +} + +bool ObjectWrapper::hasField(Key key) { + return key.has(_context, _object); +} + +void ObjectWrapper::callMethod(const char* field, + const JS::HandleValueArray& args, + JS::MutableHandleValue out) { + if (JS::Call(_context, _object, field, args, out)) + return; + + throwCurrentJSException(_context, ErrorCodes::InternalError, "Failed to call method"); +} + +void ObjectWrapper::callMethod(const char* field, JS::MutableHandleValue out) { + JS::AutoValueVector args(_context); + + callMethod(field, args, out); +} + +void ObjectWrapper::callMethod(JS::HandleValue fun, + const JS::HandleValueArray& args, + JS::MutableHandleValue out) { + if (JS::Call(_context, _object, fun, args, out)) + return; + + throwCurrentJSException(_context, ErrorCodes::InternalError, "Failed to call method"); +} + +void ObjectWrapper::callMethod(JS::HandleValue fun, JS::MutableHandleValue out) { + JS::AutoValueVector args(_context); + + callMethod(fun, args, out); +} + +void ObjectWrapper::writeThis(BSONObjBuilder* b) { + auto scope = getScope(_context); + + BSONObj* originalBSON = nullptr; + if (scope->getBsonProto().instanceOf(_object)) { + bool altered; + + std::tie(originalBSON, altered) = BSONInfo::originalBSON(_context, _object); + + if (!altered) { + b->appendElements(*originalBSON); + return; + } + } + + // We special case the _id field in top-level objects and move it to the front. + // This matches other drivers behavior and makes finding the _id field quicker in BSON. + if (_depth == 0 && hasField("_id")) { + _writeField(b, "_id", originalBSON); + } + + enumerate([&](JS::HandleId id) { + JS::RootedValue x(_context); + + IdWrapper idw(_context, id); + + if (_depth == 0 && idw.isString() && idw.equals("_id")) + return; + + _writeField(b, id, originalBSON); + }); + + const int sizeWithEOO = b->len() + 1 /*EOO*/ - 4 /*BSONObj::Holder ref count*/; + uassert(17260, + str::stream() << "Converting from JavaScript to BSON failed: " + << "Object size " << sizeWithEOO << " exceeds limit of " + << BSONObjMaxInternalSize << " bytes.", + sizeWithEOO <= BSONObjMaxInternalSize); +} + +void ObjectWrapper::_writeField(BSONObjBuilder* b, Key key, BSONObj* originalParent) { + JS::RootedValue value(_context); + key.get(_context, _object, &value); + + ValueWriter x(_context, value, _depth); + x.setOriginalBSON(originalParent); + + x.writeThis(b, key.toString(_context)); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/objectwrapper.h b/src/mongo/scripting/mozjs/objectwrapper.h new file mode 100644 index 00000000000..efac05a1e71 --- /dev/null +++ b/src/mongo/scripting/mozjs/objectwrapper.h @@ -0,0 +1,181 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <jsapi.h> +#include <string> + +#include "mongo/scripting/mozjs/exception.h" + +namespace mongo { + +class BSONObj; +class BSONObjBuilder; +class BSONElement; + +namespace mozjs { + +class MozJSImplScope; + +/** + * Wraps JSObject's with helpers for accessing their properties + * + * This wraps a RootedObject, so should only be allocated on the stack and is + * not movable or copyable + */ +class ObjectWrapper { +public: + /** + * Helper subclass that provides some easy boilerplate for accessing + * properties by string, index or id. + */ + class Key { + friend class ObjectWrapper; + + enum class Type : char { + Field, + Index, + Id, + }; + + public: + Key(const char* field) : _field(field), _type(Type::Field) {} + Key(uint32_t idx) : _idx(idx), _type(Type::Index) {} + Key(JS::HandleId id) : _id(id), _type(Type::Id) {} + + private: + void get(JSContext* cx, JS::HandleObject o, JS::MutableHandleValue value); + void set(JSContext* cx, JS::HandleObject o, JS::HandleValue value); + bool has(JSContext* cx, JS::HandleObject o); + void define(JSContext* cx, JS::HandleObject o, JS::HandleValue value, unsigned attrs); + void del(JSContext* cx, JS::HandleObject o); + std::string toString(JSContext* cx); + + union { + const char* _field; + uint32_t _idx; + jsid _id; + }; + Type _type; + }; + + /** + * The depth parameter here allows us to detect overly nested or circular + * objects and bail without blowing the stack. + */ + ObjectWrapper(JSContext* cx, JS::HandleObject obj, int depth = 0); + ObjectWrapper(JSContext* cx, JS::HandleValue value, int depth = 0); + + double getNumber(Key key); + int getNumberInt(Key key); + long long getNumberLongLong(Key key); + std::string getString(Key key); + bool getBoolean(Key key); + BSONObj getObject(Key key); + void getValue(Key key, JS::MutableHandleValue value); + + void setNumber(Key key, double val); + void setString(Key key, StringData val); + void setBoolean(Key key, bool val); + void setBSONElement(Key key, const BSONElement& elem, bool readOnly); + void setBSON(Key key, const BSONObj& obj, bool readOnly); + void setValue(Key key, JS::HandleValue value); + void setObject(Key key, JS::HandleObject value); + + /** + * See JS_DefineProperty for what sort of attributes might be useful + */ + void defineProperty(Key key, JS::HandleValue value, unsigned attrs); + + void deleteProperty(Key key); + + /** + * Returns the bson type of the property + */ + int type(Key key); + + void rename(Key key, const char* to); + + bool hasField(Key key); + + void callMethod(const char* name, const JS::HandleValueArray& args, JS::MutableHandleValue out); + void callMethod(const char* name, JS::MutableHandleValue out); + void callMethod(JS::HandleValue fun, + const JS::HandleValueArray& args, + JS::MutableHandleValue out); + void callMethod(JS::HandleValue fun, JS::MutableHandleValue out); + + /** + * Safely enumerates fields in the object, invoking a callback for each id + */ + template <typename T> + void enumerate(T&& callback) { + JS::AutoIdArray ids(_context, JS_Enumerate(_context, _object)); + + if (!ids) + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failure to enumerate object"); + + JS::RootedId rid(_context); + for (size_t i = 0; i < ids.length(); ++i) { + rid.set(ids[i]); + callback(rid); + } + } + + /** + * concatenates all of the fields in the object into the associated builder + */ + void writeThis(BSONObjBuilder* b); + + JS::HandleObject thisv() { + return _object; + } + +private: + /** + * writes the field "key" into the associated builder + * + * optional originalBSON is used to track updates to types (NumberInt + * overwritten by a float, but coercible to the original type, etc.) + */ + void _writeField(BSONObjBuilder* b, Key key, BSONObj* originalBSON); + + JSContext* _context; + JS::RootedObject _object; + + /** + * The depth of an object wrapper has to do with how many parents it has. + * Used to avoid circular object graphs and associate stack smashing. + */ + int _depth; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/oid.cpp b/src/mongo/scripting/mozjs/oid.cpp new file mode 100644 index 00000000000..cf391bf03e8 --- /dev/null +++ b/src/mongo/scripting/mozjs/oid.cpp @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/oid.h" + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/stdx/memory.h" +#include "mongo/util/mongoutils/str.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec OIDInfo::methods[2] = { + MONGO_ATTACH_JS_FUNCTION(toString), JS_FS_END, +}; + +const char* const OIDInfo::className = "ObjectId"; + +void OIDInfo::Functions::toString(JSContext* cx, JS::CallArgs args) { + ObjectWrapper o(cx, args.thisv()); + + if (!o.hasField("str")) + uasserted(ErrorCodes::BadValue, "Must have \"str\" field"); + + JS::RootedValue value(cx); + o.getValue("str", &value); + + std::string str = str::stream() << "ObjectId(\"" << ValueWriter(cx, value).toString() << "\")"; + + ValueReader(cx, args.rval()).fromStringData(str); +} + +void OIDInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + auto oid = stdx::make_unique<OID>(); + + if (args.length() == 0) { + oid->init(); + } else { + auto str = ValueWriter(cx, args.get(0)).toString(); + + Scope::validateObjectIdString(str); + oid->init(str); + } + + JS::RootedObject thisv(cx); + scope->getOidProto().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + o.setString("str", oid->toString()); + + args.rval().setObjectOrNull(thisv); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/oid.h b/src/mongo/scripting/mozjs/oid.h new file mode 100644 index 00000000000..109282be882 --- /dev/null +++ b/src/mongo/scripting/mozjs/oid.h @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "ObjectId" Javascript object. + * + * Holds a private bson OID + */ +struct OIDInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + + struct Functions { + MONGO_DEFINE_JS_FUNCTION(toString); + }; + + static const JSFunctionSpec methods[2]; + + static const char* const className; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/proxyscope.cpp b/src/mongo/scripting/mozjs/proxyscope.cpp new file mode 100644 index 00000000000..665d249f4c5 --- /dev/null +++ b/src/mongo/scripting/mozjs/proxyscope.cpp @@ -0,0 +1,318 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/proxyscope.h" + +#include "mongo/db/client.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/service_context.h" +#include "mongo/scripting/mozjs/implscope.h" + +namespace mongo { +namespace mozjs { + +MozJSProxyScope::MozJSProxyScope(MozJSScriptEngine* engine) + : _engine(engine), + _implScope(nullptr), + _mutex(), + _state(State::Idle), + _status(Status::OK()), + _condvar(), + _thread(&MozJSProxyScope::implThread, this) { + // Test the child on startup to make sure it's awake and that the + // implementation scope sucessfully constructed. + try { + runOnImplThread([] {}); + } catch (...) { + shutdownThread(); + throw; + } +} + +MozJSProxyScope::~MozJSProxyScope() { + DESTRUCTOR_GUARD(kill(); shutdownThread();); +} + +void MozJSProxyScope::init(const BSONObj* data) { + runOnImplThread([&] { _implScope->init(data); }); +} + +void MozJSProxyScope::reset() { + runOnImplThread([&] { _implScope->reset(); }); +} + +bool MozJSProxyScope::isKillPending() const { + return _implScope->isKillPending(); +} + +void MozJSProxyScope::registerOperation(OperationContext* txn) { + runOnImplThread([&] { _implScope->registerOperation(txn); }); +} + +void MozJSProxyScope::unregisterOperation() { + runOnImplThread([&] { _implScope->unregisterOperation(); }); +} + +void MozJSProxyScope::localConnectForDbEval(OperationContext* txn, const char* dbName) { + runOnImplThread([&] { _implScope->localConnectForDbEval(txn, dbName); }); +} + +void MozJSProxyScope::externalSetup() { + runOnImplThread([&] { _implScope->externalSetup(); }); +} + +std::string MozJSProxyScope::getError() { + std::string out; + runOnImplThread([&] { out = _implScope->getError(); }); + return out; +} + +/** + * This is an artifact of how out of memory errors were communicated in V8. We + * just throw out of memory errors from spidermonkey when we get them, rather + * than setting a flag and having to pick them up here. + */ +bool MozJSProxyScope::hasOutOfMemoryException() { + return false; +} + +void MozJSProxyScope::gc() { + _implScope->gc(); +} + +double MozJSProxyScope::getNumber(const char* field) { + double out; + runOnImplThread([&] { out = _implScope->getNumber(field); }); + return out; +} + +int MozJSProxyScope::getNumberInt(const char* field) { + int out; + runOnImplThread([&] { out = _implScope->getNumberInt(field); }); + return out; +} + +long long MozJSProxyScope::getNumberLongLong(const char* field) { + long long out; + runOnImplThread([&] { out = _implScope->getNumberLongLong(field); }); + return out; +} + +std::string MozJSProxyScope::getString(const char* field) { + std::string out; + runOnImplThread([&] { out = _implScope->getString(field); }); + return out; +} + +bool MozJSProxyScope::getBoolean(const char* field) { + bool out; + runOnImplThread([&] { out = _implScope->getBoolean(field); }); + return out; +} + +BSONObj MozJSProxyScope::getObject(const char* field) { + BSONObj out; + runOnImplThread([&] { out = _implScope->getObject(field); }); + return out; +} + +void MozJSProxyScope::setNumber(const char* field, double val) { + runOnImplThread([&] { _implScope->setNumber(field, val); }); +} + +void MozJSProxyScope::setString(const char* field, StringData val) { + runOnImplThread([&] { _implScope->setString(field, val); }); +} + +void MozJSProxyScope::setBoolean(const char* field, bool val) { + runOnImplThread([&] { _implScope->setBoolean(field, val); }); +} + +void MozJSProxyScope::setElement(const char* field, const BSONElement& e) { + runOnImplThread([&] { _implScope->setElement(field, e); }); +} + +void MozJSProxyScope::setObject(const char* field, const BSONObj& obj, bool readOnly) { + runOnImplThread([&] { _implScope->setObject(field, obj, readOnly); }); +} + +void MozJSProxyScope::setFunction(const char* field, const char* code) { + runOnImplThread([&] { _implScope->setFunction(field, code); }); +} + +int MozJSProxyScope::type(const char* field) { + int out; + runOnImplThread([&] { out = _implScope->type(field); }); + return out; +} + +void MozJSProxyScope::rename(const char* from, const char* to) { + runOnImplThread([&] { _implScope->rename(from, to); }); +} + +int MozJSProxyScope::invoke(ScriptingFunction func, + const BSONObj* argsObject, + const BSONObj* recv, + int timeoutMs, + bool ignoreReturn, + bool readOnlyArgs, + bool readOnlyRecv) { + int out; + runOnImplThread([&] { + out = _implScope->invoke( + func, argsObject, recv, timeoutMs, ignoreReturn, readOnlyArgs, readOnlyRecv); + }); + + return out; +} + +bool MozJSProxyScope::exec(StringData code, + const std::string& name, + bool printResult, + bool reportError, + bool assertOnError, + int timeoutMs) { + bool out; + runOnImplThread([&] { + out = _implScope->exec(code, name, printResult, reportError, assertOnError, timeoutMs); + }); + return out; +} + +void MozJSProxyScope::injectNative(const char* field, NativeFunction func, void* data) { + runOnImplThread([&] { _implScope->injectNative(field, func, data); }); +} + +ScriptingFunction MozJSProxyScope::_createFunction(const char* raw, + ScriptingFunction functionNumber) { + ScriptingFunction out; + runOnImplThread([&] { out = _implScope->_createFunction(raw, functionNumber); }); + return out; +} + +OperationContext* MozJSProxyScope::getOpContext() const { + return _implScope->getOpContext(); +} + +void MozJSProxyScope::kill() { + _implScope->kill(); +} + +/** + * Invokes a function on the implementation thread + * + * It does this by serializing the invocation through a stdx::function and + * capturing any exceptions through _status. + * + * We transition: + * + * Idle -> ProxyRequest -> ImplResponse -> Idle + */ +void MozJSProxyScope::runOnImplThread(std::function<void()> f) { + stdx::unique_lock<stdx::mutex> lk(_mutex); + _function = std::move(f); + + invariant(_state == State::Idle); + _state = State::ProxyRequest; + + _condvar.notify_one(); + + _condvar.wait(lk, [this] { return _state == State::ImplResponse; }); + + _state = State::Idle; + + // Clear the _status state and throw it if necessary + auto status = std::move(_status); + uassertStatusOK(status); +} + +void MozJSProxyScope::shutdownThread() { + { + stdx::lock_guard<stdx::mutex> lk(_mutex); + + invariant(_state == State::Idle); + + _state = State::Shutdown; + } + + _condvar.notify_one(); + + _thread.join(); +} + +/** + * The main loop for the implementation thread + * + * This owns the actual implementation scope (which needs to be created on this + * child thread) and has essentially two transition paths: + * + * Standard: ProxyRequest -> ImplResponse + * Invoke _function. Serialize exceptions to _status. + * + * Shutdown: Shutdown -> _ + * break out of the loop and return. + */ +void MozJSProxyScope::implThread() { + if (hasGlobalServiceContext()) + Client::initThread("js"); + + std::unique_ptr<MozJSImplScope> scope; + + // This will leave _status set for the first noop runOnImplThread(), which + // captures the startup exception that way + try { + scope.reset(new MozJSImplScope(_engine)); + _implScope = scope.get(); + } catch (...) { + _status = exceptionToStatus(); + } + + while (true) { + stdx::unique_lock<stdx::mutex> lk(_mutex); + _condvar.wait( + lk, [this] { return _state == State::ProxyRequest || _state == State::Shutdown; }); + + if (_state == State::Shutdown) + break; + + try { + _function(); + } catch (...) { + _status = exceptionToStatus(); + } + + _state = State::ImplResponse; + + _condvar.notify_one(); + } +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/proxyscope.h b/src/mongo/scripting/mozjs/proxyscope.h new file mode 100644 index 00000000000..8b874360a68 --- /dev/null +++ b/src/mongo/scripting/mozjs/proxyscope.h @@ -0,0 +1,195 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/client/dbclientcursor.h" +#include "mongo/scripting/mozjs/engine.h" +#include "mongo/stdx/condition_variable.h" +#include "mongo/stdx/functional.h" +#include "mongo/stdx/mutex.h" +#include "mongo/stdx/thread.h" + +namespace mongo { +namespace mozjs { + +class MozJSImplScope; + +/** + * Proxies all methods on the implementation scope over a side channel that + * allows the SpiderMonkey runtime to operate entirely on one thread. This + * implements the public scope interface and is the right way to talk to an + * implementation scope from multiple threads. + * + * In terms of implementation, it does most of it's heavy lifting through a + * stdx::function. The proxy scope owns an implementation scope transitively + * through the thread it owns. They communicate by setting a variable, then + * signaling each other. That communication has to work, there's no fallback + * for timing out. + * + * There are probably performance gains to be had from making + * the argument capture and method dispatch explicit, but I'll wait until we've + * measured it before bothering. + * + * See mongo::Scope for details on all of the overridden functions + * + */ +class MozJSProxyScope final : public Scope { + MONGO_DISALLOW_COPYING(MozJSProxyScope); + + /** + * The FSM is fairly tight: + * + * +----------+ shutdownThread() +--------------------+ + * | Shutdown | <------------------ | Idle | <+ + * +----------+ +--------------------+ | + * | | + * | runOnImplThread() | + * v | + * +--------------------+ | + * | ProxyRequest | | impl -> proxy + * +--------------------+ | + * | | + * | proxy -> impl | + * v | + * +--------------------+ | + * | ImplResponse | -+ + * +--------------------+ + * + * The regular flow: + * - We start at Idle and on the proxy thread. + * - runOnImplThread sets ProxyRequest and notifies the impl thread + * - The impl thread wakes up, invokes _function(), sets ImplResponse and notifies the proxy + * thread + * - The proxy thread wakes up and sets Idle + * + * Shutdown: + * - On destruction, The proxy thread sets Shutdown and notifies the impl thread + * - The impl thread wakes up, breaks out of it's loop and returns + * - The proxy thread joins the impl thread + * + */ + enum class State : char { + Idle, + ProxyRequest, + ImplResponse, + Shutdown, + }; + +public: + MozJSProxyScope(MozJSScriptEngine* engine); + ~MozJSProxyScope(); + + void init(const BSONObj* data) override; + + void reset() override; + + bool isKillPending() const override; + + void registerOperation(OperationContext* txn) override; + + void unregisterOperation() override; + + void localConnectForDbEval(OperationContext* txn, const char* dbName) override; + + void externalSetup() override; + + std::string getError() override; + + bool hasOutOfMemoryException() override; + + void gc() override; + + double getNumber(const char* field) override; + int getNumberInt(const char* field) override; + long long getNumberLongLong(const char* field) override; + std::string getString(const char* field) override; + bool getBoolean(const char* field) override; + BSONObj getObject(const char* field) override; + + void setNumber(const char* field, double val) override; + void setString(const char* field, StringData val) override; + void setBoolean(const char* field, bool val) override; + void setElement(const char* field, const BSONElement& e) override; + void setObject(const char* field, const BSONObj& obj, bool readOnly) override; + void setFunction(const char* field, const char* code) override; + + int type(const char* field) override; + + void rename(const char* from, const char* to) override; + + int invoke(ScriptingFunction func, + const BSONObj* args, + const BSONObj* recv, + int timeoutMs = 0, + bool ignoreReturn = false, + bool readOnlyArgs = false, + bool readOnlyRecv = false) override; + + bool exec(StringData code, + const std::string& name, + bool printResult, + bool reportError, + bool assertOnError, + int timeoutMs) override; + + void injectNative(const char* field, NativeFunction func, void* data = 0) override; + + ScriptingFunction _createFunction(const char* code, + ScriptingFunction functionNumber = 0) override; + + OperationContext* getOpContext() const; + + /** + * Thread safe. Kills the running operation + */ + void kill(); + +private: + void runOnImplThread(std::function<void()> f); + void shutdownThread(); + void implThread(); + + MozJSScriptEngine* const _engine; + MozJSImplScope* _implScope; + + /** + * This mutex protects _function, _state and _status as channels for + * function invocation and exception handling + */ + stdx::mutex _mutex; + stdx::function<void()> _function; + State _state; + Status _status; + + stdx::condition_variable _condvar; + stdx::thread _thread; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/regexp.cpp b/src/mongo/scripting/mozjs/regexp.cpp new file mode 100644 index 00000000000..b935a9fb354 --- /dev/null +++ b/src/mongo/scripting/mozjs/regexp.cpp @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/regexp.h" + +namespace mongo { +namespace mozjs { + +const char* const RegExpInfo::className = "RegExp"; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/regexp.h b/src/mongo/scripting/mozjs/regexp.h new file mode 100644 index 00000000000..b8553518127 --- /dev/null +++ b/src/mongo/scripting/mozjs/regexp.h @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "RegExp" Javascript object. + * + * Note that this installs over native. We only use this to grab the regexp + * prototype early in case users overwrite it. + */ +struct RegExpInfo : public BaseInfo { + static const char* const className; + + static const InstallType installType = InstallType::OverNative; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/timestamp.cpp b/src/mongo/scripting/mozjs/timestamp.cpp new file mode 100644 index 00000000000..f81cc4050ce --- /dev/null +++ b/src/mongo/scripting/mozjs/timestamp.cpp @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/timestamp.h" + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/util/mongoutils/str.h" + +namespace mongo { +namespace mozjs { + +const char* const TimestampInfo::className = "Timestamp"; + +void TimestampInfo::construct(JSContext* cx, JS::CallArgs args) { + auto scope = getScope(cx); + + JS::RootedObject thisv(cx); + scope->getTimestampProto().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + if (args.length() == 0) { + o.setNumber("t", 0); + o.setNumber("i", 0); + } else if (args.length() == 2) { + if (!args.get(0).isNumber()) + uasserted(ErrorCodes::BadValue, "Timestamp time must be a number"); + if (!args.get(1).isNumber()) + uasserted(ErrorCodes::BadValue, "Timestamp increment must be a number"); + + int64_t t = ValueWriter(cx, args.get(0)).toInt64(); + int64_t largestVal = int64_t(Timestamp::max().getSecs()); + if (t > largestVal) + uasserted(ErrorCodes::BadValue, + str::stream() << "The first argument must be in seconds; " << t + << " is too large (max " << largestVal << ")"); + + o.setValue("t", args.get(0)); + o.setValue("i", args.get(1)); + } else { + uasserted(ErrorCodes::BadValue, "Timestamp needs 0 or 2 arguments"); + } + + args.rval().setObjectOrNull(thisv); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/timestamp.h b/src/mongo/scripting/mozjs/timestamp.h new file mode 100644 index 00000000000..1dc4a998420 --- /dev/null +++ b/src/mongo/scripting/mozjs/timestamp.h @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "Timestamp" Javascript object. + * + * Represents a bson timestamp that looks like: + * + * { + * t : Double, + * i : Double, + * } + */ +struct TimestampInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + + static const char* const className; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/valuereader.cpp b/src/mongo/scripting/mozjs/valuereader.cpp new file mode 100644 index 00000000000..ad45fafc06e --- /dev/null +++ b/src/mongo/scripting/mozjs/valuereader.cpp @@ -0,0 +1,272 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kQuery + +#include "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/valuereader.h" + +#include <cstdio> +#include <js/CharacterEncoding.h> + +#include "mongo/base/error_codes.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/util/base64.h" +#include "mongo/util/log.h" + +namespace mongo { +namespace mozjs { + +ValueReader::ValueReader(JSContext* cx, JS::MutableHandleValue value, int depth) + : _context(cx), _value(value), _depth(depth) {} + +void ValueReader::fromBSONElement(const BSONElement& elem, bool readOnly) { + auto scope = getScope(_context); + + switch (elem.type()) { + case mongo::Code: + scope->newFunction(elem.valueStringData(), _value); + return; + case mongo::CodeWScope: + if (!elem.codeWScopeObject().isEmpty()) + warning() << "CodeWScope doesn't transfer to db.eval"; + scope->newFunction(StringData(elem.codeWScopeCode(), elem.codeWScopeCodeLen() - 1), + _value); + return; + case mongo::Symbol: + case mongo::String: + fromStringData(elem.valueStringData()); + return; + case mongo::jstOID: { + JS::AutoValueArray<1> args(_context); + + ValueReader(_context, args[0]).fromStringData(elem.OID().toString()); + + scope->getOidProto().newInstance(args, _value); + return; + } + case mongo::NumberDouble: + _value.setDouble(elem.Number()); + return; + case mongo::NumberInt: + _value.setInt32(elem.Int()); + return; + case mongo::Array: { + auto arrayPtr = JS_NewArrayObject(_context, 0); + uassert(ErrorCodes::JSInterpreterFailure, "Failed to JS_NewArrayObject", arrayPtr); + JS::RootedObject array(_context, arrayPtr); + + unsigned i = 0; + BSONForEach(subElem, elem.embeddedObject()) { + // We use an unsigned 32 bit integer, so 10 base 10 digits and + // 1 null byte + char str[11]; + sprintf(str, "%i", i++); + JS::RootedValue member(_context); + + ValueReader(_context, &member, _depth + 1).fromBSONElement(subElem, readOnly); + ObjectWrapper(_context, array, _depth + 1).setValue(str, member); + } + _value.setObjectOrNull(array); + return; + } + case mongo::Object: + fromBSON(elem.embeddedObject(), readOnly); + return; + case mongo::Date: + _value.setObjectOrNull( + JS_NewDateObjectMsec(_context, elem.Date().toMillisSinceEpoch())); + return; + case mongo::Bool: + _value.setBoolean(elem.Bool()); + return; + case mongo::EOO: + case mongo::jstNULL: + case mongo::Undefined: + _value.setNull(); + return; + case mongo::RegEx: { + // TODO parse into a custom type that can support any patterns and flags SERVER-9803 + + JS::AutoValueArray<2> args(_context); + + ValueReader(_context, args[0]).fromStringData(elem.regex()); + ValueReader(_context, args[1]).fromStringData(elem.regexFlags()); + + JS::RootedObject obj(_context); + scope->getRegExpProto().newInstance(args, &obj); + + _value.setObjectOrNull(obj); + + return; + } + case mongo::BinData: { + int len; + const char* data = elem.binData(len); + std::stringstream ss; + base64::encode(ss, data, len); + + JS::AutoValueArray<2> args(_context); + + args[0].setInt32(elem.binDataType()); + + ValueReader(_context, args[1]).fromStringData(ss.str()); + + scope->getBinDataProto().newInstance(args, _value); + return; + } + case mongo::bsonTimestamp: { + JS::AutoValueArray<2> args(_context); + + args[0].setDouble(elem.timestampTime().toMillisSinceEpoch() / 1000); + args[1].setNumber(elem.timestampInc()); + + scope->getTimestampProto().newInstance(args, _value); + + return; + } + case mongo::NumberLong: { + unsigned long long nativeUnsignedLong = elem.numberLong(); + // values above 2^53 are not accurately represented in JS + if (static_cast<long long>(nativeUnsignedLong) == + static_cast<long long>( + static_cast<double>(static_cast<long long>(nativeUnsignedLong))) && + nativeUnsignedLong < 9007199254740992ULL) { + JS::AutoValueArray<1> args(_context); + args[0].setNumber(static_cast<double>(static_cast<long long>(nativeUnsignedLong))); + + scope->getNumberLongProto().newInstance(args, _value); + } else { + JS::AutoValueArray<3> args(_context); + args[0].setNumber(static_cast<double>(static_cast<long long>(nativeUnsignedLong))); + args[1].setDouble(nativeUnsignedLong >> 32); + args[2].setDouble( + static_cast<unsigned long>(nativeUnsignedLong & 0x00000000ffffffff)); + scope->getNumberLongProto().newInstance(args, _value); + } + + return; + } + case mongo::MinKey: + scope->getMinKeyProto().newInstance(_value); + return; + case mongo::MaxKey: + scope->getMaxKeyProto().newInstance(_value); + return; + case mongo::DBRef: { + JS::AutoValueArray<1> oidArgs(_context); + ValueReader(_context, oidArgs[0]).fromStringData(elem.dbrefOID().toString()); + + JS::AutoValueArray<2> dbPointerArgs(_context); + ValueReader(_context, dbPointerArgs[0]).fromStringData(elem.dbrefNS()); + scope->getOidProto().newInstance(oidArgs, dbPointerArgs[1]); + + scope->getDbPointerProto().newInstance(dbPointerArgs, _value); + return; + } + default: + massert(16661, + str::stream() << "can't handle type: " << elem.type() << " " << elem.toString(), + false); + break; + } + + _value.setUndefined(); +} + +void ValueReader::fromBSON(const BSONObj& obj, bool readOnly) { + if (obj.firstElementType() == String && str::equals(obj.firstElementFieldName(), "$ref")) { + BSONObjIterator it(obj); + const BSONElement ref = it.next(); + const BSONElement id = it.next(); + + if (id.ok() && str::equals(id.fieldName(), "$id")) { + JS::AutoValueArray<2> args(_context); + + ValueReader(_context, args[0]).fromBSONElement(ref, readOnly); + + // id can be a subobject + ValueReader(_context, args[1], _depth + 1).fromBSONElement(id, readOnly); + + JS::RootedObject obj(_context); + + auto scope = getScope(_context); + + scope->getDbRefProto().newInstance(args, &obj); + ObjectWrapper o(_context, obj); + + while (it.more()) { + BSONElement elem = it.next(); + o.setBSONElement(elem.fieldName(), elem, readOnly); + } + + _value.setObjectOrNull(obj); + return; + } + } + + JS::RootedObject child(_context); + BSONInfo::make(_context, &child, obj, readOnly); + + _value.setObjectOrNull(child); +} + +/** + * SpiderMonkey doesn't have a direct entry point to create a jsstring from + * utf8, so we have to flow through some slightly less public interfaces. + * + * Basically, we have to use their routines to convert to utf16, then assign + * those bytes with JS_NewUCStringCopyN + */ +void ValueReader::fromStringData(StringData sd) { + size_t utf16Len; + + // TODO: we have tests that involve dropping garbage in. Do we want to + // throw, or to take the lossy conversion? + auto utf16 = JS::LossyUTF8CharsToNewTwoByteCharsZ( + _context, JS::UTF8Chars(sd.rawData(), sd.size()), &utf16Len); + + mozilla::UniquePtr<char16_t, JS::FreePolicy> utf16Deleter(utf16.get()); + + uassert(ErrorCodes::JSInterpreterFailure, + str::stream() << "Failed to encode \"" << sd << "\" as utf16", + utf16); + + auto jsStr = JS_NewUCStringCopyN(_context, utf16.get(), utf16Len); + + uassert(ErrorCodes::JSInterpreterFailure, + str::stream() << "Unable to copy \"" << sd << "\" into MozJS", + jsStr); + + _value.setString(jsStr); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/valuereader.h b/src/mongo/scripting/mozjs/valuereader.h new file mode 100644 index 00000000000..be916ccbc25 --- /dev/null +++ b/src/mongo/scripting/mozjs/valuereader.h @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <jsapi.h> +#include <string> + +#include "mongo/bson/bsonobj.h" + +namespace mongo { +namespace mozjs { + +/** + * Reads into a JS Value from some Mongo C++ primitive + */ +class ValueReader { +public: + /** + * Depth is used when readers are invoked from ObjectWrappers to avoid + * reading out overly nested objects + */ + ValueReader(JSContext* cx, JS::MutableHandleValue value, int depth = 0); + + void fromBSONElement(const BSONElement& elem, bool readOnly); + void fromBSON(const BSONObj& obj, bool readOnly); + void fromStringData(StringData sd); + +private: + JSContext* _context; + JS::MutableHandleValue _value; + int _depth; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/valuewriter.cpp b/src/mongo/scripting/mozjs/valuewriter.cpp new file mode 100644 index 00000000000..31928f7ab44 --- /dev/null +++ b/src/mongo/scripting/mozjs/valuewriter.cpp @@ -0,0 +1,252 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/valuewriter.h" + +#include <js/Conversions.h> + +#include "mongo/base/error_codes.h" +#include "mongo/scripting/mozjs/exception.h" +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/jsstringwrapper.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/util/base64.h" + +namespace mongo { +namespace mozjs { + +ValueWriter::ValueWriter(JSContext* cx, JS::HandleValue value, int depth) + : _context(cx), _value(value), _depth(depth), _originalParent(nullptr) {} + +void ValueWriter::setOriginalBSON(BSONObj* obj) { + _originalParent = obj; +} + +int ValueWriter::type() { + if (_value.isNull()) + return jstNULL; + if (_value.isUndefined()) + return Undefined; + if (_value.isString()) + return String; + if (JS_IsArrayObject(_context, _value)) + return Array; + if (_value.isBoolean()) + return Bool; + + // We could do something more sophisticated here by checking to see if we + // round trip through int32_t, int64_t and double and picking a type that + // way, for now just always come back as double for numbers though (it's + // what we did for v8) + if (_value.isNumber()) + return NumberDouble; + + if (_value.isObject()) { + JS::RootedObject obj(_context, _value.toObjectOrNull()); + if (JS_ObjectIsDate(_context, obj)) + return Date; + if (JS_ObjectIsFunction(_context, obj)) + return Code; + + return Object; + } + + uasserted(ErrorCodes::BadValue, "unable to get type"); +} + +BSONObj ValueWriter::toBSON() { + if (!_value.isObject()) + return BSONObj(); + + JS::RootedObject obj(_context, _value.toObjectOrNull()); + + if (getScope(_context)->getBsonProto().instanceOf(obj)) { + BSONObj* originalBSON; + bool altered; + + std::tie(originalBSON, altered) = BSONInfo::originalBSON(_context, obj); + + if (!altered) + return *originalBSON; + } + + BSONObjBuilder bob; + ObjectWrapper(_context, obj, _depth).writeThis(&bob); + + return bob.obj(); +} + +std::string ValueWriter::toString() { + return JSStringWrapper(_context, JS::ToString(_context, _value)).toString(); +} + +double ValueWriter::toNumber() { + double out; + if (JS::ToNumber(_context, _value, &out)) + return out; + + throwCurrentJSException(_context, ErrorCodes::BadValue, "Failure to convert value to number"); +} + +bool ValueWriter::toBoolean() { + return JS::ToBoolean(_value); +} + +int32_t ValueWriter::toInt32() { + int32_t out; + if (JS::ToInt32(_context, _value, &out)) + return out; + + throwCurrentJSException(_context, ErrorCodes::BadValue, "Failure to convert value to number"); +} + +int64_t ValueWriter::toInt64() { + int64_t out; + if (JS::ToInt64(_context, _value, &out)) + return out; + + throwCurrentJSException(_context, ErrorCodes::BadValue, "Failure to convert value to number"); +} + +void ValueWriter::writeThis(BSONObjBuilder* b, StringData sd) { + uassert(17279, + str::stream() << "Exceeded depth limit of " << 150 + << " when converting js object to BSON. Do you have a cycle?", + _depth < 149); + + // Null char should be at the end, not in the string + uassert(16985, + str::stream() << "JavaScript property (name) contains a null char " + << "which is not allowed in BSON. " + << (_originalParent ? _originalParent->jsonString() : ""), + (std::string::npos == sd.find('\0'))); + + if (_value.isString()) { + b->append(sd, toString()); + } else if (_value.isNumber()) { + double val = toNumber(); + + // if previous type was integer, keep it + int intval = static_cast<int>(val); + + if (val == intval && _originalParent) { + // This makes copying an object of numbers O(n**2) :( + BSONElement elmt = _originalParent->getField(sd); + if (elmt.type() == mongo::NumberInt) { + b->append(sd, intval); + return; + } + } + + b->append(sd, val); + } else if (_value.isObject()) { + JS::RootedObject childObj(_context, _value.toObjectOrNull()); + _writeObject(b, sd, childObj); + } else if (_value.isBoolean()) { + b->appendBool(sd, _value.toBoolean()); + } else if (_value.isUndefined()) { + b->appendUndefined(sd); + } else if (_value.isNull()) { + b->appendNull(sd); + } else { + uasserted(16662, + str::stream() << "unable to convert JavaScript property to mongo element " << sd); + } +} + +void ValueWriter::_writeObject(BSONObjBuilder* b, StringData sd, JS::HandleObject obj) { + auto scope = getScope(_context); + + ObjectWrapper o(_context, obj, _depth); + + if (JS_ObjectIsFunction(_context, _value.toObjectOrNull())) { + uassert(16716, + "cannot convert native function to BSON", + !scope->getNativeFunctionProto().instanceOf(obj)); + b->appendCode(sd, ValueWriter(_context, _value).toString()); + } else if (JS_ObjectIsRegExp(_context, obj)) { + JS::RootedValue v(_context); + v.setObjectOrNull(obj); + + std::string regex = ValueWriter(_context, v).toString(); + regex = regex.substr(1); + std::string r = regex.substr(0, regex.rfind('/')); + std::string o = regex.substr(regex.rfind('/') + 1); + + b->appendRegex(sd, r, o); + } else if (JS_ObjectIsDate(_context, obj)) { + JS::RootedValue dateval(_context); + o.callMethod("getTime", &dateval); + + auto d = Date_t::fromMillisSinceEpoch(ValueWriter(_context, dateval).toNumber()); + b->appendDate(sd, d); + } else if (scope->getOidProto().instanceOf(obj)) { + b->append(sd, OID(o.getString("str"))); + } else if (scope->getNumberLongProto().instanceOf(obj)) { + long long out = NumberLongInfo::ToNumberLong(_context, obj); + b->append(sd, out); + } else if (scope->getNumberIntProto().instanceOf(obj)) { + b->append(sd, NumberIntInfo::ToNumberInt(_context, obj)); + } else if (scope->getDbPointerProto().instanceOf(obj)) { + JS::RootedValue id(_context); + o.getValue("id", &id); + + b->appendDBRef(sd, o.getString("ns"), OID(ObjectWrapper(_context, id).getString("str"))); + } else if (scope->getBinDataProto().instanceOf(obj)) { + auto str = static_cast<std::string*>(JS_GetPrivate(obj)); + + auto binData = base64::decode(*str); + + b->appendBinData(sd, + binData.size(), + static_cast<mongo::BinDataType>(static_cast<int>(o.getNumber("type"))), + binData.c_str()); + } else if (scope->getTimestampProto().instanceOf(obj)) { + Timestamp ot(o.getNumber("t"), o.getNumber("i")); + b->append(sd, ot); + } else if (scope->getMinKeyProto().instanceOf(obj)) { + b->appendMinKey(sd); + } else if (scope->getMaxKeyProto().instanceOf(obj)) { + b->appendMaxKey(sd); + } else { + // nested object or array + + BSONObjBuilder subbob(JS_IsArrayObject(_context, obj) ? b->subarrayStart(sd) + : b->subobjStart(sd)); + + ObjectWrapper child(_context, obj, _depth + 1); + + child.writeThis(b); + } +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/valuewriter.h b/src/mongo/scripting/mozjs/valuewriter.h new file mode 100644 index 00000000000..47358398f81 --- /dev/null +++ b/src/mongo/scripting/mozjs/valuewriter.h @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <jsapi.h> +#include <string> + +#include "mongo/bson/bsonobj.h" + +namespace mongo { +namespace mozjs { + +/** + * Writes C++ values out of JS Values + * + * depth is used to trap circular objects in js and prevent stack smashing + * + * originalBSON is a hack to keep integer types in their original type when + * they're read out, manipulated in js and saved back. + */ +class ValueWriter { +public: + ValueWriter(JSContext* cx, JS::HandleValue value, int depth = 0); + + BSONObj toBSON(); + + /** + * These coercions flow through JS::To_X. I.e. they can call toString() or + * toNumber() + */ + std::string toString(); + int type(); + double toNumber(); + int32_t toInt32(); + int64_t toInt64(); + bool toBoolean(); + + /** + * Writes the value into a bsonobjbuilder under the name in sd. + */ + void writeThis(BSONObjBuilder* b, StringData sd); + + void setOriginalBSON(BSONObj* obj); + +private: + /** + * Writes the object into a bsonobjbuilder under the name in sd. + */ + void _writeObject(BSONObjBuilder* b, StringData sd, JS::HandleObject obj); + + JSContext* _context; + JS::HandleValue _value; + int _depth; + BSONObj* _originalParent; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/wraptype.h b/src/mongo/scripting/mozjs/wraptype.h new file mode 100644 index 00000000000..c5ca6095e76 --- /dev/null +++ b/src/mongo/scripting/mozjs/wraptype.h @@ -0,0 +1,474 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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 <cstddef> +#include <jsapi.h> +#include <type_traits> + +#include "mongo/scripting/mozjs/base.h" +#include "mongo/scripting/mozjs/exception.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/util/assert_util.h" + +// The purpose of this class is to take in specially crafted types and generate +// a wrapper which installs the type, along with any useful life cycle methods +// and free functions that might be associated with it. The template magic in +// here, along with some useful macros, hides a lot of the implementation +// complexity of exposing C++ code into javascript. Most prominently, we have +// to wrap every function that can be called from javascript to prevent any C++ +// exceptions from leaking out. We do this, with template and macro based +// codegen, and turn mongo exceptions into instances of Status, then convert +// those into javascript exceptions before returning. That allows all consumers +// of this library to throw exceptions freely, with the understanding that +// they'll be visible in javascript. Javascript exceptions are trapped at the +// top level and converted back to mongo exceptions by an error handler on +// ImplScope. + +// MONGO_*_JS_FUNCTION_* macros are public and allow wrapped types to install +// their own functions on types and into the global scope +#define MONGO_DEFINE_JS_FUNCTION(name) \ + static void name(JSContext* cx, JS::CallArgs args); \ + static bool WRAPPER_##name(JSContext* cx, unsigned argc, JS::Value* vp) { \ + try { \ + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \ + name(cx, args); \ + return true; \ + } catch (...) { \ + mongoToJSException(cx); \ + return false; \ + } \ + } + +#define MONGO_ATTACH_JS_FUNCTION_WITH_FLAGS(name, flags) \ + JS_FS(#name, Functions::WRAPPER_##name, 0, flags) + +#define MONGO_ATTACH_JS_FUNCTION(name) MONGO_ATTACH_JS_FUNCTION_WITH_FLAGS(name, 0) + +namespace mongo { +namespace mozjs { + +namespace smUtils { + +// Now all the spidermonkey type methods +template <typename T> +static bool addProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue v) { + try { + T::addProperty(cx, obj, id, v); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool call(JSContext* cx, unsigned argc, JS::Value* vp) { + try { + T::call(cx, JS::CallArgsFromVp(argc, vp)); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool construct(JSContext* cx, unsigned argc, JS::Value* vp) { + try { + T::construct(cx, JS::CallArgsFromVp(argc, vp)); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool convert(JSContext* cx, JS::HandleObject obj, JSType type, JS::MutableHandleValue vp) { + try { + T::convert(cx, obj, type, vp); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool delProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* succeeded) { + try { + T::delProperty(cx, obj, id, succeeded); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool enumerate(JSContext* cx, JS::HandleObject obj, JS::AutoIdVector& properties) { + try { + T::enumerate(cx, obj, properties); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool getProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue vp) { + try { + T::getProperty(cx, obj, id, vp); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool hasInstance(JSContext* cx, JS::HandleObject obj, JS::MutableHandleValue vp, bool* bp) { + try { + T::hasInstance(cx, obj, vp, bp); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool setProperty( + JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool strict, JS::MutableHandleValue vp) { + try { + T::setProperty(cx, obj, id, strict, vp); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* resolvedp) { + try { + T::resolve(cx, obj, id, resolvedp); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +} // namespace smUtils + +template <typename T> +class WrapType : public T { +public: + WrapType(JSContext* context) + : _context(context), + _proto(), + _jsclass({T::className, + T::classFlags, + T::addProperty != BaseInfo::addProperty ? smUtils::addProperty<T> : nullptr, + T::delProperty != BaseInfo::delProperty ? smUtils::delProperty<T> : nullptr, + T::getProperty != BaseInfo::getProperty ? smUtils::getProperty<T> : nullptr, + T::setProperty != BaseInfo::setProperty ? smUtils::setProperty<T> : nullptr, + // We don't use the regular enumerate because we want the fancy new one + nullptr, + T::resolve != BaseInfo::resolve ? smUtils::resolve<T> : nullptr, + T::convert != BaseInfo::convert ? smUtils::convert<T> : nullptr, + T::finalize != BaseInfo::finalize ? T::finalize : nullptr, + T::call != BaseInfo::call ? smUtils::call<T> : nullptr, + T::hasInstance != BaseInfo::hasInstance ? smUtils::hasInstance<T> : nullptr, + T::construct != BaseInfo::construct ? smUtils::construct<T> : nullptr, + nullptr}) { + _installEnumerate(T::enumerate != BaseInfo::enumerate ? smUtils::enumerate<T> : nullptr); + + // The global object is different. We need it for basic setup + // before the other types are installed. Might as well just do it + // in the constructor. + if (T::classFlags & JSCLASS_GLOBAL_FLAGS) { + JS::RootedObject proto(_context); + + _proto.init(_context, + _assertPtr(JS_NewGlobalObject( + _context, &_jsclass, nullptr, JS::DontFireOnNewGlobalHook))); + + JSAutoCompartment ac(_context, _proto); + _installFunctions(_proto, T::freeFunctions); + } + } + + ~WrapType() { + // Persistent globals don't RAII, you have to reset() them manually + _proto.reset(); + } + + void install(JS::HandleObject global) { + switch (static_cast<InstallType>(T::installType)) { + case InstallType::Global: + _installGlobal(global); + break; + case InstallType::Private: + _installPrivate(global); + break; + case InstallType::OverNative: + _installOverNative(global); + break; + } + } + + /** + * newObject methods don't invoke the constructor. So they're good for + * types without a constructor or inside the constructor + */ + void newObject(JS::MutableHandleObject out) { + // The regular form of JS_NewObject, where we pass proto as the + // third param, actually does a global object lookup for some + // reason. This way allows object creation with non-public + // prototypes and if someone deletes the symbol up the chain. + out.set(_assertPtr(JS_NewObject(_context, &_jsclass, JS::NullPtr()))); + + if (!JS_SetPrototype(_context, out, _proto)) + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to set prototype"); + } + + void newObject(JS::MutableHandleValue out) { + JS::RootedObject obj(_context); + newObject(&obj); + + out.setObjectOrNull(obj); + } + + /** + * newInstance calls the constructor, a la new Type() in js + */ + void newInstance(JS::MutableHandleObject out) { + JS::AutoValueVector args(_context); + + newInstance(args, out); + } + + void newInstance(const JS::HandleValueArray& args, JS::MutableHandleObject out) { + out.set(_assertPtr(JS_New(_context, _proto, args))); + } + + void newInstance(JS::MutableHandleValue out) { + JS::AutoValueVector args(_context); + + newInstance(args, out); + } + + void newInstance(const JS::HandleValueArray& args, JS::MutableHandleValue out) { + out.setObjectOrNull(_assertPtr(JS_New(_context, _proto, args))); + } + + // instanceOf doesn't go up the prototype tree. It's a lower level more specific match + bool instanceOf(JS::HandleObject obj) { + return JS_InstanceOf(_context, obj, &_jsclass, nullptr); + } + + bool instanceOf(JS::HandleValue value) { + if (!value.isObject()) + return false; + + JS::RootedObject obj(_context, value.toObjectOrNull()); + + return instanceOf(obj); + } + + const JSClass* getJSClass() const { + return &_jsclass; + } + + JS::HandleObject getProto() const { + return _proto; + } + +private: + /** + * Use this if you want your types installed visibly in the global scope + */ + void _installGlobal(JS::HandleObject global) { + JS::RootedObject parent(_context); + _inheritFrom(T::inheritFrom, global, &parent); + + _proto.init(_context, + _assertPtr(JS_InitClass( + _context, + global, + parent, + &_jsclass, + T::construct != BaseInfo::construct ? smUtils::construct<T> : nullptr, + 0, + nullptr, + T::methods, + nullptr, + nullptr))); + + _installFunctions(global, T::freeFunctions); + _postInstall(global, T::postInstall); + } + + // Use this if you want your types installed, but not visible in the + // global scope + void _installPrivate(JS::HandleObject global) { + JS::RootedObject parent(_context); + _inheritFrom(T::inheritFrom, global, &parent); + + // See newObject() for why we have to do this dance with the explicit + // SetPrototype + _proto.init(_context, _assertPtr(JS_NewObject(_context, &_jsclass, JS::NullPtr()))); + if (parent.get() && !JS_SetPrototype(_context, _proto, parent)) + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to set prototype"); + + _installFunctions(_proto, T::methods); + _installFunctions(global, T::freeFunctions); + + _installConstructor(T::construct != BaseInfo::construct ? smUtils::construct<T> : nullptr); + + _postInstall(global, T::postInstall); + } + + // Use this to attach things to types that we don't provide like + // Object, or Array + void _installOverNative(JS::HandleObject global) { + JS::RootedValue value(_context); + if (!JS_GetProperty(_context, global, T::className, &value)) + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Couldn't get className property"); + + if (!value.isObject()) + uasserted(ErrorCodes::BadValue, "className isn't object"); + + _proto.init(_context, value.toObjectOrNull()); + + _installFunctions(_proto, T::methods); + _installFunctions(global, T::freeFunctions); + _postInstall(global, T::postInstall); + } + + void _installFunctions(JS::HandleObject global, const JSFunctionSpec* fs) { + if (!fs) + return; + if (JS_DefineFunctions(_context, global, fs)) + return; + + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to define functions"); + } + + // We have to do this awkward dance to set the new style enumeration. + // You used to be able to set this with JSCLASS_NEW_ENUMERATE in class + // flags, in the future you'll probably only set ObjectOps, but for now + // we have this. There are a host of static_asserts in js/Class.h that + // ensure that these two structures are equal. + // + // This is a landmine to watch out for during upgrades + using enumerateT = bool (*)(JSContext*, JS::HandleObject, JS::AutoIdVector&); + void _installEnumerate(enumerateT enumerate) { + if (!enumerate) + return; + + auto implClass = reinterpret_cast<js::Class*>(&_jsclass); + + implClass->ops.enumerate = enumerate; + } + + // This is for inheriting from something other than Object + void _inheritFrom(const char* name, JS::HandleObject global, JS::MutableHandleObject out) { + if (!name) + return; + + JS::RootedValue val(_context); + + if (!JS_GetProperty(_context, global, name, &val)) { + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to get parent"); + } + + if (!val.isObject()) { + uasserted(ErrorCodes::JSInterpreterFailure, "Parent is not an object"); + } + + out.set(val.toObjectOrNull()); + } + + using postInstallT = void (*)(JSContext*, JS::HandleObject, JS::HandleObject); + void _postInstall(JS::HandleObject global, postInstallT postInstall) { + if (!postInstall) + return; + + postInstall(_context, global, _proto); + } + + void _installConstructor(JSNative ctor) { + if (!ctor) + return; + + auto ptr = JS_NewFunction(_context, ctor, 0, JSFUN_CONSTRUCTOR, JS::NullPtr(), nullptr); + if (!ptr) { + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to install constructor"); + } + + JS::RootedObject ctorObj(_context, JS_GetFunctionObject(ptr)); + + if (!JS_LinkConstructorAndPrototype(_context, ctorObj, _proto)) + throwCurrentJSException(_context, + ErrorCodes::JSInterpreterFailure, + "Failed to link constructor and prototype"); + } + + JSObject* _assertPtr(JSObject* ptr) { + if (!ptr) + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to JS_NewX"); + + return ptr; + } + + JSContext* _context; + JS::PersistentRootedObject _proto; + JSClass _jsclass; +}; + +} // namespace mozjs +} // namespace mongo |