diff options
author | Liang Qi <liang.qi@qt.io> | 2018-02-05 10:50:21 +0100 |
---|---|---|
committer | Liang Qi <liang.qi@qt.io> | 2018-02-05 10:50:21 +0100 |
commit | 65f542f4600f469bca2c539716e20ba73eb6275a (patch) | |
tree | 8fe78f8a46e64a31f5966081269359a826f13828 | |
parent | cd1d60ec71836d6c4e9cbfbf344ef9334a22cdc4 (diff) | |
parent | 2c3c2a41c55a179332ec2a076856990f36dd5ef9 (diff) |
Merge remote-tracking branch 'origin/5.10' into dev
Change-Id: I2a9533a7b630538b422a1b254a5250067fc7bc16
32 files changed, 1529 insertions, 173 deletions
diff --git a/examples/mqtt/quicksubscription/qmlmqttclient.h b/examples/mqtt/quicksubscription/qmlmqttclient.h index ffc45ea..2e1041b 100644 --- a/examples/mqtt/quicksubscription/qmlmqttclient.h +++ b/examples/mqtt/quicksubscription/qmlmqttclient.h @@ -60,7 +60,7 @@ class QmlMqttClient; class QmlMqttSubscription : public QObject { Q_OBJECT - Q_PROPERTY(QString topic MEMBER m_topic NOTIFY topicChanged) + Q_PROPERTY(QMqttTopicFilter topic MEMBER m_topic NOTIFY topicChanged) public: QmlMqttSubscription(QMqttSubscription *s, QmlMqttClient *c); ~QmlMqttSubscription(); @@ -76,7 +76,7 @@ private: Q_DISABLE_COPY(QmlMqttSubscription) QMqttSubscription *sub; QmlMqttClient *client; - QString m_topic; + QMqttTopicFilter m_topic; }; class QmlMqttClient : public QMqttClient diff --git a/examples/mqtt/simpleclient/mainwindow.cpp b/examples/mqtt/simpleclient/mainwindow.cpp index 4f5e575..e40820b 100644 --- a/examples/mqtt/simpleclient/mainwindow.cpp +++ b/examples/mqtt/simpleclient/mainwindow.cpp @@ -68,10 +68,10 @@ MainWindow::MainWindow(QWidget *parent) : connect(m_client, &QMqttClient::stateChanged, this, &MainWindow::updateLogStateChange); connect(m_client, &QMqttClient::disconnected, this, &MainWindow::brokerDisconnected); - connect(m_client, &QMqttClient::messageReceived, this, [this](const QByteArray &message, const QString &topic) { + connect(m_client, &QMqttClient::messageReceived, this, [this](const QByteArray &message, const QMqttTopicName &topic) { const QString content = QDateTime::currentDateTime().toString() + QLatin1String(" Received Topic: ") - + topic + + topic.name() + QLatin1String(" Message: ") + message + QLatin1Char('\n'); diff --git a/examples/mqtt/subscriptions/mainwindow.cpp b/examples/mqtt/subscriptions/mainwindow.cpp index 499d46a..e8fea99 100644 --- a/examples/mqtt/subscriptions/mainwindow.cpp +++ b/examples/mqtt/subscriptions/mainwindow.cpp @@ -69,10 +69,10 @@ MainWindow::MainWindow(QWidget *parent) : connect(m_client, &QMqttClient::stateChanged, this, &MainWindow::updateLogStateChange); connect(m_client, &QMqttClient::disconnected, this, &MainWindow::brokerDisconnected); - connect(m_client, &QMqttClient::messageReceived, this, [this](const QByteArray &message, const QString &topic) { + connect(m_client, &QMqttClient::messageReceived, this, [this](const QByteArray &message, const QMqttTopicName &topic) { const QString content = QDateTime::currentDateTime().toString() + QLatin1String(" Received Topic: ") - + topic + + topic.name() + QLatin1String(" Message: ") + message + QLatin1Char('\n'); @@ -156,7 +156,7 @@ void MainWindow::on_buttonSubscribe_clicked() return; } auto subWindow = new SubscriptionWindow(subscription); - subWindow->setWindowTitle(subscription->topic()); + subWindow->setWindowTitle(subscription->topic().filter()); subWindow->show(); } diff --git a/examples/mqtt/subscriptions/subscriptionwindow.cpp b/examples/mqtt/subscriptions/subscriptionwindow.cpp index 0b6e0ba..5a87a51 100644 --- a/examples/mqtt/subscriptions/subscriptionwindow.cpp +++ b/examples/mqtt/subscriptions/subscriptionwindow.cpp @@ -58,7 +58,7 @@ SubscriptionWindow::SubscriptionWindow(QMqttSubscription *sub, QWidget *parent) { ui->setupUi(this); - ui->labelSub->setText(m_sub->topic()); + ui->labelSub->setText(m_sub->topic().filter()); ui->labelQoS->setText(QString::number(m_sub->qos())); updateStatus(m_sub->state()); connect(m_sub, &QMqttSubscription::messageReceived, this, &SubscriptionWindow::updateMessage); diff --git a/src/mqtt/doc/qtmqtt.qdocconf b/src/mqtt/doc/qtmqtt.qdocconf index 6c62142..6fc50b4 100644 --- a/src/mqtt/doc/qtmqtt.qdocconf +++ b/src/mqtt/doc/qtmqtt.qdocconf @@ -37,6 +37,8 @@ imagedirs += images \ ../../../examples/mqtt/doc/images excludedirs += ../qt4support +Cpp.ignoretokens += Q_MQTT_EXPORT + depends += qtcore qtdoc qtnetwork qmake qtwebsockets #add generic thumbnail images for example documentation that does not have an image. diff --git a/src/mqtt/mqtt.pro b/src/mqtt/mqtt.pro index 3e71e07..c7866ba 100644 --- a/src/mqtt/mqtt.pro +++ b/src/mqtt/mqtt.pro @@ -11,7 +11,9 @@ PUBLIC_HEADERS += \ qmqttglobal.h \ qmqttclient.h \ qmqttmessage.h \ - qmqttsubscription.h + qmqttsubscription.h \ + qmqtttopicfilter.h \ + qmqtttopicname.h PRIVATE_HEADERS += \ qmqttclient_p.h \ @@ -23,8 +25,10 @@ SOURCES += \ qmqttclient.cpp \ qmqttconnection.cpp \ qmqttcontrolpacket.cpp \ + qmqttmessage.cpp \ qmqttsubscription.cpp \ - qmqttmessage.cpp + qmqtttopicfilter.cpp \ + qmqtttopicname.cpp HEADERS += $$PUBLIC_HEADERS $$PRIVATE_HEADERS diff --git a/src/mqtt/qmqttclient.cpp b/src/mqtt/qmqttclient.cpp index 69af887..23fb00f 100644 --- a/src/mqtt/qmqttclient.cpp +++ b/src/mqtt/qmqttclient.cpp @@ -236,7 +236,7 @@ QT_BEGIN_NAMESPACE */ /*! - \fn QMqttClient::messageReceived(const QByteArray &message, const QString &topic) + \fn QMqttClient::messageReceived(const QByteArray &message, const QMqttTopicName &topic) This signal is emitted when a new message has been received. The category of the message is specified by \a topic with the content being \a message. @@ -271,10 +271,10 @@ QT_BEGIN_NAMESPACE /*! Creates a new MQTT client instance with the specified \a parent. */ -QMqttClient::QMqttClient(QObject *parent) : QObject(*(new QMqttClientPrivate), parent) +QMqttClient::QMqttClient(QObject *parent) : QObject(*(new QMqttClientPrivate(this)), parent) { Q_D(QMqttClient); - d->m_connection.setClient(this, d); + d->m_connection.setClientPrivate(d); } /*! @@ -313,7 +313,7 @@ QIODevice *QMqttClient::transport() const is subscribed twice, the return value points to the same subscription instance. The MQTT client is the owner of the subscription. */ -QMqttSubscription *QMqttClient::subscribe(const QString &topic, quint8 qos) +QMqttSubscription *QMqttClient::subscribe(const QMqttTopicFilter &topic, quint8 qos) { Q_D(QMqttClient); @@ -330,7 +330,7 @@ QMqttSubscription *QMqttClient::subscribe(const QString &topic, quint8 qos) \note If a client disconnects from a broker without unsubscribing, the broker will store all messages and publish them on the next reconnect. */ -void QMqttClient::unsubscribe(const QString &topic) +void QMqttClient::unsubscribe(const QMqttTopicFilter &topic) { Q_D(QMqttClient); d->m_connection.sendControlUnsubscribe(topic); @@ -345,7 +345,7 @@ void QMqttClient::unsubscribe(const QString &topic) Returns an ID that is used internally to identify the message. */ -qint32 QMqttClient::publish(const QString &topic, const QByteArray &message, quint8 qos, bool retain) +qint32 QMqttClient::publish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos, bool retain) { Q_D(QMqttClient); if (qos > 2) @@ -419,8 +419,12 @@ void QMqttClient::connectToHost(bool encrypted, const QString &sslPeerName) d->setStateAndError(Disconnected, TransportInvalid); return; } + d->m_error = QMqttClient::NoError; // Fresh reconnect, unset error d->setStateAndError(Connecting); + if (d->m_cleanSession) + d->m_connection.cleanSubscriptions(); + if (!d->m_connection.ensureTransportOpen(sslPeerName)) { qWarning("Could not ensure that connection is open"); d->setStateAndError(Disconnected, TransportInvalid); @@ -670,13 +674,14 @@ void QMqttClient::setError(ClientError e) emit errorChanged(d->m_error); } -QMqttClientPrivate::QMqttClientPrivate() +QMqttClientPrivate::QMqttClientPrivate(QMqttClient *c) : QObjectPrivate() { + m_client = c; m_clientId = QUuid::createUuid().toString(); - m_clientId.remove(QLatin1Char('{'), Qt::CaseInsensitive); - m_clientId.remove(QLatin1Char('}'), Qt::CaseInsensitive); - m_clientId.remove(QLatin1Char('-'), Qt::CaseInsensitive); + m_clientId.remove(QLatin1Char('{')); + m_clientId.remove(QLatin1Char('}')); + m_clientId.remove(QLatin1Char('-')); m_clientId.resize(23); } @@ -690,7 +695,7 @@ void QMqttClientPrivate::setStateAndError(QMqttClient::ClientState s, QMqttClien if (s != m_state) q->setState(s); - if (m_error != QMqttClient::NoError && m_error != e) + if (e != QMqttClient::NoError && m_error != e) q->setError(e); } diff --git a/src/mqtt/qmqttclient.h b/src/mqtt/qmqttclient.h index 9a6990d..873f937 100644 --- a/src/mqtt/qmqttclient.h +++ b/src/mqtt/qmqttclient.h @@ -32,6 +32,7 @@ #include <QtMqtt/qmqttglobal.h> #include <QtMqtt/QMqttSubscription> +#include <QtMqtt/QMqttTopicFilter> #include <QtCore/QIODevice> #include <QtCore/QObject> @@ -97,10 +98,10 @@ public: void setTransport(QIODevice *device, TransportType transport); QIODevice *transport() const; - QMqttSubscription *subscribe(const QString &topic, quint8 qos = 0); - void unsubscribe(const QString &topic); + QMqttSubscription *subscribe(const QMqttTopicFilter &topic, quint8 qos = 0); + void unsubscribe(const QMqttTopicFilter &topic); - Q_INVOKABLE qint32 publish(const QString &topic, const QByteArray &message = QByteArray(), + Q_INVOKABLE qint32 publish(const QMqttTopicName &topic, const QByteArray &message = QByteArray(), quint8 qos = 0, bool retain = false); bool requestPing(); @@ -131,7 +132,7 @@ public: Q_SIGNALS: void connected(); void disconnected(); - void messageReceived(const QByteArray &message, const QString &topic = QString()); + void messageReceived(const QByteArray &message, const QMqttTopicName &topic = QMqttTopicName()); void messageSent(qint32 id); void pingResponseReceived(); void brokerSessionRestored(); diff --git a/src/mqtt/qmqttclient_p.h b/src/mqtt/qmqttclient_p.h index 8ae8d61..dc5760c 100644 --- a/src/mqtt/qmqttclient_p.h +++ b/src/mqtt/qmqttclient_p.h @@ -54,9 +54,10 @@ class QMqttClientPrivate : public QObjectPrivate { Q_DECLARE_PUBLIC(QMqttClient) public: - QMqttClientPrivate(); + QMqttClientPrivate(QMqttClient *c); ~QMqttClientPrivate() override; void setStateAndError(QMqttClient::ClientState s, QMqttClient::ClientError e = QMqttClient::NoError); + QMqttClient *m_client{nullptr}; QString m_hostname; quint16 m_port{0}; QMqttConnection m_connection; diff --git a/src/mqtt/qmqttconnection.cpp b/src/mqtt/qmqttconnection.cpp index 03ddab1..4db53c1 100644 --- a/src/mqtt/qmqttconnection.cpp +++ b/src/mqtt/qmqttconnection.cpp @@ -87,11 +87,15 @@ bool QMqttConnection::ensureTransport(bool createSecureIfNeeded) { qCDebug(lcMqttConnection) << Q_FUNC_INFO << m_transport; - if (m_transport) - return true; + if (m_transport) { + if (m_ownTransport) + delete m_transport; + else + return true; + } // We are asked to create a transport layer - if (m_client->hostname().isEmpty() || m_client->port() == 0) { + if (m_clientPrivate->m_hostname.isEmpty() || m_clientPrivate->m_port == 0) { qWarning("Trying to create a transport layer, but no hostname is specified"); return false; } @@ -102,7 +106,11 @@ bool QMqttConnection::ensureTransport(bool createSecureIfNeeded) new QTcpSocket(); m_transport = socket; m_ownTransport = true; - m_transportType = createSecureIfNeeded ? QMqttClient::SecureSocket : QMqttClient::AbstractSocket; + m_transportType = +#ifndef QT_NO_SSL + createSecureIfNeeded ? QMqttClient::SecureSocket : +#endif + QMqttClient::AbstractSocket; connect(socket, &QAbstractSocket::connected, this, &QMqttConnection::transportConnectionEstablished); connect(socket, &QAbstractSocket::disconnected, this, &QMqttConnection::transportConnectionClosed); @@ -132,7 +140,7 @@ bool QMqttConnection::ensureTransportOpen(const QString &sslPeerName) return sendControlConnect(); m_internalState = BrokerConnecting; - socket->connectToHost(m_client->hostname(), m_client->port()); + socket->connectToHost(m_clientPrivate->m_hostname, m_clientPrivate->m_port); } #ifndef QT_NO_SSL else if (m_transportType == QMqttClient::SecureSocket) { @@ -142,7 +150,7 @@ bool QMqttConnection::ensureTransportOpen(const QString &sslPeerName) return true; m_internalState = BrokerConnecting; - socket->connectToHostEncrypted(m_client->hostname(), m_client->port(), sslPeerName); + socket->connectToHostEncrypted(m_clientPrivate->m_hostname, m_clientPrivate->m_port, sslPeerName); if (!socket->waitForConnected()) { qWarning("Could not establish socket connection for transport"); @@ -170,50 +178,54 @@ bool QMqttConnection::sendControlConnect() // Variable header // 3.1.2.1 Protocol Name // 3.1.2.2 Protocol Level - const quint8 protocolVersion = m_client->protocolVersion(); - if (protocolVersion == 3) { + switch (m_clientPrivate->m_protocolVersion) { + case QMqttClient::MQTT_3_1: packet.append("MQIsdp"); packet.append(char(3)); // Version 3.1 - } else if (protocolVersion == 4) { + break; + case QMqttClient::MQTT_3_1_1: packet.append("MQTT"); packet.append(char(4)); // Version 3.1.1 - } else { - qFatal("Illegal MQTT VERSION"); + break; + default: + qCWarning(lcMqttConnection) << "Illegal MQTT Version"; + m_clientPrivate->setStateAndError(QMqttClient::Disconnected, QMqttClient::InvalidProtocolVersion); + return false; } // 3.1.2.3 Connect Flags quint8 flags = 0; // Clean session - if (m_client->cleanSession()) + if (m_clientPrivate->m_cleanSession) flags |= 1 << 1; - if (!m_client->willMessage().isEmpty()) { + if (!m_clientPrivate->m_willMessage.isEmpty()) { flags |= 1 << 2; - if (m_client->willQoS() > 2) { + if (m_clientPrivate->m_willQoS > 2) { qWarning("Will QoS does not have a valid value"); return false; } - if (m_client->willQoS() == 1) + if (m_clientPrivate->m_willQoS == 1) flags |= 1 << 3; - else if (m_client->willQoS() == 2) + else if (m_clientPrivate->m_willQoS == 2) flags |= 1 << 4; - if (m_client->willRetain()) + if (m_clientPrivate->m_willRetain) flags |= 1 << 5; } - if (m_client->username().size()) + if (m_clientPrivate->m_username.size()) flags |= 1 << 7; - if (m_client->password().size()) + if (m_clientPrivate->m_password.size()) flags |= 1 << 6; packet.append(char(flags)); // 3.1.2.10 Keep Alive - packet.append(m_client->keepAlive()); + packet.append(m_clientPrivate->m_keepAlive); // 3.1.3 Payload // 3.1.3.1 Client Identifier - const QByteArray clientStringArray = m_client->clientId().toUtf8(); + const QByteArray clientStringArray = m_clientPrivate->m_clientId.toUtf8(); if (clientStringArray.size()) { packet.append(clientStringArray); } else { @@ -221,16 +233,16 @@ bool QMqttConnection::sendControlConnect() packet.append(char(0)); } - if (!m_client->willMessage().isEmpty()) { - packet.append(m_client->willTopic().toUtf8()); - packet.append(m_client->willMessage()); + if (!m_clientPrivate->m_willMessage.isEmpty()) { + packet.append(m_clientPrivate->m_willTopic.toUtf8()); + packet.append(m_clientPrivate->m_willMessage); } - if (m_client->username().size()) - packet.append(m_client->username().toUtf8()); + if (m_clientPrivate->m_username.size()) + packet.append(m_clientPrivate->m_username.toUtf8()); - if (m_client->password().size()) - packet.append(m_client->password().toUtf8()); + if (m_clientPrivate->m_password.size()) + packet.append(m_clientPrivate->m_password.toUtf8()); if (!writePacketToTransport(packet)) { qWarning("Could not write CONNECT frame to transport"); @@ -241,12 +253,12 @@ bool QMqttConnection::sendControlConnect() return true; } -qint32 QMqttConnection::sendControlPublish(const QString &topic, const QByteArray &message, quint8 qos, bool retain) +qint32 QMqttConnection::sendControlPublish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos, bool retain) { qCDebug(lcMqttConnection) << Q_FUNC_INFO << topic << " Size:" << message.size() << " bytes." << "QoS:" << qos << " Retain:" << retain; - if (topic.contains(QLatin1Char('#')) || topic.contains('+')) + if (!topic.isValid()) return -1; quint8 header = QMqttControlPacket::PUBLISH; @@ -259,15 +271,7 @@ qint32 QMqttConnection::sendControlPublish(const QString &topic, const QByteArra header |= 0x01; QSharedPointer<QMqttControlPacket> packet(new QMqttControlPacket(header)); - - QByteArray topicArray = topic.toUtf8(); - const std::uint16_t u16max = std::numeric_limits<std::uint16_t>::max(); - if (topicArray.size() > u16max) { - qWarning("Published topic is too long. Need to truncate"); - topicArray.truncate(u16max); - } - - packet->append(topicArray); + packet->append(topic.name().toUtf8()); quint16 identifier = 0; if (qos > 0) { identifier = unusedPacketIdentifier(); @@ -318,7 +322,7 @@ bool QMqttConnection::sendControlPublishComp(quint16 id) return writePacketToTransport(packet); } -QMqttSubscription *QMqttConnection::sendControlSubscribe(const QString &topic, quint8 qos) +QMqttSubscription *QMqttConnection::sendControlSubscribe(const QMqttTopicFilter &topic, quint8 qos) { qCDebug(lcMqttConnection) << Q_FUNC_INFO << " Topic:" << topic << " qos:" << qos; @@ -336,13 +340,12 @@ QMqttSubscription *QMqttConnection::sendControlSubscribe(const QString &topic, q packet.append(identifier); // Overflow protection - QByteArray topicArray = topic.toUtf8(); - if (topicArray.size() > std::numeric_limits<std::uint16_t>::max()) { - qWarning("Subscribed topic is too long."); + if (!topic.isValid()) { + qWarning("Subscribed topic filter is not valid."); return nullptr; } - packet.append(topicArray); + packet.append(topic.filter().toUtf8()); switch (qos) { case 0: packet.append(char(0x0)); break; @@ -352,8 +355,8 @@ QMqttSubscription *QMqttConnection::sendControlSubscribe(const QString &topic, q } auto result = new QMqttSubscription(this); - result->setTopic(QString::fromUtf8(topicArray)); - result->setClient(m_client); + result->setTopic(topic); + result->setClient(m_clientPrivate->m_client); result->setQos(qos); result->setState(QMqttSubscription::SubscriptionPending); @@ -368,12 +371,12 @@ QMqttSubscription *QMqttConnection::sendControlSubscribe(const QString &topic, q return result; } -bool QMqttConnection::sendControlUnsubscribe(const QString &topic) +bool QMqttConnection::sendControlUnsubscribe(const QMqttTopicFilter &topic) { qCDebug(lcMqttConnection) << Q_FUNC_INFO << " Topic:" << topic; // MQTT-3.10.3-2 - if (topic.isEmpty()) + if (!topic.isValid()) return false; if (!m_activeSubscriptions.contains(topic)) @@ -394,7 +397,7 @@ bool QMqttConnection::sendControlUnsubscribe(const QString &topic) packet.append(identifier); - packet.append(topic.toUtf8()); + packet.append(topic.filter().toUtf8()); auto sub = m_activeSubscriptions[topic]; sub->setState(QMqttSubscription::UnsubscriptionPending); @@ -446,9 +449,8 @@ bool QMqttConnection::sendControlDisconnect() return false; } -void QMqttConnection::setClient(QMqttClient *client, QMqttClientPrivate *clientPrivate) +void QMqttConnection::setClientPrivate(QMqttClientPrivate *clientPrivate) { - m_client = client; m_clientPrivate = clientPrivate; } @@ -477,6 +479,21 @@ quint16 QMqttConnection::unusedPacketIdentifier() const return packetIdentifierCounter; } +void QMqttConnection::cleanSubscriptions() +{ + for (auto item : m_pendingSubscriptionAck) + item->setState(QMqttSubscription::Unsubscribed); + m_pendingSubscriptionAck.clear(); + + for (auto item : m_pendingUnsubscriptions) + item->setState(QMqttSubscription::Unsubscribed); + m_pendingUnsubscriptions.clear(); + + for (auto item : m_activeSubscriptions) + item->setState(QMqttSubscription::Unsubscribed); + m_activeSubscriptions.clear(); +} + void QMqttConnection::transportConnectionEstablished() { if (m_internalState != BrokerConnecting) { @@ -495,8 +512,7 @@ void QMqttConnection::transportConnectionClosed() { m_readBuffer.clear(); m_pingTimer.stop(); - m_client->setState(QMqttClient::Disconnected); - m_client->setError(QMqttClient::TransportInvalid); + m_clientPrivate->setStateAndError(QMqttClient::Disconnected, QMqttClient::TransportInvalid); } void QMqttConnection::transportReadReady() @@ -544,9 +560,13 @@ void QMqttConnection::finalize_connack() // MQTT-3.2.2-1 & MQTT-3.2.2-2 if (sessionPresent) { - emit m_client->brokerSessionRestored(); - if (m_client->cleanSession()) + emit m_clientPrivate->m_client->brokerSessionRestored(); + if (m_clientPrivate->m_cleanSession) qWarning("Connected with a clean session, ack contains session present"); + } else { + // MQTT-4.1.0.-1 MQTT-4.1.0-2 Session not stored on broker side + // regardless whether cleanSession is false + cleanSubscriptions(); } quint8 connectResultValue; @@ -562,9 +582,9 @@ void QMqttConnection::finalize_connack() return; } m_internalState = BrokerConnected; - m_client->setState(QMqttClient::Connected); + m_clientPrivate->setStateAndError(QMqttClient::Connected); - m_pingTimer.setInterval(m_client->keepAlive() * 1000); + m_pingTimer.setInterval(m_clientPrivate->m_keepAlive * 1000); m_pingTimer.start(); } @@ -617,7 +637,7 @@ void QMqttConnection::finalize_publish() { // String topic const quint16 topicLength = qFromBigEndian<quint16>(reinterpret_cast<const quint16 *>(readBuffer(2).constData())); - const QString topic = QString::fromUtf8(reinterpret_cast<const char *>(readBuffer(topicLength).constData())); + const QMqttTopicName topic = QString::fromUtf8(reinterpret_cast<const char *>(readBuffer(topicLength).constData())); quint16 id = 0; if (m_currentPublish.qos > 0) { @@ -631,39 +651,14 @@ void QMqttConnection::finalize_publish() qCDebug(lcMqttConnectionVerbose) << "Finalize PUBLISH: topic:" << topic << " payloadLength:" << payloadLength;; - emit m_client->messageReceived(message, topic); + emit m_clientPrivate->m_client->messageReceived(message, topic); QMqttMessage qmsg(topic, message, id, m_currentPublish.qos, m_currentPublish.dup, m_currentPublish.retain); for (auto sub = m_activeSubscriptions.constBegin(); sub != m_activeSubscriptions.constEnd(); sub++) { - const QString subTopic = sub.key(); - - if (subTopic == topic) { - emit sub.value()->messageReceived(qmsg); - continue; - } else if (subTopic.endsWith(QLatin1Char('#')) && topic.startsWith(subTopic.leftRef(subTopic.size() - 1))) { - emit sub.value()->messageReceived(qmsg); - continue; - } - - if (!subTopic.contains(QLatin1Char('+'))) - continue; - - const QVector<QStringRef> subTopicSplit = subTopic.splitRef(QLatin1Char('/')); - const QVector<QStringRef> topicSplit = topic.splitRef(QLatin1Char('/')); - if (subTopicSplit.size() != topicSplit.size()) - continue; - bool match = true; - for (int i = 0; i < subTopicSplit.size() && match; ++i) { - if (subTopicSplit.at(i) == QLatin1Char('+') || subTopicSplit.at(i) == topicSplit.at(i)) - continue; - match = false; - } - - if (match) { + if (sub.key().match(topic)) emit sub.value()->messageReceived(qmsg); - } } if (m_currentPublish.qos == 1) @@ -684,7 +679,7 @@ void QMqttConnection::finalize_pubAckRecComp() auto pendingRelease = m_pendingReleaseMessages.take(id); if (!pendingRelease) qWarning("Received PUBCOMP for unknown released message"); - emit m_client->messageSent(id); + emit m_clientPrivate->m_client->messageSent(id); return; } @@ -699,7 +694,7 @@ void QMqttConnection::finalize_pubAckRecComp() sendControlPublishRelease(id); } else { qCDebug(lcMqttConnectionVerbose) << " PUBACK:" << id; - emit m_client->messageSent(id); + emit m_clientPrivate->m_client->messageSent(id); } } @@ -726,7 +721,7 @@ void QMqttConnection::finalize_pingresp() closeConnection(QMqttClient::ProtocolViolation); return; } - emit m_client->pingResponseReceived(); + emit m_clientPrivate->m_client->pingResponseReceived(); } void QMqttConnection::processData() @@ -769,12 +764,24 @@ void QMqttConnection::processData() // MQTT-2.2 A fixed header of a control packet must be at least 2 bytes. If the payload is // longer than 127 bytes the header can be up to 5 bytes long. - const int readBufferSize = m_readBuffer.size(); - if (readBufferSize < 2 - || (readBufferSize == 2 && (m_readBuffer.at(1) & 128) != 0) - || (readBufferSize == 3 && (m_readBuffer.at(2) & 128) != 0) - || (readBufferSize == 4 && (m_readBuffer.at(3) & 128) != 0)) { + switch (m_readBuffer.size()) { + case 0: + case 1: return; + case 2: + if ((m_readBuffer.at(1) & 128) != 0) + return; + break; + case 3: + if ((m_readBuffer.at(1) & 128) != 0 && (m_readBuffer.at(2) & 128) != 0) + return; + break; + case 4: + if ((m_readBuffer.at(1) & 128) != 0 && (m_readBuffer.at(2) & 128) != 0 && (m_readBuffer.at(3) & 128) != 0) + return; + break; + default: + break; } readBuffer((char*)&m_currentPacket, 1); diff --git a/src/mqtt/qmqttconnection_p.h b/src/mqtt/qmqttconnection_p.h index d747771..cf9c912 100644 --- a/src/mqtt/qmqttconnection_p.h +++ b/src/mqtt/qmqttconnection_p.h @@ -77,21 +77,23 @@ public: bool ensureTransportOpen(const QString &sslPeerName = QString()); bool sendControlConnect(); - qint32 sendControlPublish(const QString &topic, const QByteArray &message, quint8 qos = 0, bool retain = false); + qint32 sendControlPublish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos = 0, bool retain = false); bool sendControlPublishAcknowledge(quint16 id); bool sendControlPublishRelease(quint16 id); bool sendControlPublishReceive(quint16 id); bool sendControlPublishComp(quint16 id); - QMqttSubscription *sendControlSubscribe(const QString &topic, quint8 qos = 0); - bool sendControlUnsubscribe(const QString &topic); + QMqttSubscription *sendControlSubscribe(const QMqttTopicFilter &topic, quint8 qos = 0); + bool sendControlUnsubscribe(const QMqttTopicFilter &topic); bool sendControlPingRequest(); bool sendControlDisconnect(); - void setClient(QMqttClient *client, QMqttClientPrivate *clientPrivate); + void setClientPrivate(QMqttClientPrivate *clientPrivate); inline quint16 unusedPacketIdentifier() const; inline InternalConnectionState internalState() const { return m_internalState; } + void cleanSubscriptions(); + public Q_SLOTS: void transportConnectionEstablished(); void transportConnectionClosed(); @@ -101,7 +103,6 @@ public: QIODevice *m_transport{nullptr}; QMqttClient::TransportType m_transportType{QMqttClient::IODevice}; bool m_ownTransport{false}; - QMqttClient *m_client{nullptr}; QMqttClientPrivate *m_clientPrivate{nullptr}; private: Q_DISABLE_COPY(QMqttConnection) @@ -130,7 +131,7 @@ private: bool writePacketToTransport(const QMqttControlPacket &p); QMap<quint16, QMqttSubscription *> m_pendingSubscriptionAck; QMap<quint16, QMqttSubscription *> m_pendingUnsubscriptions; - QMap<QString, QMqttSubscription *> m_activeSubscriptions; + QMap<QMqttTopicFilter, QMqttSubscription *> m_activeSubscriptions; QMap<quint16, QSharedPointer<QMqttControlPacket>> m_pendingMessages; QMap<quint16, QSharedPointer<QMqttControlPacket>> m_pendingReleaseMessages; InternalConnectionState m_internalState{BrokerDisconnected}; diff --git a/src/mqtt/qmqttcontrolpacket.cpp b/src/mqtt/qmqttcontrolpacket.cpp index f929f37..3d0db8d 100644 --- a/src/mqtt/qmqttcontrolpacket.cpp +++ b/src/mqtt/qmqttcontrolpacket.cpp @@ -80,12 +80,12 @@ void QMqttControlPacket::append(quint16 value) void QMqttControlPacket::append(const QByteArray &data) { append(static_cast<quint16>(data.size())); - m_payload.append(data.constData()); + m_payload.append(data); } void QMqttControlPacket::appendRaw(const QByteArray &data) { - m_payload.append(data.constData()); + m_payload.append(data); } QByteArray QMqttControlPacket::serialize() const diff --git a/src/mqtt/qmqttmessage.cpp b/src/mqtt/qmqttmessage.cpp index 1bdbdf7..25ed768 100644 --- a/src/mqtt/qmqttmessage.cpp +++ b/src/mqtt/qmqttmessage.cpp @@ -86,44 +86,117 @@ QT_BEGIN_NAMESPACE live update. A broker can store only one retained message per topic. */ -QByteArray QMqttMessage::payload() const +class QMqttMessagePrivate : public QSharedData { - return m_payload; +public: + bool operator==(const QMqttMessagePrivate &other) { + return m_topic == other.m_topic && + m_payload == other.m_payload && + m_id == other.m_id && + m_qos == other.m_qos && + m_duplicate == other.m_duplicate && + m_retain == other.m_retain; + } + QMqttTopicName m_topic; + QByteArray m_payload; + quint16 m_id{0}; + quint8 m_qos{0}; + bool m_duplicate{false}; + bool m_retain{false}; +}; + +/*! + Creates a new MQTT message. +*/ +QMqttMessage::QMqttMessage() + : d(new QMqttMessagePrivate()) +{ +} + +/*! + Constructs a new MQTT message that is a copy of \a other. +*/ +QMqttMessage::QMqttMessage(const QMqttMessage &other) + : d(other.d) +{ +} + +QMqttMessage::~QMqttMessage() +{ +} + +/*! + Makes this object a copy of \a other and returns the new value of this object. +*/ +QMqttMessage &QMqttMessage::operator=(const QMqttMessage &other) +{ + d = other.d; + return *this; +} + +/*! + Returns \c true if the message and \a other are equal, otherwise returns \c false. +*/ +bool QMqttMessage::operator==(const QMqttMessage &other) const +{ + return d == other.d; +} + +/*! + Returns \c true if the message and \a other are not equal, otherwise returns \c false. +*/ +bool QMqttMessage::operator!=(const QMqttMessage &other) const +{ + return !(*this == other); +} + +const QByteArray &QMqttMessage::payload() const +{ + return d->m_payload; } quint8 QMqttMessage::qos() const { - return m_qos; + return d->m_qos; } quint16 QMqttMessage::id() const { - return m_id; + return d->m_id; } -QString QMqttMessage::topic() const +QMqttTopicName QMqttMessage::topic() const { - return m_topic; + return d->m_topic; } bool QMqttMessage::duplicate() const { - return m_duplicate; + return d->m_duplicate; } bool QMqttMessage::retain() const { - return m_retain; + return d->m_retain; } -QMqttMessage::QMqttMessage(const QString &topic, const QByteArray &content, quint16 id, quint8 qos, bool dup, bool retain) - : m_topic(topic) - , m_payload(content) - , m_id(id) - , m_qos(qos) - , m_duplicate(dup) - , m_retain(retain) +/*! + \internal + Constructs a new MQTT message with \a content on the topic \a topic. + Furthermore the properties \a id, \a qos, \a dup, \a retain must be specified. + + This constructor is mostly used internally to construct a QMqttMessage at message + receival. +*/ +QMqttMessage::QMqttMessage(const QMqttTopicName &topic, const QByteArray &content, quint16 id, quint8 qos, bool dup, bool retain) + : d(new QMqttMessagePrivate) { + d->m_topic = topic; + d->m_payload = content; + d->m_id = id; + d->m_qos = qos; + d->m_duplicate = dup; + d->m_retain = retain; } QT_END_NAMESPACE diff --git a/src/mqtt/qmqttmessage.h b/src/mqtt/qmqttmessage.h index 3f25d08..509e631 100644 --- a/src/mqtt/qmqttmessage.h +++ b/src/mqtt/qmqttmessage.h @@ -32,40 +32,51 @@ #include "qmqttglobal.h" +#include <QtMqtt/QMqttTopicName> + #include <QtCore/QObject> +#include <QtCore/QSharedDataPointer> QT_BEGIN_NAMESPACE +class QMqttMessagePrivate; + class Q_MQTT_EXPORT QMqttMessage { Q_GADGET - Q_PROPERTY(QString topic READ topic CONSTANT) + Q_PROPERTY(QMqttTopicName topic READ topic CONSTANT) Q_PROPERTY(QByteArray payload READ payload CONSTANT) Q_PROPERTY(quint16 id READ id CONSTANT) Q_PROPERTY(quint8 qos READ qos CONSTANT) Q_PROPERTY(bool duplicate READ duplicate CONSTANT) Q_PROPERTY(bool retain READ retain CONSTANT) + public: - QByteArray payload() const; + QMqttMessage(); + QMqttMessage(const QMqttMessage& other); + ~QMqttMessage(); + + QMqttMessage& operator=(const QMqttMessage &other); + bool operator==(const QMqttMessage &other) const; + inline bool operator!=(const QMqttMessage &other) const; + + const QByteArray &payload() const; quint8 qos() const; quint16 id() const; - QString topic() const; + QMqttTopicName topic() const; bool duplicate() const; bool retain() const; private: friend class QMqttConnection; - explicit QMqttMessage(const QString &topic, const QByteArray &payload, + QMqttMessage(const QMqttTopicName &topic, const QByteArray &payload, quint16 id, quint8 qos, bool dup, bool retain); - QString m_topic; - QByteArray m_payload; - quint16 m_id; - quint8 m_qos; - bool m_duplicate; - bool m_retain; + QExplicitlySharedDataPointer<QMqttMessagePrivate> d; }; QT_END_NAMESPACE +Q_DECLARE_METATYPE(QMqttMessage) + #endif // QMQTTMESSAGE_H diff --git a/src/mqtt/qmqttsubscription.cpp b/src/mqtt/qmqttsubscription.cpp index 80e8aed..750707f 100644 --- a/src/mqtt/qmqttsubscription.cpp +++ b/src/mqtt/qmqttsubscription.cpp @@ -108,7 +108,7 @@ QMqttSubscription::SubscriptionState QMqttSubscription::state() const return d->m_state; } -QString QMqttSubscription::topic() const +QMqttTopicFilter QMqttSubscription::topic() const { Q_D(const QMqttSubscription); return d->m_topic; @@ -143,7 +143,7 @@ void QMqttSubscription::unsubscribe() setState(Unsubscribed); } -void QMqttSubscription::setTopic(const QString &topic) +void QMqttSubscription::setTopic(const QMqttTopicFilter &topic) { Q_D(QMqttSubscription); d->m_topic = topic; diff --git a/src/mqtt/qmqttsubscription.h b/src/mqtt/qmqttsubscription.h index a4dc53b..054e4ea 100644 --- a/src/mqtt/qmqttsubscription.h +++ b/src/mqtt/qmqttsubscription.h @@ -33,6 +33,8 @@ #include "qmqttmessage.h" #include <QtMqtt/qmqttglobal.h> +#include <QtMqtt/QMqttTopicFilter> + #include <QtCore/QObject> QT_BEGIN_NAMESPACE @@ -46,7 +48,7 @@ class Q_MQTT_EXPORT QMqttSubscription : public QObject Q_ENUMS(SubscriptionState) Q_PROPERTY(SubscriptionState state READ state NOTIFY stateChanged) Q_PROPERTY(quint8 qos READ qos NOTIFY qosChanged) - Q_PROPERTY(QString topic READ topic) + Q_PROPERTY(QMqttTopicFilter topic READ topic) public: ~QMqttSubscription() override; enum SubscriptionState { @@ -58,7 +60,7 @@ public: }; SubscriptionState state() const; - QString topic() const; + QMqttTopicFilter topic() const; quint8 qos() const; Q_SIGNALS: @@ -73,7 +75,7 @@ private: Q_DECLARE_PRIVATE(QMqttSubscription) Q_DISABLE_COPY(QMqttSubscription) void setState(SubscriptionState state); - void setTopic(const QString &topic); + void setTopic(const QMqttTopicFilter &topic); void setClient(QMqttClient *client); void setQos(quint8 qos); friend class QMqttConnection; diff --git a/src/mqtt/qmqttsubscription_p.h b/src/mqtt/qmqttsubscription_p.h index 104c4fc..89ffdf0 100644 --- a/src/mqtt/qmqttsubscription_p.h +++ b/src/mqtt/qmqttsubscription_p.h @@ -54,7 +54,7 @@ public: ~QMqttSubscriptionPrivate() override = default; QMqttClient *m_client{nullptr}; QMqttSubscription::SubscriptionState m_state{QMqttSubscription::Unsubscribed}; - QString m_topic; + QMqttTopicFilter m_topic; quint8 m_qos{0}; }; diff --git a/src/mqtt/qmqtttopicfilter.cpp b/src/mqtt/qmqtttopicfilter.cpp new file mode 100644 index 0000000..d2071c7 --- /dev/null +++ b/src/mqtt/qmqtttopicfilter.cpp @@ -0,0 +1,314 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "qmqtttopicfilter.h" + +#include <QtCore/QDebug> +#include <QtCore/QVector> + +QT_BEGIN_NAMESPACE + +/*! + \class QMqttTopicFilter + \inmodule QtMqtt + \reentrant + \ingroup shared + + \brief The QMqttTopicFilter class represents a MQTT topic filter. + + QMqttTopicFilter is a thin wrapper around a QString providing an expressive + data type for MQTT topic filters. Beside the benefits of having a strong + type preventing unintended misuse, QMqttTopicFilter provides convenient + functions related to topic filters like isValid() or match(). + + For example, the following code would fail to compile and prevent a possible + unintended and meaningless matching of two filters, especially if the + variable names were less expressive: + + \code + QMqttTopicFilter globalFilter{"foo/#"}; + QMqttTopicFilter specificFilter{"foo/bar"}; + if (globalFilter.match(specificFilter)) { + //... + } + \endcode + + The usability, however, is not affected since the following snippet compiles + and runs as expected: + + \code + QMqttTopicFilter globalFilter{"foo/#"}; + if (globalFilter.match("foo/bar")) { + //... + } + \endcode + + \sa QMqttTopicName + */ + +/*! + \fn void QMqttTopicFilter::swap(QMqttTopicFilter &other) + Swaps the MQTT topic filter \a other with this MQTT topic filter. This + operation is very fast and never fails. + */ + +/*! + \enum QMqttTopicFilter::MatchOption + + This enum value holds the matching options for the topic filter. + + \value NoMatchOption + No match options are set. + \value WildcardsDontMatchDollarTopicMatchOption + A wildcard at the filter's beginning does not match a topic name that + starts with the dollar sign ($). + */ + +class QMqttTopicFilterPrivate : public QSharedData +{ +public: + QString filter; +}; + +/*! + Creates a new MQTT topic filter with the specified \a filter. + */ +QMqttTopicFilter::QMqttTopicFilter(const QString &filter) : d(new QMqttTopicFilterPrivate) +{ + d->filter = filter; +} + +/*! + Creates a new MQTT topic filter with the specified \a filter. + */ +QMqttTopicFilter::QMqttTopicFilter(const QLatin1String &filter) : d(new QMqttTopicFilterPrivate) +{ + d->filter = filter; +} + +/*! + Creates a new MQTT topic filter as a copy of \a filter. + */ +QMqttTopicFilter::QMqttTopicFilter(const QMqttTopicFilter &filter) : d(filter.d) +{ +} + +/*! + Destroys the QMqttTopicFilter object. + */ +QMqttTopicFilter::~QMqttTopicFilter() +{ +} + +/*! + Assigns the MQTT topic filter \a filter to this object, and returns a + reference to the copy. + */ +QMqttTopicFilter &QMqttTopicFilter::operator=(const QMqttTopicFilter &filter) +{ + d = filter.d; + return *this; +} + +/*! + Returns the topic filter. + */ +QString QMqttTopicFilter::filter() const +{ + return d->filter; +} + +/*! + Sets the topic filter to \a filter. + */ +void QMqttTopicFilter::setFilter(const QString &filter) +{ + d.detach(); + d->filter = filter; +} + +/*! + Returns \c true if the topic filter is valid according to the MQTT standard + section 4.7, or \c false otherwise. + */ +bool QMqttTopicFilter::isValid() const +{ + // MQTT-4.7.3-1, MQTT-4.7.3-3, and MQTT-4.7.3-2 + const int size = d->filter.size(); + if (size == 0 || size > 65535 || d->filter.contains(QChar(QChar::Null))) + return false; + + if (size == 1) + return true; + + // '#' MUST be last and its own level. It MUST NOT appear more than at most once. + const int multiLevelPosition = d->filter.indexOf(QLatin1Char('#')); + if (multiLevelPosition != -1 + && (multiLevelPosition != size - 1 || d->filter.at(size-2) != QLatin1Char('/'))) { + return false; + } + + // '+' MAY occur multiple times but MUST be its own level. + int singleLevelPosition = d->filter.indexOf(QLatin1Char('+')); + while (singleLevelPosition != -1) { + if ((singleLevelPosition != 0 && d->filter.at(singleLevelPosition - 1) != QLatin1Char('/')) + || (singleLevelPosition < size - 1 && d->filter.at(singleLevelPosition + 1) != QLatin1Char('/'))) { + return false; + } + singleLevelPosition = d->filter.indexOf(QLatin1Char('#'), singleLevelPosition + 1); + } + + return true; +} + +/*! + Returns \c true if the topic filter matches the topic name \a name + honoring the given \a matchOptions, or \c false otherwise. + */ +bool QMqttTopicFilter::match(const QMqttTopicName &name, MatchOptions matchOptions) const +{ + if (!name.isValid() || !isValid()) + return false; + + const QString topic = name.name(); + if (topic == d->filter) + return true; + + if (matchOptions.testFlag(WildcardsDontMatchDollarTopicMatchOption) + && topic.startsWith(QLatin1Char('$')) + && (d->filter.startsWith(QLatin1Char('+')) + || d->filter == QLatin1Char('#') + || d->filter == QLatin1String("/#"))) { + return false; + } + + if (d->filter.endsWith(QLatin1Char('#'))) { + QStringRef root = d->filter.leftRef(d->filter.size() - 1); + if (root.endsWith(QLatin1Char('/'))) // '#' also represents the parent level! + root = root.left(root.size() - 1); + return topic.startsWith(root); + } + + if (d->filter.contains(QLatin1Char('+'))) { + const QVector<QStringRef> filterLevels = d->filter.splitRef(QLatin1Char('/')); + const QVector<QStringRef> topicLevels = topic.splitRef(QLatin1Char('/')); + if (filterLevels.size() != topicLevels.size()) + return false; + for (int i = 0; i < filterLevels.size(); ++i) { + const QStringRef &level = filterLevels.at(i); + if (level != QLatin1Char('+') && level != topicLevels.at(i)) + return false; + } + return true; + } + + return false; +} + +/*! + \relates QMqttTopicFilter + + Returns \c true if the topic filters \a lhs and \a rhs are equal, + otherwise returns \c false. + */ +bool operator==(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW +{ + return (lhs.d == rhs.d) || (lhs.d->filter == rhs.d->filter); +} + +/*! + \fn bool operator!=(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) + \relates QMqttTopicFilter + + Returns \c true if the topic filters \a lhs and \a rhs are different, + otherwise returns \c false. + */ + +/*! + \relates QMqttTopicFilter + + Returns \c true if the topic filter \a lhs is lexically less than the topic + filter \a rhs; otherwise returns \c false. + */ +bool operator<(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW +{ + return lhs.d->filter < rhs.d->filter; +} + +/*! + \relates QHash + + Returns the hash value for \a filter. If specified, \a seed is used to + initialize the hash. +*/ +uint qHash(const QMqttTopicFilter &filter, uint seed) Q_DECL_NOTHROW +{ + return qHash(filter.d->filter, seed); +} + +#ifndef QT_NO_DATASTREAM +/*! \relates QMqttTopicFilter + + Writes the topic filter \a filter to the stream \a out and returns a + reference to the stream. + + \sa{Serializing Qt Data Types}{Format of the QDataStream operators} +*/ +QDataStream &operator<<(QDataStream &out, const QMqttTopicFilter &filter) +{ + out << filter.filter(); + return out; +} + +/*! \relates QMqttTopicFilter + + Reads a topic filter into \a filter from the stream \a in and returns a + reference to the stream. + + \sa{Serializing Qt Data Types}{Format of the QDataStream operators} +*/ +QDataStream &operator>>(QDataStream &in, QMqttTopicFilter &filter) +{ + QString f; + in >> f; + filter.setFilter(f); + return in; +} +#endif // QT_NO_DATASTREAM + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug d, const QMqttTopicFilter &filter) +{ + QDebugStateSaver saver(d); + d.nospace() << "QMqttTopicFilter(" << filter.filter() << ')'; + return d; +} +#endif + +QT_END_NAMESPACE diff --git a/src/mqtt/qmqtttopicfilter.h b/src/mqtt/qmqtttopicfilter.h new file mode 100644 index 0000000..8834a9a --- /dev/null +++ b/src/mqtt/qmqtttopicfilter.h @@ -0,0 +1,101 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef QMQTTTOPICFILTER_H +#define QMQTTTOPICFILTER_H + +#include "qmqttglobal.h" + +#include <QtMqtt/QMqttTopicName> + +#include <QtCore/QExplicitlySharedDataPointer> +#include <QtCore/QMetaType> +#include <QtCore/QString> + +QT_BEGIN_NAMESPACE + +class QMqttTopicFilterPrivate; + +class QMqttTopicFilter; +// qHash is a friend, but we can't use default arguments for friends (§8.3.6.4) +Q_MQTT_EXPORT uint qHash(const QMqttTopicFilter &name, uint seed = 0) Q_DECL_NOTHROW; + +class Q_MQTT_EXPORT QMqttTopicFilter +{ +public: + enum MatchOption { + NoMatchOption = 0x0000, + WildcardsDontMatchDollarTopicMatchOption = 0x0001 + }; + Q_DECLARE_FLAGS(MatchOptions, MatchOption) + + QMqttTopicFilter(const QString &filter = QString()); + QMqttTopicFilter(const QLatin1String &filter); + QMqttTopicFilter(const QMqttTopicFilter &filter); + ~QMqttTopicFilter(); + QMqttTopicFilter &operator=(const QMqttTopicFilter &filter); + +#ifdef Q_COMPILER_RVALUE_REFS + inline QMqttTopicFilter &operator=(QMqttTopicFilter &&other) Q_DECL_NOTHROW { qSwap(d, other.d); return *this; } +#endif + + inline void swap(QMqttTopicFilter &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + + QString filter() const; + void setFilter(const QString &filter); + + Q_REQUIRED_RESULT bool isValid() const; + Q_REQUIRED_RESULT bool match(const QMqttTopicName &name, MatchOptions matchOptions = NoMatchOption) const; + + friend Q_MQTT_EXPORT bool operator==(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW; + friend inline bool operator!=(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW { return !(lhs == rhs); } + friend Q_MQTT_EXPORT bool operator<(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW; + friend Q_MQTT_EXPORT uint qHash(const QMqttTopicFilter &filter, uint seed) Q_DECL_NOTHROW; + +private: + QExplicitlySharedDataPointer<QMqttTopicFilterPrivate> d; +}; + +Q_DECLARE_SHARED(QMqttTopicFilter) +Q_DECLARE_OPERATORS_FOR_FLAGS(QMqttTopicFilter::MatchOptions) + +#ifndef QT_NO_DATASTREAM +Q_MQTT_EXPORT QDataStream &operator<<(QDataStream &, const QMqttTopicFilter &); +Q_MQTT_EXPORT QDataStream &operator>>(QDataStream &, QMqttTopicFilter &); +#endif + +#ifndef QT_NO_DEBUG_STREAM +Q_MQTT_EXPORT QDebug operator<<(QDebug, const QMqttTopicFilter &); +#endif + +QT_END_NAMESPACE + +Q_DECLARE_METATYPE(QMqttTopicFilter) + +#endif // QMQTTTOPICFILTER_H diff --git a/src/mqtt/qmqtttopicname.cpp b/src/mqtt/qmqtttopicname.cpp new file mode 100644 index 0000000..d414db0 --- /dev/null +++ b/src/mqtt/qmqtttopicname.cpp @@ -0,0 +1,231 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "qmqtttopicname.h" + +#include <QtCore/QDebug> + +QT_BEGIN_NAMESPACE + +/*! + \class QMqttTopicName + \inmodule QtMqtt + \reentrant + \ingroup shared + + \brief The QMqttTopicName class represents a MQTT topic name. + + QMqttTopicName is a thin wrapper around a QString providing an expressive + data type for MQTT topic names. Beside the benefits of having a strong type + preventing unintended misuse, QMqttTopicName provides convenient functions + related to topic names like isValid() or levels(). + + \sa QMqttTopicFilter + */ + +/*! + \fn void QMqttTopicName::swap(QMqttTopicName &other) + Swaps the MQTT topic name \a other with this MQTT topic name. This + operation is very fast and never fails. + */ + +class QMqttTopicNamePrivate : public QSharedData +{ +public: + QString name; +}; + +/*! + Creates a new MQTT topic name with the specified \a name. + */ +QMqttTopicName::QMqttTopicName(const QString &name) : d(new QMqttTopicNamePrivate) +{ + d->name = name; +} + +/*! + Creates a new MQTT topic name with the specified \a name. + */ +QMqttTopicName::QMqttTopicName(const QLatin1String &name) : d(new QMqttTopicNamePrivate) +{ + d->name = name; +} + +/*! + Creates a new MQTT topic name as a copy of \a name. + */ +QMqttTopicName::QMqttTopicName(const QMqttTopicName &name) : d(name.d) +{ +} + +/*! + Destroys the QMqttTopicName object. + */ +QMqttTopicName::~QMqttTopicName() +{ +} + +/*! + Assigns the MQTT topic name \a name to this object, and returns a reference + to the copy. + */ +QMqttTopicName &QMqttTopicName::operator=(const QMqttTopicName &name) +{ + d = name.d; + return *this; +} + +/*! + Returns the topic name. + */ +QString QMqttTopicName::name() const +{ + return d->name; +} + +/*! + Sets the topic name to \a name. + */ +void QMqttTopicName::setName(const QString &name) +{ + d.detach(); + d->name = name; +} + +/*! + Returns \c true if the topic name is valid according to the MQTT standard + section 4.7, or \c false otherwise. + */ +bool QMqttTopicName::isValid() const +{ + const int bytes = d->name.size(); + return bytes > 0 // [MQTT-4.7.3-1] + && bytes < 65536 // [MQTT-4.7.3-3] + && !d->name.contains(QLatin1Char('#')) // [MQTT-4.7.1-1] + && !d->name.contains(QLatin1Char('+')) // [MQTT-4.7.1-1] + && !d->name.contains(QChar(QChar::Null)); // [MQTT-4.7.3-2] +} + +/*! + Returns the total number of topic levels. + */ +int QMqttTopicName::levelCount() const +{ + return d->name.isEmpty() ? 0 : d->name.count(QLatin1Char('/')) + 1; +} + +/*! + Returns the topic levels. + */ +QStringList QMqttTopicName::levels() const +{ + return d->name.split(QLatin1Char('/'), QString::KeepEmptyParts); +} + +/*! + \relates QMqttTopicName + + Returns \c true if the topic names \a lhs and \a rhs are equal, + otherwise returns \c false. + */ +bool operator==(const QMqttTopicName &lhs, const QMqttTopicName &rhs) Q_DECL_NOTHROW +{ + return (lhs.d == rhs.d) || (lhs.d->name == rhs.d->name); +} + +/*! + \fn bool operator!=(const QMqttTopicName &lhs, const QMqttTopicName &rhs) + \relates QMqttTopicName + + Returns \c true if the topic names \a lhs and \a rhs are different, + otherwise returns \c false. + */ + +/*! + \relates QMqttTopicName + + Returns \c true if the topic name \a lhs is lexically less than the topic + name \a rhs; otherwise returns \c false. + */ +bool operator<(const QMqttTopicName &lhs, const QMqttTopicName &rhs) Q_DECL_NOTHROW +{ + return lhs.d->name < rhs.d->name; +} + +/*! + \relates QHash + + Returns the hash value for \a name. If specified, \a seed is used to + initialize the hash. +*/ +uint qHash(const QMqttTopicName &name, uint seed) Q_DECL_NOTHROW +{ + return qHash(name.d->name, seed); +} + +#ifndef QT_NO_DATASTREAM +/*! \relates QMqttTopicName + + Writes the topic name \a name to the stream \a out and returns a reference + to the stream. + + \sa{Serializing Qt Data Types}{Format of the QDataStream operators} +*/ +QDataStream &operator<<(QDataStream &out, const QMqttTopicName &name) +{ + out << name.name(); + return out; +} + +/*! \relates QMqttTopicName + + Reads a topic name into \a name from the stream \a in and returns a + reference to the stream. + + \sa{Serializing Qt Data Types}{Format of the QDataStream operators} +*/ +QDataStream &operator>>(QDataStream &in, QMqttTopicName &name) +{ + QString n; + in >> n; + name.setName(n); + return in; +} +#endif // QT_NO_DATASTREAM + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug d, const QMqttTopicName &name) +{ + QDebugStateSaver saver(d); + d.nospace() << "QMqttTopicName(" << name.name() << ')'; + return d; +} +#endif + +QT_END_NAMESPACE diff --git a/src/mqtt/qmqtttopicname.h b/src/mqtt/qmqtttopicname.h new file mode 100644 index 0000000..09898bf --- /dev/null +++ b/src/mqtt/qmqtttopicname.h @@ -0,0 +1,94 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef QMQTTTOPICNAME_H +#define QMQTTTOPICNAME_H + +#include "qmqttglobal.h" + +#include <QtCore/QExplicitlySharedDataPointer> +#include <QtCore/QMetaType> +#include <QtCore/QString> +#include <QtCore/QStringList> + +QT_BEGIN_NAMESPACE + +class QMqttTopicNamePrivate; + +class QMqttTopicName; +// qHash is a friend, but we can't use default arguments for friends (§8.3.6.4) +Q_MQTT_EXPORT uint qHash(const QMqttTopicName &name, uint seed = 0) Q_DECL_NOTHROW; + +class Q_MQTT_EXPORT QMqttTopicName +{ +public: + QMqttTopicName(const QString &name = QString()); + QMqttTopicName(const QLatin1String &name); + QMqttTopicName(const QMqttTopicName &name); + ~QMqttTopicName(); + QMqttTopicName &operator=(const QMqttTopicName &name); + +#ifdef Q_COMPILER_RVALUE_REFS + inline QMqttTopicName &operator=(QMqttTopicName &&other) Q_DECL_NOTHROW { qSwap(d, other.d); return *this; } +#endif + + inline void swap(QMqttTopicName &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + + QString name() const; + void setName(const QString &name); + + Q_REQUIRED_RESULT bool isValid() const; + Q_REQUIRED_RESULT int levelCount() const; + Q_REQUIRED_RESULT QStringList levels() const; + + friend Q_MQTT_EXPORT bool operator==(const QMqttTopicName &lhs, const QMqttTopicName &rhs) Q_DECL_NOTHROW; + friend inline bool operator!=(const QMqttTopicName &lhs, const QMqttTopicName &rhs) Q_DECL_NOTHROW { return !(lhs == rhs); } + friend Q_MQTT_EXPORT bool operator<(const QMqttTopicName &lhs, const QMqttTopicName &rhs) Q_DECL_NOTHROW; + friend Q_MQTT_EXPORT uint qHash(const QMqttTopicName &name, uint seed) Q_DECL_NOTHROW; + +private: + QExplicitlySharedDataPointer<QMqttTopicNamePrivate> d; +}; + +Q_DECLARE_SHARED(QMqttTopicName) + +#ifndef QT_NO_DATASTREAM +Q_MQTT_EXPORT QDataStream &operator<<(QDataStream &, const QMqttTopicName &); +Q_MQTT_EXPORT QDataStream &operator>>(QDataStream &, QMqttTopicName &); +#endif + +#ifndef QT_NO_DEBUG_STREAM +Q_MQTT_EXPORT QDebug operator<<(QDebug, const QMqttTopicName &); +#endif + +QT_END_NAMESPACE + +Q_DECLARE_METATYPE(QMqttTopicName) + +#endif // QMQTTTOPICNAME_H diff --git a/sync.profile b/sync.profile index 77d0e2e..a9e8018 100644 --- a/sync.profile +++ b/sync.profile @@ -3,5 +3,5 @@ ); %dependencies = ( - "qtbase" => "" + "qtbase" => "", ); diff --git a/tests/README.txt b/tests/README.txt new file mode 100644 index 0000000..3a8bf51 --- /dev/null +++ b/tests/README.txt @@ -0,0 +1,21 @@ +The tests included in the subdirectories check for functionality and +conformance of the Qt MQTT module. + +To be able to run the tests successfully, a broker needs to be available and +reachable. + +The continuous integration utilized the paho conformance test broker. It can +be obtained at this location: +https://github.com/eclipse/paho.mqtt.testing + +For the unit tests being able to locate this script, use the +MQTT_TEST_BROKER_LOCATION environment variable and set it +to “<install-path>/interoperability/startbroker.py”. + +Alternatively, any broker can be instantiated and passed to the auto tests +by using the environment variable MQTT_TEST_BROKER. This must point to a +valid url. The broker must run on the standardized port 1883. + +Note, that the unit tests verify functionality against the MQTT 3.1.1 version +of the standard, hence the broker needs to be compliant to the adherent +specifications. diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index dfa905c..ecd1207 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -4,4 +4,6 @@ win32|if(linux:!cross_compile): SUBDIRS += cmake \ conformance \ qmqttcontrolpacket \ qmqttclient \ - qmqttsubscription + qmqttsubscription \ + qmqtttopicname \ + qmqtttopicfilter diff --git a/tests/auto/conformance/tst_conformance.cpp b/tests/auto/conformance/tst_conformance.cpp index c748fb9..ca3cc61 100644 --- a/tests/auto/conformance/tst_conformance.cpp +++ b/tests/auto/conformance/tst_conformance.cpp @@ -274,15 +274,15 @@ void Tst_MqttConformance::offline_message_queueing_test() QTRY_VERIFY2(publisher.state() == QMqttClient::Connected, "Could not connect to broker."); QSignalSpy pubCounter(&publisher, SIGNAL(messageSent(qint32))); - publisher.publish("Qt/offline/foo/bar", "msg1", 1); - publisher.publish("Qt/offline/foo/bar2", "msg2", 1); - publisher.publish("Qt/offline/foo2/bar", "msg3", 1); + publisher.publish(QLatin1String("Qt/offline/foo/bar"), "msg1", 1); + publisher.publish(QLatin1String("Qt/offline/foo/bar2"), "msg2", 1); + publisher.publish(QLatin1String("Qt/offline/foo2/bar"), "msg3", 1); QTRY_VERIFY2(pubCounter.size() == 3, "Could not publish all messages."); publisher.disconnectFromHost(); QTRY_VERIFY2(publisher.state() == QMqttClient::Disconnected, "Could not disconnect."); - QSignalSpy receiveCounter(&client, SIGNAL(messageReceived(QByteArray,QString))); + QSignalSpy receiveCounter(&client, SIGNAL(messageReceived(QByteArray,QMqttTopicName))); client.connectToHost(); QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); @@ -306,7 +306,7 @@ void Tst_MqttConformance::subscribe_failure_test() client.connectToHost(); QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); - auto sub = client.subscribe(forbiddenTopic, 1); + auto sub = client.subscribe(QMqttTopicFilter(forbiddenTopic), 1); QVERIFY2(sub->state() == QMqttSubscription::SubscriptionPending, "Could not initiate subscription"); QTRY_VERIFY2(sub->state() == QMqttSubscription::Error, "Did not receive error state for sub."); diff --git a/tests/auto/qmqttclient/tst_qmqttclient.cpp b/tests/auto/qmqttclient/tst_qmqttclient.cpp index 58290e4..6c70007 100644 --- a/tests/auto/qmqttclient/tst_qmqttclient.cpp +++ b/tests/auto/qmqttclient/tst_qmqttclient.cpp @@ -29,6 +29,7 @@ #include "broker_connection.h" #include <QtCore/QString> +#include <QtNetwork/QTcpServer> #include <QtTest/QtTest> #include <QtTest/QSignalSpy> #include <QtMqtt/QMqttClient> @@ -50,9 +51,12 @@ private Q_SLOTS: void sendReceive(); void retainMessage(); void willMessage(); - void longTopic_data(); - void longTopic(); + void compliantTopic_data(); + void compliantTopic(); void subscribeLongTopic(); + void dataIncludingZero(); + void publishLongTopic(); + void reconnect_QTBUG65726(); private: QProcess m_brokerProcess; QString m_testBroker; @@ -201,7 +205,7 @@ void Tst_QMqttClient::retainMessage() msgCount++; }); - QSignalSpy messageSpy(&sub, SIGNAL(messageReceived(QByteArray,QString))); + QSignalSpy messageSpy(&sub, SIGNAL(messageReceived(QByteArray,QMqttTopicName))); sub.connectToHost(); QTRY_COMPARE(sub.state(), QMqttClient::Connected); @@ -270,7 +274,7 @@ void Tst_QMqttClient::willMessage() } } -void Tst_QMqttClient::longTopic_data() +void Tst_QMqttClient::compliantTopic_data() { QTest::addColumn<QString>("topic"); QTest::newRow("simple") << QString::fromLatin1("topic"); @@ -279,11 +283,9 @@ void Tst_QMqttClient::longTopic_data() QString l; l.fill(QLatin1Char('T'), std::numeric_limits<std::uint16_t>::max()); QTest::newRow("maxSize") << l; - l.fill(QLatin1Char('M'), 2 * std::numeric_limits<std::uint16_t>::max()); - QTest::newRow("overflow") << l; } -void Tst_QMqttClient::longTopic() +void Tst_QMqttClient::compliantTopic() { QFETCH(QString, topic); QString truncTopic = topic; @@ -307,7 +309,7 @@ void Tst_QMqttClient::longTopic() bool received = false; bool verified = false; - connect(&subscriber, &QMqttClient::messageReceived, [&](const QByteArray &, const QString &t) { + connect(&subscriber, &QMqttClient::messageReceived, [&](const QByteArray &, const QMqttTopicName &t) { received = true; verified = t == truncTopic; }); @@ -337,6 +339,114 @@ void Tst_QMqttClient::subscribeLongTopic() QCOMPARE(sub, nullptr); } +void Tst_QMqttClient::dataIncludingZero() +{ + QByteArray data; + const int dataSize = 200; + data.fill('A', dataSize); + data[100] = '\0'; + + QMqttClient client; + client.setHostname(m_testBroker); + client.setPort(m_port); + + client.connectToHost(); + QTRY_COMPARE(client.state(), QMqttClient::Connected); + + bool received = false; + bool verified = false; + bool correctSize = false; + const QString testTopic(QLatin1String("some/topic")); + auto sub = client.subscribe(testTopic, 1); + QVERIFY(sub); + connect(sub, &QMqttSubscription::messageReceived, [&](QMqttMessage msg) { + verified = msg.payload() == data; + correctSize = msg.payload().size() == dataSize; + received = true; + }); + + QTRY_COMPARE(sub->state(), QMqttSubscription::Subscribed); + + client.publish(testTopic, data, 1); + + QTRY_VERIFY2(received, "Subscriber did not receive message"); + QVERIFY2(verified, "Subscriber received different message"); + QVERIFY2(correctSize, "Subscriber received message of different size"); +} + +void Tst_QMqttClient::publishLongTopic() +{ + QMqttClient publisher; + publisher.setClientId(QLatin1String("publisher")); + publisher.setHostname(m_testBroker); + publisher.setPort(m_port); + + publisher.connectToHost(); + QTRY_COMPARE(publisher.state(), QMqttClient::Connected); + + QString topic; + topic.fill(QLatin1Char('s'), 2 * std::numeric_limits<std::uint16_t>::max()); + auto pub = publisher.publish(topic); + QCOMPARE(pub, -1); +} + +class FakeServer : public QObject +{ + Q_OBJECT +public: + FakeServer() { + server = new QTcpServer(); + connect(server, &QTcpServer::newConnection, this, &FakeServer::createSocket); + server->listen(QHostAddress::Any, 5726); + } +public slots: + void createSocket() { + socket = server->nextPendingConnection(); + connect(socket, &QTcpSocket::readyRead, this, &FakeServer::connectionRequested); + } + + void connectionRequested() { + // We assume it is always a connect statement, so no verification is done + socket->readAll(); + QByteArray response; + response += 0x20; + response += quint8(2); // Payload size + if (!connectionSuccess) { + response += quint8(255); // Causes ProtocolViolation + response += quint8(13); + } else { + response += char(0); // ackFlags + response += char(0); // result + } + qDebug() << "Fake server response:" << connectionSuccess; + socket->write(response); + } +public: + QTcpServer *server; + QTcpSocket *socket; + bool connectionSuccess{false}; +}; + +void Tst_QMqttClient::reconnect_QTBUG65726() +{ + FakeServer server; + + QMqttClient client; + client.setClientId(QLatin1String("bugclient")); + client.setHostname(QLatin1String("localhost")); + client.setPort(5726); + + client.connectToHost(); + QTRY_COMPARE(client.state(), QMqttClient::Disconnected); + QTRY_COMPARE(client.error(), QMqttClient::ProtocolViolation); + + server.connectionSuccess = true; + + client.connectToHost(); + QTRY_COMPARE(client.state(), QMqttClient::Connected); + QTRY_COMPARE(client.error(), QMqttClient::NoError); +} + QTEST_MAIN(Tst_QMqttClient) #include "tst_qmqttclient.moc" diff --git a/tests/auto/qmqttcontrolpacket/tst_qmqttcontrolpacket.cpp b/tests/auto/qmqttcontrolpacket/tst_qmqttcontrolpacket.cpp index 7a0827d..de0e404 100644 --- a/tests/auto/qmqttcontrolpacket/tst_qmqttcontrolpacket.cpp +++ b/tests/auto/qmqttcontrolpacket/tst_qmqttcontrolpacket.cpp @@ -123,6 +123,19 @@ void Tst_QMqttControlPacket::append() payload = packet.payload(); QCOMPARE(payload.size(), data.size()); QCOMPARE(payload, data); + + const QByteArray containsZero("Some data\0 with zero", 21); + packet.clear(); + packet.appendRaw(containsZero); + payload = packet.payload(); + QCOMPARE(containsZero, payload); + QCOMPARE(payload.size(), 21); + + packet.clear(); + packet.append(containsZero); + payload = packet.payload().mid(2); // mid because size got prepended + QCOMPARE(containsZero, payload); + QCOMPARE(payload.size(), 21); #else QSKIP("This test requires a Qt -developer-build."); #endif diff --git a/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp b/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp index 7f51dd5..fda8ef6 100644 --- a/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp +++ b/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp @@ -47,6 +47,7 @@ private Q_SLOTS: void getSetCheck(); void wildCards_data(); void wildCards(); + void reconnect(); private: QProcess m_brokerProcess; QString m_testBroker; @@ -141,6 +142,81 @@ void Tst_QMqttSubscription::wildCards() QTRY_VERIFY2(publisher.state() == QMqttClient::Disconnected, "Could not disconnect."); } +void Tst_QMqttSubscription::reconnect() +{ + // QTBUG-64042 + // - Connect with clean session + QMqttClient client; + + client.setHostname(m_testBroker); + client.setPort(m_port); + client.setCleanSession(true); + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + + // - Subscribe to topic A + const QString subscription("topics/resub"); + auto sub = client.subscribe(subscription, 1); + QTRY_VERIFY2(sub->state() == QMqttSubscription::Subscribed, "Could not subscribe to topic."); + + // - Loose connection / connection drop + QAbstractSocket *transport = qobject_cast<QAbstractSocket *>(client.transport()); + QVERIFY2(transport, "Transport has to be QAbstractSocket-based."); + transport->disconnectFromHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "State not correctly switched."); + + // - Reconnect (keeping cleansession) + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + // old subs should get updated / invalidated + QCOMPARE(sub->state(), QMqttSubscription::Unsubscribed); + + // - Resubscribe + auto reSub = client.subscribe(subscription, 1); + QTRY_VERIFY2(reSub->state() == QMqttSubscription::Subscribed, "Could not re-subscribe to topic."); + QSignalSpy receivalSpy(reSub, SIGNAL(messageReceived(QMqttMessage))); + + QSignalSpy pubSpy(&client, SIGNAL(messageSent(qint32))); + client.publish(subscription, "Sending after reconnect 1", 1); + QTRY_VERIFY2(pubSpy.size() == 1, "Could not publish message."); + + QTRY_VERIFY2(receivalSpy.size() == 1, "Did not receive message on re-subscribe."); + + // - Loose connection / connection drop + transport = qobject_cast<QAbstractSocket *>(client.transport()); + QVERIFY2(transport, "Transport has to be QAbstractSocket-based."); + transport->disconnectFromHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "State not correctly switched."); + + // - Reconnect (no cleansession) + QSignalSpy restoredSpy(&client, SIGNAL(brokerSessionRestored())); + + client.setCleanSession(false); + client.connectToHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); + // Could not identify a broker doing this. The specs states: + // [MQTT-4.1.0-1] The Client and Server MUST store Session state for the entire + // duration of the Session. + // [MQTT-4.1.0-2] A Session MUST last at least as long it has an active Network Connection. + // All testbrokers delete the session at transport disconnect, regardless of DISCONNECT been + // send before or not. + if (restoredSpy.count() > 0) { + QCOMPARE(reSub->state(), QMqttSubscription::Subscribed); + pubSpy.clear(); + receivalSpy.clear(); + client.publish(subscription, "Sending after reconnect 2", 1); + QTRY_VERIFY2(pubSpy.size() == 1, "Could not publish message."); + QTRY_VERIFY2(receivalSpy.size() == 1, "Did not receive message on re-subscribe."); + } else { + // No need to test this + qDebug() << "Test broker does not support long-livety sessions."; + } + // - Old subscription is still active + + client.disconnectFromHost(); + QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "Could not disconnect."); +} + QTEST_MAIN(Tst_QMqttSubscription) #include "tst_qmqttsubscription.moc" diff --git a/tests/auto/qmqtttopicfilter/qmqtttopicfilter.pro b/tests/auto/qmqtttopicfilter/qmqtttopicfilter.pro new file mode 100644 index 0000000..3ce2d77 --- /dev/null +++ b/tests/auto/qmqtttopicfilter/qmqtttopicfilter.pro @@ -0,0 +1,11 @@ +CONFIG += testcase +QT += testlib mqtt +QT -= gui +QT_PRIVATE += mqtt-private + +TARGET = tst_qmqtttopicfilter + +SOURCES += \ + tst_qmqtttopicfilter.cpp + +DEFINES += SRCDIR=\\\"$$PWD/\\\" diff --git a/tests/auto/qmqtttopicfilter/tst_qmqtttopicfilter.cpp b/tests/auto/qmqtttopicfilter/tst_qmqtttopicfilter.cpp new file mode 100644 index 0000000..f83999c --- /dev/null +++ b/tests/auto/qmqtttopicfilter/tst_qmqtttopicfilter.cpp @@ -0,0 +1,144 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include <QtCore/QHash> +#include <QtCore/QMap> +#include <QtCore/QVector> +#include <QtMqtt/QMqttTopicFilter> +#include <QtTest/QtTest> + +class Tst_QMqttTopicFilter : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void checkValidity(); + void matches(); + + void usableWithQVector(); + void usableWithQMap(); + void usableWithQHash(); +}; + +void Tst_QMqttTopicFilter::checkValidity() +{ + QVERIFY(QMqttTopicFilter("a").isValid()); + QVERIFY(QMqttTopicFilter("/").isValid()); + QVERIFY(QMqttTopicFilter("a b").isValid()); + QVERIFY(QMqttTopicFilter("#").isValid()); + QVERIFY(QMqttTopicFilter("/#").isValid()); + QVERIFY(QMqttTopicFilter("a/#").isValid()); + QVERIFY(QMqttTopicFilter("/a/#").isValid()); + QVERIFY(QMqttTopicFilter("+").isValid()); + QVERIFY(QMqttTopicFilter("/+").isValid()); + QVERIFY(QMqttTopicFilter("+/").isValid()); + QVERIFY(QMqttTopicFilter("/+/").isValid()); + QVERIFY(QMqttTopicFilter("/+/+").isValid()); + QVERIFY(QMqttTopicFilter("+/#").isValid()); + QVERIFY(QMqttTopicFilter("a/+/b").isValid()); + + QVERIFY(!QMqttTopicFilter("").isValid()); + QVERIFY(!QMqttTopicFilter("#/").isValid()); + QVERIFY(!QMqttTopicFilter("/a/#/").isValid()); + QVERIFY(!QMqttTopicFilter("#/#").isValid()); + QVERIFY(!QMqttTopicFilter("a#").isValid()); + QVERIFY(!QMqttTopicFilter("/a#").isValid()); + + QVERIFY(!QMqttTopicFilter("a+").isValid()); + QVERIFY(!QMqttTopicFilter("+a").isValid()); + QVERIFY(!QMqttTopicFilter("++").isValid()); + + QVERIFY(!QMqttTopicFilter(QString(3, QChar(QChar::Null))).isValid()); +} + +void Tst_QMqttTopicFilter::matches() +{ + // Non normative comment's examples [4.7.1.2] + QMqttTopicFilter filter("sport/tennis/player1/#"); + QVERIFY(filter.match(QMqttTopicName("sport/tennis/player1"))); + QVERIFY(filter.match(QMqttTopicName("sport/tennis/player1/ranking"))); + QVERIFY(filter.match(QMqttTopicName("sport/tennis/player1/score/wimbledon"))); + + filter = QMqttTopicFilter("sport/#"); + QVERIFY(filter.match(QMqttTopicName("sport"))); + + // Non normative comment's examples [4.7.1.3] + filter = QMqttTopicFilter("sport/tennis/+"); + QVERIFY(filter.match(QMqttTopicName("sport/tennis/player1"))); + QVERIFY(!filter.match(QMqttTopicName("sport/tennis/player1/ranking"))); + + filter = QMqttTopicFilter("sport/+"); + QVERIFY(filter.match(QMqttTopicName("sport/"))); + QVERIFY(!filter.match(QMqttTopicName("sport"))); + + QVERIFY(QMqttTopicFilter("+/+").match(QMqttTopicName("/finance"))); + QVERIFY(QMqttTopicFilter("/+").match(QMqttTopicName("/finance"))); + QVERIFY(!QMqttTopicFilter("+").match(QMqttTopicName("/finance"))); + + // Non normative comment's examples [4.7.2] + QVERIFY(QMqttTopicFilter("#").match(QMqttTopicName("$SYS/foo"))); + QVERIFY(!QMqttTopicFilter("#").match(QMqttTopicName("$SYS/foo"), QMqttTopicFilter::WildcardsDontMatchDollarTopicMatchOption)); + + QVERIFY(QMqttTopicFilter("+/monitor/Clients").match(QMqttTopicName("$SYS/monitor/Clients"))); + QVERIFY(!QMqttTopicFilter("+/monitor/Clients").match(QMqttTopicName("$SYS/monitor/Clients"), QMqttTopicFilter::WildcardsDontMatchDollarTopicMatchOption)); + + QVERIFY(QMqttTopicFilter("$SYS/#").match(QMqttTopicName("$SYS/foo"))); + QVERIFY(QMqttTopicFilter("$SYS/#").match(QMqttTopicName("$SYS/foo"), QMqttTopicFilter::WildcardsDontMatchDollarTopicMatchOption)); + + QVERIFY(QMqttTopicFilter("$SYS/monitor/+").match(QMqttTopicName("$SYS/monitor/Clients"))); + QVERIFY(QMqttTopicFilter("$SYS/monitor/+").match(QMqttTopicName("$SYS/monitor/Clients"), QMqttTopicFilter::WildcardsDontMatchDollarTopicMatchOption)); +} + +void Tst_QMqttTopicFilter::usableWithQVector() +{ + const QMqttTopicFilter topic{"a/b"}; + QVector<QMqttTopicFilter> names; + names.append(topic); + QCOMPARE(topic, names.constFirst()); +} + +void Tst_QMqttTopicFilter::usableWithQMap() +{ + const QMqttTopicFilter topic{"a/b"}; + QMap<QMqttTopicFilter, int> names; + names.insert(topic, 42); + QCOMPARE(names[topic], 42); +} + +void Tst_QMqttTopicFilter::usableWithQHash() +{ + const QMqttTopicFilter topic{"a/b"}; + QHash<QMqttTopicFilter, int> names; + names.insert(topic, 42); + QCOMPARE(names[topic], 42); +} + +QTEST_MAIN(Tst_QMqttTopicFilter) + +#include "tst_qmqtttopicfilter.moc" diff --git a/tests/auto/qmqtttopicname/qmqtttopicname.pro b/tests/auto/qmqtttopicname/qmqtttopicname.pro new file mode 100644 index 0000000..30d7d05 --- /dev/null +++ b/tests/auto/qmqtttopicname/qmqtttopicname.pro @@ -0,0 +1,11 @@ +CONFIG += testcase +QT += testlib mqtt +QT -= gui +QT_PRIVATE += mqtt-private + +TARGET = tst_qmqtttopicname + +SOURCES += \ + tst_qmqtttopicname.cpp + +DEFINES += SRCDIR=\\\"$$PWD/\\\" diff --git a/tests/auto/qmqtttopicname/tst_qmqtttopicname.cpp b/tests/auto/qmqtttopicname/tst_qmqtttopicname.cpp new file mode 100644 index 0000000..6f33e8c --- /dev/null +++ b/tests/auto/qmqtttopicname/tst_qmqtttopicname.cpp @@ -0,0 +1,121 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include <QtCore/QHash> +#include <QtCore/QMap> +#include <QtCore/QVector> +#include <QtMqtt/QMqttTopicName> +#include <QtTest/QtTest> + +class Tst_QMqttTopicName : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void checkValidity(); + void checkLevelCount(); + void checkLevels_data(); + void checkLevels(); + void usableWithQVector(); + void usableWithQMap(); + void usableWithQHash(); +}; + +void Tst_QMqttTopicName::checkValidity() +{ + QVERIFY(QMqttTopicName("a").isValid()); + QVERIFY(QMqttTopicName("/").isValid()); + QVERIFY(QMqttTopicName("a b").isValid()); + + QVERIFY(!QMqttTopicName("").isValid()); + QVERIFY(!QMqttTopicName("/a/#").isValid()); + QVERIFY(!QMqttTopicName("/+/a").isValid()); + QVERIFY(!QMqttTopicName(QString(3, QChar(QChar::Null))).isValid()); +} + +void Tst_QMqttTopicName::checkLevelCount() +{ + QCOMPARE(QMqttTopicName("a").levelCount(), 1); + QCOMPARE(QMqttTopicName("/").levelCount(), 2); + QCOMPARE(QMqttTopicName("/a").levelCount(), 2); + QCOMPARE(QMqttTopicName("a/").levelCount(), 2); + QCOMPARE(QMqttTopicName("a/b").levelCount(), 2); + QCOMPARE(QMqttTopicName("a/b/").levelCount(), 3); +} + +void Tst_QMqttTopicName::checkLevels_data() +{ + QTest::addColumn<QMqttTopicName>("name"); + QTest::addColumn<QStringList>("levels"); + + QTest::newRow("1") << QMqttTopicName("a") << QStringList{"a"}; + QTest::newRow("2") << QMqttTopicName("/") << QStringList{"", ""}; + QTest::newRow("3") << QMqttTopicName("//") << QStringList{"", "", ""}; + QTest::newRow("4") << QMqttTopicName("a/") << QStringList{"a", ""}; + QTest::newRow("5") << QMqttTopicName("/a") << QStringList{"", "a"}; + QTest::newRow("6") << QMqttTopicName("a/b") << QStringList{"a", "b"}; + QTest::newRow("7") << QMqttTopicName("a/b/") << QStringList{"a", "b", ""}; + QTest::newRow("8") << QMqttTopicName("/a/b") << QStringList{"", "a", "b"}; +} + +void Tst_QMqttTopicName::checkLevels() +{ + QFETCH(QMqttTopicName, name); + QFETCH(QStringList, levels); + + QCOMPARE(name.levels(), levels); +} + +void Tst_QMqttTopicName::usableWithQVector() +{ + const QMqttTopicName topic{"a/b"}; + QVector<QMqttTopicName> names; + names.append(topic); + QCOMPARE(topic, names.constFirst()); +} + +void Tst_QMqttTopicName::usableWithQMap() +{ + const QMqttTopicName topic{"a/b"}; + QMap<QMqttTopicName, int> names; + names.insert(topic, 42); + QCOMPARE(names[topic], 42); +} + +void Tst_QMqttTopicName::usableWithQHash() +{ + const QMqttTopicName topic{"a/b"}; + QHash<QMqttTopicName, int> names; + names.insert(topic, 42); + QCOMPARE(names[topic], 42); +} + +QTEST_MAIN(Tst_QMqttTopicName) + +#include "tst_qmqtttopicname.moc" |