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

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