Revision control

++ // Copyright (C) 2024 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant
++
++ #include "qv4variantassociationobject_p.h"
++
++ #include <private/qqmlengine_p.h>
++
++ QT_BEGIN_NAMESPACE
++
++ /*!
++ * \class QV4::VariantAssociationObject
++ * \internal
++ *
++ * A VariantAssociationObject stores the contents of a QVariantMap or QVariantHash
++ * and makes them acccessible to the JavaScript engine. It behaves mostly like a
++ * regular JavaScript object. The entries of the QVariantMap or QVariantHash are
++ * exposed as properties.
++ *
++ * VariantAssociationObject is a ReferenceObject. Therefore it writes back its contents
++ * to the property it was retrieved from whenever it changes. It also re-reads
++ * the property whenever that one changes.
++ *
++ * As long as a VariantAssociationObject is attached to a property this way, it is the
++ * responsibility of the property's surrounding (C++) object to keep the contents valid.
++ * It has to, for example, track pointers to QObjects potentially deleted in other places
++ * so that they don't become dangling.
++ *
++ * However, the VariantAssociation can also be detached. This happens predominantly by
++ * assigning it to a QML-declared property. In that case, it becomes the
++ * VariantAssociationObject's responsibility to track its contents. To do so, it does not
++ * keep an actual QVariantMap or QVariantHash in this case, but rather stores its contents
++ * as actual JavaScript object properties. This includes QObjectWrappers for all QObject
++ * pointers it may contain. The contents are then marked like all JavaScript properties
++ * when the garbage collector runs, and QObjectWrapper also guards against external
++ * deletion. There is no property to read or write back in this case, and neither
++ * does the internal QVariantMap or QVariantHash need to be updated. Therefore, the
++ * objects stored in the individual properties are also created detached and won't
++ * read or write back.
++ */
++
++ template<typename Return, typename MapCallable, typename HashCallable>
++ Return visitVariantAssociation(
++ const QV4::Heap::VariantAssociationObject *association,
++ MapCallable &&mapCallable, HashCallable &&hashCallable)
++ {
++ switch (association->m_type) {
++ case QV4::Heap::VariantAssociationObject::AssociationType::VariantMap:
++ return std::invoke(
++ std::forward<MapCallable>(mapCallable),
++ reinterpret_cast<const QVariantMap *>(&association->m_variantAssociation));
++ case QV4::Heap::VariantAssociationObject::AssociationType::VariantHash:
++ return std::invoke(
++ std::forward<HashCallable>(hashCallable),
++ reinterpret_cast<const QVariantHash *>(&association->m_variantAssociation));
++ default: Q_UNREACHABLE();
++ };
++ }
++
++ template<typename Return, typename MapCallable, typename HashCallable>
++ Return visitVariantAssociation(
++ QV4::Heap::VariantAssociationObject *association,
++ MapCallable &&mapCallable, HashCallable &&hashCallable)
++ {
++ switch (association->m_type) {
++ case QV4::Heap::VariantAssociationObject::AssociationType::VariantMap:
++ return std::invoke(
++ std::forward<MapCallable>(mapCallable),
++ reinterpret_cast<QVariantMap *>(&association->m_variantAssociation));
++ case QV4::Heap::VariantAssociationObject::AssociationType::VariantHash:
++ return std::invoke(
++ std::forward<HashCallable>(hashCallable),
++ reinterpret_cast<QVariantHash *>(&association->m_variantAssociation));
++ default: Q_UNREACHABLE();
++ };
++ }
++
++ template<typename Return, typename Callable>
++ Return visitVariantAssociation(
++ const QV4::Heap::VariantAssociationObject *association, Callable &&callable)
++ {
++ return visitVariantAssociation<Return>(association, callable, callable);
++ }
++
++ template<typename Return, typename Callable>
++ Return visitVariantAssociation(
++ QV4::Heap::VariantAssociationObject *association, Callable &&callable)
++ {
++ return visitVariantAssociation<Return>(association, callable, callable);
++ }
++
++ namespace QV4 {
++
++ DEFINE_OBJECT_VTABLE(VariantAssociationObject);
++
++ ReturnedValue VariantAssociationPrototype::fromQVariantMap(
++ ExecutionEngine *engine, const QVariantMap &variantMap, QV4::Heap::Object *container,
++ int property, Heap::ReferenceObject::Flags flags)
++ {
++ return engine->memoryManager->allocate<VariantAssociationObject>(
++ variantMap, container, property, flags)->asReturnedValue();
++ }
++
++ ReturnedValue VariantAssociationPrototype::fromQVariantHash(
++ ExecutionEngine *engine, const QVariantHash &variantHash, QV4::Heap::Object *container,
++ int property, Heap::ReferenceObject::Flags flags)
++ {
++ return engine->memoryManager->allocate<VariantAssociationObject>(
++ variantHash, container, property, flags)->asReturnedValue();
++ }
++
++ namespace Heap {
++
++ void VariantAssociationObject::init(
++ const QVariantMap &variantMap, QV4::Heap::Object *container, int property,
++ Heap::ReferenceObject::Flags flags)
++ {
++ ReferenceObject::init(container, property, flags);
++ m_type = AssociationType::VariantMap;
++
++ if (container)
++ new (m_variantAssociation) QVariantMap(variantMap);
++ else
++ createElementWrappers(variantMap);
++ }
++
++ void VariantAssociationObject::init(
++ const QVariantHash &variantHash, QV4::Heap::Object *container, int property,
++ Heap::ReferenceObject::Flags flags)
++ {
++ ReferenceObject::init(container, property, flags);
++ m_type = AssociationType::VariantHash;
++
++ if (container)
++ new (m_variantAssociation) QVariantHash(variantHash);
++ else
++ createElementWrappers(variantHash);
++ }
++
++ void VariantAssociationObject::destroy()
++ {
++ if (object()) {
++ visitVariantAssociation<void>(
++ this, std::destroy_at<QVariantMap>, std::destroy_at<QVariantHash>);
++ }
++ ReferenceObject::destroy();
++ }
++
++ QVariant VariantAssociationObject::toVariant()
++ {
++ if (object()) {
++ return visitVariantAssociation<QVariant>(
++ this, [](const auto *association){ return QVariant(*association); });
++ }
++
++ QV4::Scope scope(internalClass->engine);
++ QV4::ScopedObject self(scope, this);
++
++ switch (m_type) {
++ case AssociationType::VariantMap:
++ return QV4::ExecutionEngine::variantMapFromJS(self);
++ case AssociationType::VariantHash:
++ return QV4::ExecutionEngine::variantHashFromJS(self);
++ default:
++ break;
++ }
++
++ Q_UNREACHABLE_RETURN(QVariant());
++ }
++
++ bool VariantAssociationObject::setVariant(const QVariant &variant)
++ {
++ // Should only happen from readReference(). Therefore we are attached.
++ Q_ASSERT(object());
++
++ auto metatypeId = variant.metaType().id();
++
++ if (metatypeId != QMetaType::QVariantMap && metatypeId != QMetaType::QVariantHash)
++ return false;
++
++ if (metatypeId == QMetaType::QVariantMap && m_type == AssociationType::VariantMap) {
++ *reinterpret_cast<QVariantMap *>(&m_variantAssociation) = variant.toMap();
++ } else if (metatypeId == QMetaType::QVariantMap && m_type == AssociationType::VariantHash) {
++ std::destroy_at(reinterpret_cast<QVariantHash *>(&m_variantAssociation));
++ new (m_variantAssociation) QVariantMap(variant.toMap());
++ m_type = AssociationType::VariantMap;
++ } else if (metatypeId == QMetaType::QVariantHash && m_type == AssociationType::VariantHash) {
++ *reinterpret_cast<QVariantHash *>(&m_variantAssociation) = variant.toHash();
++ } else if (metatypeId == QMetaType::QVariantHash && m_type == AssociationType::VariantMap) {
++ std::destroy_at(reinterpret_cast<QVariantMap *>(&m_variantAssociation));
++ new (m_variantAssociation) QVariantHash(variant.toHash());
++ m_type = AssociationType::VariantHash;
++ }
++
++ return true;
++ }
++
++ template<typename Association>
++ VariantAssociationObject *createDetached(
++ QV4::ExecutionEngine *engine, const Association &association)
++ {
++ return engine->memoryManager->allocate<QV4::VariantAssociationObject>(
++ association, nullptr, -1, ReferenceObject::Flag::NoFlag);
++ }
++
++ VariantAssociationObject *VariantAssociationObject::detached()
++ {
++ if (object()) {
++ return visitVariantAssociation<VariantAssociationObject *>(
++ this, [this](const auto *association) {
++ return createDetached(internalClass->engine, *association);
++ });
++ }
++
++ QV4::Scope scope(internalClass->engine);
++ QV4::ScopedObject self(scope, this);
++
++ switch (m_type) {
++ case AssociationType::VariantMap:
++ return createDetached(scope.engine, QV4::ExecutionEngine::variantMapFromJS(self));
++ case AssociationType::VariantHash:
++ return createDetached(scope.engine, QV4::ExecutionEngine::variantHashFromJS(self));
++ default:
++ break;
++ }
++
++ Q_UNREACHABLE_RETURN(nullptr);
++ }
++
++ QV4::ReturnedValue VariantAssociationObject::getElement(const QString &key, bool *hasProperty)
++ {
++ // Must be attached. Otherwise we should use memberData/arrayData
++ Q_ASSERT(object());
++ Q_ASSERT(hasProperty);
++
++ QV4::ReferenceObject::readReference(this);
++
++ return visitVariantAssociation<QV4::ReturnedValue>(this, [&](const auto *association) {
++ auto it = association->constFind(key);
++ if (it == association->constEnd()) {
++ *hasProperty = false;
++ return Encode::undefined();
++ }
++
++ *hasProperty = true;
++
++ Scope scope(internalClass->engine);
++ ScopedString scopedKey(scope, scope.engine->newString(key));
++
++ uint i = 0;
++ if (propertyIndexMapping) {
++ Scoped<QV4::ArrayData> arrayData(scope, propertyIndexMapping->arrayData);
++ const uint end = arrayData->length();
++ for (; i < end; ++i) {
++ QV4::ScopedString value(scope, arrayData->get(i));
++ Q_ASSERT(value);
++ if (value->equals(scopedKey))
++ break;
++ }
++
++ if (i == end) {
++ ScopedArrayObject mapping(scope, propertyIndexMapping);
++ mapping->push_back(scopedKey);
++ }
++ } else {
++ propertyIndexMapping.set(scope.engine, scope.engine->newArrayObject(scopedKey, 1));
++ }
++
++ return scope.engine->fromVariant(
++ *it, this, i,
++ ReferenceObject::Flag::CanWriteBack | ReferenceObject::Flag::IsVariant);
++ });
++ }
++
++ } // namespace Heap
++
++ ReturnedValue VariantAssociationObject::virtualGet(
++ const Managed *that, PropertyKey id, const Value *, bool *hasProperty)
++ {
++ Heap::VariantAssociationObject *heapAssociation
++ = static_cast<const VariantAssociationObject *>(that)->d();
++
++ // If this is detached we rely on the element wrappers to hold the value
++ if (!heapAssociation->object())
++ return ReferenceObject::virtualGet(that, id, that, hasProperty);
++
++ bool found = false;
++ if (ReturnedValue result = heapAssociation->getElement(id.toQString(), &found); found) {
++ if (hasProperty)
++ *hasProperty = true;
++ return result;
++ }
++
++ return ReferenceObject::virtualGet(that, id, that, hasProperty);
++ }
++
++ bool VariantAssociationObject::virtualPut(
++ Managed *that, PropertyKey id, const Value &value, Value *)
++ {
++ Heap::VariantAssociationObject *heapAssociation
++ = static_cast<VariantAssociationObject *>(that)->d();
++
++ if (!heapAssociation->object())
++ return ReferenceObject::virtualPut(that, id, value, that);
++
++ // Convert before dispatching on the association type. toVariant() reads the
++ // properties of the value, which runs any accessors it has, which can change
++ // the association object itself between QVariantMap and QVariantHash. We
++ // can't let that happen while we are holding a pointer to the the map or hash.
++ const QString key = id.toQString();
++ const QVariant converted
++ = heapAssociation->internalClass->engine->toVariant(value, QMetaType(), false);
++
++ visitVariantAssociation<void>(heapAssociation, [&](auto *association) {
++ association->insert(key, converted);
++ });
++
++ QV4::ReferenceObject::writeBack(heapAssociation);
++ return true;
++ }
++
++ bool VariantAssociationObject::virtualDeleteProperty(Managed *that, PropertyKey id)
++ {
++ Heap::VariantAssociationObject *heapAssociation
++ = static_cast<VariantAssociationObject *>(that)->d();
++
++ if (!heapAssociation->object())
++ return ReferenceObject::virtualDeleteProperty(that, id);
++
++ const QString key = id.toQString();
++ if (!visitVariantAssociation<bool>(heapAssociation, [&](auto *association) {
++ return association->remove(key);
++ })) {
++ return false;
++ }
++
++ QV4::ReferenceObject::writeBack(heapAssociation);
++ return true;
++ }
++
++ OwnPropertyKeyIterator *VariantAssociationObject::virtualOwnPropertyKeys(
++ const Object *m, Value *target)
++ {
++ Heap::VariantAssociationObject *heapAssociation
++ = static_cast<const VariantAssociationObject *>(m)->d();
++
++ if (!heapAssociation->object())
++ return ReferenceObject::virtualOwnPropertyKeys(m, target);
++
++ struct VariantAssociationOwnPropertyKeyIterator : ObjectOwnPropertyKeyIterator
++ {
++ QStringList keys;
++
++ ~VariantAssociationOwnPropertyKeyIterator() override = default;
++
++ PropertyKey next(
++ const Object *o, Property *pd = nullptr,
++ PropertyAttributes *attrs = nullptr) override
++ {
++ Heap::VariantAssociationObject *heapAssociation
++ = static_cast<const VariantAssociationObject *>(o)->d();
++
++ if (memberIndex == 0) {
++ keys = visitVariantAssociation<QStringList>(
++ heapAssociation, [](const auto *association) {
++ return association->keys();
++ });
++ keys.sort();
++ }
++
++ if (static_cast<qsizetype>(memberIndex) < keys.count()) {
++ Scope scope(heapAssociation->internalClass->engine);
++ ScopedString propertyName(scope, scope.engine->newString(keys[memberIndex]));
++ ScopedPropertyKey id(scope, propertyName->toPropertyKey());
++
++ if (attrs)
++ *attrs = QV4::Attr_Data;
++ if (pd) {
++ bool found = false;
++ pd->value = heapAssociation->getElement(keys[memberIndex], &found);
++ Q_ASSERT(found);
++ }
++
++ ++memberIndex;
++
++ return id;
++ }
++
++ return PropertyKey::invalid();
++ }
++ };
++
++ QV4::ReferenceObject::readReference(heapAssociation);
++
++ *target = *m;
++ return new VariantAssociationOwnPropertyKeyIterator;
++ }
++
++ PropertyAttributes VariantAssociationObject::virtualGetOwnProperty(
++ const Managed *m, PropertyKey id, Property *p)
++ {
++ Heap::VariantAssociationObject *heapAssociation
++ = static_cast<const VariantAssociationObject *>(m)->d();
++
++ if (!heapAssociation->object())
++ return ReferenceObject::virtualGetOwnProperty(m, id, p);
++
++ bool hasElement = false;
++ Scope scope(heapAssociation->internalClass->engine);
++ ScopedValue element(scope, heapAssociation->getElement(id.toQString(), &hasElement));
++
++ if (!hasElement)
++ return Attr_Invalid;
++
++ if (p)
++ p->value = element->asReturnedValue();
++
++ return Attr_Data;
++ }
++
++ int VariantAssociationObject::virtualMetacall(
++ Object *object, QMetaObject::Call call, int index, void **a)
++ {
++ Heap::VariantAssociationObject *heapAssociation
++ = static_cast<VariantAssociationObject *>(object)->d();
++
++ // We only create attached wrappers if this variant association is itself attached.
++ // When detaching, we re-create everything. Therefore, we can't get a metaCall if
++ // we are detached.
++ Q_ASSERT(heapAssociation->object());
++
++ Q_ASSERT(heapAssociation->propertyIndexMapping);
++
++ Scope scope(heapAssociation->internalClass->engine);
++ Scoped<QV4::ArrayData> arrayData(scope, heapAssociation->propertyIndexMapping->arrayData);
++ if (index < 0 || uint(index) >= arrayData->length())
++ return 0;
++
++ ScopedString scopedKey(scope, arrayData->get(index));
++
++ switch (call) {
++ case QMetaObject::ReadProperty: {
++ QV4::ReferenceObject::readReference(heapAssociation);
++
++ if (!visitVariantAssociation<bool>(heapAssociation, [&](const auto *association) {
++ const auto it = association->constFind(scopedKey->toQString());
++ if (it == association->constEnd())
++ return false;
++
++ *static_cast<QVariant *>(a[0]) = *it;
++ return true;
++ })) {
++ return 0;
++ }
++
++ break;
++ }
++ case QMetaObject::WriteProperty: {
++ if (!visitVariantAssociation<bool>(heapAssociation, [&](auto *association) {
++ const auto it = association->find(scopedKey->toQString());
++ if (it == association->end())
++ return false;
++
++ *it = *static_cast<const QVariant *>(a[0]);
++ return true;
++ })) {
++ return 0;
++ }
++
++ QV4::ReferenceObject::writeBack(heapAssociation);
++ break;
++ }
++ default:
++ return 0; // not supported
++ }
++
++ return -1;
++ }
++
++ } // namespace QV4
++
++ QT_END_NAMESPACE