1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qndeffilter.h" |
5 | #include "qndefmessage.h" |
6 | |
7 | #include <QtCore/QList> |
8 | #include <QtCore/QMap> |
9 | #include <QtCore/QVarLengthArray> |
10 | |
11 | QT_BEGIN_NAMESPACE |
12 | |
13 | /*! |
14 | \class QNdefFilter |
15 | \brief The QNdefFilter class provides a filter for matching NDEF messages. |
16 | |
17 | \ingroup connectivity-nfc |
18 | \inmodule QtNfc |
19 | \since 5.2 |
20 | |
21 | The QNdefFilter encapsulates the structure of an NDEF message and is used for |
22 | matching messages that have a particular structure. |
23 | |
24 | The following filter matches NDEF messages that contain a single smart poster record: |
25 | |
26 | \code |
27 | QNdefFilter filter; |
28 | filter.append(QNdefRecord::NfcRtd, "Sp"); |
29 | \endcode |
30 | |
31 | The following filter matches NDEF messages that contain a URI, a localized piece of text and an |
32 | optional JPEG image. The order of the records must be in the order specified: |
33 | |
34 | \code |
35 | QNdefFilter filter; |
36 | filter.setOrderMatch(true); |
37 | filter.appendRecord(QNdefRecord::NfcRtd, "U"); |
38 | filter.appendRecord<QNdefNfcTextRecord>(); |
39 | filter.appendRecord(QNdefRecord::Mime, "image/jpeg", 0, 1); |
40 | \endcode |
41 | |
42 | The \l match() method can be used to check if a message matches the filter. |
43 | |
44 | \section1 Matching Algorithms |
45 | |
46 | The filter behavior depends on the value of \l orderMatch() parameter. |
47 | |
48 | \note In the discussion below we will consider the filter records to be |
49 | equal if their \c typeNameFormat and \c type parameters match. Joining |
50 | two records means adding their \c minimum and \c maximum values, |
51 | respectively. |
52 | |
53 | \section2 Unordered Matching |
54 | |
55 | If the record order is not taken into account, all the equal records in the |
56 | filter can be joined. The resulting filter will contain only unique records, |
57 | each with the updated \c minimum and \c maximum value. |
58 | |
59 | Consider the following example: |
60 | |
61 | \code |
62 | QNdefFilter filter; |
63 | filter.appendRecord<QNdefNfcTextRecord>(0, 1); |
64 | filter.appendRecord<QNdefNfcTextRecord>(0, 1); |
65 | filter.appendRecord(QNdefRecord::Mime, "", 1, 1); |
66 | filter.appendRecord<QNdefNfcTextRecord>(1, 1); |
67 | filter.setOrderMatch(false); |
68 | \endcode |
69 | |
70 | With the unordered matching, the filter will be simplified to the following: |
71 | |
72 | \code |
73 | QNdefFilter filter; |
74 | filter.appendRecord<QNdefNfcTextRecord>(1, 3); |
75 | filter.appendRecord(QNdefRecord::Mime, "", 1, 1); |
76 | filter.setOrderMatch(false); |
77 | \endcode |
78 | |
79 | Once the filter contains only the unique records, the matching algorithm |
80 | iterates through the message and calculates the actual amount of records of |
81 | each type. If all the actual amounts fit in the corresponding |
82 | [minimum, maximum] ranges, the matching algorithm returns \c true. |
83 | |
84 | \section2 Ordered Matching |
85 | |
86 | If the record order is important, a different approach is applied. In this |
87 | case the equal records can't be simply joined together. However, the |
88 | consecutive equal records can still be joined. Then, the matching |
89 | algorithm iterates through the message, this time also taking the positions |
90 | of the records into account. |
91 | |
92 | \section2 Handling Empty Type in Filter Record |
93 | |
94 | It's possible to add a filter record with an empty \c type. In this case |
95 | the empty type will act as a wildcard for any type. |
96 | |
97 | For example, the filter can be defined as follows: |
98 | |
99 | \code |
100 | QNdefFilter filter; |
101 | filter.addRecord(QNdefRecord::Mime, "", 1, 1); |
102 | \endcode |
103 | |
104 | This filter specifies that the message must contain exactly one NDEF record |
105 | with \l {QNdefRecord::}{Mime} \l {QNdefRecord::}{typeNameFormat}(), and any |
106 | \l {QNdefRecord::}{type}(). |
107 | |
108 | \section2 Handling Extra Records in the Message |
109 | |
110 | If the message contains some records that do not match \e any record in the |
111 | filter, the matching algorithm will return \c false. |
112 | |
113 | \section2 Filter Examples |
114 | |
115 | In the table below, each filter record is specified by the following |
116 | parameters (in the given order): |
117 | |
118 | \list |
119 | \li \c typeNameFormat - contains the \l {QNdefRecord::}{typeNameFormat}() of |
120 | the record. |
121 | \li \c type - contains the \l {QNdefRecord::}{type}() of the record. |
122 | \li \c minimum - contains the minimum amount of occurrences of the record |
123 | in the message. |
124 | \li \c maximum - contains the maximum amount of occurrences of the record |
125 | in the message. |
126 | \endlist |
127 | |
128 | The filter contains multiple records. |
129 | |
130 | The message consists of multiple \l {QNdefRecord}s. In the table below, only |
131 | the \l {QNdefRecord::}{typeNameFormat}() and \l {QNdefRecord::}{type}() of |
132 | each record will be shown, because the other parameters do not matter for |
133 | filtering. |
134 | |
135 | \table |
136 | \header |
137 | \li Filter |
138 | \li Message |
139 | \li Match Result |
140 | \li Comment |
141 | \row |
142 | \li Empty filter |
143 | \li Empty message |
144 | \li Match |
145 | \li |
146 | \row |
147 | \li Empty filter |
148 | \li Non-empty message |
149 | \li No match |
150 | \li |
151 | \row |
152 | \li Non-empty filter |
153 | \li Empty message |
154 | \li No match |
155 | \li |
156 | \row |
157 | \li {1, 2}[(QNdefRecord::NfcRtd, "T", 1, 2), |
158 | (QNdefRecord::Mime, "", 1, 1), (QNdefRecord::Empty, "", 0, 100)] |
159 | \li {1, 2}[(QNdefRecord::Mime, "image/jpeg"), (QNdefRecord::Empty, ""), |
160 | (QNdefRecord::NfcRtd, "T"), (QNdefRecord::Empty, ""), |
161 | (QNdefRecord::NfcRtd, "T")] |
162 | \li Unordered: match |
163 | \li {1, 2} Ordered filter does not match because the message must start |
164 | with a QNdefRecord::NfcRtd record, but it starts with |
165 | QNdefRecord::Mime. |
166 | \row |
167 | \li Ordered: no match |
168 | \row |
169 | \li {1, 2}[(QNdefRecord::NfcRtd, "T", 0, 2), |
170 | (QNdefRecord::Mime, "", 1, 1), (QNdefRecord::NfcRtd, "T", 1, 1)] |
171 | \li {1, 2}[(QNdefRecord::NfcRtd, "T"), (QNdefRecord::NfcRtd, "T"), |
172 | (QNdefRecord::Mime, "image/jpeg")] |
173 | \li Unordered: match |
174 | \li {1, 2} Ordered filter does not match because an QNdefRecord::NfcRtd |
175 | record is expected after QNdefRecord::Mime, but the message does not |
176 | have it. |
177 | \row |
178 | \li Ordered: no match |
179 | \row |
180 | \li {1, 2}[(QNdefRecord::NfcRtd, "T", 0, 2), |
181 | (QNdefRecord::NfcRtd, "T", 1, 1), (QNdefRecord::Mime, "", 1, 1)] |
182 | \li {1, 2}[(QNdefRecord::NfcRtd, "T"), |
183 | (QNdefRecord::Mime, "image/jpeg")] |
184 | \li Unordered: match |
185 | \li {1, 2} Both cases match because the message contains the required |
186 | minimum of records in the correct order. |
187 | \row |
188 | \li Ordered: match |
189 | \endtable |
190 | |
191 | */ |
192 | |
193 | /*! |
194 | \class QNdefFilter::Record |
195 | \brief The QNdefFilter::Record struct contains the information about a |
196 | filter record. |
197 | |
198 | \ingroup connectivity-nfc |
199 | \inmodule QtNfc |
200 | \since 5.2 |
201 | |
202 | The QNdefFilter::Record struct is used to populate the QNdefFilter object. |
203 | Each record contains the following information: |
204 | \list |
205 | \li \l {QNdefRecord::TypeNameFormat} of the corresponding \l QNdefRecord. |
206 | \li Type of the \l QNdefRecord. |
207 | \li Minimum and maximum amount of the records with such parameters in the |
208 | filter. |
209 | \endlist |
210 | */ |
211 | |
212 | /*! |
213 | \fn template<typename T> bool QNdefFilter::appendRecord(unsigned int min, unsigned int max) |
214 | |
215 | Appends a record matching the template parameter to the NDEF filter. |
216 | The record must occur between \a min and \a max times in the NDEF message. |
217 | |
218 | Returns \c true if the record was appended successfully. Otherwise returns |
219 | \c false. |
220 | */ |
221 | |
222 | class QNdefFilterPrivate : public QSharedData |
223 | { |
224 | public: |
225 | QNdefFilterPrivate(); |
226 | |
227 | bool orderMatching; |
228 | QList<QNdefFilter::Record> filterRecords; |
229 | }; |
230 | |
231 | QNdefFilterPrivate::QNdefFilterPrivate() |
232 | : orderMatching(false) |
233 | { |
234 | } |
235 | |
236 | /*! |
237 | Constructs a new NDEF filter. |
238 | */ |
239 | QNdefFilter::QNdefFilter() |
240 | : d(new QNdefFilterPrivate) |
241 | { |
242 | } |
243 | |
244 | /*! |
245 | Constructs a new NDEF filter that is a copy of \a other. |
246 | */ |
247 | QNdefFilter::QNdefFilter(const QNdefFilter &other) |
248 | : d(other.d) |
249 | { |
250 | } |
251 | |
252 | /*! |
253 | Destroys the NDEF filter. |
254 | */ |
255 | QNdefFilter::~QNdefFilter() |
256 | { |
257 | } |
258 | |
259 | /*! |
260 | Assigns \a other to this filter and returns a reference to this filter. |
261 | */ |
262 | QNdefFilter &QNdefFilter::operator=(const QNdefFilter &other) |
263 | { |
264 | if (d != other.d) |
265 | d = other.d; |
266 | |
267 | return *this; |
268 | } |
269 | |
270 | /*! |
271 | \since 6.2 |
272 | |
273 | Returns \c true if the \a message matches the given filter. |
274 | Otherwise returns \c false. |
275 | |
276 | See \l {Matching Algorithms} for more detailed explanation of matching. |
277 | */ |
278 | bool QNdefFilter::match(const QNdefMessage &message) const |
279 | { |
280 | // empty filter matches only empty message |
281 | if (d->filterRecords.isEmpty()) |
282 | return message.isEmpty(); |
283 | |
284 | bool matched = true; |
285 | int totalCount = 0; |
286 | |
287 | if (!d->orderMatching) { |
288 | // Order is not important. The most reliable way is to merge all the |
289 | // similar records, and then simply check the amount of occurrences. |
290 | using MapKey = QPair<QNdefRecord::TypeNameFormat, QByteArray>; |
291 | |
292 | // Creating a map from the list of filter records. |
293 | QMap<MapKey, Record> joinedFilterRecords; |
294 | for (const auto &rec : d->filterRecords) { |
295 | const auto key = qMakePair(value1: rec.typeNameFormat, value2: rec.type); |
296 | if (joinedFilterRecords.contains(key)) { |
297 | joinedFilterRecords[key].minimum += rec.minimum; |
298 | joinedFilterRecords[key].maximum += rec.maximum; |
299 | } else { |
300 | joinedFilterRecords.insert(key, value: rec); |
301 | } |
302 | } |
303 | // Checking the message, calculate occurrences. |
304 | QMap<MapKey, unsigned int> counts; // current number of occurrences |
305 | for (const auto &record : message) { |
306 | const auto key = qMakePair(value1: record.typeNameFormat(), value2: record.type()); |
307 | // Do not forget that we handle an empty type as "any type". |
308 | const auto emptyTypeKey = qMakePair(value1: record.typeNameFormat(), value2: QByteArray()); |
309 | |
310 | if (joinedFilterRecords.contains(key)) |
311 | counts[key] += 1; |
312 | else if (joinedFilterRecords.contains(key: emptyTypeKey)) |
313 | counts[emptyTypeKey] += 1; |
314 | } |
315 | // Check that the occurrences match [min; max] range. |
316 | for (auto it = joinedFilterRecords.cbegin(); it != joinedFilterRecords.cend(); ++it) { |
317 | const auto count = counts.value(key: it.key(), defaultValue: 0); |
318 | totalCount += count; |
319 | if (count < it.value().minimum || count > it.value().maximum) { |
320 | matched = false; |
321 | break; |
322 | } |
323 | } |
324 | } else { |
325 | // Order *is* important. Need to iterate the list. |
326 | |
327 | // Here we need to merge consecutive records with the same parameters. |
328 | QList<Record> mergedRecords; |
329 | // we know that filer is not empty here |
330 | Record currentRecord = d->filterRecords.first(); |
331 | for (qsizetype i = 1; i < d->filterRecords.size(); ++i) { |
332 | const auto &rec = d->filterRecords.at(i); |
333 | if (rec.typeNameFormat == currentRecord.typeNameFormat |
334 | && rec.type == currentRecord.type) { |
335 | currentRecord.minimum += rec.minimum; |
336 | currentRecord.maximum += rec.maximum; |
337 | } else { |
338 | mergedRecords.push_back(t: currentRecord); |
339 | currentRecord = rec; |
340 | } |
341 | } |
342 | mergedRecords.push_back(t: currentRecord); |
343 | |
344 | // The list contains the current number of occurrences of each record. |
345 | QVarLengthArray<unsigned int> counts(mergedRecords.size(), 0); |
346 | |
347 | // Iterate through the messages and calculate the number of occurrences. |
348 | qsizetype filterIndex = 0; |
349 | for (qsizetype messageIndex = 0; matched && messageIndex < message.size(); ++messageIndex) { |
350 | const auto &messageRec = message.at(i: messageIndex); |
351 | // Try to find a filter record that matches the message record. |
352 | // We start from the last processed filter record, not from the very |
353 | // beginning (because the order matters). |
354 | qsizetype idx = filterIndex; |
355 | for (; idx < mergedRecords.size(); ++idx) { |
356 | const auto &filterRec = mergedRecords.at(i: idx); |
357 | if (filterRec.typeNameFormat == messageRec.typeNameFormat() |
358 | && (filterRec.type == messageRec.type() || filterRec.type.isEmpty())) { |
359 | counts[idx] += 1; |
360 | break; |
361 | } else if (counts[idx] < filterRec.minimum || counts[idx] > filterRec.maximum) { |
362 | // The current message record does not match the current |
363 | // filter record, but we didn't get enough records to |
364 | // fulfill the filter => that's an error. |
365 | matched = false; |
366 | break; |
367 | } |
368 | } |
369 | filterIndex = idx; |
370 | } |
371 | |
372 | if (matched) { |
373 | // Check that the occurrences match [min; max] range. |
374 | for (qsizetype i = 0; i < mergedRecords.size(); ++i) { |
375 | const auto &rec = mergedRecords.at(i); |
376 | totalCount += counts[i]; |
377 | if (counts[i] < rec.minimum || counts[i] > rec.maximum) { |
378 | matched = false; |
379 | break; |
380 | } |
381 | } |
382 | } |
383 | } |
384 | |
385 | // Check if the message has records that do not match any record from the |
386 | // filter. To do it we just need to compare the total count of records, |
387 | // detected by filter, with the full message size. If they do not match, |
388 | // we have some records, that are not covered by the filter. |
389 | if (matched && (totalCount != message.size())) { |
390 | matched = false; |
391 | } |
392 | |
393 | return matched; |
394 | } |
395 | |
396 | /*! |
397 | Clears the filter. |
398 | */ |
399 | void QNdefFilter::clear() |
400 | { |
401 | d->orderMatching = false; |
402 | d->filterRecords.clear(); |
403 | } |
404 | |
405 | /*! |
406 | Sets the ordering requirements of the filter. If \a on is \c {true}, the |
407 | filter will only match if the order of records in the filter matches the |
408 | order of the records in the NDEF message. If \a on is \c {false}, the order |
409 | of the records is not taken into account when matching. |
410 | |
411 | By default record order is not taken into account. |
412 | */ |
413 | void QNdefFilter::setOrderMatch(bool on) |
414 | { |
415 | d->orderMatching = on; |
416 | } |
417 | |
418 | /*! |
419 | Returns \c true if the filter takes NDEF record order into account when |
420 | matching. Otherwise returns \c false. |
421 | */ |
422 | bool QNdefFilter::orderMatch() const |
423 | { |
424 | return d->orderMatching; |
425 | } |
426 | |
427 | /*! |
428 | Appends a record with type name format \a typeNameFormat and type \a type |
429 | to the NDEF filter. The record must occur between \a min and \a max times |
430 | in the NDEF message. |
431 | |
432 | Returns \c true if the record was appended successfully. Otherwise returns |
433 | \c false. |
434 | */ |
435 | bool QNdefFilter::appendRecord(QNdefRecord::TypeNameFormat typeNameFormat, const QByteArray &type, |
436 | unsigned int min, unsigned int max) |
437 | { |
438 | QNdefFilter::Record record; |
439 | |
440 | record.typeNameFormat = typeNameFormat; |
441 | record.type = type; |
442 | record.minimum = min; |
443 | record.maximum = max; |
444 | |
445 | return appendRecord(record); |
446 | } |
447 | |
448 | static bool verifyRecord(const QNdefFilter::Record &record) |
449 | { |
450 | return record.minimum <= record.maximum; |
451 | } |
452 | |
453 | /*! |
454 | Verifies the \a record and appends it to the NDEF filter. |
455 | |
456 | Returns \c true if the record was appended successfully. Otherwise returns |
457 | \c false. |
458 | */ |
459 | bool QNdefFilter::appendRecord(const Record &record) |
460 | { |
461 | if (verifyRecord(record)) { |
462 | d->filterRecords.append(t: record); |
463 | return true; |
464 | } |
465 | return false; |
466 | } |
467 | |
468 | /*! |
469 | Returns the NDEF record at index \a i. |
470 | |
471 | \a i must be a valid index (i.e. 0 <= i < \l recordCount()). |
472 | |
473 | \sa recordCount() |
474 | */ |
475 | QNdefFilter::Record QNdefFilter::recordAt(qsizetype i) const |
476 | { |
477 | return d->filterRecords.at(i); |
478 | } |
479 | |
480 | /*! |
481 | Returns the number of NDEF records in the filter. |
482 | */ |
483 | qsizetype QNdefFilter::recordCount() const |
484 | { |
485 | return d->filterRecords.size(); |
486 | } |
487 | |
488 | QT_END_NAMESPACE |
489 | |