1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2018 The Qt Company Ltd. |
4 | ** Copyright (C) 2018 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure <david.faure@kdab.com> |
5 | ** Copyright (C) 2019 Intel Corporation. |
6 | ** Contact: https://www.qt.io/licensing/ |
7 | ** |
8 | ** This file is part of the QtCore module of the Qt Toolkit. |
9 | ** |
10 | ** $QT_BEGIN_LICENSE:LGPL$ |
11 | ** Commercial License Usage |
12 | ** Licensees holding valid commercial Qt licenses may use this file in |
13 | ** accordance with the commercial license agreement provided with the |
14 | ** Software or, alternatively, in accordance with the terms contained in |
15 | ** a written agreement between you and The Qt Company. For licensing terms |
16 | ** and conditions see https://www.qt.io/terms-conditions. For further |
17 | ** information use the contact form at https://www.qt.io/contact-us. |
18 | ** |
19 | ** GNU Lesser General Public License Usage |
20 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
21 | ** General Public License version 3 as published by the Free Software |
22 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
23 | ** packaging of this file. Please review the following information to |
24 | ** ensure the GNU Lesser General Public License version 3 requirements |
25 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
26 | ** |
27 | ** GNU General Public License Usage |
28 | ** Alternatively, this file may be used under the terms of the GNU |
29 | ** General Public License version 2.0 or (at your option) the GNU General |
30 | ** Public license version 3 or any later version approved by the KDE Free |
31 | ** Qt Foundation. The licenses are as published by the Free Software |
32 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
33 | ** included in the packaging of this file. Please review the following |
34 | ** information to ensure the GNU General Public License requirements will |
35 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
36 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
37 | ** |
38 | ** $QT_END_LICENSE$ |
39 | ** |
40 | ****************************************************************************/ |
41 | |
42 | #include "qmimeprovider_p.h" |
43 | |
44 | #include "qmimetypeparser_p.h" |
45 | #include <qstandardpaths.h> |
46 | #include "qmimemagicrulematcher_p.h" |
47 | |
48 | #include <QXmlStreamReader> |
49 | #include <QBuffer> |
50 | #include <QDir> |
51 | #include <QFile> |
52 | #include <QByteArrayMatcher> |
53 | #include <QDebug> |
54 | #include <QDateTime> |
55 | #include <QtEndian> |
56 | |
57 | #if QT_CONFIG(mimetype_database) |
58 | # if defined(Q_CC_MSVC) |
59 | # pragma section(".qtmimedatabase", read, shared) |
60 | __declspec(allocate(".qtmimedatabase" )) __declspec(align(4096)) |
61 | # elif defined(Q_OS_DARWIN) |
62 | __attribute__((section("__TEXT,.qtmimedatabase" ), aligned(4096))) |
63 | # elif (defined(Q_OF_ELF) || defined(Q_OS_WIN)) && defined(Q_CC_GNU) |
64 | __attribute__((section(".qtmimedatabase" ), aligned(4096))) |
65 | # endif |
66 | |
67 | # include "qmimeprovider_database.cpp" |
68 | |
69 | # ifdef MIME_DATABASE_IS_ZSTD |
70 | # if !QT_CONFIG(zstd) |
71 | # error "MIME database is zstd but no support compiled in!" |
72 | # endif |
73 | # include <zstd.h> |
74 | # endif |
75 | # ifdef MIME_DATABASE_IS_GZIP |
76 | # ifdef QT_NO_COMPRESS |
77 | # error "MIME database is zlib but no support compiled in!" |
78 | # endif |
79 | # define ZLIB_CONST |
80 | # include <zconf.h> |
81 | # include <zlib.h> |
82 | # endif |
83 | #endif |
84 | |
85 | QT_BEGIN_NAMESPACE |
86 | |
87 | QMimeProviderBase::QMimeProviderBase(QMimeDatabasePrivate *db, const QString &directory) |
88 | : m_db(db), m_directory(directory) |
89 | { |
90 | } |
91 | |
92 | |
93 | QMimeBinaryProvider::QMimeBinaryProvider(QMimeDatabasePrivate *db, const QString &directory) |
94 | : QMimeProviderBase(db, directory), m_mimetypeListLoaded(false) |
95 | { |
96 | ensureLoaded(); |
97 | } |
98 | |
99 | struct QMimeBinaryProvider::CacheFile |
100 | { |
101 | CacheFile(const QString &fileName); |
102 | ~CacheFile(); |
103 | |
104 | bool isValid() const { return m_valid; } |
105 | inline quint16 getUint16(int offset) const |
106 | { |
107 | return qFromBigEndian(*reinterpret_cast<quint16 *>(data + offset)); |
108 | } |
109 | inline quint32 getUint32(int offset) const |
110 | { |
111 | return qFromBigEndian(*reinterpret_cast<quint32 *>(data + offset)); |
112 | } |
113 | inline const char *getCharStar(int offset) const |
114 | { |
115 | return reinterpret_cast<const char *>(data + offset); |
116 | } |
117 | bool load(); |
118 | bool reload(); |
119 | |
120 | QFile file; |
121 | uchar *data; |
122 | QDateTime m_mtime; |
123 | bool m_valid; |
124 | }; |
125 | |
126 | QMimeBinaryProvider::CacheFile::CacheFile(const QString &fileName) |
127 | : file(fileName), m_valid(false) |
128 | { |
129 | load(); |
130 | } |
131 | |
132 | QMimeBinaryProvider::CacheFile::~CacheFile() |
133 | { |
134 | } |
135 | |
136 | bool QMimeBinaryProvider::CacheFile::load() |
137 | { |
138 | if (!file.open(QIODevice::ReadOnly)) |
139 | return false; |
140 | data = file.map(0, file.size()); |
141 | if (data) { |
142 | const int major = getUint16(0); |
143 | const int minor = getUint16(2); |
144 | m_valid = (major == 1 && minor >= 1 && minor <= 2); |
145 | } |
146 | m_mtime = QFileInfo(file).lastModified(); |
147 | return m_valid; |
148 | } |
149 | |
150 | bool QMimeBinaryProvider::CacheFile::reload() |
151 | { |
152 | m_valid = false; |
153 | if (file.isOpen()) { |
154 | file.close(); |
155 | } |
156 | data = nullptr; |
157 | return load(); |
158 | } |
159 | |
160 | QMimeBinaryProvider::~QMimeBinaryProvider() |
161 | { |
162 | delete m_cacheFile; |
163 | } |
164 | |
165 | bool QMimeBinaryProvider::isValid() |
166 | { |
167 | return m_cacheFile != nullptr; |
168 | } |
169 | |
170 | bool QMimeBinaryProvider::isInternalDatabase() const |
171 | { |
172 | return false; |
173 | } |
174 | |
175 | // Position of the "list offsets" values, at the beginning of the mime.cache file |
176 | enum { |
177 | PosAliasListOffset = 4, |
178 | PosParentListOffset = 8, |
179 | PosLiteralListOffset = 12, |
180 | PosReverseSuffixTreeOffset = 16, |
181 | PosGlobListOffset = 20, |
182 | PosMagicListOffset = 24, |
183 | // PosNamespaceListOffset = 28, |
184 | PosIconsListOffset = 32, |
185 | PosGenericIconsListOffset = 36 |
186 | }; |
187 | |
188 | bool QMimeBinaryProvider::checkCacheChanged() |
189 | { |
190 | QFileInfo fileInfo(m_cacheFile->file); |
191 | if (fileInfo.lastModified() > m_cacheFile->m_mtime) { |
192 | // Deletion can't happen by just running update-mime-database. |
193 | // But the user could use rm -rf :-) |
194 | m_cacheFile->reload(); // will mark itself as invalid on failure |
195 | return true; |
196 | } |
197 | return false; |
198 | } |
199 | |
200 | void QMimeBinaryProvider::ensureLoaded() |
201 | { |
202 | if (!m_cacheFile) { |
203 | const QString cacheFileName = m_directory + QLatin1String("/mime.cache" ); |
204 | m_cacheFile = new CacheFile(cacheFileName); |
205 | m_mimetypeListLoaded = false; |
206 | } else { |
207 | if (checkCacheChanged()) |
208 | m_mimetypeListLoaded = false; |
209 | else |
210 | return; // nothing to do |
211 | } |
212 | if (!m_cacheFile->isValid()) { // verify existence and version |
213 | delete m_cacheFile; |
214 | m_cacheFile = nullptr; |
215 | } |
216 | } |
217 | |
218 | static QMimeType mimeTypeForNameUnchecked(const QString &name) |
219 | { |
220 | QMimeTypePrivate data; |
221 | data.name = name; |
222 | data.fromCache = true; |
223 | // The rest is retrieved on demand. |
224 | // comment and globPatterns: in loadMimeTypePrivate |
225 | // iconName: in loadIcon |
226 | // genericIconName: in loadGenericIcon |
227 | return QMimeType(data); |
228 | } |
229 | |
230 | QMimeType QMimeBinaryProvider::mimeTypeForName(const QString &name) |
231 | { |
232 | if (!m_mimetypeListLoaded) |
233 | loadMimeTypeList(); |
234 | if (!m_mimetypeNames.contains(name)) |
235 | return QMimeType(); // unknown mimetype |
236 | return mimeTypeForNameUnchecked(name); |
237 | } |
238 | |
239 | void QMimeBinaryProvider::addFileNameMatches(const QString &fileName, QMimeGlobMatchResult &result) |
240 | { |
241 | if (fileName.isEmpty()) |
242 | return; |
243 | Q_ASSERT(m_cacheFile); |
244 | const QString lowerFileName = fileName.toLower(); |
245 | // Check literals (e.g. "Makefile") |
246 | matchGlobList(result, m_cacheFile, m_cacheFile->getUint32(PosLiteralListOffset), fileName); |
247 | // Check complex globs (e.g. "callgrind.out[0-9]*") |
248 | matchGlobList(result, m_cacheFile, m_cacheFile->getUint32(PosGlobListOffset), fileName); |
249 | // Check the very common *.txt cases with the suffix tree |
250 | const int reverseSuffixTreeOffset = m_cacheFile->getUint32(PosReverseSuffixTreeOffset); |
251 | const int numRoots = m_cacheFile->getUint32(reverseSuffixTreeOffset); |
252 | const int firstRootOffset = m_cacheFile->getUint32(reverseSuffixTreeOffset + 4); |
253 | matchSuffixTree(result, m_cacheFile, numRoots, firstRootOffset, lowerFileName, lowerFileName.length() - 1, false); |
254 | if (result.m_matchingMimeTypes.isEmpty()) |
255 | matchSuffixTree(result, m_cacheFile, numRoots, firstRootOffset, fileName, fileName.length() - 1, true); |
256 | } |
257 | |
258 | void QMimeBinaryProvider::matchGlobList(QMimeGlobMatchResult &result, CacheFile *cacheFile, int off, const QString &fileName) |
259 | { |
260 | const int numGlobs = cacheFile->getUint32(off); |
261 | //qDebug() << "Loading" << numGlobs << "globs from" << cacheFile->file.fileName() << "at offset" << cacheFile->globListOffset; |
262 | for (int i = 0; i < numGlobs; ++i) { |
263 | const int globOffset = cacheFile->getUint32(off + 4 + 12 * i); |
264 | const int mimeTypeOffset = cacheFile->getUint32(off + 4 + 12 * i + 4); |
265 | const int flagsAndWeight = cacheFile->getUint32(off + 4 + 12 * i + 8); |
266 | const int weight = flagsAndWeight & 0xff; |
267 | const bool caseSensitive = flagsAndWeight & 0x100; |
268 | const Qt::CaseSensitivity qtCaseSensitive = caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive; |
269 | const QString pattern = QLatin1String(cacheFile->getCharStar(globOffset)); |
270 | |
271 | const char *mimeType = cacheFile->getCharStar(mimeTypeOffset); |
272 | //qDebug() << pattern << mimeType << weight << caseSensitive; |
273 | QMimeGlobPattern glob(pattern, QString() /*unused*/, weight, qtCaseSensitive); |
274 | |
275 | // TODO: this could be done faster for literals where a simple == would do. |
276 | if (glob.matchFileName(fileName)) |
277 | result.addMatch(QLatin1String(mimeType), weight, pattern); |
278 | } |
279 | } |
280 | |
281 | bool QMimeBinaryProvider::matchSuffixTree(QMimeGlobMatchResult &result, QMimeBinaryProvider::CacheFile *cacheFile, int numEntries, int firstOffset, const QString &fileName, int charPos, bool caseSensitiveCheck) |
282 | { |
283 | QChar fileChar = fileName[charPos]; |
284 | int min = 0; |
285 | int max = numEntries - 1; |
286 | while (min <= max) { |
287 | const int mid = (min + max) / 2; |
288 | const int off = firstOffset + 12 * mid; |
289 | const QChar ch = char16_t(cacheFile->getUint32(off)); |
290 | if (ch < fileChar) |
291 | min = mid + 1; |
292 | else if (ch > fileChar) |
293 | max = mid - 1; |
294 | else { |
295 | --charPos; |
296 | int numChildren = cacheFile->getUint32(off + 4); |
297 | int childrenOffset = cacheFile->getUint32(off + 8); |
298 | bool success = false; |
299 | if (charPos > 0) |
300 | success = matchSuffixTree(result, cacheFile, numChildren, childrenOffset, fileName, charPos, caseSensitiveCheck); |
301 | if (!success) { |
302 | for (int i = 0; i < numChildren; ++i) { |
303 | const int childOff = childrenOffset + 12 * i; |
304 | const int mch = cacheFile->getUint32(childOff); |
305 | if (mch != 0) |
306 | break; |
307 | const int mimeTypeOffset = cacheFile->getUint32(childOff + 4); |
308 | const char *mimeType = cacheFile->getCharStar(mimeTypeOffset); |
309 | const int flagsAndWeight = cacheFile->getUint32(childOff + 8); |
310 | const int weight = flagsAndWeight & 0xff; |
311 | const bool caseSensitive = flagsAndWeight & 0x100; |
312 | if (caseSensitiveCheck || !caseSensitive) { |
313 | result.addMatch(QLatin1String(mimeType), weight, |
314 | QLatin1Char('*') + QStringView{fileName}.mid(charPos + 1), fileName.size() - charPos - 2); |
315 | success = true; |
316 | } |
317 | } |
318 | } |
319 | return success; |
320 | } |
321 | } |
322 | return false; |
323 | } |
324 | |
325 | bool QMimeBinaryProvider::matchMagicRule(QMimeBinaryProvider::CacheFile *cacheFile, int numMatchlets, int firstOffset, const QByteArray &data) |
326 | { |
327 | const char *dataPtr = data.constData(); |
328 | const int dataSize = data.size(); |
329 | for (int matchlet = 0; matchlet < numMatchlets; ++matchlet) { |
330 | const int off = firstOffset + matchlet * 32; |
331 | const int rangeStart = cacheFile->getUint32(off); |
332 | const int rangeLength = cacheFile->getUint32(off + 4); |
333 | //const int wordSize = cacheFile->getUint32(off + 8); |
334 | const int valueLength = cacheFile->getUint32(off + 12); |
335 | const int valueOffset = cacheFile->getUint32(off + 16); |
336 | const int maskOffset = cacheFile->getUint32(off + 20); |
337 | const char *mask = maskOffset ? cacheFile->getCharStar(maskOffset) : nullptr; |
338 | |
339 | if (!QMimeMagicRule::matchSubstring(dataPtr, dataSize, rangeStart, rangeLength, valueLength, cacheFile->getCharStar(valueOffset), mask)) |
340 | continue; |
341 | |
342 | const int numChildren = cacheFile->getUint32(off + 24); |
343 | const int firstChildOffset = cacheFile->getUint32(off + 28); |
344 | if (numChildren == 0) // No submatch? Then we are done. |
345 | return true; |
346 | // Check that one of the submatches matches too |
347 | if (matchMagicRule(cacheFile, numChildren, firstChildOffset, data)) |
348 | return true; |
349 | } |
350 | return false; |
351 | } |
352 | |
353 | void QMimeBinaryProvider::findByMagic(const QByteArray &data, int *accuracyPtr, QMimeType &candidate) |
354 | { |
355 | const int magicListOffset = m_cacheFile->getUint32(PosMagicListOffset); |
356 | const int numMatches = m_cacheFile->getUint32(magicListOffset); |
357 | //const int maxExtent = cacheFile->getUint32(magicListOffset + 4); |
358 | const int firstMatchOffset = m_cacheFile->getUint32(magicListOffset + 8); |
359 | |
360 | for (int i = 0; i < numMatches; ++i) { |
361 | const int off = firstMatchOffset + i * 16; |
362 | const int numMatchlets = m_cacheFile->getUint32(off + 8); |
363 | const int firstMatchletOffset = m_cacheFile->getUint32(off + 12); |
364 | if (matchMagicRule(m_cacheFile, numMatchlets, firstMatchletOffset, data)) { |
365 | const int mimeTypeOffset = m_cacheFile->getUint32(off + 4); |
366 | const char *mimeType = m_cacheFile->getCharStar(mimeTypeOffset); |
367 | *accuracyPtr = m_cacheFile->getUint32(off); |
368 | // Return the first match. We have no rules for conflicting magic data... |
369 | // (mime.cache itself is sorted, but what about local overrides with a lower prio?) |
370 | candidate = mimeTypeForNameUnchecked(QLatin1String(mimeType)); |
371 | return; |
372 | } |
373 | } |
374 | } |
375 | |
376 | void QMimeBinaryProvider::addParents(const QString &mime, QStringList &result) |
377 | { |
378 | const QByteArray mimeStr = mime.toLatin1(); |
379 | const int parentListOffset = m_cacheFile->getUint32(PosParentListOffset); |
380 | const int numEntries = m_cacheFile->getUint32(parentListOffset); |
381 | |
382 | int begin = 0; |
383 | int end = numEntries - 1; |
384 | while (begin <= end) { |
385 | const int medium = (begin + end) / 2; |
386 | const int off = parentListOffset + 4 + 8 * medium; |
387 | const int mimeOffset = m_cacheFile->getUint32(off); |
388 | const char *aMime = m_cacheFile->getCharStar(mimeOffset); |
389 | const int cmp = qstrcmp(aMime, mimeStr); |
390 | if (cmp < 0) { |
391 | begin = medium + 1; |
392 | } else if (cmp > 0) { |
393 | end = medium - 1; |
394 | } else { |
395 | const int parentsOffset = m_cacheFile->getUint32(off + 4); |
396 | const int numParents = m_cacheFile->getUint32(parentsOffset); |
397 | for (int i = 0; i < numParents; ++i) { |
398 | const int parentOffset = m_cacheFile->getUint32(parentsOffset + 4 + 4 * i); |
399 | const char *aParent = m_cacheFile->getCharStar(parentOffset); |
400 | const QString strParent = QString::fromLatin1(aParent); |
401 | if (!result.contains(strParent)) |
402 | result.append(strParent); |
403 | } |
404 | break; |
405 | } |
406 | } |
407 | } |
408 | |
409 | QString QMimeBinaryProvider::resolveAlias(const QString &name) |
410 | { |
411 | const QByteArray input = name.toLatin1(); |
412 | const int aliasListOffset = m_cacheFile->getUint32(PosAliasListOffset); |
413 | const int numEntries = m_cacheFile->getUint32(aliasListOffset); |
414 | int begin = 0; |
415 | int end = numEntries - 1; |
416 | while (begin <= end) { |
417 | const int medium = (begin + end) / 2; |
418 | const int off = aliasListOffset + 4 + 8 * medium; |
419 | const int aliasOffset = m_cacheFile->getUint32(off); |
420 | const char *alias = m_cacheFile->getCharStar(aliasOffset); |
421 | const int cmp = qstrcmp(alias, input); |
422 | if (cmp < 0) { |
423 | begin = medium + 1; |
424 | } else if (cmp > 0) { |
425 | end = medium - 1; |
426 | } else { |
427 | const int mimeOffset = m_cacheFile->getUint32(off + 4); |
428 | const char *mimeType = m_cacheFile->getCharStar(mimeOffset); |
429 | return QLatin1String(mimeType); |
430 | } |
431 | } |
432 | return QString(); |
433 | } |
434 | |
435 | void QMimeBinaryProvider::addAliases(const QString &name, QStringList &result) |
436 | { |
437 | const QByteArray input = name.toLatin1(); |
438 | const int aliasListOffset = m_cacheFile->getUint32(PosAliasListOffset); |
439 | const int numEntries = m_cacheFile->getUint32(aliasListOffset); |
440 | for (int pos = 0; pos < numEntries; ++pos) { |
441 | const int off = aliasListOffset + 4 + 8 * pos; |
442 | const int mimeOffset = m_cacheFile->getUint32(off + 4); |
443 | const char *mimeType = m_cacheFile->getCharStar(mimeOffset); |
444 | |
445 | if (input == mimeType) { |
446 | const int aliasOffset = m_cacheFile->getUint32(off); |
447 | const char *alias = m_cacheFile->getCharStar(aliasOffset); |
448 | const QString strAlias = QString::fromLatin1(alias); |
449 | if (!result.contains(strAlias)) |
450 | result.append(strAlias); |
451 | } |
452 | } |
453 | } |
454 | |
455 | void QMimeBinaryProvider::loadMimeTypeList() |
456 | { |
457 | if (!m_mimetypeListLoaded) { |
458 | m_mimetypeListLoaded = true; |
459 | m_mimetypeNames.clear(); |
460 | // Unfortunately mime.cache doesn't have a full list of all mimetypes. |
461 | // So we have to parse the plain-text files called "types". |
462 | QFile file(m_directory + QStringLiteral("/types" )); |
463 | if (file.open(QIODevice::ReadOnly)) { |
464 | while (!file.atEnd()) { |
465 | QByteArray line = file.readLine(); |
466 | if (line.endsWith('\n')) |
467 | line.chop(1); |
468 | m_mimetypeNames.insert(QString::fromLatin1(line)); |
469 | } |
470 | } |
471 | } |
472 | } |
473 | |
474 | void QMimeBinaryProvider::addAllMimeTypes(QList<QMimeType> &result) |
475 | { |
476 | loadMimeTypeList(); |
477 | if (result.isEmpty()) { |
478 | result.reserve(m_mimetypeNames.count()); |
479 | for (const QString &name : qAsConst(m_mimetypeNames)) |
480 | result.append(mimeTypeForNameUnchecked(name)); |
481 | } else { |
482 | for (const QString &name : qAsConst(m_mimetypeNames)) |
483 | if (std::find_if(result.constBegin(), result.constEnd(), [name](const QMimeType &mime) -> bool { return mime.name() == name; }) |
484 | == result.constEnd()) |
485 | result.append(mimeTypeForNameUnchecked(name)); |
486 | } |
487 | } |
488 | |
489 | void QMimeBinaryProvider::loadMimeTypePrivate(QMimeTypePrivate &data) |
490 | { |
491 | #ifdef QT_NO_XMLSTREAMREADER |
492 | Q_UNUSED(data); |
493 | qWarning("Cannot load mime type since QXmlStreamReader is not available." ); |
494 | return; |
495 | #else |
496 | if (data.loaded) |
497 | return; |
498 | data.loaded = true; |
499 | // load comment and globPatterns |
500 | |
501 | const QString file = data.name + QLatin1String(".xml" ); |
502 | // shared-mime-info since 1.3 lowercases the xml files |
503 | QStringList mimeFiles = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QLatin1String("mime/" ) + file.toLower()); |
504 | if (mimeFiles.isEmpty()) |
505 | mimeFiles = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QLatin1String("mime/" ) + file); // pre-1.3 |
506 | if (mimeFiles.isEmpty()) { |
507 | qWarning() << "No file found for" << file << ", even though update-mime-info said it would exist.\n" |
508 | "Either it was just removed, or the directory doesn't have executable permission..." |
509 | << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QLatin1String("mime" ), QStandardPaths::LocateDirectory); |
510 | return; |
511 | } |
512 | |
513 | QString mainPattern; |
514 | |
515 | for (QStringList::const_reverse_iterator it = mimeFiles.crbegin(), end = mimeFiles.crend(); it != end; ++it) { // global first, then local. |
516 | QFile qfile(*it); |
517 | if (!qfile.open(QFile::ReadOnly)) |
518 | continue; |
519 | |
520 | QXmlStreamReader xml(&qfile); |
521 | if (xml.readNextStartElement()) { |
522 | if (xml.name() != QLatin1String("mime-type" )) { |
523 | continue; |
524 | } |
525 | const auto name = xml.attributes().value(QLatin1String("type" )); |
526 | if (name.isEmpty()) |
527 | continue; |
528 | if (name.compare(data.name, Qt::CaseInsensitive)) |
529 | qWarning() << "Got name" << name << "in file" << file << "expected" << data.name; |
530 | |
531 | while (xml.readNextStartElement()) { |
532 | const auto tag = xml.name(); |
533 | if (tag == QLatin1String("comment" )) { |
534 | QString lang = xml.attributes().value(QLatin1String("xml:lang" )).toString(); |
535 | const QString text = xml.readElementText(); |
536 | if (lang.isEmpty()) { |
537 | lang = QLatin1String("default" ); // no locale attribute provided, treat it as default. |
538 | } |
539 | data.localeComments.insert(lang, text); |
540 | continue; // we called readElementText, so we're at the EndElement already. |
541 | } else if (tag == QLatin1String("icon" )) { // as written out by shared-mime-info >= 0.40 |
542 | data.iconName = xml.attributes().value(QLatin1String("name" )).toString(); |
543 | } else if (tag == QLatin1String("glob-deleteall" )) { // as written out by shared-mime-info >= 0.70 |
544 | data.globPatterns.clear(); |
545 | mainPattern.clear(); |
546 | } else if (tag == QLatin1String("glob" )) { // as written out by shared-mime-info >= 0.70 |
547 | const QString pattern = xml.attributes().value(QLatin1String("pattern" )).toString(); |
548 | if (mainPattern.isEmpty() && pattern.startsWith(QLatin1Char('*'))) { |
549 | mainPattern = pattern; |
550 | } |
551 | if (!data.globPatterns.contains(pattern)) |
552 | data.globPatterns.append(pattern); |
553 | } |
554 | xml.skipCurrentElement(); |
555 | } |
556 | Q_ASSERT(xml.name() == QLatin1String("mime-type" )); |
557 | } |
558 | } |
559 | |
560 | // Let's assume that shared-mime-info is at least version 0.70 |
561 | // Otherwise we would need 1) a version check, and 2) code for parsing patterns from the globs file. |
562 | #if 1 |
563 | if (!mainPattern.isEmpty() && (data.globPatterns.isEmpty() || data.globPatterns.constFirst() != mainPattern)) { |
564 | // ensure it's first in the list of patterns |
565 | data.globPatterns.removeAll(mainPattern); |
566 | data.globPatterns.prepend(mainPattern); |
567 | } |
568 | #else |
569 | const bool globsInXml = sharedMimeInfoVersion() >= QT_VERSION_CHECK(0, 70, 0); |
570 | if (globsInXml) { |
571 | if (!mainPattern.isEmpty() && data.globPatterns.constFirst() != mainPattern) { |
572 | // ensure it's first in the list of patterns |
573 | data.globPatterns.removeAll(mainPattern); |
574 | data.globPatterns.prepend(mainPattern); |
575 | } |
576 | } else { |
577 | // Fallback: get the patterns from the globs file |
578 | // TODO: This would be the only way to support shared-mime-info < 0.70 |
579 | // But is this really worth the effort? |
580 | } |
581 | #endif |
582 | #endif //QT_NO_XMLSTREAMREADER |
583 | } |
584 | |
585 | // Binary search in the icons or generic-icons list |
586 | QLatin1String QMimeBinaryProvider::iconForMime(CacheFile *cacheFile, int posListOffset, const QByteArray &inputMime) |
587 | { |
588 | const int iconsListOffset = cacheFile->getUint32(posListOffset); |
589 | const int numIcons = cacheFile->getUint32(iconsListOffset); |
590 | int begin = 0; |
591 | int end = numIcons - 1; |
592 | while (begin <= end) { |
593 | const int medium = (begin + end) / 2; |
594 | const int off = iconsListOffset + 4 + 8 * medium; |
595 | const int mimeOffset = cacheFile->getUint32(off); |
596 | const char *mime = cacheFile->getCharStar(mimeOffset); |
597 | const int cmp = qstrcmp(mime, inputMime); |
598 | if (cmp < 0) |
599 | begin = medium + 1; |
600 | else if (cmp > 0) |
601 | end = medium - 1; |
602 | else { |
603 | const int iconOffset = cacheFile->getUint32(off + 4); |
604 | return QLatin1String(cacheFile->getCharStar(iconOffset)); |
605 | } |
606 | } |
607 | return QLatin1String(); |
608 | } |
609 | |
610 | void QMimeBinaryProvider::loadIcon(QMimeTypePrivate &data) |
611 | { |
612 | const QByteArray inputMime = data.name.toLatin1(); |
613 | const QLatin1String icon = iconForMime(m_cacheFile, PosIconsListOffset, inputMime); |
614 | if (!icon.isEmpty()) { |
615 | data.iconName = icon; |
616 | } |
617 | } |
618 | |
619 | void QMimeBinaryProvider::loadGenericIcon(QMimeTypePrivate &data) |
620 | { |
621 | const QByteArray inputMime = data.name.toLatin1(); |
622 | const QLatin1String icon = iconForMime(m_cacheFile, PosGenericIconsListOffset, inputMime); |
623 | if (!icon.isEmpty()) { |
624 | data.genericIconName = icon; |
625 | } |
626 | } |
627 | |
628 | //// |
629 | |
630 | #if QT_CONFIG(mimetype_database) |
631 | static QString internalMimeFileName() |
632 | { |
633 | return QStringLiteral("<internal MIME data>" ); |
634 | } |
635 | |
636 | QMimeXMLProvider::QMimeXMLProvider(QMimeDatabasePrivate *db, InternalDatabaseEnum) |
637 | : QMimeProviderBase(db, internalMimeFileName()) |
638 | { |
639 | static_assert(sizeof(mimetype_database), "Bundled MIME database is empty" ); |
640 | static_assert(sizeof(mimetype_database) <= MimeTypeDatabaseOriginalSize, |
641 | "Compressed MIME database is larger than the original size" ); |
642 | static_assert(MimeTypeDatabaseOriginalSize <= 16*1024*1024, |
643 | "Bundled MIME database is too big" ); |
644 | const char *data = reinterpret_cast<const char *>(mimetype_database); |
645 | qsizetype size = MimeTypeDatabaseOriginalSize; |
646 | |
647 | #ifdef MIME_DATABASE_IS_ZSTD |
648 | // uncompress with libzstd |
649 | std::unique_ptr<char []> uncompressed(new char[size]); |
650 | size = ZSTD_decompress(uncompressed.get(), size, mimetype_database, sizeof(mimetype_database)); |
651 | Q_ASSERT(!ZSTD_isError(size)); |
652 | data = uncompressed.get(); |
653 | #elif defined(MIME_DATABASE_IS_GZIP) |
654 | std::unique_ptr<char []> uncompressed(new char[size]); |
655 | z_stream zs = {}; |
656 | zs.next_in = const_cast<Bytef *>(mimetype_database); |
657 | zs.avail_in = sizeof(mimetype_database); |
658 | zs.next_out = reinterpret_cast<Bytef *>(uncompressed.get()); |
659 | zs.avail_out = size; |
660 | |
661 | int res = inflateInit2(&zs, MAX_WBITS | 32); |
662 | Q_ASSERT(res == Z_OK); |
663 | res = inflate(&zs, Z_FINISH); |
664 | Q_ASSERT(res == Z_STREAM_END); |
665 | res = inflateEnd(&zs); |
666 | Q_ASSERT(res == Z_OK); |
667 | |
668 | data = uncompressed.get(); |
669 | size = zs.total_out; |
670 | #endif |
671 | |
672 | load(data, size); |
673 | } |
674 | #else // !QT_CONFIG(mimetype_database) |
675 | // never called in release mode, but some debug builds may need |
676 | // this to be defined. |
677 | QMimeXMLProvider::QMimeXMLProvider(QMimeDatabasePrivate *db, InternalDatabaseEnum) |
678 | : QMimeProviderBase(db, QString()) |
679 | { |
680 | Q_UNREACHABLE(); |
681 | } |
682 | #endif // QT_CONFIG(mimetype_database) |
683 | |
684 | QMimeXMLProvider::QMimeXMLProvider(QMimeDatabasePrivate *db, const QString &directory) |
685 | : QMimeProviderBase(db, directory) |
686 | { |
687 | ensureLoaded(); |
688 | } |
689 | |
690 | QMimeXMLProvider::~QMimeXMLProvider() |
691 | { |
692 | } |
693 | |
694 | bool QMimeXMLProvider::isValid() |
695 | { |
696 | // If you change this method, adjust the logic in QMimeDatabasePrivate::loadProviders, |
697 | // which assumes isValid==false is only possible in QMimeBinaryProvider. |
698 | return true; |
699 | } |
700 | |
701 | bool QMimeXMLProvider::isInternalDatabase() const |
702 | { |
703 | #if QT_CONFIG(mimetype_database) |
704 | return m_directory == internalMimeFileName(); |
705 | #else |
706 | return false; |
707 | #endif |
708 | } |
709 | |
710 | QMimeType QMimeXMLProvider::mimeTypeForName(const QString &name) |
711 | { |
712 | return m_nameMimeTypeMap.value(name); |
713 | } |
714 | |
715 | void QMimeXMLProvider::addFileNameMatches(const QString &fileName, QMimeGlobMatchResult &result) |
716 | { |
717 | m_mimeTypeGlobs.matchingGlobs(fileName, result); |
718 | } |
719 | |
720 | void QMimeXMLProvider::findByMagic(const QByteArray &data, int *accuracyPtr, QMimeType &candidate) |
721 | { |
722 | QString candidateName; |
723 | bool foundOne = false; |
724 | for (const QMimeMagicRuleMatcher &matcher : qAsConst(m_magicMatchers)) { |
725 | if (matcher.matches(data)) { |
726 | const int priority = matcher.priority(); |
727 | if (priority > *accuracyPtr) { |
728 | *accuracyPtr = priority; |
729 | candidateName = matcher.mimetype(); |
730 | foundOne = true; |
731 | } |
732 | } |
733 | } |
734 | if (foundOne) |
735 | candidate = mimeTypeForName(candidateName); |
736 | } |
737 | |
738 | void QMimeXMLProvider::ensureLoaded() |
739 | { |
740 | QStringList allFiles; |
741 | const QString packageDir = m_directory + QStringLiteral("/packages" ); |
742 | QDir dir(packageDir); |
743 | const QStringList files = dir.entryList(QDir::Files | QDir::NoDotAndDotDot); |
744 | allFiles.reserve(files.count()); |
745 | for (const QString &xmlFile : files) |
746 | allFiles.append(packageDir + QLatin1Char('/') + xmlFile); |
747 | |
748 | if (m_allFiles == allFiles) |
749 | return; |
750 | m_allFiles = allFiles; |
751 | |
752 | m_nameMimeTypeMap.clear(); |
753 | m_aliases.clear(); |
754 | m_parents.clear(); |
755 | m_mimeTypeGlobs.clear(); |
756 | m_magicMatchers.clear(); |
757 | |
758 | //qDebug() << "Loading" << m_allFiles; |
759 | |
760 | for (const QString &file : qAsConst(allFiles)) |
761 | load(file); |
762 | } |
763 | |
764 | void QMimeXMLProvider::load(const QString &fileName) |
765 | { |
766 | QString errorMessage; |
767 | if (!load(fileName, &errorMessage)) |
768 | qWarning("QMimeDatabase: Error loading %ls\n%ls" , qUtf16Printable(fileName), qUtf16Printable(errorMessage)); |
769 | } |
770 | |
771 | bool QMimeXMLProvider::load(const QString &fileName, QString *errorMessage) |
772 | { |
773 | QFile file(fileName); |
774 | if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { |
775 | if (errorMessage) |
776 | *errorMessage = QLatin1String("Cannot open " ) + fileName + QLatin1String(": " ) + file.errorString(); |
777 | return false; |
778 | } |
779 | |
780 | if (errorMessage) |
781 | errorMessage->clear(); |
782 | |
783 | QMimeTypeParser parser(*this); |
784 | return parser.parse(&file, fileName, errorMessage); |
785 | } |
786 | |
787 | #if QT_CONFIG(mimetype_database) |
788 | void QMimeXMLProvider::load(const char *data, qsizetype len) |
789 | { |
790 | QBuffer buffer; |
791 | buffer.setData(QByteArray::fromRawData(data, len)); |
792 | buffer.open(QIODevice::ReadOnly); |
793 | QString errorMessage; |
794 | QMimeTypeParser parser(*this); |
795 | if (!parser.parse(&buffer, internalMimeFileName(), &errorMessage)) |
796 | qWarning("QMimeDatabase: Error loading internal MIME data\n%s" , qPrintable(errorMessage)); |
797 | } |
798 | #endif |
799 | |
800 | void QMimeXMLProvider::addGlobPattern(const QMimeGlobPattern &glob) |
801 | { |
802 | m_mimeTypeGlobs.addGlob(glob); |
803 | } |
804 | |
805 | void QMimeXMLProvider::addMimeType(const QMimeType &mt) |
806 | { |
807 | Q_ASSERT(!mt.d.data()->fromCache); |
808 | m_nameMimeTypeMap.insert(mt.name(), mt); |
809 | } |
810 | |
811 | void QMimeXMLProvider::addParents(const QString &mime, QStringList &result) |
812 | { |
813 | for (const QString &parent : m_parents.value(mime)) { |
814 | if (!result.contains(parent)) |
815 | result.append(parent); |
816 | } |
817 | } |
818 | |
819 | void QMimeXMLProvider::addParent(const QString &child, const QString &parent) |
820 | { |
821 | m_parents[child].append(parent); |
822 | } |
823 | |
824 | void QMimeXMLProvider::addAliases(const QString &name, QStringList &result) |
825 | { |
826 | // Iterate through the whole hash. This method is rarely used. |
827 | for (auto it = m_aliases.constBegin(), end = m_aliases.constEnd() ; it != end ; ++it) { |
828 | if (it.value() == name) { |
829 | if (!result.contains(it.key())) |
830 | result.append(it.key()); |
831 | } |
832 | } |
833 | |
834 | } |
835 | |
836 | QString QMimeXMLProvider::resolveAlias(const QString &name) |
837 | { |
838 | return m_aliases.value(name); |
839 | } |
840 | |
841 | void QMimeXMLProvider::addAlias(const QString &alias, const QString &name) |
842 | { |
843 | m_aliases.insert(alias, name); |
844 | } |
845 | |
846 | void QMimeXMLProvider::addAllMimeTypes(QList<QMimeType> &result) |
847 | { |
848 | if (result.isEmpty()) { // fast path |
849 | result = m_nameMimeTypeMap.values(); |
850 | } else { |
851 | for (auto it = m_nameMimeTypeMap.constBegin(), end = m_nameMimeTypeMap.constEnd() ; it != end ; ++it) { |
852 | const QString newMime = it.key(); |
853 | if (std::find_if(result.constBegin(), result.constEnd(), [newMime](const QMimeType &mime) -> bool { return mime.name() == newMime; }) |
854 | == result.constEnd()) |
855 | result.append(it.value()); |
856 | } |
857 | } |
858 | } |
859 | |
860 | void QMimeXMLProvider::addMagicMatcher(const QMimeMagicRuleMatcher &matcher) |
861 | { |
862 | m_magicMatchers.append(matcher); |
863 | } |
864 | |
865 | QT_END_NAMESPACE |
866 | |