/** * 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 . * * 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 #include #include "mongo/base/error_codes.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/platform/decimal128.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 { const int ObjectWrapper::kMaxWriteFieldDepth; 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; } case Type::InternedString: { InternedStringId id(cx, _internedString); 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; } case Type::InternedString: { InternedStringId id(cx, _internedString); 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; } case Type::InternedString: { InternedStringId id(cx, _internedString); 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; } case Type::InternedString: { InternedStringId id(cx, _internedString); if (JS_HasPropertyById(cx, o, id, &has)) return has; break; } } throwCurrentJSException(cx, ErrorCodes::InternalError, "Failed to has value on a JSObject"); } bool ObjectWrapper::Key::hasOwn(JSContext* cx, JS::HandleObject o) { bool has; switch (_type) { case Type::Field: if (JS_AlreadyHasOwnProperty(cx, o, _field, &has)) return has; break; case Type::Index: if (JS_AlreadyHasOwnElement(cx, o, _idx, &has)) return has; break; case Type::Id: { JS::RootedId id(cx, _id); if (JS_AlreadyHasOwnPropertyById(cx, o, id, &has)) return has; break; } case Type::InternedString: { InternedStringId id(cx, _internedString); if (JS_AlreadyHasOwnPropertyById(cx, o, id, &has)) return has; break; } } throwCurrentJSException(cx, ErrorCodes::InternalError, "Failed to hasOwn 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; } case Type::InternedString: { InternedStringId id(cx, _internedString); if (JS_DeleteProperty(cx, o, IdWrapper(cx, id).toString().c_str())) break; } } throwCurrentJSException(cx, ErrorCodes::InternalError, "Failed to delete value on a JSObject"); } std::string ObjectWrapper::Key::toString(JSContext* cx) { JSStringWrapper jsstr; return toStringData(cx, &jsstr).toString(); } StringData ObjectWrapper::Key::toStringData(JSContext* cx, JSStringWrapper* jsstr) { if (_type == Type::Field) { return _field; } if (_type == Type::Index) { *jsstr = JSStringWrapper(_idx); return jsstr->toStringData(); } JS::RootedId rid(cx); if (_type == Type::Id) { rid.set(_id); } else { InternedStringId id(cx, _internedString); rid.set(id); } if (JSID_IS_INT(rid)) { *jsstr = JSStringWrapper(JSID_TO_INT(rid)); return jsstr->toStringData(); } if (JSID_IS_STRING(rid)) { *jsstr = JSStringWrapper(cx, JSID_TO_STRING(rid)); return jsstr->toStringData(); } uasserted(ErrorCodes::BadValue, "Couldn't convert key to String"); } ObjectWrapper::ObjectWrapper(JSContext* cx, JS::HandleObject obj) : _context(cx), _object(cx, obj) {} ObjectWrapper::ObjectWrapper(JSContext* cx, JS::HandleValue value) : _context(cx), _object(cx, value.toObjectOrNull()) {} 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(); } Decimal128 ObjectWrapper::getNumberDecimal(Key key) { JS::RootedValue x(_context); getValue(key, &x); return ValueWriter(_context, x).toDecimal128(); } 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).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); ValueReader(_context, &jsValue).fromDouble(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, const BSONObj& parent, bool readOnly) { JS::RootedValue value(_context); ValueReader(_context, &value).fromBSONElement(elem, parent, readOnly); setValue(key, value); } void ObjectWrapper::setBSON(Key key, const BSONObj& obj, bool readOnly) { JS::RootedValue value(_context); ValueReader(_context, &value).fromBSON(obj, nullptr, readOnly); setValue(key, value); } void ObjectWrapper::setBSONArray(Key key, const BSONObj& obj, bool readOnly) { JS::RootedValue value(_context); ValueReader(_context, &value).fromBSONArray(obj, nullptr, 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); } bool ObjectWrapper::hasOwnField(Key key) { return key.hasOwn(_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); } BSONObj ObjectWrapper::toBSON() { if (getScope(_context)->getProto().instanceOf(_object)) { BSONObj* originalBSON = nullptr; bool altered; std::tie(originalBSON, altered) = BSONInfo::originalBSON(_context, _object); if (originalBSON && !altered) return *originalBSON; } JS::RootedId id(_context); // INCREDIBLY SUBTLE BEHAVIOR: // // (jcarey): Be very careful about how the Rooting API is used in // relationship to WriteFieldRecursionFrames. Mozilla'a API more or less // demands that the rooting types are on the stack and only manipulated as // regular objects, which we aren't doing here. The reason they do this is // because the rooting types must be global created and destroyed in an // entirely linear order. This is impossible to screw up in regular use, // but our unwinding of the recursion frames makes it easy to do here. // // The roots above need to be before the first frame is emplaced (so // they'll be destroyed after it) and none of the roots in the below code // (or in ValueWriter::writeThis) can live longer than until the call to // emplace() inside ValueWriter. The runtime asserts enabled by MozJS's // debug mode will catch runtime errors, but be aware of how difficult this // is to get right and what to look for if one of them bites you. BSONObjBuilder b; { // NOTE: Keep the frames in a scope so that it is clear that // we always destroy them before we destroy 'b'. It is // important to do so: if 'b' is destroyed before the frames, // and we don't pop all of the frames (say, due to an // exeption), then the frame dtors would write to freed // memory. WriteFieldRecursionFrames frames; frames.emplace(_context, _object, nullptr, StringData{}); // 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 (hasOwnField(InternedString::_id)) { _writeField(&b, InternedString::_id, &frames, frames.top().originalBSON); } while (frames.size()) { auto& frame = frames.top(); // If the index is the same as length, we've seen all the keys at this // level and should go up a level if (frame.idx == frame.ids.length()) { frames.pop(); continue; } if (frame.idx == 0 && frame.originalBSON && !frame.altered) { // If this is our first look at the object and it has an unaltered // bson behind it, move idx to the end so we'll roll up on the next // pass through the loop. frame.subbob_or(&b)->appendElements(*frame.originalBSON); frame.idx = frame.ids.length(); continue; } id.set(frame.ids[frame.idx++]); if (frames.size() == 1) { IdWrapper idw(_context, id); // TODO: check if it's cheaper to just compare with an interned // string of "_id" rather than with ascii if (idw.isString() && idw.equalsAscii("_id")) { continue; } } // writeField invokes ValueWriter with the frame stack, which will push // onto frames for subobjects, which will effectively recurse the loop. _writeField(frame.subbob_or(&b), JS::HandleId(id), &frames, frame.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); return b.obj(); } ObjectWrapper::WriteFieldRecursionFrame::WriteFieldRecursionFrame(JSContext* cx, JSObject* obj, BSONObjBuilder* parent, StringData sd) : thisv(cx, obj), ids(cx, JS::IdVector(cx)) { bool isArray = false; if (parent) { if (!JS_IsArrayObject(cx, thisv, &isArray)) { throwCurrentJSException( cx, ErrorCodes::JSInterpreterFailure, "Failure to check object is an array"); } subbob.emplace(isArray ? parent->subarrayStart(sd) : parent->subobjStart(sd)); } if (isArray) { uint32_t length; if (!JS_GetArrayLength(cx, thisv, &length)) { throwCurrentJSException( cx, ErrorCodes::JSInterpreterFailure, "Failure to get array length"); } if (!ids.reserve(length)) { throwCurrentJSException( cx, ErrorCodes::JSInterpreterFailure, "Failure to reserve array"); } JS::RootedId rid(cx); for (uint32_t i = 0; i < length; i++) { rid.set(INT_TO_JSID(i)); ids.infallibleAppend(rid); } } else { if (!JS_Enumerate(cx, thisv, &ids)) { throwCurrentJSException( cx, ErrorCodes::JSInterpreterFailure, "Failure to enumerate object"); } } if (getScope(cx)->getProto().instanceOf(thisv)) { std::tie(originalBSON, altered) = BSONInfo::originalBSON(cx, thisv); } } void ObjectWrapper::_writeField(BSONObjBuilder* b, Key key, WriteFieldRecursionFrames* frames, BSONObj* originalParent) { JS::RootedValue value(_context); key.get(_context, frames->top().thisv, &value); ValueWriter x(_context, value); x.setOriginalBSON(originalParent); JSStringWrapper jsstr; x.writeThis(b, key.toStringData(_context, &jsstr), frames); } std::string ObjectWrapper::getClassName() { auto jsclass = JS_GetClass(_object); if (jsclass) return jsclass->name; JS::RootedValue ctor(_context); getValue(InternedString::constructor, &ctor); return ObjectWrapper(_context, ctor).getString(InternedString::name); } } // namespace mozjs } // namespace mongo