Revision control
++ // 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