mirror of
https://github.com/qt/qtmqtt.git
synced 2026-10-25 16:16:26 +00:00
The QMqttTopicFilter::isValid() method parses the user-provided string and validates it according to the MQTT protocol. Task-number: QTBUG-135764 Pick-to: 6.10 6.9 6.8 Change-Id: I841bf83bbf29a2cdf4cf3faec93615a33f597241 Reviewed-by: Matthias Rauter <matthias.rauter@qt.io>
332 lines
9.2 KiB
C++
332 lines
9.2 KiB
C++
// Copyright (C) 2017 Lorenz Haas
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
|
// Qt-Security score:critical reason:data-parser
|
|
|
|
#include "qmqtttopicfilter.h"
|
|
|
|
#include <QtCore/QDebug>
|
|
#include <QtCore/QList>
|
|
|
|
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;
|
|
}
|
|
|
|
/*!
|
|
\since 5.12
|
|
|
|
Returns the name of a share if the topic filter has been specified as
|
|
a shared subscription. The format of shared subscriptions is defined
|
|
as \c $share/sharename/topicfilter.
|
|
*/
|
|
QString QMqttTopicFilter::sharedSubscriptionName() const
|
|
{
|
|
QString result;
|
|
if (d->filter.startsWith(QLatin1String("$share/"))) {
|
|
// Has to have at least two /
|
|
// $share/<sharename>/topicfilter
|
|
result = d->filter.section(QLatin1Char('/'), 1, 1);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/*!
|
|
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);
|
|
}
|
|
|
|
// Shared Subscription syntax
|
|
// $share/shareName/TopicFilter -- must have at least 2 '/'
|
|
if (d->filter.startsWith(QLatin1String("$share/"))) {
|
|
const int index = d->filter.indexOf(QLatin1Char('/'), 7);
|
|
if (index == -1 || index == 7)
|
|
return false;
|
|
}
|
|
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('#'))) {
|
|
QStringView root = QStringView{d->filter}.left(d->filter.size() - 1);
|
|
|
|
if (root.isEmpty()) // Filter: #
|
|
return true;
|
|
|
|
if (root.endsWith(QLatin1Char('/'))) // '#' also represents the parent level!
|
|
root = root.left(root.size() - 1);
|
|
|
|
const auto filterLevels = root.split(QLatin1Char('/'));
|
|
const auto topicLevels = QStringView{topic}.split(QLatin1Char('/'));
|
|
|
|
if (topicLevels.size() < filterLevels.size())
|
|
return false;
|
|
|
|
for (int i = 0; i < filterLevels.size(); ++i) {
|
|
if (filterLevels.at(i) != topicLevels.at(i) && filterLevels.at(i) != QLatin1Char('+'))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (d->filter.contains(QLatin1Char('+'))) {
|
|
const auto filterLevels = QStringView{d->filter}.split(QLatin1Char('/'));
|
|
const auto topicLevels = QStringView{topic}.split(QLatin1Char('/'));
|
|
if (filterLevels.size() != topicLevels.size())
|
|
return false;
|
|
for (int i = 0; i < filterLevels.size(); ++i) {
|
|
const auto &level = filterLevels.at(i);
|
|
if (level != QLatin1Char('+') && level != topicLevels.at(i))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*!
|
|
//! friend
|
|
\fn bool QMqttTopicFilter::operator==(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs)
|
|
|
|
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);
|
|
}
|
|
|
|
/*!
|
|
//! friend
|
|
\fn bool QMqttTopicFilter::operator!=(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs)
|
|
|
|
Returns \c true if the topic filters \a lhs and \a rhs are different,
|
|
otherwise returns \c false.
|
|
*/
|
|
|
|
/*!
|
|
//! friend
|
|
\fn bool QMqttTopicFilter::operator<(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs)
|
|
|
|
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.
|
|
*/
|
|
size_t qHash(const QMqttTopicFilter &filter, size_t 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
|