/* -*- 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/. */ #include "builtin/MapObject.h" #include "jscntxt.h" #include "jsiter.h" #include "jsobj.h" #include "ds/OrderedHashTable.h" #include "gc/Marking.h" #include "js/Utility.h" #include "vm/GlobalObject.h" #include "vm/Interpreter.h" #include "vm/Symbol.h" #include "jsobjinlines.h" #include "vm/Interpreter-inl.h" #include "vm/NativeObject-inl.h" using namespace js; using mozilla::ArrayLength; using mozilla::IsNaN; using mozilla::NumberEqualsInt32; using JS::DoubleNaNValue; using JS::ForOfIterator; /*** HashableValue *******************************************************************************/ bool HashableValue::setValue(JSContext* cx, HandleValue v) { if (v.isString()) { // Atomize so that hash() and operator==() are fast and infallible. JSString* str = AtomizeString(cx, v.toString(), DoNotPinAtom); if (!str) return false; value = StringValue(str); } else if (v.isDouble()) { double d = v.toDouble(); int32_t i; if (NumberEqualsInt32(d, &i)) { // Normalize int32_t-valued doubles to int32_t for faster hashing and testing. value = Int32Value(i); } else if (IsNaN(d)) { // NaNs with different bits must hash and test identically. value = DoubleNaNValue(); } else { value = v; } } else { value = v; } MOZ_ASSERT(value.isUndefined() || value.isNull() || value.isBoolean() || value.isNumber() || value.isString() || value.isSymbol() || value.isObject()); return true; } static HashNumber HashValue(const Value& v, const mozilla::HashCodeScrambler& hcs) { // HashableValue::setValue normalizes values so that the SameValue relation // on HashableValues is the same as the == relationship on // value.asRawBits(). So why not just return that? Security. // // To avoid revealing GC of atoms, string-based hash codes are computed // from the string contents rather than any pointer; to avoid revealing // addresses, pointer-based hash codes are computed using the // HashCodeScrambler. if (v.isString()) return v.toString()->asAtom().hash(); if (v.isSymbol()) return v.toSymbol()->hash(); #if(0) // Remove this code (M1312001/M1330769). The exploit cannot succeed on PowerPC // and this code has a performance impact. if (v.isObject()) return hcs.scramble(v.asRawBits()); MOZ_ASSERT(v.isNull() || !v.isGCThing(), "do not reveal pointers via hash codes"); #endif return v.asRawBits(); } HashNumber HashableValue::hash(const mozilla::HashCodeScrambler& hcs) const { return HashValue(value, hcs); } bool HashableValue::operator==(const HashableValue& other) const { // Two HashableValues are equal if they have equal bits. bool b = (value.asRawBits() == other.value.asRawBits()); #ifdef DEBUG bool same; PerThreadData* data = TlsPerThreadData.get(); RootedValue valueRoot(data, value); RootedValue otherRoot(data, other.value); MOZ_ASSERT(SameValue(nullptr, valueRoot, otherRoot, &same)); MOZ_ASSERT(same == b); #endif return b; } HashableValue HashableValue::mark(JSTracer* trc) const { HashableValue hv(*this); TraceEdge(trc, &hv.value, "key"); return hv; } /*** MapIterator *********************************************************************************/ namespace { } /* anonymous namespace */ const Class MapIteratorObject::class_ = { "Map Iterator", JSCLASS_HAS_RESERVED_SLOTS(MapIteratorObject::SlotCount), nullptr, /* addProperty */ nullptr, /* delProperty */ nullptr, /* getProperty */ nullptr, /* setProperty */ nullptr, /* enumerate */ nullptr, /* resolve */ nullptr, /* mayResolve */ MapIteratorObject::finalize }; const JSFunctionSpec MapIteratorObject::methods[] = { JS_SELF_HOSTED_FN("next", "MapIteratorNext", 0, 0), JS_FS_END }; static inline ValueMap::Range* MapIteratorObjectRange(NativeObject* obj) { MOZ_ASSERT(obj->is()); return static_cast(obj->getSlot(MapIteratorObject::RangeSlot).toPrivate()); } inline MapObject::IteratorKind MapIteratorObject::kind() const { int32_t i = getSlot(KindSlot).toInt32(); MOZ_ASSERT(i == MapObject::Keys || i == MapObject::Values || i == MapObject::Entries); return MapObject::IteratorKind(i); } bool GlobalObject::initMapIteratorProto(JSContext* cx, Handle global) { Rooted base(cx, GlobalObject::getOrCreateIteratorPrototype(cx, global)); if (!base) return false; RootedPlainObject proto(cx, NewObjectWithGivenProto(cx, base)); if (!proto) return false; if (!JS_DefineFunctions(cx, proto, MapIteratorObject::methods) || !DefineToStringTag(cx, proto, cx->names().MapIterator)) return false; global->setReservedSlot(MAP_ITERATOR_PROTO, ObjectValue(*proto)); return true; } MapIteratorObject* MapIteratorObject::create(JSContext* cx, HandleObject mapobj, ValueMap* data, MapObject::IteratorKind kind) { Rooted global(cx, &mapobj->global()); Rooted proto(cx, GlobalObject::getOrCreateMapIteratorPrototype(cx, global)); if (!proto) return nullptr; ValueMap::Range* range = cx->new_(data->all()); if (!range) return nullptr; MapIteratorObject* iterobj = NewObjectWithGivenProto(cx, proto); if (!iterobj) { js_delete(range); return nullptr; } iterobj->setSlot(TargetSlot, ObjectValue(*mapobj)); iterobj->setSlot(RangeSlot, PrivateValue(range)); iterobj->setSlot(KindSlot, Int32Value(int32_t(kind))); return iterobj; } void MapIteratorObject::finalize(FreeOp* fop, JSObject* obj) { fop->delete_(MapIteratorObjectRange(static_cast(obj))); } bool MapIteratorObject::next(JSContext* cx, Handle mapIterator, HandleArrayObject resultPairObj) { MOZ_ASSERT(resultPairObj->getDenseInitializedLength() == 2); ValueMap::Range* range = MapIteratorObjectRange(mapIterator); if (!range || range->empty()) { js_delete(range); mapIterator->setReservedSlot(RangeSlot, PrivateValue(nullptr)); return true; } switch (mapIterator->kind()) { case MapObject::Keys: resultPairObj->setDenseElementWithType(cx, 0, range->front().key.get()); break; case MapObject::Values: resultPairObj->setDenseElementWithType(cx, 1, range->front().value); break; case MapObject::Entries: { resultPairObj->setDenseElementWithType(cx, 0, range->front().key.get()); resultPairObj->setDenseElementWithType(cx, 1, range->front().value); break; } } range->popFront(); return false; } /*** Map *****************************************************************************************/ const Class MapObject::class_ = { "Map", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_CACHED_PROTO(JSProto_Map), nullptr, // addProperty nullptr, // delProperty nullptr, // getProperty nullptr, // setProperty nullptr, // enumerate nullptr, // resolve nullptr, // mayResolve finalize, nullptr, // call nullptr, // hasInstance nullptr, // construct mark }; const JSPropertySpec MapObject::properties[] = { JS_PSG("size", size, 0), JS_PS_END }; const JSFunctionSpec MapObject::methods[] = { JS_FN("get", get, 1, 0), JS_FN("has", has, 1, 0), JS_FN("set", set, 2, 0), JS_FN("delete", delete_, 1, 0), JS_FN("keys", keys, 0, 0), JS_FN("values", values, 0, 0), JS_FN("clear", clear, 0, 0), JS_SELF_HOSTED_FN("forEach", "MapForEach", 2, 0), JS_FS_END }; const JSPropertySpec MapObject::staticProperties[] = { JS_SELF_HOSTED_SYM_GET(species, "MapSpecies", 0), JS_PS_END }; static JSObject* InitClass(JSContext* cx, Handle global, const Class* clasp, JSProtoKey key, Native construct, const JSPropertySpec* properties, const JSFunctionSpec* methods, const JSPropertySpec* staticProperties) { RootedPlainObject proto(cx, NewBuiltinClassInstance(cx)); if (!proto) return nullptr; Rooted ctor(cx, global->createConstructor(cx, construct, ClassName(key, cx), 0)); if (!ctor || !JS_DefineProperties(cx, ctor, staticProperties) || !LinkConstructorAndPrototype(cx, ctor, proto) || !DefinePropertiesAndFunctions(cx, proto, properties, methods) || !GlobalObject::initBuiltinConstructor(cx, global, key, ctor, proto)) { return nullptr; } return proto; } JSObject* MapObject::initClass(JSContext* cx, JSObject* obj) { Rooted global(cx, &obj->as()); RootedObject proto(cx, InitClass(cx, global, &class_, JSProto_Map, construct, properties, methods, staticProperties)); if (proto) { // Define the "entries" method. JSFunction* fun = JS_DefineFunction(cx, proto, "entries", entries, 0, 0); if (!fun) return nullptr; // Define its alias. RootedValue funval(cx, ObjectValue(*fun)); RootedId iteratorId(cx, SYMBOL_TO_JSID(cx->wellKnownSymbols().iterator)); if (!JS_DefinePropertyById(cx, proto, iteratorId, funval, 0)) return nullptr; // Define Map.prototype[@@toStringTag]. if (!DefineToStringTag(cx, proto, cx->names().Map)) return nullptr; } return proto; } template static void MarkKey(Range& r, const HashableValue& key, JSTracer* trc) { HashableValue newKey = key.mark(trc); if (newKey.get() != key.get()) { // The hash function only uses the bits of the Value, so it is safe to // rekey even when the object or string has been modified by the GC. r.rekeyFront(newKey); } } void MapObject::mark(JSTracer* trc, JSObject* obj) { if (ValueMap* map = obj->as().getData()) { for (ValueMap::Range r = map->all(); !r.empty(); r.popFront()) { MarkKey(r, r.front().key, trc); TraceEdge(trc, &r.front().value, "value"); } } } struct UnbarrieredHashPolicy { typedef Value Lookup; static HashNumber hash(const Lookup& v, const mozilla::HashCodeScrambler& hcs) { return HashValue(v, hcs); } static bool match(const Value& k, const Lookup& l) { return k == l; } static bool isEmpty(const Value& v) { return v.isMagic(JS_HASH_KEY_EMPTY); } static void makeEmpty(Value* vp) { vp->setMagic(JS_HASH_KEY_EMPTY); } }; template class OrderedHashTableRef : public gc::BufferableRef { TableType* table; Value key; public: explicit OrderedHashTableRef(TableType* t, const Value& k) : table(t), key(k) {} void trace(JSTracer* trc) override { MOZ_ASSERT(reinterpret_cast(table) ->hash(*reinterpret_cast(&key)) == table->hash(key)); Value prior = key; TraceManuallyBarrieredEdge(trc, &key, "ordered hash table key"); table->rekeyOneEntry(prior, key); } }; inline static void WriteBarrierPost(JSRuntime* rt, ValueMap* map, const Value& key) { typedef OrderedHashMap UnbarrieredMap; if (MOZ_UNLIKELY(key.isObject() && IsInsideNursery(&key.toObject()))) { rt->gc.storeBuffer.putGeneric(OrderedHashTableRef( reinterpret_cast(map), key)); } } inline static void WriteBarrierPost(JSRuntime* rt, ValueSet* set, const Value& key) { typedef OrderedHashSet UnbarrieredSet; if (MOZ_UNLIKELY(key.isObject() && IsInsideNursery(&key.toObject()))) { rt->gc.storeBuffer.putGeneric(OrderedHashTableRef( reinterpret_cast(set), key)); } } bool MapObject::getKeysAndValuesInterleaved(JSContext* cx, HandleObject obj, JS::AutoValueVector* entries) { ValueMap* map = obj->as().getData(); if (!map) return false; for (ValueMap::Range r = map->all(); !r.empty(); r.popFront()) { if (!entries->append(r.front().key.get()) || !entries->append(r.front().value)) { return false; } } return true; } bool MapObject::set(JSContext* cx, HandleObject obj, HandleValue k, HandleValue v) { ValueMap* map = obj->as().getData(); if (!map) return false; Rooted key(cx); if (!key.setValue(cx, k)) return false; RelocatableValue rval(v); if (!map->put(key, rval)) { ReportOutOfMemory(cx); return false; } WriteBarrierPost(cx->runtime(), map, key.value()); return true; } MapObject* MapObject::create(JSContext* cx, HandleObject proto /* = nullptr */) { auto map = cx->make_unique(cx->runtime(), cx->compartment()->randomHashCodeScrambler()); if (!map || !map->init()) { ReportOutOfMemory(cx); return nullptr; } MapObject* mapObj = NewObjectWithClassProto(cx, proto); if (!mapObj) return nullptr; mapObj->setPrivate(map.release()); return mapObj; } void MapObject::finalize(FreeOp* fop, JSObject* obj) { if (ValueMap* map = obj->as().getData()) fop->delete_(map); } bool MapObject::construct(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); if (!ThrowIfNotConstructing(cx, args, "Map")) return false; RootedObject proto(cx); RootedObject newTarget(cx, &args.newTarget().toObject()); if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) return false; Rooted obj(cx, MapObject::create(cx, proto)); if (!obj) return false; if (!args.get(0).isNullOrUndefined()) { RootedValue adderVal(cx); if (!GetProperty(cx, obj, obj, cx->names().set, &adderVal)) return false; if (!IsCallable(adderVal)) return ReportIsNotFunction(cx, adderVal); bool isOriginalAdder = IsNativeFunction(adderVal, MapObject::set); RootedValue mapVal(cx, ObjectValue(*obj)); FastInvokeGuard fig(cx, adderVal); InvokeArgs& args2 = fig.args(); ForOfIterator iter(cx); if (!iter.init(args[0])) return false; RootedValue pairVal(cx); RootedObject pairObj(cx); Rooted hkey(cx); ValueMap* map = obj->getData(); while (true) { bool done; if (!iter.next(&pairVal, &done)) return false; if (done) break; if (!pairVal.isObject()) { JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INVALID_MAP_ITERABLE, "Map"); return false; } pairObj = &pairVal.toObject(); if (!pairObj) return false; RootedValue key(cx); if (!GetElement(cx, pairObj, pairObj, 0, &key)) return false; RootedValue val(cx); if (!GetElement(cx, pairObj, pairObj, 1, &val)) return false; if (isOriginalAdder) { if (!hkey.setValue(cx, key)) return false; RelocatableValue rval(val); if (!map->put(hkey, rval)) { ReportOutOfMemory(cx); return false; } WriteBarrierPost(cx->runtime(), map, key); } else { if (!args2.init(cx, 2)) return false; args2.setCallee(adderVal); args2.setThis(mapVal); args2[0].set(key); args2[1].set(val); if (!fig.invoke(cx)) return false; } } } args.rval().setObject(*obj); return true; } bool MapObject::is(HandleValue v) { return v.isObject() && v.toObject().hasClass(&class_) && v.toObject().as().getPrivate(); } bool MapObject::is(HandleObject o) { return o->hasClass(&class_) && o->as().getPrivate(); } #define ARG0_KEY(cx, args, key) \ Rooted key(cx); \ if (args.length() > 0 && !key.setValue(cx, args[0])) \ return false ValueMap& MapObject::extract(HandleObject o) { MOZ_ASSERT(o->hasClass(&MapObject::class_)); return *o->as().getData(); } ValueMap& MapObject::extract(CallReceiver call) { MOZ_ASSERT(call.thisv().isObject()); MOZ_ASSERT(call.thisv().toObject().hasClass(&MapObject::class_)); return *call.thisv().toObject().as().getData(); } uint32_t MapObject::size(JSContext* cx, HandleObject obj) { ValueMap& map = extract(obj); static_assert(sizeof(map.count()) <= sizeof(uint32_t), "map count must be precisely representable as a JS number"); return map.count(); } bool MapObject::size_impl(JSContext* cx, const CallArgs& args) { RootedObject obj(cx, &args.thisv().toObject()); args.rval().setNumber(size(cx, obj)); return true; } bool MapObject::size(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool MapObject::get(JSContext* cx, HandleObject obj, HandleValue key, MutableHandleValue rval) { ValueMap& map = extract(obj); Rooted k(cx); if (!k.setValue(cx, key)) return false; if (ValueMap::Entry* p = map.get(k)) rval.set(p->value); else rval.setUndefined(); return true; } bool MapObject::get_impl(JSContext* cx, const CallArgs& args) { RootedObject obj(cx, &args.thisv().toObject()); return get(cx, obj, args.get(0), args.rval()); } bool MapObject::get(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool MapObject::has(JSContext* cx, HandleObject obj, HandleValue key, bool* rval) { ValueMap& map = extract(obj); Rooted k(cx); if (!k.setValue(cx, key)) return false; *rval = map.has(k); return true; } bool MapObject::has_impl(JSContext* cx, const CallArgs& args) { bool found; RootedObject obj(cx, &args.thisv().toObject()); if (has(cx, obj, args.get(0), &found)) { args.rval().setBoolean(found); return true; } return false; } bool MapObject::has(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool MapObject::set_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(MapObject::is(args.thisv())); ValueMap& map = extract(args); ARG0_KEY(cx, args, key); RelocatableValue rval(args.get(1)); if (!map.put(key, rval)) { ReportOutOfMemory(cx); return false; } WriteBarrierPost(cx->runtime(), &map, key.value()); args.rval().set(args.thisv()); return true; } bool MapObject::set(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool MapObject::delete_(JSContext *cx, HandleObject obj, HandleValue key, bool *rval) { ValueMap &map = extract(obj); Rooted k(cx); if (!k.setValue(cx, key)) return false; if (!map.remove(k, rval)) { ReportOutOfMemory(cx); return false; } return true; } bool MapObject::delete_impl(JSContext *cx, const CallArgs& args) { // MapObject::mark does not mark deleted entries. Incremental GC therefore // requires that no RelocatableValue objects pointing to heap values be // left alive in the ValueMap. // // OrderedHashMap::remove() doesn't destroy the removed entry. It merely // calls OrderedHashMap::MapOps::makeEmpty. But that is sufficient, because // makeEmpty clears the value by doing e->value = Value(), and in the case // of a ValueMap, Value() means RelocatableValue(), which is the same as // RelocatableValue(UndefinedValue()). MOZ_ASSERT(MapObject::is(args.thisv())); ValueMap& map = extract(args); ARG0_KEY(cx, args, key); bool found; if (!map.remove(key, &found)) { ReportOutOfMemory(cx); return false; } args.rval().setBoolean(found); return true; } bool MapObject::delete_(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool MapObject::iterator(JSContext* cx, IteratorKind kind, HandleObject obj, MutableHandleValue iter) { ValueMap& map = extract(obj); Rooted iterobj(cx, MapIteratorObject::create(cx, obj, &map, kind)); return iterobj && (iter.setObject(*iterobj), true); } bool MapObject::iterator_impl(JSContext* cx, const CallArgs& args, IteratorKind kind) { RootedObject obj(cx, &args.thisv().toObject()); return iterator(cx, kind, obj, args.rval()); } bool MapObject::keys_impl(JSContext* cx, const CallArgs& args) { return iterator_impl(cx, args, Keys); } bool MapObject::keys(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, keys_impl, args); } bool MapObject::values_impl(JSContext* cx, const CallArgs& args) { return iterator_impl(cx, args, Values); } bool MapObject::values(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, values_impl, args); } bool MapObject::entries_impl(JSContext* cx, const CallArgs& args) { return iterator_impl(cx, args, Entries); } bool MapObject::entries(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, entries_impl, args); } bool MapObject::clear_impl(JSContext* cx, const CallArgs& args) { RootedObject obj(cx, &args.thisv().toObject()); args.rval().setUndefined(); return clear(cx, obj); } bool MapObject::clear(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, clear_impl, args); } bool MapObject::clear(JSContext* cx, HandleObject obj) { ValueMap& map = extract(obj); if (!map.clear()) { ReportOutOfMemory(cx); return false; } return true; } JSObject* js::InitMapClass(JSContext* cx, HandleObject obj) { return MapObject::initClass(cx, obj); } /*** SetIterator *********************************************************************************/ namespace { class SetIteratorObject : public NativeObject { public: static const Class class_; enum { TargetSlot, KindSlot, RangeSlot, SlotCount }; static const JSFunctionSpec methods[]; static SetIteratorObject* create(JSContext* cx, HandleObject setobj, ValueSet* data, SetObject::IteratorKind kind); static bool next(JSContext* cx, unsigned argc, Value* vp); static void finalize(FreeOp* fop, JSObject* obj); private: static inline bool is(HandleValue v); inline ValueSet::Range* range(); inline SetObject::IteratorKind kind() const; static bool next_impl(JSContext* cx, const CallArgs& args); }; } /* anonymous namespace */ const Class SetIteratorObject::class_ = { "Set Iterator", JSCLASS_HAS_RESERVED_SLOTS(SetIteratorObject::SlotCount), nullptr, /* addProperty */ nullptr, /* delProperty */ nullptr, /* getProperty */ nullptr, /* setProperty */ nullptr, /* enumerate */ nullptr, /* resolve */ nullptr, /* mayResolve */ SetIteratorObject::finalize }; const JSFunctionSpec SetIteratorObject::methods[] = { JS_FN("next", next, 0, 0), JS_FS_END }; inline ValueSet::Range* SetIteratorObject::range() { return static_cast(getSlot(RangeSlot).toPrivate()); } inline SetObject::IteratorKind SetIteratorObject::kind() const { int32_t i = getSlot(KindSlot).toInt32(); MOZ_ASSERT(i == SetObject::Values || i == SetObject::Entries); return SetObject::IteratorKind(i); } bool GlobalObject::initSetIteratorProto(JSContext* cx, Handle global) { Rooted base(cx, GlobalObject::getOrCreateIteratorPrototype(cx, global)); if (!base) return false; RootedPlainObject proto(cx, NewObjectWithGivenProto(cx, base)); if (!proto) return false; if (!JS_DefineFunctions(cx, proto, SetIteratorObject::methods) || !DefineToStringTag(cx, proto, cx->names().SetIterator)) return false; global->setReservedSlot(SET_ITERATOR_PROTO, ObjectValue(*proto)); return true; } SetIteratorObject* SetIteratorObject::create(JSContext* cx, HandleObject setobj, ValueSet* data, SetObject::IteratorKind kind) { Rooted global(cx, &setobj->global()); Rooted proto(cx, GlobalObject::getOrCreateSetIteratorPrototype(cx, global)); if (!proto) return nullptr; ValueSet::Range* range = cx->new_(data->all()); if (!range) return nullptr; SetIteratorObject* iterobj = NewObjectWithGivenProto(cx, proto); if (!iterobj) { js_delete(range); return nullptr; } iterobj->setSlot(TargetSlot, ObjectValue(*setobj)); iterobj->setSlot(KindSlot, Int32Value(int32_t(kind))); iterobj->setSlot(RangeSlot, PrivateValue(range)); return iterobj; } void SetIteratorObject::finalize(FreeOp* fop, JSObject* obj) { fop->delete_(obj->as().range()); } bool SetIteratorObject::is(HandleValue v) { return v.isObject() && v.toObject().is(); } bool SetIteratorObject::next_impl(JSContext* cx, const CallArgs& args) { SetIteratorObject& thisobj = args.thisv().toObject().as(); ValueSet::Range* range = thisobj.range(); RootedValue value(cx); bool done; if (!range || range->empty()) { js_delete(range); thisobj.setReservedSlot(RangeSlot, PrivateValue(nullptr)); value.setUndefined(); done = true; } else { switch (thisobj.kind()) { case SetObject::Values: value = range->front().get(); break; case SetObject::Entries: { JS::AutoValueArray<2> pair(cx); pair[0].set(range->front().get()); pair[1].set(range->front().get()); JSObject* pairObj = NewDenseCopiedArray(cx, 2, pair.begin()); if (!pairObj) return false; value.setObject(*pairObj); break; } } range->popFront(); done = false; } RootedObject result(cx, CreateItrResultObject(cx, value, done)); if (!result) return false; args.rval().setObject(*result); return true; } bool SetIteratorObject::next(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, next_impl, args); } /*** Set *****************************************************************************************/ const Class SetObject::class_ = { "Set", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_CACHED_PROTO(JSProto_Set), nullptr, // addProperty nullptr, // delProperty nullptr, // getProperty nullptr, // setProperty nullptr, // enumerate nullptr, // resolve nullptr, // mayResolve finalize, nullptr, // call nullptr, // hasInstance nullptr, // construct mark }; const JSPropertySpec SetObject::properties[] = { JS_PSG("size", size, 0), JS_PS_END }; const JSFunctionSpec SetObject::methods[] = { JS_FN("has", has, 1, 0), JS_FN("add", add, 1, 0), JS_FN("delete", delete_, 1, 0), JS_FN("entries", entries, 0, 0), JS_FN("clear", clear, 0, 0), JS_SELF_HOSTED_FN("forEach", "SetForEach", 2, 0), JS_FS_END }; const JSPropertySpec SetObject::staticProperties[] = { JS_SELF_HOSTED_SYM_GET(species, "SetSpecies", 0), JS_PS_END }; JSObject* SetObject::initClass(JSContext* cx, JSObject* obj) { Rooted global(cx, &obj->as()); RootedObject proto(cx, InitClass(cx, global, &class_, JSProto_Set, construct, properties, methods, staticProperties)); if (proto) { // Define the "values" method. JSFunction* fun = JS_DefineFunction(cx, proto, "values", values, 0, 0); if (!fun) return nullptr; // Define its aliases. RootedValue funval(cx, ObjectValue(*fun)); if (!JS_DefineProperty(cx, proto, "keys", funval, 0)) return nullptr; RootedId iteratorId(cx, SYMBOL_TO_JSID(cx->wellKnownSymbols().iterator)); if (!JS_DefinePropertyById(cx, proto, iteratorId, funval, 0)) return nullptr; // Define Set.prototype[@@toStringTag]. if (!DefineToStringTag(cx, proto, cx->names().Set)) return nullptr; } return proto; } bool SetObject::keys(JSContext* cx, HandleObject obj, JS::AutoValueVector* keys) { ValueSet* set = obj->as().getData(); if (!set) return false; for (ValueSet::Range r = set->all(); !r.empty(); r.popFront()) { if (!keys->append(r.front().get())) return false; } return true; } bool SetObject::add(JSContext* cx, HandleObject obj, HandleValue k) { ValueSet* set = obj->as().getData(); if (!set) return false; Rooted key(cx); if (!key.setValue(cx, k)) return false; if (!set->put(key)) { ReportOutOfMemory(cx); return false; } WriteBarrierPost(cx->runtime(), set, key.value()); return true; } SetObject* SetObject::create(JSContext* cx, HandleObject proto /* = nullptr */) { auto set = cx->make_unique(cx->runtime(), cx->compartment()->randomHashCodeScrambler()); if (!set || !set->init()) { ReportOutOfMemory(cx); return nullptr; } SetObject* obj = NewObjectWithClassProto(cx, proto); if (!obj) return nullptr; obj->setPrivate(set.release()); return obj; } void SetObject::mark(JSTracer* trc, JSObject* obj) { SetObject* setobj = static_cast(obj); if (ValueSet* set = setobj->getData()) { for (ValueSet::Range r = set->all(); !r.empty(); r.popFront()) MarkKey(r, r.front(), trc); } } void SetObject::finalize(FreeOp* fop, JSObject* obj) { SetObject* setobj = static_cast(obj); if (ValueSet* set = setobj->getData()) fop->delete_(set); } bool SetObject::construct(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); if (!ThrowIfNotConstructing(cx, args, "Set")) return false; RootedObject proto(cx); RootedObject newTarget(cx, &args.newTarget().toObject()); if (!GetPrototypeFromConstructor(cx, newTarget, &proto)) return false; Rooted obj(cx, SetObject::create(cx, proto)); if (!obj) return false; if (!args.get(0).isNullOrUndefined()) { RootedValue adderVal(cx); if (!GetProperty(cx, obj, obj, cx->names().add, &adderVal)) return false; if (!IsCallable(adderVal)) return ReportIsNotFunction(cx, adderVal); bool isOriginalAdder = IsNativeFunction(adderVal, SetObject::add); RootedValue setVal(cx, ObjectValue(*obj)); FastInvokeGuard fig(cx, adderVal); InvokeArgs& args2 = fig.args(); RootedValue keyVal(cx); ForOfIterator iter(cx); if (!iter.init(args[0])) return false; Rooted key(cx); ValueSet* set = obj->getData(); while (true) { bool done; if (!iter.next(&keyVal, &done)) return false; if (done) break; if (isOriginalAdder) { if (!key.setValue(cx, keyVal)) return false; if (!set->put(key)) { ReportOutOfMemory(cx); return false; } WriteBarrierPost(cx->runtime(), set, keyVal); } else { if (!args2.init(cx, 1)) return false; args2.setCallee(adderVal); args2.setThis(setVal); args2[0].set(keyVal); if (!fig.invoke(cx)) return false; } } } args.rval().setObject(*obj); return true; } bool SetObject::is(HandleValue v) { return v.isObject() && v.toObject().hasClass(&class_) && v.toObject().as().getPrivate(); } bool SetObject::is(HandleObject o) { return o->hasClass(&class_) && o->as().getPrivate(); } ValueSet & SetObject::extract(HandleObject o) { MOZ_ASSERT(o->hasClass(&SetObject::class_)); return *o->as().getData(); } ValueSet & SetObject::extract(CallReceiver call) { MOZ_ASSERT(call.thisv().isObject()); MOZ_ASSERT(call.thisv().toObject().hasClass(&SetObject::class_)); return *static_cast(call.thisv().toObject()).getData(); } uint32_t SetObject::size(JSContext *cx, HandleObject obj) { MOZ_ASSERT(SetObject::is(obj)); ValueSet &set = extract(obj); static_assert(sizeof(set.count()) <= sizeof(uint32_t), "set count must be precisely representable as a JS number"); return set.count(); } bool SetObject::size_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(is(args.thisv())); ValueSet& set = extract(args); static_assert(sizeof(set.count()) <= sizeof(uint32_t), "set count must be precisely representable as a JS number"); args.rval().setNumber(set.count()); return true; } bool SetObject::size(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool SetObject::has_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(is(args.thisv())); ValueSet& set = extract(args); ARG0_KEY(cx, args, key); args.rval().setBoolean(set.has(key)); return true; } bool SetObject::has(JSContext *cx, HandleObject obj, HandleValue key, bool *rval) { MOZ_ASSERT(SetObject::is(obj)); ValueSet &set = extract(obj); Rooted k(cx); if (!k.setValue(cx, key)) return false; *rval = set.has(k); return true; } bool SetObject::has(JSContext *cx, unsigned argc, Value *vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool SetObject::add_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(is(args.thisv())); ValueSet& set = extract(args); ARG0_KEY(cx, args, key); if (!set.put(key)) { ReportOutOfMemory(cx); return false; } WriteBarrierPost(cx->runtime(), &set, key.value()); args.rval().set(args.thisv()); return true; } bool SetObject::add(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool SetObject::delete_(JSContext *cx, HandleObject obj, HandleValue key, bool *rval) { MOZ_ASSERT(SetObject::is(obj)); ValueSet &set = extract(obj); Rooted k(cx); if (!k.setValue(cx, key)) return false; if (!set.remove(k, rval)) { ReportOutOfMemory(cx); return false; } return true; } bool SetObject::delete_impl(JSContext *cx, const CallArgs& args) { MOZ_ASSERT(is(args.thisv())); ValueSet& set = extract(args); ARG0_KEY(cx, args, key); bool found; if (!set.remove(key, &found)) { ReportOutOfMemory(cx); return false; } args.rval().setBoolean(found); return true; } bool SetObject::delete_(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool SetObject::iterator(JSContext *cx, IteratorKind kind, HandleObject obj, MutableHandleValue iter) { MOZ_ASSERT(SetObject::is(obj)); ValueSet &set = extract(obj); Rooted iterobj(cx, SetIteratorObject::create(cx, obj, &set, kind)); return iterobj && (iter.setObject(*iterobj), true); } bool SetObject::iterator_impl(JSContext *cx, const CallArgs& args, IteratorKind kind) { Rooted setobj(cx, &args.thisv().toObject().as()); ValueSet& set = *setobj->getData(); Rooted iterobj(cx, SetIteratorObject::create(cx, setobj, &set, kind)); if (!iterobj) return false; args.rval().setObject(*iterobj); return true; } bool SetObject::values_impl(JSContext* cx, const CallArgs& args) { return iterator_impl(cx, args, Values); } bool SetObject::values(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, values_impl, args); } bool SetObject::entries_impl(JSContext* cx, const CallArgs& args) { return iterator_impl(cx, args, Entries); } bool SetObject::entries(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, entries_impl, args); } bool SetObject::clear(JSContext *cx, HandleObject obj) { MOZ_ASSERT(SetObject::is(obj)); ValueSet &set = extract(obj); if (!set.clear()) { ReportOutOfMemory(cx); return false; } return true; } bool SetObject::clear_impl(JSContext *cx, const CallArgs& args) { Rooted setobj(cx, &args.thisv().toObject().as()); if (!setobj->getData()->clear()) { ReportOutOfMemory(cx); return false; } args.rval().setUndefined(); return true; } bool SetObject::clear(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, clear_impl, args); } JSObject* js::InitSetClass(JSContext* cx, HandleObject obj) { return SetObject::initClass(cx, obj); } const JSFunctionSpec selfhosting_collection_iterator_methods[] = { JS_FN("std_Set_iterator_next", SetIteratorObject::next, 0, 0), JS_FS_END }; bool js::InitSelfHostingCollectionIteratorFunctions(JSContext* cx, HandleObject obj) { return JS_DefineFunctions(cx, obj, selfhosting_collection_iterator_methods); } /*** JS static utility functions *********************************************/ static bool forEach(const char* funcName, JSContext *cx, HandleObject obj, HandleValue callbackFn, HandleValue thisArg) { CHECK_REQUEST(cx); RootedId forEachId(cx, NameToId(cx->names().forEach)); RootedFunction forEachFunc(cx, JS::GetSelfHostedFunction(cx, funcName, forEachId, 2)); if (!forEachFunc) return false; InvokeArgs args(cx); if (!args.init(cx, 2)) return false; args.setCallee(JS::ObjectValue(*forEachFunc)); args.setThis(JS::ObjectValue(*obj)); args[0].set(callbackFn); args[1].set(thisArg); return Invoke(cx, args); } // Handles Clear/Size for public jsapi map/set access template RetT CallObjFunc(RetT(*ObjFunc)(JSContext*, HandleObject), JSContext* cx, HandleObject obj) { CHECK_REQUEST(cx); assertSameCompartment(cx, obj); // Always unwrap, in case this is an xray or cross-compartment wrapper. RootedObject unwrappedObj(cx); unwrappedObj = UncheckedUnwrap(obj); // Enter the compartment of the backing object before calling functions on // it. JSAutoCompartment ac(cx, unwrappedObj); return ObjFunc(cx, unwrappedObj); } // Handles Has/Delete for public jsapi map/set access bool CallObjFunc(bool(*ObjFunc)(JSContext *cx, HandleObject obj, HandleValue key, bool *rval), JSContext *cx, HandleObject obj, HandleValue key, bool *rval) { CHECK_REQUEST(cx); assertSameCompartment(cx, obj, key); // Always unwrap, in case this is an xray or cross-compartment wrapper. RootedObject unwrappedObj(cx); unwrappedObj = UncheckedUnwrap(obj); JSAutoCompartment ac(cx, unwrappedObj); // If we're working with a wrapped map/set, rewrap the key into the // compartment of the unwrapped map/set. RootedValue wrappedKey(cx, key); if (obj != unwrappedObj) { if (!JS_WrapValue(cx, &wrappedKey)) return false; } return ObjFunc(cx, unwrappedObj, wrappedKey, rval); } // Handles iterator generation for public jsapi map/set access template bool CallObjFunc(bool(*ObjFunc)(JSContext* cx, Iter kind, HandleObject obj, MutableHandleValue iter), JSContext *cx, Iter iterType, HandleObject obj, MutableHandleValue rval) { CHECK_REQUEST(cx); assertSameCompartment(cx, obj); // Always unwrap, in case this is an xray or cross-compartment wrapper. RootedObject unwrappedObj(cx); unwrappedObj = UncheckedUnwrap(obj); { // Retrieve the iterator while in the unwrapped map/set's compartment, // otherwise we'll crash on a compartment assert. JSAutoCompartment ac(cx, unwrappedObj); if (!ObjFunc(cx, iterType, unwrappedObj, rval)) return false; } // If the caller is in a different compartment than the map/set, rewrap the // iterator object into the caller's compartment. if (obj != unwrappedObj) { if (!JS_WrapValue(cx, rval)) return false; } return true; } /*** JS public APIs **********************************************************/ JS_PUBLIC_API(JSObject*) JS::NewMapObject(JSContext* cx) { return MapObject::create(cx); } JS_PUBLIC_API(uint32_t) JS::MapSize(JSContext* cx, HandleObject obj) { return CallObjFunc(&MapObject::size, cx, obj); } JS_PUBLIC_API(bool) JS::MapGet(JSContext* cx, HandleObject obj, HandleValue key, MutableHandleValue rval) { CHECK_REQUEST(cx); assertSameCompartment(cx, obj, key, rval); // Unwrap the object, and enter its compartment. If object isn't wrapped, // this is essentially a noop. RootedObject unwrappedObj(cx); unwrappedObj = UncheckedUnwrap(obj); { JSAutoCompartment ac(cx, unwrappedObj); RootedValue wrappedKey(cx, key); // If we passed in a wrapper, wrap our key into its compartment now. if (obj != unwrappedObj) { if (!JS_WrapValue(cx, &wrappedKey)) return false; } if (!MapObject::get(cx, unwrappedObj, wrappedKey, rval)) return false; } // If we passed in a wrapper, wrap our return value on the way out. if (obj != unwrappedObj) { if (!JS_WrapValue(cx, rval)) return false; } return true; } JS_PUBLIC_API(bool) JS::MapSet(JSContext *cx, HandleObject obj, HandleValue key, HandleValue val) { CHECK_REQUEST(cx); assertSameCompartment(cx, obj, key, val); // Unwrap the object, and enter its compartment. If object isn't wrapped, // this is essentially a noop. RootedObject unwrappedObj(cx); unwrappedObj = UncheckedUnwrap(obj); { JSAutoCompartment ac(cx, unwrappedObj); // If we passed in a wrapper, wrap both key and value before adding to // the map RootedValue wrappedKey(cx, key); RootedValue wrappedValue(cx, val); if (obj != unwrappedObj) { if (!JS_WrapValue(cx, &wrappedKey) || !JS_WrapValue(cx, &wrappedValue)) { return false; } } return MapObject::set(cx, unwrappedObj, wrappedKey, wrappedValue); } } JS_PUBLIC_API(bool) JS::MapHas(JSContext* cx, HandleObject obj, HandleValue key, bool* rval) { return CallObjFunc(MapObject::has, cx, obj, key, rval); } JS_PUBLIC_API(bool) JS::MapDelete(JSContext *cx, HandleObject obj, HandleValue key, bool* rval) { return CallObjFunc(MapObject::delete_, cx, obj, key, rval); } JS_PUBLIC_API(bool) JS::MapClear(JSContext* cx, HandleObject obj) { return CallObjFunc(&MapObject::clear, cx, obj); } JS_PUBLIC_API(bool) JS::MapKeys(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return CallObjFunc(&MapObject::iterator, cx, MapObject::Keys, obj, rval); } JS_PUBLIC_API(bool) JS::MapValues(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return CallObjFunc(&MapObject::iterator, cx, MapObject::Values, obj, rval); } JS_PUBLIC_API(bool) JS::MapEntries(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return CallObjFunc(&MapObject::iterator, cx, MapObject::Entries, obj, rval); } JS_PUBLIC_API(bool) JS::MapForEach(JSContext *cx, HandleObject obj, HandleValue callbackFn, HandleValue thisVal) { return forEach("MapForEach", cx, obj, callbackFn, thisVal); } JS_PUBLIC_API(JSObject *) JS::NewSetObject(JSContext *cx) { return SetObject::create(cx); } JS_PUBLIC_API(uint32_t) JS::SetSize(JSContext *cx, HandleObject obj) { return CallObjFunc(&SetObject::size, cx, obj); } JS_PUBLIC_API(bool) JS::SetAdd(JSContext *cx, HandleObject obj, HandleValue key) { CHECK_REQUEST(cx); assertSameCompartment(cx, obj, key); // Unwrap the object, and enter its compartment. If object isn't wrapped, // this is essentially a noop. RootedObject unwrappedObj(cx); unwrappedObj = UncheckedUnwrap(obj); { JSAutoCompartment ac(cx, unwrappedObj); // If we passed in a wrapper, wrap key before adding to the set RootedValue wrappedKey(cx, key); if (obj != unwrappedObj) { if (!JS_WrapValue(cx, &wrappedKey)) return false; } return SetObject::add(cx, unwrappedObj, wrappedKey); } } JS_PUBLIC_API(bool) JS::SetHas(JSContext* cx, HandleObject obj, HandleValue key, bool* rval) { return CallObjFunc(SetObject::has, cx, obj, key, rval); } JS_PUBLIC_API(bool) JS::SetDelete(JSContext *cx, HandleObject obj, HandleValue key, bool *rval) { return CallObjFunc(SetObject::delete_, cx, obj, key, rval); } JS_PUBLIC_API(bool) JS::SetClear(JSContext* cx, HandleObject obj) { return CallObjFunc(&SetObject::clear, cx, obj); } JS_PUBLIC_API(bool) JS::SetKeys(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return SetValues(cx, obj, rval); } JS_PUBLIC_API(bool) JS::SetValues(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return CallObjFunc(&SetObject::iterator, cx, SetObject::Values, obj, rval); } JS_PUBLIC_API(bool) JS::SetEntries(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return CallObjFunc(&SetObject::iterator, cx, SetObject::Entries, obj, rval); } JS_PUBLIC_API(bool) JS::SetForEach(JSContext *cx, HandleObject obj, HandleValue callbackFn, HandleValue thisVal) { return forEach("SetForEach", cx, obj, callbackFn, thisVal); }