// 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 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 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), reinterpret_cast(&association->m_variantAssociation)); case QV4::Heap::VariantAssociationObject::AssociationType::VariantHash: return std::invoke( std::forward(hashCallable), reinterpret_cast(&association->m_variantAssociation)); default: Q_UNREACHABLE(); }; } template 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), reinterpret_cast(&association->m_variantAssociation)); case QV4::Heap::VariantAssociationObject::AssociationType::VariantHash: return std::invoke( std::forward(hashCallable), reinterpret_cast(&association->m_variantAssociation)); default: Q_UNREACHABLE(); }; } template Return visitVariantAssociation( const QV4::Heap::VariantAssociationObject *association, Callable &&callable) { return visitVariantAssociation(association, callable, callable); } template Return visitVariantAssociation( QV4::Heap::VariantAssociationObject *association, Callable &&callable) { return visitVariantAssociation(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( 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( 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( this, std::destroy_at, std::destroy_at); } ReferenceObject::destroy(); } QVariant VariantAssociationObject::toVariant() { if (object()) { return visitVariantAssociation( 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(&m_variantAssociation) = variant.toMap(); } else if (metatypeId == QMetaType::QVariantMap && m_type == AssociationType::VariantHash) { std::destroy_at(reinterpret_cast(&m_variantAssociation)); new (m_variantAssociation) QVariantMap(variant.toMap()); m_type = AssociationType::VariantMap; } else if (metatypeId == QMetaType::QVariantHash && m_type == AssociationType::VariantHash) { *reinterpret_cast(&m_variantAssociation) = variant.toHash(); } else if (metatypeId == QMetaType::QVariantHash && m_type == AssociationType::VariantMap) { std::destroy_at(reinterpret_cast(&m_variantAssociation)); new (m_variantAssociation) QVariantHash(variant.toHash()); m_type = AssociationType::VariantHash; } return true; } template VariantAssociationObject *createDetached( QV4::ExecutionEngine *engine, const Association &association) { return engine->memoryManager->allocate( association, nullptr, -1, ReferenceObject::Flag::NoFlag); } VariantAssociationObject *VariantAssociationObject::detached() { if (object()) { return visitVariantAssociation( 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(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 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(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(that)->d(); if (!heapAssociation->object()) return ReferenceObject::virtualPut(that, id, value, that); visitVariantAssociation(heapAssociation, [&](auto *association) { association->insert( id.toQString(), heapAssociation->internalClass->engine->toVariant(value, QMetaType(), false)); }); QV4::ReferenceObject::writeBack(heapAssociation); return true; } bool VariantAssociationObject::virtualDeleteProperty(Managed *that, PropertyKey id) { Heap::VariantAssociationObject *heapAssociation = static_cast(that)->d(); if (!heapAssociation->object()) return ReferenceObject::virtualDeleteProperty(that, id); if (!visitVariantAssociation(heapAssociation, [&](auto *association) { return association->remove(id.toQString()); })) { return false; } QV4::ReferenceObject::writeBack(heapAssociation); return true; } OwnPropertyKeyIterator *VariantAssociationObject::virtualOwnPropertyKeys( const Object *m, Value *target) { Heap::VariantAssociationObject *heapAssociation = static_cast(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(o)->d(); if (memberIndex == 0) { keys = visitVariantAssociation( heapAssociation, [](const auto *association) { return association->keys(); }); keys.sort(); } if (static_cast(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(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(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 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(heapAssociation, [&](const auto *association) { const auto it = association->constFind(scopedKey->toQString()); if (it == association->constEnd()) return false; *static_cast(a[0]) = *it; return true; })) { return 0; } break; } case QMetaObject::WriteProperty: { if (!visitVariantAssociation(heapAssociation, [&](auto *association) { const auto it = association->find(scopedKey->toQString()); if (it == association->end()) return false; *it = *static_cast(a[0]); return true; })) { return 0; } QV4::ReferenceObject::writeBack(heapAssociation); break; } default: return 0; // not supported } return -1; } } // namespace QV4 QT_END_NAMESPACE