Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qmimemagicrule.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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// Qt-Security score:critical reason:data-parser
4
5#define QT_NO_CAST_FROM_ASCII
6
8
10#include <QtCore/QList>
11#include <QtCore/QDebug>
12#include <qendian.h>
13
14#include <private/qoffsetstringarray_p.h>
15#include <private/qtools_p.h>
16
18
19using namespace Qt::StringLiterals;
20using namespace QtMiscUtils;
21
22// in the same order as Type!
23static constexpr auto magicRuleTypes = qOffsetStringArray(
24 "invalid",
25 "string",
26 "host16",
27 "host32",
28 "big16",
29 "big32",
30 "little16",
31 "little32",
32 "byte",
33 "regex"
34);
35
36QMimeMagicRule::Type QMimeMagicRule::type(const QByteArray &theTypeName)
37{
38 for (int i = String; i <= Regex; ++i) {
39 if (theTypeName == magicRuleTypes.viewAt(i))
40 return Type(i);
41 }
42 return Invalid;
43}
44
46{
47 return magicRuleTypes.at(theType);
48}
49
50bool QMimeMagicRule::operator==(const QMimeMagicRule &other) const
51{
52 return m_type == other.m_type &&
53 m_value == other.m_value &&
54 m_startPos == other.m_startPos &&
55 m_endPos == other.m_endPos &&
56 m_mask == other.m_mask &&
57 m_pattern == other.m_pattern &&
58 m_number == other.m_number &&
59 m_numberMask == other.m_numberMask &&
60 m_matchFunction == other.m_matchFunction;
61}
62
63// Used by both providers
64bool QMimeMagicRule::matchSubstring(const char *dataPtr, qsizetype dataSize, quint64 rangeStart,
65 quint64 rangeLength, quint64 valueLength, const char *valueData,
66 const char *mask)
67{
68 // The range and the value length can come straight out of the binary mime.cache, so
69 // nothing can be assumed about them: rangeStart can point past the data, and the value
70 // read at the last position of the range has to stay inside it, too.
71 const quint64 size = quint64(dataSize);
72 if (rangeStart >= size || valueLength == 0 || rangeLength == 0
73 || valueLength > size - rangeStart)
74 return false;
75 // Clamp the range to the data that is actually there, so that rangeStart + rangeLength
76 // neither overflows nor points past the end.
77 rangeLength = qMin(rangeLength, size - rangeStart);
78
79 // Size of searched data.
80 // Example: value="ABC", rangeLength=3 -> we need 3+3-1=5 bytes (ABCxx,xABCx,xxABC would match)
81 const quint64 dataNeeded = qMin(rangeLength + valueLength - 1, size - rangeStart);
82
83 if (!mask) {
84 // callgrind says QByteArray::indexOf is much slower, since our strings are typically too
85 // short for be worth Boyer-Moore matching (1 to 71 bytes, 11 bytes on average).
86 bool found = false;
87 for (quint64 i = rangeStart; i < rangeStart + rangeLength; ++i) {
88 if (i + valueLength > size)
89 break;
90
91 if (memcmp(valueData, dataPtr + i, size_t(valueLength)) == 0) {
92 found = true;
93 break;
94 }
95 }
96 if (!found)
97 return false;
98 } else {
99 bool found = false;
100 const char *readDataBase = dataPtr + rangeStart;
101 // Example (continued from above):
102 // deviceSize is 4, so dataNeeded was max'ed to 4.
103 // maxStartPos = 4 - 3 + 1 = 2, and indeed
104 // we need to check for a match a positions 0 and 1 (ABCx and xABC).
105 const quint64 maxStartPos = dataNeeded - valueLength + 1;
106 for (quint64 i = 0; i < maxStartPos; ++i) {
107 const char *d = readDataBase + i;
108 bool valid = true;
109 for (quint64 idx = 0; idx < valueLength; ++idx) {
110 if (((*d++) & mask[idx]) != (valueData[idx] & mask[idx])) {
111 valid = false;
112 break;
113 }
114 }
115 if (valid) {
116 found = true;
117 break;
118 }
119 }
120 if (!found)
121 return false;
122 }
123 //qDebug() << "Found" << value << "in" << searchedData;
124 return true;
125}
126
127bool QMimeMagicRule::matchString(const QMimeMagicData &magicData) const
128{
129 const QByteArray &data = magicData.bytes();
130 const int rangeLength = m_endPos - m_startPos + 1;
131 return QMimeMagicRule::matchSubstring(data.constData(), data.size(), m_startPos, rangeLength, m_pattern.size(), m_pattern.constData(), m_mask.constData());
132}
133
134template <typename T>
135bool QMimeMagicRule::matchNumber(const QMimeMagicData &magicData) const
136{
137 const QByteArray &data = magicData.bytes();
138 const T value(m_number);
139 const T mask(m_numberMask);
140
141 //qDebug() << "matchNumber" << "0x" << QString::number(m_number, 16) << "size" << sizeof(T);
142 //qDebug() << "mask" << QString::number(m_numberMask, 16);
143
144 const char *p = data.constData() + m_startPos;
145 const char *e = data.constData() + qMin(data.size() - int(sizeof(T)), m_endPos);
146 for ( ; p <= e; ++p) {
147 if ((qFromUnaligned<T>(p) & mask) == (value & mask))
148 return true;
149 }
150
151 return false;
152}
153
154// The regexp sees the data from the start of the range to this far past its end,
155// decoded as Latin-1, and the match must start within the range.
156// Unlike tika, which tries each offset in turn, this is a single search, so in a
157// ranged rule ^ only matches at the range start, \b or a lookbehind can see
158// the data before the match, and a match starting early in the range can be
159// longer than RegExpLookAhead. No rule in Qt's database depends on that.
160static constexpr qsizetype RegExpLookAhead = 8 * 1024;
161
162bool QMimeMagicRule::matchRegExp(const QMimeMagicData &magicData) const
163{
164#if QT_CONFIG(regularexpression)
165 const qsizetype size = magicData.bytes().size();
166 if (size <= m_startPos)
167 return false;
168 const qsizetype lastStart = m_endPos - m_startPos;
169 const qsizetype available = size - m_startPos;
170 // Same as qMin(available, lastStart + RegExpLookAhead), without overflow
171 const qsizetype length = lastStart + qMin(available - lastStart, RegExpLookAhead);
172 const QStringView subject = magicData.decodedLatin1(m_startPos + length).sliced(m_startPos);
173 // Decoded from Latin-1, so always valid UTF-16
174 constexpr auto options = QRegularExpression::DontCheckSubjectStringMatchOption;
175 if (lastStart == 0) {
176 return m_regExp->matchView(subject, 0, QRegularExpression::NormalMatch,
177 options | QRegularExpression::AnchorAtOffsetMatchOption)
178 .hasMatch();
179 }
180 const QRegularExpressionMatch match =
181 m_regExp->matchView(subject, 0, QRegularExpression::NormalMatch, options);
182 return match.hasMatch() && match.capturedStart() <= lastStart;
183#else
184 Q_UNUSED(magicData);
185 return false;
186#endif
187}
188
189static inline QByteArray makePattern(const QByteArray &value)
190{
191 QByteArray pattern(value.size(), Qt::Uninitialized);
192 char *data = pattern.data();
193
194 const char *p = value.constData();
195 const char *e = p + value.size();
196 for ( ; p < e; ++p) {
197 if (*p == '\\' && ++p < e) {
198 if (*p == 'x') { // hex (\\xff), up to 2 digits; a literal 'x' if none
199 char c = 'x';
200 for (int i = 0; i < 2 && p + 1 < e; ++i) {
201 const int h = fromHex(p[1]);
202 if (h == -1)
203 break;
204 ++p;
205 c = i == 0 ? char(h) : char((c << 4) + h);
206 }
207 *data++ = c;
208 } else if (isOctalDigit(*p)) { // oct (\\7, or \\77, or \\377)
209 char c = *p - '0';
210 if (p + 1 < e && isOctalDigit(p[1])) {
211 c = (c << 3) + *(++p) - '0';
212 if (p + 1 < e && isOctalDigit(p[1]) && p[-1] <= '3')
213 c = (c << 3) + *(++p) - '0';
214 }
215 *data++ = c;
216 } else if (*p == 'n') {
217 *data++ = '\n';
218 } else if (*p == 'r') {
219 *data++ = '\r';
220 } else if (*p == 't') {
221 *data++ = '\t';
222 } else { // escaped
223 *data++ = *p;
224 }
225 } else {
226 *data++ = *p;
227 }
228 }
229 pattern.truncate(data - pattern.data());
230
231 return pattern;
232}
233
234// Evaluate a magic match rule like
235// <match value="must be converted with BinHex" type="string" offset="11"/>
236// <match value="0x9501" type="big16" offset="0:64"/>
237
238QMimeMagicRule::QMimeMagicRule(const QString &type,
239 const QByteArray &value,
240 const QString &offsets,
241 const QByteArray &mask,
242 RegexRules regexRules,
243 QString *errorString)
244 : m_type(QMimeMagicRule::type(type.toLatin1())),
245 m_value(value),
246 m_mask(mask),
247 m_matchFunction(nullptr)
248{
249 if (Q_UNLIKELY(m_type == Regex && regexRules == RegexRules::Rejected)) {
250 m_type = Invalid;
251 if (errorString)
252 *errorString = "Type regex is only supported in Qt's internal database"_L1;
253 return;
254 }
255 if (Q_UNLIKELY(m_type == Invalid)) {
256 if (errorString)
257 *errorString = "Type "_L1 + type + " is not supported"_L1;
258 return;
259 }
260
261 // Parse for offset as "1" or "1:10"
262 const qsizetype colonIndex = offsets.indexOf(u':');
263 const QStringView startPosStr = QStringView{offsets}.mid(0, colonIndex); // \ These decay to returning 'offsets'
264 const QStringView endPosStr = QStringView{offsets}.mid(colonIndex + 1);// / unchanged when colonIndex == -1
265 if (Q_UNLIKELY(!QMimeTypeParserBase::parseNumber(startPosStr, &m_startPos, errorString)) ||
266 Q_UNLIKELY(!QMimeTypeParserBase::parseNumber(endPosStr, &m_endPos, errorString))) {
267 m_type = Invalid;
268 return;
269 }
270 if (m_startPos < 0 || m_endPos < 0 || m_endPos < m_startPos) {
271 if (errorString)
272 *errorString = "Invalid offset range \""_L1 + offsets + u'"';
273 m_type = Invalid;
274 return;
275 }
276
277 if (Q_UNLIKELY(m_value.isEmpty())) {
278 m_type = Invalid;
279 if (errorString)
280 *errorString = QStringLiteral("Invalid empty magic rule value");
281 return;
282 }
283
284 if (m_type >= Host16 && m_type <= Byte) {
285 bool ok;
286 m_number = m_value.toUInt(&ok, 0); // autodetect base
287 if (Q_UNLIKELY(!ok)) {
288 m_type = Invalid;
289 if (errorString)
290 *errorString = "Invalid magic rule value \""_L1 + QLatin1StringView(m_value) + u'"';
291 return;
292 }
293 m_numberMask = !m_mask.isEmpty() ? m_mask.toUInt(&ok, 0) : 0; // autodetect base
294 }
295
296 switch (m_type) {
297 case String:
298 m_pattern = makePattern(m_value);
299 m_pattern.squeeze();
300 if (!m_mask.isEmpty()) {
301 if (Q_UNLIKELY(m_mask.size() < 4 || !m_mask.startsWith("0x"))) {
302 m_type = Invalid;
303 if (errorString)
304 *errorString = "Invalid magic rule mask \""_L1 + QLatin1StringView(m_mask) + u'"';
305 return;
306 }
307 const QByteArray &tempMask = QByteArray::fromHex(QByteArray::fromRawData(
308 m_mask.constData() + 2, m_mask.size() - 2));
309 if (Q_UNLIKELY(tempMask.size() != m_pattern.size())) {
310 m_type = Invalid;
311 if (errorString)
312 *errorString = "Invalid magic rule mask size \""_L1 + QLatin1StringView(m_mask) + u'"';
313 return;
314 }
315 m_mask = tempMask;
316 } else {
317 m_mask.fill(char(-1), m_pattern.size());
318 }
319 m_mask.squeeze();
320 m_matchFunction = &QMimeMagicRule::matchString;
321 break;
322 case Byte:
323 if (m_number <= quint8(-1)) {
324 if (m_numberMask == 0)
325 m_numberMask = quint8(-1);
326 m_matchFunction = &QMimeMagicRule::matchNumber<quint8>;
327 }
328 break;
329 case Big16:
330 case Little16:
331 if (m_number <= quint16(-1)) {
332 m_number = m_type == Little16 ? qFromLittleEndian<quint16>(m_number) : qFromBigEndian<quint16>(m_number);
333 if (m_numberMask != 0)
334 m_numberMask = m_type == Little16 ? qFromLittleEndian<quint16>(m_numberMask) : qFromBigEndian<quint16>(m_numberMask);
335 }
336 Q_FALLTHROUGH();
337 case Host16:
338 if (m_number <= quint16(-1)) {
339 if (m_numberMask == 0)
340 m_numberMask = quint16(-1);
341 m_matchFunction = &QMimeMagicRule::matchNumber<quint16>;
342 }
343 break;
344 case Big32:
345 case Little32:
346 m_number = m_type == Little32 ? qFromLittleEndian<quint32>(m_number) : qFromBigEndian<quint32>(m_number);
347 if (m_numberMask != 0)
348 m_numberMask = m_type == Little32 ? qFromLittleEndian<quint32>(m_numberMask) : qFromBigEndian<quint32>(m_numberMask);
349 Q_FALLTHROUGH();
350 case Host32:
351 if (m_numberMask == 0)
352 m_numberMask = quint32(-1);
353 m_matchFunction = &QMimeMagicRule::matchNumber<quint32>;
354 break;
355 case Regex: {
356 if (Q_UNLIKELY(!m_mask.isEmpty())) {
357 m_type = Invalid;
358 if (errorString)
359 *errorString = "Invalid magic rule mask \""_L1 + QLatin1StringView(m_mask)
360 + "\": regex rules have no mask"_L1;
361 return;
362 }
363#if QT_CONFIG(regularexpression)
364 // Unescaped like a string, then interpreted as UTF-8, like tika does
365 m_regExp.emplace(QString::fromUtf8(makePattern(m_value)));
366 if (Q_UNLIKELY(!m_regExp->isValid())) {
367 m_type = Invalid;
368 if (errorString)
369 *errorString = "Invalid regular expression \""_L1 + QString::fromUtf8(m_value)
370 + "\": "_L1 + m_regExp->errorString();
371 return;
372 }
373#endif
374 // Without regularexpression support the rule never matches
375 m_matchFunction = &QMimeMagicRule::matchRegExp;
376 break;
377 }
378 default:
379 break;
380 }
381}
382
384{
385 QByteArray result = m_mask;
386 if (m_type == String) {
387 // restore '0x'
388 result = "0x" + result.toHex();
389 }
390 return result;
391}
392
393bool QMimeMagicRule::matches(const QMimeMagicData &data, int depth) const
394{
395 const bool ok = m_matchFunction && (this->*m_matchFunction)(data);
396 if (!ok)
397 return false;
398
399 // No submatch? Then we are done.
400 if (m_subMatches.isEmpty())
401 return true;
402
403 // Rules can be nested arbitrarily deeply in the XML files, so refuse to
404 // recurse any further and treat the rule as not matching.
405 if (depth <= 0)
406 return false;
407
408 //qDebug() << "Checking" << m_subMatches.count() << "sub-rules";
409 // Check that one of the submatches matches too
410 for (QList<QMimeMagicRule>::const_iterator it = m_subMatches.begin(), end = m_subMatches.end();
411 it != end ; ++it) {
412 if ((*it).matches(data, depth - 1)) {
413 // One of the hierarchies matched -> mimetype recognized.
414 return true;
415 }
416 }
417 return false;
418
419
420}
421
422QT_END_NAMESPACE
QByteArray mask() const
bool matches(const QMimeMagicData &data, int depth=MaxRecursionDepth) const
Type type() const
bool operator==(const QMimeMagicRule &other) const
Combined button and popup list for selecting options.
static QByteArray makePattern(const QByteArray &value)
static constexpr qsizetype RegExpLookAhead
static constexpr auto magicRuleTypes