Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "rep_mismatch_replica.h"
#include <QtRemoteObjects/QRemoteObjectDynamicReplica>
#include <QtRemoteObjects/QRemoteObjectHost>
#include <QtRemoteObjects/QRemoteObjectNode>
#include <QtRemoteObjects/private/qconnectionfactories_p.h>
#include <QtTest/QSignalSpy>
#include <QtTest/QTest>
#include <QtCore/QDataStream>
#include <QtCore/QRegularExpression>
#include <QtNetwork/qtcpserver.h>
#include <QtNetwork/qtcpsocket.h>
#include <memory>
using namespace Qt::StringLiterals;
// The API of this source intentionally does not match the API of the
// replica generated from mismatch.rep.
// The source is registered as a dynamic one, so no signature check is done
// when the replica connects to it.
// The order of the properties, signals and slots must match the order in
// mismatch.rep, so that the indices line up. The notify signals must come
// first, followed by the other signals in the order of SIGNAL()s.
class MismatchedSource : public QObject
{
Q_OBJECT
Q_PROPERTY(QString convertibleValue READ convertibleValue WRITE setConvertibleValue
NOTIFY convertibleValueChanged)
Q_PROPERTY(QString nonConvertibleValue READ nonConvertibleValue WRITE setNonConvertibleValue
NOTIFY nonConvertibleValueChanged)
public:
using QObject::QObject;
QString convertibleValue() const { return m_convertibleValue; }
void setConvertibleValue(const QString &value)
{
if (m_convertibleValue == value)
return;
m_convertibleValue = value;
emit convertibleValueChanged(m_convertibleValue);
}
QString nonConvertibleValue() const { return m_nonConvertibleValue; }
void setNonConvertibleValue(const QString &value)
{
if (m_nonConvertibleValue == value)
return;
m_nonConvertibleValue = value;
emit nonConvertibleValueChanged(m_nonConvertibleValue);
}
int convertibleSlotCallCount() const { return m_convertibleSlotCallCount; }
QString convertibleSlotArgument() const { return m_convertibleSlotArgument; }
int nonConvertibleSlotCallCount() const { return m_nonConvertibleSlotCallCount; }
QString nonConvertibleSlotArgument() const { return m_nonConvertibleSlotArgument; }
int tooManyArgsSlotCallCount() const { return m_tooManyArgsSlotCallCount; }
int tooFewArgsSlotCallCount() const { return m_tooFewArgsSlotCallCount; }
public Q_SLOTS:
void convertibleSlot(const QString &arg)
{
++m_convertibleSlotCallCount;
m_convertibleSlotArgument = arg;
}
void nonConvertibleSlot(const QString &arg)
{
++m_nonConvertibleSlotCallCount;
m_nonConvertibleSlotArgument = arg;
}
// The replica sends two arguments
void tooManyArgsSlot(int) { ++m_tooManyArgsSlotCallCount; }
// The replica sends one argument
void tooFewArgsSlot(int, int) { ++m_tooFewArgsSlotCallCount; }
void noArgSlot() {}
Q_SIGNALS:
void convertibleValueChanged(const QString &value);
void nonConvertibleValueChanged(const QString &value);
// The replica signal has one argument
void tooManyArgsSignal(int a, int b);
// The replica signal has two arguments
void tooFewArgsSignal(int a);
// The replica signal has one argument
void noArgsSignal();
private:
QString m_convertibleValue = u"initial"_s;
QString m_nonConvertibleValue = u"initial"_s;
QString m_convertibleSlotArgument = u"initial"_s;
QString m_nonConvertibleSlotArgument = u"initial"_s;
int m_convertibleSlotCallCount = 0;
int m_nonConvertibleSlotCallCount = 0;
int m_tooManyArgsSlotCallCount = 0;
int m_tooFewArgsSlotCallCount = 0;
};
class tst_MalformedPackets : public QObject
{
Q_OBJECT
private Q_SLOTS:
void init();
void cleanup();
void slotArgumentTypeMismatch();
void propertyTypeMismatch();
void slotArgumentCountMismatch();
void signalArgumentCountMismatch();
void registrySlotWithNoArgs();
void writePropertyWithNoArgs();
void propertyChangeWithInvalidIndex_data();
void propertyChangeWithInvalidIndex();
void propertyCountMismatch_data();
void propertyCountMismatch();
void childPropertyCountMismatch_data();
void childPropertyCountMismatch();
void dynamicInitForStaticReplica();
void repeatedDynamicInitForDynamicReplica_data();
void repeatedDynamicInitForDynamicReplica();
private:
std::unique_ptr<MismatchedSource> m_source;
std::unique_ptr<QRemoteObjectHost> m_host;
std::unique_ptr<QRemoteObjectNode> m_client;
std::unique_ptr<MismatchedObjectReplica> m_replica;
};
static quint16 findPort()
{
QTcpServer probe;
if (!probe.listen(QHostAddress::LocalHost, 0))
return 0;
quint16 port = probe.serverPort();
probe.close();
return port;
}
void tst_MalformedPackets::init()
{
qint16 port = findPort();
QVERIFY(port != 0);
m_source = std::make_unique<MismatchedSource>();
m_source->setObjectName(u"MismatchedObject"_s);
m_host = std::make_unique<QRemoteObjectHost>(url);
QVERIFY(m_host->enableRemoting(m_source.get()));
m_client = std::make_unique<QRemoteObjectNode>();
QVERIFY(m_client->connectToNode(url));
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"provided type QString .* QRectF"_s));
m_replica.reset(m_client->acquire<MismatchedObjectReplica>());
QVERIFY(m_replica);
QVERIFY(m_replica->waitForSource());
}
void tst_MalformedPackets::cleanup()
{
m_replica.reset();
m_client.reset();
m_host.reset();
m_source.reset();
}
void tst_MalformedPackets::slotArgumentTypeMismatch()
{
// The source expects a QString, but receives an int, which is converted
{
m_replica->convertibleSlot(42);
QTRY_COMPARE(m_source->convertibleSlotCallCount(), 1);
QCOMPARE(m_source->convertibleSlotArgument(), u"42"_s);
}
// The source expects a QString, but receives a QRectF, which cannot be
// converted, so the slot is invoked with a default-constructed QString
{
QTest::ignoreMessage(QtWarningMsg,
QRegularExpression(u"provided type QRectF .* QString"_s));
m_replica->nonConvertibleSlot(QRectF(1, 2, 3, 4));
QTRY_COMPARE(m_source->nonConvertibleSlotCallCount(), 1);
QCOMPARE(m_source->nonConvertibleSlotArgument(), QString());
}
}
void tst_MalformedPackets::propertyTypeMismatch()
{
// The source property is a QString, but the replica sends an int,
// which is converted
{
// Also make sure that the signal from the source to replica is
// handled properly
int value = 99; // some non-default value
connect(m_replica.get(), &MismatchedObjectReplica::convertibleValueChanged,
this, [&value](int newVal) { value = newVal; });
m_replica->setConvertibleValue(42);
QTRY_COMPARE(m_source->convertibleValue(), u"42"_s);
QTRY_COMPARE(m_replica->convertibleValue(), 42);
QTRY_COMPARE(value, 42);
}
// The source property is a QString, but the replica sends a QRectF,
// which cannot be converted, so the property is reset to a
// default-constructed QString
{
// Also make sure that the signal from the source to replica is
// handled properly
QRectF value = QRectF(5, 5, 5, 5); // some non-default value
connect(m_replica.get(), &MismatchedObjectReplica::nonConvertibleValueChanged,
this, [&value](QRectF newVal) { value = newVal; });
// Ignore QRectF -> QString on the source when writing the property
QTest::ignoreMessage(QtWarningMsg,
QRegularExpression(u"provided type QRectF .* QString"_s));
// Ignore QString -> QRectF on the replica when handling PropertyChangePacket
QTest::ignoreMessage(QtWarningMsg,
QRegularExpression(u"provided type QString .* QRectF"_s));
// Ignore QString -> QRectF on the replica when handling InvokePacket
QTest::ignoreMessage(QtWarningMsg,
QRegularExpression(u"provided type QString .* QRectF"_s));
m_replica->setNonConvertibleValue(QRectF(1, 2, 3, 4));
QTRY_COMPARE(m_source->nonConvertibleValue(), QString());
QTRY_COMPARE(m_replica->nonConvertibleValue(), QRectF());
QTRY_COMPARE(value, QRectF());
}
}
void tst_MalformedPackets::slotArgumentCountMismatch()
{
// The packets are handled in order, so we call a "normal" convertibleSlot()
// to make sure that the previous call is handled.
// The replica sends more arguments than the source slot takes
{
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Argument count mismatch"_s));
m_replica->tooManyArgsSlot(1, 2);
m_replica->convertibleSlot(1);
QTRY_COMPARE(m_source->convertibleSlotCallCount(), 1);
QCOMPARE(m_source->tooManyArgsSlotCallCount(), 0);
}
// The replica sends fewer arguments than the source slot takes
{
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Argument count mismatch"_s));
m_replica->tooFewArgsSlot(1);
m_replica->convertibleSlot(2);
QTRY_COMPARE(m_source->convertibleSlotCallCount(), 2);
QCOMPARE(m_source->tooFewArgsSlotCallCount(), 0);
}
}
void tst_MalformedPackets::signalArgumentCountMismatch()
{
// The packets are handled in order, so we update a property to make sure
// that the malformed signal has been handled.
// The source emits more arguments than the replica signal takes.
// This does not raise a warning and actually emits the signal on the
// replica side for backwards compatibility, because there was a separate
// bug on the Source side.
{
QSignalSpy spy(m_replica.get(), &MismatchedObjectReplica::tooManyArgsSignal);
emit m_source->tooManyArgsSignal(1, 2);
m_source->setConvertibleValue(u"1"_s);
QTRY_COMPARE(m_replica->convertibleValue(), 1);
QCOMPARE(spy.size(), 1);
}
// The source emits fewer arguments than the replica signal takes
{
QSignalSpy spy(m_replica.get(), &MismatchedObjectReplica::tooFewArgsSignal);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Argument count mismatch"_s));
emit m_source->tooFewArgsSignal(1);
m_source->setConvertibleValue(u"2"_s);
QTRY_COMPARE(m_replica->convertibleValue(), 2);
QCOMPARE(spy.size(), 0);
}
// Same as above, but the source signal has no arguments at all. This is
// handled by a separate code path on the replica side.
{
QSignalSpy spy(m_replica.get(), &MismatchedObjectReplica::noArgsSignal);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Argument count mismatch"_s));
emit m_source->noArgsSignal();
m_source->setConvertibleValue(u"3"_s);
QTRY_COMPARE(m_replica->convertibleValue(), 3);
QCOMPARE(spy.size(), 0);
}
}
void tst_MalformedPackets::registrySlotWithNoArgs()
{
MismatchedSource sourceAsRegistry;
sourceAsRegistry.setObjectName("Registry");
m_host->enableRemoting(&sourceAsRegistry);
// "Registry" is a reserved name, but the module still allows registering
// custom objects under this name.
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"provided type QString .* QRectF"_s));
std::unique_ptr<MismatchedObjectReplica> registryReplica(
m_client->acquire<MismatchedObjectReplica>("Registry"));
QVERIFY(registryReplica);
QVERIFY(registryReplica->waitForSource());
// The remote objects Registry always expects a slot to have at least one
// argument - the QRemoteObjectSourceLocation instance.
// Calling a slot with no arguments should not crash!
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Unexpected packet format"_s));
registryReplica->noArgSlot();
// Call a "normal" slot of another replica (that is registered under a
// "normal" name) to make sure that the first call is actually processed.
m_replica->convertibleSlot(1);
QTRY_COMPARE(m_source->convertibleSlotCallCount(), 1);
}
void tst_MalformedPackets::writePropertyWithNoArgs()
{
// A hand-crafted property change packet with no arguments.
// The format matches QDataStreamCodec::serializeInvokePacket().
QByteArray packet;
{
QDataStream ds(&packet, QIODevice::WriteOnly);
ds.setVersion(QtRemoteObjects::dataStreamVersion);
ds.setByteOrder(QDataStream::LittleEndian);
const QMetaObject &mo = MismatchedSource::staticMetaObject;
const int propertyIndex = mo.indexOfProperty("convertibleValue") - mo.propertyOffset();
ds << quint32(0); // packet size, filled in below
ds << quint16(QtRemoteObjects::InvokePacket);
ds << m_source->objectName();
ds << int(QMetaObject::WriteProperty);
ds << propertyIndex;
ds << quint32(0); // argument count
ds << int(-1); // serialId
ds << int(-1); // propertyIndex
QCOMPARE(ds.status(), QDataStream::Ok);
ds.device()->seek(0);
ds << quint32(packet.size() - sizeof(quint32));
}
const QUrl url = m_host->hostUrl();
QTcpSocket socket;
socket.connectToHost(url.host(), url.port());
QVERIFY(socket.waitForConnected());
// Create a new client using the custom connection
QRemoteObjectNode client;
client.addClientSideConnection(&socket);
// Acquire a replica
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"provided type QString .* QRectF"_s));
std::unique_ptr<MismatchedObjectReplica> replica(client.acquire<MismatchedObjectReplica>());
QVERIFY(replica);
QVERIFY(replica->waitForSource());
// Now send a custom malformed packet - the host should not crash!
// It should simply discard the command.
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Invalid argument count"_s));
QCOMPARE(socket.write(packet), packet.size());
QVERIFY(socket.waitForBytesWritten());
// After that update the property in a normal way to verify that the
// previous command was processed.
replica->setConvertibleValue(42);
QTRY_COMPARE(m_source->convertibleValue(), u"42"_s);
}
void tst_MalformedPackets::propertyChangeWithInvalidIndex_data()
{
QTest::addColumn<int>("propertyIndex");
const QMetaObject &mo = MismatchedObjectReplica::staticMetaObject;
const int propertyCount = mo.propertyCount() - mo.propertyOffset();
QTest::newRow("equal_to_count") << propertyCount;
QTest::newRow("large") << propertyCount + 1000;
QTest::newRow("negative") << -1;
}
void tst_MalformedPackets::propertyChangeWithInvalidIndex()
{
QFETCH(const int, propertyIndex);
// A hand-crafted property change packet with an out-of-range index.
// The format matches QDataStreamCodec::serializePropertyChangePacket().
QByteArray packet;
{
QDataStream ds(&packet, QIODevice::WriteOnly);
ds.setVersion(QtRemoteObjects::dataStreamVersion);
ds.setByteOrder(QDataStream::LittleEndian);
ds << quint32(0); // packet size, filled in below
ds << quint16(QtRemoteObjects::PropertyChangePacket);
ds << m_source->objectName();
ds << propertyIndex;
ds << QVariant(42);
QCOMPARE(ds.status(), QDataStream::Ok);
ds.device()->seek(0);
ds << quint32(packet.size() - sizeof(quint32));
}
// Create the connection manually, so that we can write to the host side
// of it, and the replica sees the data as if it was sent by the source.
quint16 port = findPort();
QVERIFY(port);
QTcpServer server;
QVERIFY(server.listen(QHostAddress::LocalHost, port));
QTcpSocket clientSocket;
clientSocket.connectToHost(QHostAddress::LocalHost, port);
QVERIFY(server.waitForNewConnection(5000));
QTcpSocket *hostSocket = server.nextPendingConnection();
QVERIFY(hostSocket);
m_host->addHostSideConnection(hostSocket);
QRemoteObjectNode client;
client.addClientSideConnection(&clientSocket);
// Acquire a replica. The source and the replica have the same number of
// properties, so the initialization succeeds.
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"provided type QString .* QRectF"_s));
std::unique_ptr<MismatchedObjectReplica> replica(client.acquire<MismatchedObjectReplica>());
QVERIFY(replica);
QVERIFY(replica->waitForSource());
// Now send a custom malformed packet from the host side - the replica
// should not crash! It should simply discard the packet.
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Invalid property index"_s));
QCOMPARE(hostSocket->write(packet), packet.size());
QVERIFY(hostSocket->waitForBytesWritten());
// After that update the property on the source in a normal way. The
// packets are handled in order, so this verifies that the previous packet
// was processed.
m_source->setConvertibleValue(u"1"_s);
QTRY_COMPARE(replica->convertibleValue(), 1);
}
// Source objects to match the TwoPropertyObject from the replica. One of them
// has less properties, the other one - more.
class OnePropertySource : public QObject
{
Q_OBJECT
Q_PROPERTY(int prop1 READ prop1 WRITE setProp1 NOTIFY prop1Changed)
public:
using QObject::QObject;
int prop1() const { return m_prop1; }
void setProp1(int val)
{
if (m_prop1 == val)
return;
m_prop1 = val;
Q_EMIT prop1Changed(m_prop1);
}
signals:
void prop1Changed(int);
private:
int m_prop1 = 0;
};
class ThreePropertiesSource : public QObject
{
Q_OBJECT
Q_PROPERTY(int prop1 READ prop1 WRITE setProp1 NOTIFY prop1Changed)
Q_PROPERTY(int prop2 READ prop2 WRITE setProp2 NOTIFY prop2Changed)
Q_PROPERTY(int prop3 READ prop3 WRITE setProp3 NOTIFY prop3Changed)
public:
using QObject::QObject;
int prop1() const { return m_prop1; }
void setProp1(int val)
{
if (m_prop1 == val)
return;
m_prop1 = val;
Q_EMIT prop1Changed(m_prop1);
}
int prop2() const { return m_prop2; }
void setProp2(int val)
{
if (m_prop2 == val)
return;
m_prop2 = val;
Q_EMIT prop2Changed(m_prop2);
}
int prop3() const { return m_prop3; }
void setProp3(int val)
{
if (m_prop3 == val)
return;
m_prop3 = val;
Q_EMIT prop3Changed(m_prop3);
}
signals:
void prop1Changed(int);
void prop2Changed(int);
void prop3Changed(int);
private:
int m_prop1 = 0;
int m_prop2 = 0;
int m_prop3 = 0;
};
enum class MismatchType
{
ReplicaHasLessProperties,
ReplicaHasMoreProperties,
};
void tst_MalformedPackets::propertyCountMismatch_data()
{
QTest::addColumn<MismatchType>("type");
QTest::newRow("replica_has_less_properties") << MismatchType::ReplicaHasLessProperties;
QTest::newRow("replica_has_more_properties") << MismatchType::ReplicaHasMoreProperties;
}
void tst_MalformedPackets::propertyCountMismatch()
{
QFETCH(const MismatchType, type);
// Create both sources
OnePropertySource src1;
ThreePropertiesSource src3;
// But enable remoting for only one of them
switch (type) {
case MismatchType::ReplicaHasLessProperties:
src1.setObjectName("PropertyMismatch");
m_host->enableRemoting(&src1);
break;
case MismatchType::ReplicaHasMoreProperties:
src3.setObjectName("PropertyMismatch");
m_host->enableRemoting(&src3);
break;
}
// Acquire a replica that has a different number of properties.
// This should not crash!
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Property count mismatch"_s));
std::unique_ptr<TwoPropertyObjectReplica> replica(
m_client->acquire<TwoPropertyObjectReplica>("PropertyMismatch"));
QVERIFY(replica);
QTRY_COMPARE(replica->state(), QRemoteObjectReplica::SignatureMismatch);
}
// The source child objects for the ParentWithChild replica. The type name is
// set explicitly, so that it matches the TwoPropertyObject class from
// mismatch.rep. The replica side needs it to find the metaobject for the new
// child replica when the child pointer changes on the source side.
class OnePropertyChildSource : public QObject
{
Q_OBJECT
Q_CLASSINFO(QCLASSINFO_REMOTEOBJECT_TYPE, "TwoPropertyObject")
Q_PROPERTY(int prop1 READ prop1 CONSTANT)
public:
using QObject::QObject;
int prop1() const { return 1; }
};
class ThreePropertiesChildSource : public QObject
{
Q_OBJECT
Q_CLASSINFO(QCLASSINFO_REMOTEOBJECT_TYPE, "TwoPropertyObject")
Q_PROPERTY(int prop1 READ prop1 CONSTANT)
Q_PROPERTY(int prop2 READ prop2 CONSTANT)
Q_PROPERTY(int prop3 READ prop3 CONSTANT)
public:
using QObject::QObject;
int prop1() const { return 1; }
int prop2() const { return 2; }
int prop3() const { return 3; }
};
// The objectName must match the class name in mismatch.rep, because the
// generated replica uses it to build the name of the child replica.
class ParentWithChildSource : public QObject
{
Q_OBJECT
Q_PROPERTY(int value READ value NOTIFY valueChanged)
Q_PROPERTY(QObject *child READ child NOTIFY childChanged)
public:
explicit ParentWithChildSource(QObject *child) : m_child(child)
{
setObjectName(u"ParentWithChild"_s);
}
int value() const { return 42; }
QObject *child() const { return m_child; }
void setChild(QObject *child)
{
if (m_child == child)
return;
m_child = child;
Q_EMIT childChanged();
}
Q_SIGNALS:
void valueChanged();
void childChanged();
private:
QObject *m_child = nullptr;
};
static std::unique_ptr<QObject> createChildSource(MismatchType type)
{
switch (type) {
case MismatchType::ReplicaHasLessProperties:
return std::make_unique<ThreePropertiesChildSource>();
case MismatchType::ReplicaHasMoreProperties:
return std::make_unique<OnePropertyChildSource>();
}
return nullptr;
}
void tst_MalformedPackets::childPropertyCountMismatch_data()
{
propertyCountMismatch_data();
}
void tst_MalformedPackets::childPropertyCountMismatch()
{
QFETCH(const MismatchType, type);
const auto child = createChildSource(type);
ParentWithChildSource parent(child.get());
QVERIFY(m_host->enableRemoting(&parent));
// The child replica has a different number of properties than the source
// child. This should not crash! The child replica is not initialized,
// but the parent replica still is.
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Property count mismatch for .*"
"Class::ParentWithChild::child"_s));
std::unique_ptr<ParentWithChildReplica> replica(
m_client->acquire<ParentWithChildReplica>());
QVERIFY(replica);
QTRY_COMPARE(replica->state(), QRemoteObjectReplica::Valid);
QCOMPARE(replica->value(), 42);
TwoPropertyObjectReplica *childReplica = replica->child();
QVERIFY(childReplica);
QCOMPARE(childReplica->state(), QRemoteObjectReplica::SignatureMismatch);
// Changing the child pointer after the parent is initialized makes the
// replica side acquire a new dynamic replica for the child. Its property
// count does not match either. This should not crash either!
const auto newChild = createChildSource(type);
QSignalSpy childChangedSpy(replica.get(), &ParentWithChildReplica::childChanged);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Property count mismatch for .*"
"Class::ParentWithChild::child"_s));
parent.setChild(newChild.get());
QTRY_COMPARE(childChangedSpy.size(), 1);
TwoPropertyObjectReplica *newChildReplica = replica->child();
QVERIFY(newChildReplica);
QVERIFY(childReplica != newChildReplica);
QCOMPARE(newChildReplica->state(), QRemoteObjectReplica::SignatureMismatch);
}
// A hand-crafted InitDynamicPacket that describes a type with a single int
// property without a notify signal, and no signals or slots. All the sources
// in this test have more properties, so any property index above zero becomes
// stale once the replica applies this definition.
// The format matches QDataStreamCodec::serializeInitDynamicPacket().
static QByteArray createInitDynamicPacket(const QString &name,
const QString &typeName = u"InjectedType"_s)
{
QByteArray packet;
QDataStream ds(&packet, QIODevice::WriteOnly);
ds.setVersion(QtRemoteObjects::dataStreamVersion);
ds.setByteOrder(QDataStream::LittleEndian);
ds << quint32(0); // packet size, filled in below
ds << quint16(QtRemoteObjects::InitDynamicPacket);
ds << name;
// The definition, see QDataStreamCodec::serializeDefinition()
ds << typeName; // type name
ds << quint32(0); // enum count
ds << quint32(0); // Qt enum count
ds << quint32(0); // gadget count
ds << quint32(0); // signal count
ds << quint32(0); // slot count
ds << quint32(1); // property count
ds << "injected"; // property name
ds << "int"; // property type
ds << QByteArray(); // notify signal
// The property values, see QDataStreamCodec::serializeProperties()
ds << quint32(1); // property count
ds << QVariant(42);
if (ds.status() != QDataStream::Ok)
return {};
ds.device()->seek(0);
ds << quint32(packet.size() - sizeof(quint32));
return packet;
}
void tst_MalformedPackets::dynamicInitForStaticReplica()
{
const QByteArray packet = createInitDynamicPacket(m_source->objectName());
QVERIFY(!packet.isEmpty());
// Create the connection manually, so that we can write to the host side
// of it, and the replica sees the data as if it was sent by the source.
quint16 port = findPort();
QVERIFY(port);
QTcpServer server;
QVERIFY(server.listen(QHostAddress::LocalHost, port));
QTcpSocket clientSocket;
clientSocket.connectToHost(QHostAddress::LocalHost, port);
QVERIFY(server.waitForNewConnection(5000));
QTcpSocket *hostSocket = server.nextPendingConnection();
QVERIFY(hostSocket);
m_host->addHostSideConnection(hostSocket);
QRemoteObjectNode client;
client.addClientSideConnection(&clientSocket);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"provided type QString .* QRectF"_s));
std::unique_ptr<MismatchedObjectReplica> replica(client.acquire<MismatchedObjectReplica>());
QVERIFY(replica);
QVERIFY(replica->waitForSource());
// A static replica has a compiled-in metaobject and is initialized with
// an InitPacket, so it should never receive an InitDynamicPacket.
// The replica should not crash! It should simply discard the packet.
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Unexpected InitDynamicPacket"_s));
QCOMPARE(hostSocket->write(packet), packet.size());
QVERIFY(hostSocket->waitForBytesWritten());
// After that update the property on the source in a normal way. The
// packets are handled in order, so this verifies that the previous packet
// was processed.
m_source->setConvertibleValue(u"1"_s);
QTRY_COMPARE(replica->convertibleValue(), 1);
// The compiled replica accesses the properties by the compiled indices.
// The injected definition has only one property, so reading the second
// one would access the property storage out of bounds, if the packet was
// applied.
QCOMPARE(replica->nonConvertibleValue(), QRectF());
// The slots are also invoked using the compiled indices.
replica->convertibleSlot(2);
QTRY_COMPARE(m_source->convertibleSlotCallCount(), 1);
QCOMPARE(m_source->convertibleSlotArgument(), u"2"_s);
}
// The source child object for the ParentWithChildSource. The type name is set
// explicitly, so that the injected definition can use the same one.
class CachedChildSource : public QObject
{
Q_OBJECT
Q_CLASSINFO(QCLASSINFO_REMOTEOBJECT_TYPE, "CachedChild")
Q_PROPERTY(int value1 READ value1 WRITE setValue1 NOTIFY value1Changed)
Q_PROPERTY(int value2 READ value2 CONSTANT)
public:
CachedChildSource(int value1, int value2) : m_value1(value1), m_value2(value2) {}
int value1() const { return m_value1; }
void setValue1(int value)
{
if (m_value1 == value)
return;
m_value1 = value;
Q_EMIT value1Changed(m_value1);
}
int value2() const { return m_value2; }
Q_SIGNALS:
void value1Changed(int value);
private:
int m_value1;
int m_value2;
};
void tst_MalformedPackets::repeatedDynamicInitForDynamicReplica_data()
{
QTest::addColumn<QString>("name");
QTest::addColumn<bool>("expectWarning");
// The packet is sent for an already initialized dynamic replica
QTest::newRow("parent") << u"ParentWithChild"_s << true;
// The packet is sent for an already initialized dynamic child replica
QTest::newRow("child") << u"Class::ParentWithChild::child"_s << true;
// The packet is sent for an object that the client never acquired
QTest::newRow("not_acquired") << u"NotAcquired"_s << false;
}
void tst_MalformedPackets::repeatedDynamicInitForDynamicReplica()
{
QFETCH(const QString, name);
QFETCH(const bool, expectWarning);
// The injected definition uses the type name of the child, so applying
// it would also replace the cached definition of the child type.
const QByteArray packet = createInitDynamicPacket(name, u"CachedChild"_s);
QVERIFY(!packet.isEmpty());
CachedChildSource child(1, 10);
CachedChildSource newChild(2, 20);
ParentWithChildSource parent(&child);
QVERIFY(m_host->enableRemoting(&parent));
// Create the connection manually, so that we can write to the host side
// of it, and the replica sees the data as if it was sent by the source.
quint16 port = findPort();
QVERIFY(port);
QTcpServer server;
QVERIFY(server.listen(QHostAddress::LocalHost, port));
QTcpSocket clientSocket;
clientSocket.connectToHost(QHostAddress::LocalHost, port);
QVERIFY(server.waitForNewConnection(5000));
QTcpSocket *hostSocket = server.nextPendingConnection();
QVERIFY(hostSocket);
m_host->addHostSideConnection(hostSocket);
QRemoteObjectNode client;
client.addClientSideConnection(&clientSocket);
// The dynamic replicas receive their metaobjects from the
// InitDynamicPacket that the source sends during the initialization.
// The definition of the child type is sent together with the initial
// data of the parent, and is also stored in the cache of dynamic types.
std::unique_ptr<QRemoteObjectDynamicReplica> replica(
client.acquireDynamic(parent.objectName()));
QVERIFY(replica);
QVERIFY(replica->waitForSource());
const QMetaObject *mo = replica->metaObject();
auto *childReplica = qobject_cast<QRemoteObjectDynamicReplica *>(
replica->property("child").value<QObject *>());
QVERIFY(childReplica);
QCOMPARE(childReplica->state(), QRemoteObjectReplica::Valid);
// Keep the metaobject data obtained after the initialization, like e.g.
// QML bindings or the user code connecting to the replica would do.
const QMetaObject *childMo = childReplica->metaObject();
const QMetaProperty value1Prop = childMo->property(childMo->indexOfProperty("value1"));
QVERIFY(value1Prop.isValid());
QVERIFY(value1Prop.hasNotifySignal());
const QMetaProperty value2Prop = childMo->property(childMo->indexOfProperty("value2"));
QVERIFY(value2Prop.isValid());
QSignalSpy value1Spy(childReplica, value1Prop.notifySignal());
QVERIFY(value1Spy.isValid());
// The replicas are already initialized, so they should not receive
// another InitDynamicPacket. The replica should not crash! It should
// simply discard the packet.
if (expectWarning)
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(u"Unexpected InitDynamicPacket"_s));
QCOMPARE(hostSocket->write(packet), packet.size());
QVERIFY(hostSocket->waitForBytesWritten());
// After that update the property of the child on the source in a normal
// way. The packets are handled in order, so this verifies that the
// previous packet was processed.
child.setValue1(11);
QTRY_COMPARE(value1Spy.size(), 1);
QCOMPARE(value1Spy.at(0).at(0).toInt(), 11);
// The replicas must keep the metaobjects that they got during the
// initialization.
QCOMPARE(replica->metaObject(), mo);
QCOMPARE(childReplica->metaObject(), childMo);
QCOMPARE(value1Prop.read(childReplica), QVariant(11));
// The injected definition has only one property, so reading the second
// one using the stored index would access the property storage out of
// bounds, if the packet was applied.
QCOMPARE(value2Prop.read(childReplica), QVariant(10));
// Change the child pointer on the source. The source has already sent the
// definition of the child type, so it sends the new child without it, and
// the replica side takes the definition from the cache. The cached
// definition must not be replaced by the injected one.
QSignalSpy childSpy(replica.get(), SIGNAL(childChanged()));
QVERIFY(childSpy.isValid());
parent.setChild(&newChild);
QTRY_COMPARE(childSpy.size(), 1);
// Check that the child replica is actually updated but its metaobject
// did not change.
childReplica = qobject_cast<QRemoteObjectDynamicReplica *>(
replica->property("child").value<QObject *>());
QVERIFY(childReplica);
QCOMPARE(childReplica->metaObject(), childMo);
QCOMPARE(childReplica->property("value1"), QVariant(2));
QCOMPARE(childReplica->property("value2"), QVariant(20));
}
QTEST_MAIN(tst_MalformedPackets)
#include "tst_malformedpackets.moc"