1// Copyright (C) 2017 Lorenz Haas
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
5#include "qmqtttopicfilter.h"
6
7#include <QtCore/QDebug>
8#include <QtCore/QList>
9
10QT_BEGIN_NAMESPACE
11
12/*!
13 \class QMqttTopicFilter
14 \inmodule QtMqtt
15 \reentrant
16 \ingroup shared
17
18 \brief The QMqttTopicFilter class represents a MQTT topic filter.
19
20 QMqttTopicFilter is a thin wrapper around a QString providing an expressive
21 data type for MQTT topic filters. Beside the benefits of having a strong
22 type preventing unintended misuse, QMqttTopicFilter provides convenient
23 functions related to topic filters like isValid() or match().
24
25 For example, the following code would fail to compile and prevent a possible
26 unintended and meaningless matching of two filters, especially if the
27 variable names were less expressive:
28
29 \code
30 QMqttTopicFilter globalFilter{"foo/#"};
31 QMqttTopicFilter specificFilter{"foo/bar"};
32 if (globalFilter.match(specificFilter)) {
33 //...
34 }
35 \endcode
36
37 The usability, however, is not affected since the following snippet compiles
38 and runs as expected:
39
40 \code
41 QMqttTopicFilter globalFilter{"foo/#"};
42 if (globalFilter.match("foo/bar")) {
43 //...
44 }
45 \endcode
46
47 \sa QMqttTopicName
48 */
49
50/*!
51 \fn void QMqttTopicFilter::swap(QMqttTopicFilter &other)
52 Swaps the MQTT topic filter \a other with this MQTT topic filter. This
53 operation is very fast and never fails.
54 */
55
56/*!
57 \enum QMqttTopicFilter::MatchOption
58
59 This enum value holds the matching options for the topic filter.
60
61 \value NoMatchOption
62 No match options are set.
63 \value WildcardsDontMatchDollarTopicMatchOption
64 A wildcard at the filter's beginning does not match a topic name that
65 starts with the dollar sign ($).
66 */
67
68class QMqttTopicFilterPrivate : public QSharedData
69{
70public:
71 QString filter;
72};
73
74/*!
75 Creates a new MQTT topic filter with the specified \a filter.
76 */
77QMqttTopicFilter::QMqttTopicFilter(const QString &filter) : d(new QMqttTopicFilterPrivate)
78{
79 d->filter = filter;
80}
81
82/*!
83 Creates a new MQTT topic filter with the specified \a filter.
84 */
85QMqttTopicFilter::QMqttTopicFilter(const QLatin1String &filter) : d(new QMqttTopicFilterPrivate)
86{
87 d->filter = filter;
88}
89
90/*!
91 Creates a new MQTT topic filter as a copy of \a filter.
92 */
93QMqttTopicFilter::QMqttTopicFilter(const QMqttTopicFilter &filter) : d(filter.d)
94{
95}
96
97/*!
98 Destroys the QMqttTopicFilter object.
99 */
100QMqttTopicFilter::~QMqttTopicFilter()
101{
102}
103
104/*!
105 Assigns the MQTT topic filter \a filter to this object, and returns a
106 reference to the copy.
107 */
108QMqttTopicFilter &QMqttTopicFilter::operator=(const QMqttTopicFilter &filter)
109{
110 d = filter.d;
111 return *this;
112}
113
114/*!
115 Returns the topic filter.
116 */
117QString QMqttTopicFilter::filter() const
118{
119 return d->filter;
120}
121
122/*!
123 Sets the topic filter to \a filter.
124 */
125void QMqttTopicFilter::setFilter(const QString &filter)
126{
127 d.detach();
128 d->filter = filter;
129}
130
131/*!
132 \since 5.12
133
134 Returns the name of a share if the topic filter has been specified as
135 a shared subscription. The format of shared subscriptions is defined
136 as \c $share/sharename/topicfilter.
137*/
138QString QMqttTopicFilter::sharedSubscriptionName() const
139{
140 QString result;
141 if (d->filter.startsWith(s: QLatin1String("$share/"))) {
142 // Has to have at least two /
143 // $share/<sharename>/topicfilter
144 result = d->filter.section(asep: QLatin1Char('/'), astart: 1, aend: 1);
145 }
146 return result;
147}
148
149/*!
150 Returns \c true if the topic filter is valid according to the MQTT standard
151 section 4.7, or \c false otherwise.
152 */
153bool QMqttTopicFilter::isValid() const
154{
155 // MQTT-4.7.3-1, MQTT-4.7.3-3, and MQTT-4.7.3-2
156 const int size = d->filter.size();
157 if (size == 0 || size > 65535 || d->filter.contains(c: QChar(QChar::Null)))
158 return false;
159
160 if (size == 1)
161 return true;
162
163 // '#' MUST be last and its own level. It MUST NOT appear more than at most once.
164 const int multiLevelPosition = d->filter.indexOf(ch: QLatin1Char('#'));
165 if (multiLevelPosition != -1
166 && (multiLevelPosition != size - 1 || d->filter.at(i: size-2) != QLatin1Char('/'))) {
167 return false;
168 }
169
170 // '+' MAY occur multiple times but MUST be its own level.
171 int singleLevelPosition = d->filter.indexOf(ch: QLatin1Char('+'));
172 while (singleLevelPosition != -1) {
173 if ((singleLevelPosition != 0 && d->filter.at(i: singleLevelPosition - 1) != QLatin1Char('/'))
174 || (singleLevelPosition < size - 1 && d->filter.at(i: singleLevelPosition + 1) != QLatin1Char('/'))) {
175 return false;
176 }
177 singleLevelPosition = d->filter.indexOf(ch: QLatin1Char('+'), from: singleLevelPosition + 1);
178 }
179
180 // Shared Subscription syntax
181 // $share/shareName/TopicFilter -- must have at least 2 '/'
182 if (d->filter.startsWith(s: QLatin1String("$share/"))) {
183 const int index = d->filter.indexOf(ch: QLatin1Char('/'), from: 7);
184 if (index == -1 || index == 7)
185 return false;
186 }
187 return true;
188}
189
190/*!
191 Returns \c true if the topic filter matches the topic name \a name
192 honoring the given \a matchOptions, or \c false otherwise.
193 */
194bool QMqttTopicFilter::match(const QMqttTopicName &name, MatchOptions matchOptions) const
195{
196 if (!name.isValid() || !isValid())
197 return false;
198
199 const QString topic = name.name();
200 if (topic == d->filter)
201 return true;
202
203 if (matchOptions.testFlag(flag: WildcardsDontMatchDollarTopicMatchOption)
204 && topic.startsWith(c: QLatin1Char('$'))
205 && (d->filter.startsWith(c: QLatin1Char('+'))
206 || d->filter == QLatin1Char('#')
207 || d->filter == QLatin1String("/#"))) {
208 return false;
209 }
210
211 if (d->filter.endsWith(c: QLatin1Char('#'))) {
212 QStringView root = QStringView{d->filter}.left(n: d->filter.size() - 1);
213
214 if (root.isEmpty()) // Filter: #
215 return true;
216
217 if (root.endsWith(c: QLatin1Char('/'))) // '#' also represents the parent level!
218 root = root.left(n: root.size() - 1);
219
220 const auto filterLevels = root.split(sep: QLatin1Char('/'));
221 const auto topicLevels = QStringView{topic}.split(sep: QLatin1Char('/'));
222
223 if (topicLevels.size() < filterLevels.size())
224 return false;
225
226 for (int i = 0; i < filterLevels.size(); ++i) {
227 if (filterLevels.at(i) != topicLevels.at(i) && filterLevels.at(i) != QLatin1Char('+'))
228 return false;
229 }
230 return true;
231 }
232
233 if (d->filter.contains(c: QLatin1Char('+'))) {
234 const auto filterLevels = QStringView{d->filter}.split(sep: QLatin1Char('/'));
235 const auto topicLevels = QStringView{topic}.split(sep: QLatin1Char('/'));
236 if (filterLevels.size() != topicLevels.size())
237 return false;
238 for (int i = 0; i < filterLevels.size(); ++i) {
239 const auto &level = filterLevels.at(i);
240 if (level != QLatin1Char('+') && level != topicLevels.at(i))
241 return false;
242 }
243 return true;
244 }
245
246 return false;
247}
248
249/*!
250 //! friend
251 \fn bool QMqttTopicFilter::operator==(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs)
252
253 Returns \c true if the topic filters \a lhs and \a rhs are equal,
254 otherwise returns \c false.
255 */
256bool operator==(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW
257{
258 return (lhs.d == rhs.d) || (lhs.d->filter == rhs.d->filter);
259}
260
261/*!
262 //! friend
263 \fn bool QMqttTopicFilter::operator!=(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs)
264
265 Returns \c true if the topic filters \a lhs and \a rhs are different,
266 otherwise returns \c false.
267 */
268
269/*!
270 //! friend
271 \fn bool QMqttTopicFilter::operator<(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs)
272
273 Returns \c true if the topic filter \a lhs is lexically less than the topic
274 filter \a rhs; otherwise returns \c false.
275 */
276bool operator<(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW
277{
278 return lhs.d->filter < rhs.d->filter;
279}
280
281/*!
282 \relates QHash
283
284 Returns the hash value for \a filter. If specified, \a seed is used to
285 initialize the hash.
286*/
287size_t qHash(const QMqttTopicFilter &filter, size_t seed) Q_DECL_NOTHROW
288{
289 return qHash(key: filter.d->filter, seed);
290}
291
292#ifndef QT_NO_DATASTREAM
293/*! \relates QMqttTopicFilter
294
295 Writes the topic filter \a filter to the stream \a out and returns a
296 reference to the stream.
297
298 \sa{Serializing Qt Data Types}{Format of the QDataStream operators}
299*/
300QDataStream &operator<<(QDataStream &out, const QMqttTopicFilter &filter)
301{
302 out << filter.filter();
303 return out;
304}
305
306/*! \relates QMqttTopicFilter
307
308 Reads a topic filter into \a filter from the stream \a in and returns a
309 reference to the stream.
310
311 \sa{Serializing Qt Data Types}{Format of the QDataStream operators}
312*/
313QDataStream &operator>>(QDataStream &in, QMqttTopicFilter &filter)
314{
315 QString f;
316 in >> f;
317 filter.setFilter(f);
318 return in;
319}
320#endif // QT_NO_DATASTREAM
321
322#ifndef QT_NO_DEBUG_STREAM
323QDebug operator<<(QDebug d, const QMqttTopicFilter &filter)
324{
325 QDebugStateSaver saver(d);
326 d.nospace() << "QMqttTopicFilter(" << filter.filter() << ')';
327 return d;
328}
329#endif
330
331QT_END_NAMESPACE
332

source code of qtmqtt/src/mqtt/qmqtttopicfilter.cpp