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