/** * 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/valuewriter.h" #include #include "mongo/base/error_codes.h" #include "mongo/platform/decimal128.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" #include "mongo/util/represent_as.h" namespace mongo { namespace mozjs { ValueWriter::ValueWriter(JSContext* cx, JS::HandleValue value) : _context(cx), _value(value), _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; bool isArray; if (!JS_IsArrayObject(_context, _value, &isArray)) { uasserted(ErrorCodes::BadValue, "unable to check if type is an array"); } if (isArray) 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()); bool isDate; if (!JS_ObjectIsDate(_context, obj, &isDate)) { uasserted(ErrorCodes::BadValue, "unable to check if type is a date"); } if (isDate) return Date; if (JS_ObjectIsFunction(_context, obj)) return Code; return Object; } uasserted(ErrorCodes::BadValue, "unable to get type"); } std::string ValueWriter::typeAsString() { if (_value.isNull()) return "null"; if (_value.isUndefined()) return "undefined"; if (_value.isString()) return "string"; bool isArray; if (!JS_IsArrayObject(_context, _value, &isArray)) { uasserted(ErrorCodes::BadValue, "unable to check if type is an array"); } if (isArray) return "array"; if (_value.isBoolean()) return "boolean"; if (_value.isNumber()) return "number"; if (_value.isObject()) { JS::RootedObject obj(_context, _value.toObjectOrNull()); if (!JS_IsArrayObject(_context, obj, &isArray)) { uasserted(ErrorCodes::BadValue, "unable to check if type is an array"); } if (isArray) return "array"; bool isDate; if (!JS_ObjectIsDate(_context, obj, &isDate)) { uasserted(ErrorCodes::BadValue, "unable to check if type is a date"); } if (isDate) return "date"; if (JS_ObjectIsFunction(_context, obj)) return "function"; return ObjectWrapper(_context, _value).getClassName(); } uasserted(ErrorCodes::BadValue, "unable to get type"); } BSONObj ValueWriter::toBSON() { if (!_value.isObject()) return BSONObj(); JS::RootedObject obj(_context, _value.toObjectOrNull()); return ObjectWrapper(_context, obj).toBSON(); } std::string ValueWriter::toString() { JSStringWrapper jsstr; return toStringData(&jsstr).toString(); } StringData ValueWriter::toStringData(JSStringWrapper* jsstr) { *jsstr = JSStringWrapper(_context, JS::ToString(_context, _value)); return jsstr->toStringData(); } 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 (getScope(_context)->getProto().instanceOf(_value)) return NumberLongInfo::ToNumberLong(_context, _value); if (JS::ToInt64(_context, _value, &out)) return out; throwCurrentJSException(_context, ErrorCodes::BadValue, "Failure to convert value to number"); } Decimal128 ValueWriter::toDecimal128() { std::uint32_t signalingFlags = 0; if (_value.isNumber()) { return Decimal128(toNumber(), Decimal128::kRoundTo15Digits); } if (getScope(_context)->getProto().instanceOf(_value)) return Decimal128(NumberIntInfo::ToNumberInt(_context, _value)); if (getScope(_context)->getProto().instanceOf(_value)) return Decimal128(static_cast(NumberLongInfo::ToNumberLong(_context, _value))); if (getScope(_context)->getProto().instanceOf(_value)) return NumberDecimalInfo::ToNumberDecimal(_context, _value); if (_value.isString()) { std::string input = toString(); Decimal128 decimal = Decimal128(input, &signalingFlags); uassert(ErrorCodes::BadValue, str::stream() << "Input is not a valid Decimal128 value.", !Decimal128::hasFlag(signalingFlags, Decimal128::SignalingFlag::kInvalid)); uassert(ErrorCodes::BadValue, str::stream() << "Input out of range of Decimal128 value (inexact).", !Decimal128::hasFlag(signalingFlags, Decimal128::SignalingFlag::kInexact)); uassert(ErrorCodes::BadValue, str::stream() << "Input out of range of Decimal128 value (underflow).", !Decimal128::hasFlag(signalingFlags, Decimal128::SignalingFlag::kUnderflow)); uassert(ErrorCodes::BadValue, str::stream() << "Input out of range of Decimal128 value (overflow).", !Decimal128::hasFlag(signalingFlags, Decimal128::SignalingFlag::kOverflow)); return decimal; } uasserted(ErrorCodes::BadValue, str::stream() << "Unable to write Decimal128 value."); } void ValueWriter::writeThis(BSONObjBuilder* b, StringData sd, ObjectWrapper::WriteFieldRecursionFrames* frames) { uassert(17279, str::stream() << "Exceeded depth limit of " << ObjectWrapper::kMaxWriteFieldDepth << " when converting js object to BSON. Do you have a cycle?", frames->size() < ObjectWrapper::kMaxWriteFieldDepth); // 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()) { JSStringWrapper jsstr; b->append(sd, toStringData(&jsstr)); } else if (_value.isNumber()) { double val = toNumber(); // if previous type was integer, attempt to represent 'val' as an integer auto intval = representAs(val); if (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()) { _writeObject(b, sd, frames); } 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, ObjectWrapper::WriteFieldRecursionFrames* frames) { auto scope = getScope(_context); // We open a block here because it's important that the two rooting types // we need (obj and o) go out of scope before we actually open a // new WriteFieldFrame (in the emplace at the bottom of the function). If // we don't do this, we'll destroy the local roots in this function body // before the frame we added, which will break the gc rooting list. { JS::RootedObject obj(_context, _value.toObjectOrNull()); ObjectWrapper o(_context, obj); auto jsclass = JS_GetClass(obj); if (jsclass) { if (scope->getProto().getJSClass() == jsclass) { b->append(sd, OIDInfo::getOID(_context, obj)); return; } if (scope->getProto().getJSClass() == jsclass) { long long out = NumberLongInfo::ToNumberLong(_context, obj); b->append(sd, out); return; } if (scope->getProto().getJSClass() == jsclass) { b->append(sd, NumberIntInfo::ToNumberInt(_context, obj)); return; } if (scope->getProto().getJSClass() == jsclass) { if (o.hasOwnField(InternedString::scope) // CodeWScope && o.type(InternedString::scope) == mongo::Object) { if (o.type(InternedString::code) != mongo::String) { uasserted(ErrorCodes::BadValue, "code must be a string"); } b->appendCodeWScope( sd, o.getString(InternedString::code), o.getObject(InternedString::scope)); } else { // Code if (o.type(InternedString::code) != mongo::String) { uasserted(ErrorCodes::BadValue, "code must be a string"); } b->appendCode(sd, o.getString(InternedString::code)); } return; } if (scope->getProto().getJSClass() == jsclass) { b->append(sd, NumberDecimalInfo::ToNumberDecimal(_context, obj)); return; } if (scope->getProto().getJSClass() == jsclass) { uassert(ErrorCodes::BadValue, "can't serialize DBPointer prototype", scope->getProto().getProto() != obj); JS::RootedValue id(_context); o.getValue("id", &id); b->appendDBRef(sd, o.getString("ns"), OIDInfo::getOID(_context, id)); return; } if (scope->getProto().getJSClass() == jsclass) { auto str = static_cast(JS_GetPrivate(obj)); uassert(ErrorCodes::BadValue, "Cannot call getter on BinData prototype", str); auto binData = base64::decode(*str); b->appendBinData(sd, binData.size(), static_cast( static_cast(o.getNumber(InternedString::type))), binData.c_str()); return; } if (scope->getProto().getJSClass() == jsclass) { Timestamp ot(o.getNumber("t"), o.getNumber("i")); b->append(sd, ot); return; } if (scope->getProto().getJSClass() == jsclass) { b->appendMinKey(sd); return; } if (scope->getProto().getJSClass() == jsclass) { b->appendMaxKey(sd); return; } } auto protoKey = JS::IdentifyStandardInstanceOrPrototype(obj); switch (protoKey) { case JSProto_Function: { uassert(16716, "cannot convert native function to BSON", !scope->getProto().instanceOf(obj)); JSStringWrapper jsstr; b->appendCode(sd, ValueWriter(_context, _value).toStringData(&jsstr)); return; } case JSProto_RegExp: { 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); return; } case JSProto_Date: { Date_t d; if (JS::IdentifyStandardPrototype(obj) == JSProto_Date) { d = Date_t::fromMillisSinceEpoch(0); } else { JS::RootedValue dateval(_context); o.callMethod("getTime", &dateval); d = Date_t::fromMillisSinceEpoch(ValueWriter(_context, dateval).toInt64()); } b->appendDate(sd, d); return; } default: break; } } // nested object or array // This emplace is effectively a recursive function call, as this code path // unwinds back to ObjectWrapper::toBSON. In that function we'll actually // write the child we've just pushed onto the frames stack. frames->emplace(_context, _value.toObjectOrNull(), b, sd); } } // namespace mozjs } // namespace mongo