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
qmimeprovider.cpp
Go to the documentation of this file.
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// Qt-Security score:critical reason:data-parser
6
8
10#include <qstandardpaths.h>
12
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
50QT_BEGIN_NAMESPACE
51
52using namespace Qt::StringLiterals;
53// Position of the "list offsets" values, at the beginning of the mime.cache file
54enum {
61 // PosNamespaceListOffset = 28,
64};
65
66// Size of one record of each of the lists, as defined by the mime.cache format
67enum {
75};
76
77enum {
80};
81
82/*
83 A list of fixed-size records in the mime.cache file, whose bounds have been
84 checked once: for any index below count, the whole record at
85 recordOffset(index) is inside the file, so walking the list needs no further
86 checking - read the fields with CacheFile::recordUint32(). An empty list
87 means either a mime.cache is empty or corrupted.
88
89 Anything reached by following an offset stored inside a record is still
90 untrusted and has to be checked where it is used.
91 */
92class QMimeBinaryProvider::RecordList
93{
94public:
95 // The lists whose offsets live in the first 40 bytes of the file are read
96 // by these two functions, once, when the cache file is loaded.
97 bool load(const CacheFile *cacheFile, quint64 posListOffset, quint32 stride);
98 bool loadIndirect(const CacheFile *cacheFile, quint64 posListOffset,
99 quint64 firstRecordPos, quint32 stride);
100
101 quint32 count() const { return m_count; } // N_ALIASES/N_ENTRIES/N_PARENTS/etc => CARD32 type
102 quint32 stride() const { return m_stride; }
103 quint64 firstRecord() const { return m_firstRecord; }
104 quint64 recordOffset(quint32 index) const
105 {
106 Q_ASSERT(index < m_count);
107 return m_firstRecord + quint64(index) * m_stride;
108 }
109
110private:
111 // The state is private, and setRecords() is the only thing that writes it,
112 // so a RecordList that is not empty has been bounds-checked.
113 bool setRecords(const CacheFile *cacheFile, quint64 firstRecord,
114 quint32 count, quint32 stride);
115
116 quint64 m_firstRecord = 0;
117 quint32 m_stride = 0;
118 quint32 m_count = 0;
119};
120
121struct QMimeBinaryProvider::CacheFile
122{
123 CacheFile(const QString &fileName);
125
126 bool isValid() const { return m_valid; }
127 inline std::optional<quint16> getUint16(quint64 offset) const
128 {
129 if (offset > m_fileSize || m_fileSize - offset < sizeof(quint16))
130 return std::nullopt;
131 return qFromBigEndian<quint16>(m_data + offset);
132 }
133 inline std::optional<quint32> getUint32(quint64 offset, quint64 extra = 0) const
134 {
135 if (offset > m_fileSize || extra > m_fileSize - offset)
136 return std::nullopt;
137 const quint64 pos = offset + extra;
138 if (m_fileSize - pos < sizeof(quint32))
139 return std::nullopt;
140 return qFromBigEndian<quint32>(m_data + pos);
141 }
142 inline QLatin1StringView getLatin1String(quint64 offset) const
143 {
144 if (offset >= m_fileSize)
145 return QLatin1StringView();
146 const char *str = reinterpret_cast<const char *>(m_data + offset);
147 const void *end = std::memchr(str, '\0', m_fileSize - offset);
148 if (!end)
149 return QLatin1StringView();
150 return QLatin1StringView(str, static_cast<const char *>(end));
151 }
152 inline const char *getData(quint64 offset, quint64 length) const
153 {
154 if (offset > m_fileSize || length > m_fileSize - offset)
155 return nullptr;
156 return reinterpret_cast<const char *>(m_data + offset);
157 }
158 // Check base + headerSize + index * stride can not overflow.
159 std::optional<quint64> safeRecordOffset(quint64 base, quint64 headerSize,
160 quint32 index, quint32 stride) const
161 {
162 if (base > m_fileSize || headerSize > m_fileSize - base)
163 return std::nullopt;
164 const quint64 avail = m_fileSize - base - headerSize;
165 // A zero stride would make every index alias the same record: corrupt cache.
166 if (!stride || index >= avail / stride)
167 return std::nullopt;
168 return base + headerSize + quint64(index) * stride;
169 }
170 // A quint32 field of a record of a bounds-checked RecordList. load() has
171 // verified that every record below list.count() lies inside the mapping.
172 inline quint32 recordUint32(const RecordList &list, quint32 index, quint32 fieldOffset) const
173 {
174 // note: fieldOffset is in range: { 0, 4, 8, 12 }
175 const quint64 pos = list.recordOffset(index) + fieldOffset;
176 return qFromBigEndian<quint32>(m_data + pos);
177 }
178 bool load();
179 bool reload();
180
182 uchar *m_data = nullptr;
185 bool m_valid = false;
186
187 // The lists whose offsets are stored in the first 40 bytes of the file,
188 // bounds-checked once by load() so that lookups don't have to.
197};
198
199// Leaves the list empty if the records don't all fit.
200bool QMimeBinaryProvider::RecordList::setRecords(const CacheFile *cacheFile, quint64 firstRecord,
201 quint32 count, quint32 stride)
202{
203 if (!stride || (firstRecord > cacheFile->m_fileSize)) // corrupt cache
204 return false;
205 if (count > (cacheFile->m_fileSize - firstRecord) / stride)
206 return false; // corrupt cache, or more records than can possibly fit
207 m_firstRecord = firstRecord;
208 m_stride = stride;
209 m_count = count;
210 return true;
211}
212
213// Reads the list header pointed to by the offset at posListOffset: the number
214// of records sits at +0 and the records themselves follow at +4.
215bool QMimeBinaryProvider::RecordList::load(const CacheFile *cacheFile, quint64 posListOffset,
216 quint32 stride)
217{
218 const std::optional<quint32> listOffset = cacheFile->getUint32(posListOffset);
219 if (!listOffset)
220 return false; // corrupt cache
221 const std::optional<quint32> n = cacheFile->getUint32(*listOffset);
222 if (!n)
223 return false; // corrupt cache
224 return setRecords(cacheFile, quint64(*listOffset) + 4, *n, stride);
225}
226
227bool QMimeBinaryProvider::RecordList::loadIndirect(const CacheFile *cacheFile,
228 quint64 posListOffset, quint64 firstRecordPos,
229 quint32 stride)
230{
231 const std::optional<quint32> listOffset = cacheFile->getUint32(posListOffset);
232 if (!listOffset)
233 return false; // corrupt cache
234 const std::optional<quint32> n = cacheFile->getUint32(*listOffset);
235 const std::optional<quint32> first = cacheFile->getUint32(*listOffset + firstRecordPos);
236 if (!n || !first)
237 return false; // corrupt cache
238 return setRecords(cacheFile, *first, *n, stride);
239}
240
241static inline void appendIfNew(QStringList &list, const QString &str)
242{
243 if (!list.contains(str))
244 list.push_back(str);
245}
246
247QMimeProviderBase::QMimeProviderBase(QMimeDatabasePrivate *db, const QString &directory)
248 : m_db(db), m_directory(directory)
249{
250}
251
256
261
262bool QMimeProviderBase::isMimeTypeGlobsExcluded(const QString &name) const
263{
264 if (m_overrideProvider) {
265 if (m_overrideProvider->hasGlobDeleteAll(name))
266 return true;
267 return m_overrideProvider->isMimeTypeGlobsExcluded(name);
268 }
269 return false;
270}
271
272QMimeBinaryProvider::QMimeBinaryProvider(QMimeDatabasePrivate *db, const QString &directory)
273 : QMimeProviderBase(db, directory), m_mimetypeListLoaded(false)
274{
276}
277
278QMimeBinaryProvider::CacheFile::CacheFile(const QString &fileName)
280{
281 load();
282}
283
284QMimeBinaryProvider::CacheFile::~CacheFile()
285{
286}
287
288bool QMimeBinaryProvider::CacheFile::load()
289{
290 if (!m_file.open(QIODevice::ReadOnly))
291 return false;
292 const qint64 fileSize = m_file.size();
293 m_fileSize = fileSize > 0 ? fileSize : 0;
294 m_data = m_file.map(0, m_fileSize);
295 if (m_data && m_fileSize >= 4) {
296 const quint16 major = *getUint16(0);
297 const quint16 minor = *getUint16(2);
298 m_valid = (major == 1 && minor >= 1 && minor <= 2)
299 && m_aliases.load(this, PosAliasListOffset, AliasRecordSize)
300 && m_parents.load(this, PosParentListOffset, ParentRecordSize)
301 && m_literals.load(this, PosLiteralListOffset, GlobRecordSize)
302 && m_globs.load(this, PosGlobListOffset, GlobRecordSize)
303 && m_icons.load(this, PosIconsListOffset, IconRecordSize)
304 && m_genericIcons.load(this, PosGenericIconsListOffset, IconRecordSize)
305 && m_suffixTreeRoots.loadIndirect(this, PosReverseSuffixTreeOffset,
306 PosFirstRootOffset, SuffixNodeSize)
307 && m_magicMatches.loadIndirect(this, PosMagicListOffset, PosFirstMatchOffset,
308 MagicMatchSize);
309 }
310 m_mtime = QFileInfo(m_file).lastModified(QTimeZone::UTC);
311 return m_valid;
312}
313
314bool QMimeBinaryProvider::CacheFile::reload()
315{
316 m_valid = false;
317 if (m_file.isOpen()) {
318 m_file.close();
319 }
320 m_data = nullptr;
321 m_fileSize = 0;
322 m_aliases = RecordList();
323 m_parents = RecordList();
324 m_literals = RecordList();
325 m_globs = RecordList();
326 m_suffixTreeRoots = RecordList();
327 m_magicMatches = RecordList();
328 m_icons = RecordList();
329 m_genericIcons = RecordList();
330 return load();
331}
332
333QMimeBinaryProvider::~QMimeBinaryProvider() = default;
334
335bool QMimeBinaryProvider::isValid()
336{
337 return m_cacheFile != nullptr;
338}
339
340bool QMimeBinaryProvider::isInternalDatabase() const
341{
342 return false;
343}
344
345bool QMimeBinaryProvider::checkCacheChanged()
346{
347 QFileInfo fileInfo(m_cacheFile->m_file);
348 if (fileInfo.lastModified(QTimeZone::UTC) > m_cacheFile->m_mtime) {
349 // Deletion can't happen by just running update-mime-database.
350 // But the user could use rm -rf :-)
351 m_cacheFile->reload(); // will mark itself as invalid on failure
352 return true;
353 }
354 return false;
355}
356
357void QMimeBinaryProvider::ensureLoaded()
358{
359 if (!m_cacheFile) {
360 const QString cacheFileName = m_directory + "/mime.cache"_L1;
361 m_cacheFile = std::make_unique<CacheFile>(cacheFileName);
362 m_mimetypeListLoaded = false;
363 m_mimetypeExtra.clear();
364 } else {
365 if (checkCacheChanged()) {
366 m_mimetypeListLoaded = false;
367 m_mimetypeExtra.clear();
368 } else {
369 return; // nothing to do
370 }
371 }
372 if (!m_cacheFile->isValid()) // verify existence and version
373 m_cacheFile.reset();
374}
375
376bool QMimeBinaryProvider::knowsMimeType(const QString &name)
377{
378 if (!m_mimetypeListLoaded)
379 loadMimeTypeList();
380 return m_mimetypeNames.contains(name);
381}
382
383void QMimeBinaryProvider::addFileNameMatches(const QString &fileName, QMimeGlobMatchResult &result)
384{
385 if (fileName.isEmpty())
386 return;
387 Q_ASSERT(m_cacheFile);
388 // Check literals (e.g. "Makefile")
389 quint32 numMatches = matchGlobList(result, m_cacheFile.get(), m_cacheFile->m_literals, fileName);
390 // Check the very common *.txt cases with the suffix tree
391 if (numMatches == 0) {
392 const QString lowerFileName = fileName.toLower();
393 const RecordList &roots = m_cacheFile->m_suffixTreeRoots;
394 if (matchSuffixTree(result, m_cacheFile.get(), roots.count(), roots.firstRecord(),
395 lowerFileName, lowerFileName.size() - 1, false)) {
396 ++numMatches;
397 } else if (matchSuffixTree(result, m_cacheFile.get(), roots.count(), roots.firstRecord(),
398 fileName, fileName.size() - 1, true)) {
399 ++numMatches;
400 }
401 }
402 // Check complex globs (e.g. "callgrind.out[0-9]*" or "README*")
403 if (numMatches == 0)
404 matchGlobList(result, m_cacheFile.get(), m_cacheFile->m_globs, fileName);
405}
406
407quint32 QMimeBinaryProvider::matchGlobList(QMimeGlobMatchResult &result, CacheFile *cacheFile,
408 const RecordList &globs, const QString &fileName)
409{
410 quint32 numMatches = 0;
411 //qDebug() << "Loading" << globs.count() << "globs from" << cacheFile->m_file.fileName();
412 for (quint32 i = 0; i < globs.count(); ++i) {
413 const quint32 globOffset = cacheFile->recordUint32(globs, i, 0);
414 const quint32 mimeTypeOffset = cacheFile->recordUint32(globs, i, 4);
415 const quint32 flagsAndWeight = cacheFile->recordUint32(globs, i, 8);
416 const quint32 weight = flagsAndWeight & 0xff;
417 const bool caseSensitive = flagsAndWeight & 0x100;
418 const Qt::CaseSensitivity qtCaseSensitive = caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive;
419 const QLatin1StringView patternL1 = cacheFile->getLatin1String(globOffset);
420 const QLatin1StringView mimeType = cacheFile->getLatin1String(mimeTypeOffset);
421 if (patternL1.isNull() || mimeType.isNull()) // corrupt cache: skip this record
422 continue;
423
424 //qDebug() << pattern << mimeType << weight << caseSensitive;
425 if (isMimeTypeGlobsExcluded(mimeType))
426 continue;
427
428 const QString pattern = patternL1;
429 QMimeGlobPattern glob(pattern, QString() /*unused*/, weight, qtCaseSensitive);
430 if (glob.matchFileName(fileName)) {
431 result.addMatch(mimeType, weight, pattern);
432 ++numMatches;
433 }
434 }
435 return numMatches;
436}
437
438bool QMimeBinaryProvider::matchSuffixTree(QMimeGlobMatchResult &result,
439 QMimeBinaryProvider::CacheFile *cacheFile, quint32 numEntries,
440 quint64 firstOffset, const QString &fileName,
441 qsizetype charPos, bool caseSensitiveCheck)
442{
443 QChar fileChar = fileName[charPos];
444 if (fileChar.isNull())
445 return false;
446 if (numEntries == 0)
447 return false;
448 quint32 min = 0;
449 quint32 max = numEntries - 1;
450 while (min <= max) {
451 const quint32 mid = (min + max) / 2;
452 const std::optional<quint64> off = cacheFile->safeRecordOffset(firstOffset, 0, mid, SuffixNodeSize);
453 if (!off) // corrupt cache
454 break;
455 const std::optional<quint32> chValue = cacheFile->getUint32(*off);
456 if (!chValue) // corrupt cache
457 break;
458 const QChar ch = char16_t(*chValue);
459 if (ch < fileChar) {
460 min = mid + 1;
461 } else if (ch > fileChar) {
462 if (!mid)
463 break;
464 max = mid - 1;
465 } else {
466 --charPos;
467 const std::optional<quint32> numChildrenOpt = cacheFile->getUint32(*off, 4);
468 const std::optional<quint32> childrenOffsetOpt = cacheFile->getUint32(*off, 8);
469 if (!numChildrenOpt || !childrenOffsetOpt) // corrupt cache
470 return false;
471 const quint32 numChildren = *numChildrenOpt;
472 const quint32 childrenOffset = *childrenOffsetOpt;
473 bool success = false;
474 if (charPos > 0)
475 success = matchSuffixTree(result, cacheFile, numChildren, childrenOffset, fileName, charPos, caseSensitiveCheck);
476 if (!success) {
477 for (quint32 i = 0; i < numChildren; ++i) {
478 const std::optional<quint64> childOff =
479 cacheFile->safeRecordOffset(childrenOffset, 0, i, SuffixNodeSize);
480 if (!childOff) // corrupt cache, or numChildren bigger than can possibly fit
481 break;
482 const std::optional<quint32> mchOpt = cacheFile->getUint32(*childOff);
483 if (!mchOpt) // corrupt cache: no later record can be inside the file either
484 break;
485 if (*mchOpt != 0) // not a leaf entry: end of the leaf list
486 break;
487 const std::optional<quint32> mimeTypeOffset = cacheFile->getUint32(*childOff, 4);
488 const std::optional<quint32> flagsAndWeight = cacheFile->getUint32(*childOff, 8);
489 if (!mimeTypeOffset || !flagsAndWeight) // corrupt cache: nor is any later one
490 break;
491 const QLatin1StringView mimeType = cacheFile->getLatin1String(*mimeTypeOffset);
492 if (mimeType.isNull()) // corrupt cache: skip this record
493 continue;
494 if (isMimeTypeGlobsExcluded(mimeType))
495 continue;
496 const quint32 weight = *flagsAndWeight & 0xff;
497 const bool caseSensitive = *flagsAndWeight & 0x100;
498 if (caseSensitiveCheck || !caseSensitive) {
499 result.addMatch(mimeType, weight,
500 u'*' + QStringView{ fileName }.mid(charPos + 1),
501 fileName.size() - charPos - 2);
502 success = true;
503 }
504 }
505 }
506 return success;
507 }
508 }
509 return false;
510}
511
512bool QMimeBinaryProvider::matchMagicRule(QMimeBinaryProvider::CacheFile *cacheFile, quint32 numMatchlets, quint64 firstOffset, const QByteArray &data)
513{
514 const char *dataPtr = data.constData();
515 const qsizetype dataSize = data.size();
516 for (quint32 matchlet = 0; matchlet < numMatchlets; ++matchlet) {
517 const std::optional<quint64> off = cacheFile->safeRecordOffset(firstOffset, 0, matchlet, MagicMatchletSize);
518 if (!off) // corrupt cache, or numMatchlets bigger than can possibly fit
519 break;
520 const std::optional<quint32> rangeStartOpt = cacheFile->getUint32(*off);
521 const std::optional<quint32> rangeLengthOpt = cacheFile->getUint32(*off, 4);
522 //const auto wordSize = cacheFile->getUint32(*off, 8);
523 const std::optional<quint32> valueLengthOpt = cacheFile->getUint32(*off, 12);
524 const std::optional<quint32> valueOffsetOpt = cacheFile->getUint32(*off, 16);
525 const std::optional<quint32> maskOffsetOpt = cacheFile->getUint32(*off, 20);
526 if (!rangeStartOpt || !rangeLengthOpt || !valueLengthOpt || !valueOffsetOpt
527 || !maskOffsetOpt) // corrupt cache: no later matchlet can be inside the file either
528 break;
529 const quint32 rangeStart = *rangeStartOpt;
530 const quint32 rangeLength = *rangeLengthOpt;
531 const quint32 valueLength = *valueLengthOpt;
532 const quint32 valueOffset = *valueOffsetOpt;
533 const quint32 maskOffset = *maskOffsetOpt;
534 const char *value = cacheFile->getData(valueOffset, valueLength);
535 const char *mask = maskOffset ? cacheFile->getData(maskOffset, valueLength) : nullptr;
536 if (!value || (maskOffset && !mask)) // corrupt cache
537 continue;
538
539 if (!QMimeMagicRule::matchSubstring(dataPtr, dataSize, rangeStart, rangeLength, valueLength, value, mask))
540 continue;
541
542 const std::optional<quint32> numChildrenOpt = cacheFile->getUint32(*off, 24);
543 const std::optional<quint32> firstChildOffsetOpt = cacheFile->getUint32(*off, 28);
544 if (!numChildrenOpt || !firstChildOffsetOpt) // corrupt cache: nor is any later matchlet
545 break;
546 const quint32 numChildren = *numChildrenOpt;
547 const quint32 firstChildOffset = *firstChildOffsetOpt;
548 if (numChildren == 0) // No submatch? Then we are done.
549 return true;
550 // Check that one of the submatches matches too
551 if (matchMagicRule(cacheFile, numChildren, firstChildOffset, data))
552 return true;
553 }
554 return false;
555}
556
557bool QMimeBinaryProvider::magicRecordMatches(quint32 index, const QByteArray &data) const
558{
559 const RecordList &matches = m_cacheFile->m_magicMatches;
560 const quint32 numMatchlets = m_cacheFile->recordUint32(matches, index, 8);
561 const quint32 firstMatchletOffset = m_cacheFile->recordUint32(matches, index, 12);
562 return matchMagicRule(m_cacheFile.get(), numMatchlets, firstMatchletOffset, data);
563}
564
565void QMimeBinaryProvider::findByMagic(const QByteArray &data, QMimeMagicResult &result)
566{
567 const RecordList &matches = m_cacheFile->m_magicMatches;
568 for (quint32 i = 0; i < matches.count(); ++i) {
569 if (magicRecordMatches(i, data)) {
570 const int accuracy = static_cast<int>(m_cacheFile->recordUint32(matches, i, 0));
571 if (accuracy > result.accuracy) {
572 const quint32 mimeTypeOffset = m_cacheFile->recordUint32(matches, i, 4);
573 const QLatin1StringView candidate = m_cacheFile->getLatin1String(mimeTypeOffset);
574 if (candidate.isNull()) // corrupt cache: skip this record
575 continue;
576 result.accuracy = accuracy;
577 result.candidate = candidate;
578 // Return the first match, mime.cache is sorted
579 return;
580 }
581 }
582 }
583}
584
586QMimeBinaryProvider::checkMagicRules(const QString &mime, const QByteArray &data) const
587{
589 const RecordList &matches = m_cacheFile->m_magicMatches;
590 for (quint32 i = 0; i < matches.count(); ++i) {
591 const quint32 mimeTypeOffset = m_cacheFile->recordUint32(matches, i, 4);
592 if (m_cacheFile->getLatin1String(mimeTypeOffset) != mime)
593 continue;
594 result.hasRules = true;
595 if (magicRecordMatches(i, data)) {
596 result.matched = true;
597 break;
598 }
599 }
600 return result;
601}
602
603void QMimeBinaryProvider::addParents(const QString &mime, QStringList &result)
604{
605 const RecordList &parentList = m_cacheFile->m_parents;
606 const quint32 count = parentList.count();
607 if (!count)
608 return;
609 quint32 begin = 0;
610 quint32 end = count - 1;
611 while (begin <= end) {
612 const quint32 medium = (begin + end) / 2;
613 const quint32 mimeOffset = m_cacheFile->recordUint32(parentList, medium, 0);
614 const QLatin1StringView aMime = m_cacheFile->getLatin1String(mimeOffset);
615 if (aMime.isNull()) // corrupt cache: the search cannot continue
616 break;
617 const int cmp = aMime.compare(mime);
618 if (cmp < 0) {
619 begin = medium + 1;
620 } else if (cmp > 0) {
621 if (!medium)
622 break;
623 end = medium - 1;
624 } else {
625 // The parent list is reached by following an offset stored in the
626 // record, so it has to be bounds-checked here.
627 const quint32 parentsOffset = m_cacheFile->recordUint32(parentList, medium, 4);
628 const std::optional<quint32> numParentsOpt = m_cacheFile->getUint32(parentsOffset);
629 if (!numParentsOpt) // corrupt cache
630 break;
631 const quint32 numParents = *numParentsOpt;
632 for (quint32 i = 0; i < numParents; ++i) {
633 const std::optional<quint64> parentOff =
634 m_cacheFile->safeRecordOffset(parentsOffset, 4, i, 4);
635 if (!parentOff) // past end of file: no later index can be inside it
636 break;
637 const std::optional<quint32> parentOffsetOpt = m_cacheFile->getUint32(*parentOff);
638 if (!parentOffsetOpt) // corrupt cache: no later index can be inside the file either
639 break;
640 const QLatin1StringView parentL1 = m_cacheFile->getLatin1String(*parentOffsetOpt);
641 if (parentL1.isNull()) // corrupt cache: skip this record
642 break;
643 appendIfNew(result, QString(parentL1));
644 }
645 break;
646 }
647 }
648}
649
650QString QMimeBinaryProvider::resolveAlias(const QString &name)
651{
652 const RecordList &aliases = m_cacheFile->m_aliases;
653 quint32 begin = 0;
654 const quint32 count = aliases.count();
655 if (!count)
656 return QString();
657 quint32 end = count - 1;
658 while (begin <= end) {
659 const quint32 medium = (begin + end) / 2;
660 const quint32 aliasOffset = m_cacheFile->recordUint32(aliases, medium, 0);
661 const QLatin1StringView alias = m_cacheFile->getLatin1String(aliasOffset);
662 if (alias.isNull()) // corrupt cache: the search cannot continue
663 break;
664 const int cmp = alias.compare(name);
665 if (cmp < 0) {
666 begin = medium + 1;
667 } else if (cmp > 0) {
668 if (!medium)
669 break;
670 end = medium - 1;
671 } else {
672 const quint32 mimeOffset = m_cacheFile->recordUint32(aliases, medium, 4);
673 // if corrupt cache, returns QLatin1StringView(), which converts to a null QString
674 return m_cacheFile->getLatin1String(mimeOffset);
675 }
676 }
677 return QString();
678}
679
680void QMimeBinaryProvider::addAliases(const QString &name, QStringList &result)
681{
682 const RecordList &aliases = m_cacheFile->m_aliases;
683 for (quint32 pos = 0; pos < aliases.count(); ++pos) {
684 const quint32 mimeOffset = m_cacheFile->recordUint32(aliases, pos, 4);
685 const QLatin1StringView mimeType = m_cacheFile->getLatin1String(mimeOffset);
686 if (mimeType.isNull()) // corrupt cache: skip this record
687 continue;
688
689 if (name == QLatin1StringView(mimeType)) {
690 const quint32 aliasOffset = m_cacheFile->recordUint32(aliases, pos, 0);
691 const QLatin1StringView aliasL1 = m_cacheFile->getLatin1String(aliasOffset);
692 if (aliasL1.isNull()) // corrupt cache: skip this record
693 continue;
694 appendIfNew(result, QString(aliasL1));
695 }
696 }
697}
698
699void QMimeBinaryProvider::loadMimeTypeList()
700{
701 if (!m_mimetypeListLoaded) {
702 m_mimetypeListLoaded = true;
703 m_mimetypeNames.clear();
704 // Unfortunately mime.cache doesn't have a full list of all mimetypes.
705 // So we have to parse the plain-text files called "types".
706 QFile file(m_directory + QStringView(u"/types"));
707 if (file.open(QIODevice::ReadOnly)) {
708 QByteArray line;
709 while (file.readLineInto(&line)) {
710 auto lineView = QByteArrayView(line);
711 if (lineView.endsWith('\n'))
712 lineView.chop(1);
713 m_mimetypeNames.insert(QString::fromLatin1(lineView));
714 }
715 }
716 }
717}
718
719void QMimeBinaryProvider::addAllMimeTypes(QList<QMimeType> &result)
720{
721 loadMimeTypeList();
722 if (result.isEmpty()) {
723 result.reserve(m_mimetypeNames.size());
724 for (const QString &name : std::as_const(m_mimetypeNames))
725 result.append(QMimeType(QMimeTypePrivate(name)));
726 } else {
727 for (const QString &name : std::as_const(m_mimetypeNames))
728 if (std::find_if(result.constBegin(), result.constEnd(), [name](const QMimeType &mime) -> bool { return mime.name() == name; })
729 == result.constEnd())
730 result.append(QMimeType(QMimeTypePrivate(name)));
731 }
732}
733
734QMimeTypePrivate::LocaleHash QMimeBinaryProvider::localeComments(const QString &name)
735{
736 MimeTypeExtraMap::const_iterator it = loadMimeTypeExtra(name);
737 if (it != m_mimetypeExtra.cend())
738 return it->second.localeComments;
739 return {};
740}
741
742bool QMimeBinaryProvider::hasGlobDeleteAll(const QString &name)
743{
744 MimeTypeExtraMap::const_iterator it = loadMimeTypeExtra(name);
745 if (it != m_mimetypeExtra.cend())
746 return it->second.hasGlobDeleteAll;
747 return {};
748}
749
750QStringList QMimeBinaryProvider::globPatterns(const QString &name)
751{
752 MimeTypeExtraMap::const_iterator it = loadMimeTypeExtra(name);
753 if (it != m_mimetypeExtra.cend())
754 return it->second.globPatterns;
755 return {};
756}
757
758QMimeBinaryProvider::MimeTypeExtraMap::const_iterator
759QMimeBinaryProvider::loadMimeTypeExtra(const QString &mimeName)
760{
761#if QT_CONFIG(xmlstreamreader)
762 auto [it, insertionOccurred] = m_mimetypeExtra.try_emplace(mimeName);
763 if (insertionOccurred) {
764 // load comment and globPatterns
765
766 // shared-mime-info since 1.3 lowercases the xml files
767 QFile qfile;
768 const QString mimeFile = m_directory + u'/' + mimeName.toLower() + ".xml"_L1;
769 qfile.setFileName(mimeFile);
770 if (!qfile.open(QFile::ReadOnly)) {
771 const QString fallbackMimeFile = m_directory + u'/' + mimeName + ".xml"_L1; // pre-1.3
772 qfile.setFileName(fallbackMimeFile);
773 if (!qfile.open(QFile::ReadOnly))
774 return it;
775 }
776
777 MimeTypeExtra &extra = it->second;
778 QString mainPattern;
779
780 QXmlStreamReader xml(&qfile);
781 if (xml.readNextStartElement()) {
782 if (xml.name() != "mime-type"_L1) {
783 return m_mimetypeExtra.cend();
784 }
785 const auto name = xml.attributes().value("type"_L1);
786 if (name.isEmpty())
787 return m_mimetypeExtra.cend();
788 if (name.compare(mimeName, Qt::CaseInsensitive))
789 qWarning() << "Got name" << name << "in file" << mimeFile << "expected" << mimeName;
790
791 while (xml.readNextStartElement()) {
792 const auto tag = xml.name();
793 if (tag == "comment"_L1) {
794 QString lang = xml.attributes().value("xml:lang"_L1).toString();
795 const QString text = xml.readElementText();
796 if (lang.isEmpty()) {
797 lang = "default"_L1; // no locale attribute provided, treat it as default.
798 }
799 extra.localeComments.insert(lang, text);
800 continue; // we called readElementText, so we're at the EndElement already.
801 } else if (tag == "glob-deleteall"_L1) { // as written out by shared-mime-info >= 0.70
802 extra.hasGlobDeleteAll = true;
803 } else if (tag == "glob"_L1) { // as written out by shared-mime-info >= 0.70
804 const QString pattern = xml.attributes().value("pattern"_L1).toString();
805 if (mainPattern.isEmpty() && pattern.startsWith(u'*')) {
806 mainPattern = pattern;
807 }
808 appendIfNew(extra.globPatterns, pattern);
809 }
810 xml.skipCurrentElement();
811 }
812 Q_ASSERT(xml.name() == "mime-type"_L1);
813 }
814
815 // Let's assume that shared-mime-info is at least version 0.70
816 // Otherwise we would need 1) a version check, and 2) code for parsing patterns from the globs file.
817 if (!mainPattern.isEmpty() &&
818 (extra.globPatterns.isEmpty() || extra.globPatterns.constFirst() != mainPattern)) {
819 // ensure it's first in the list of patterns
820 extra.globPatterns.removeAll(mainPattern);
821 extra.globPatterns.prepend(mainPattern);
822 }
823 }
824 return it;
825#else
826 Q_UNUSED(mimeName);
827 qWarning("Cannot load mime type since QXmlStreamReader is not available.");
828 return m_mimetypeExtra.cend();
829#endif // feature xmlstreamreader
830}
831
832// Binary search in the icons or generic-icons list
833QLatin1StringView QMimeBinaryProvider::iconForMime(CacheFile *cacheFile, const RecordList &icons,
834 QStringView inputMime)
835{
836 quint32 begin = 0;
837 const quint32 count = icons.count();
838 if (!count)
839 return QLatin1StringView();
840 quint32 end = count - 1;
841 while (begin <= end) {
842 const quint32 medium = (begin + end) / 2;
843 const quint32 mimeOffset = cacheFile->recordUint32(icons, medium, 0);
844 const QLatin1StringView mime = cacheFile->getLatin1String(mimeOffset);
845 if (mime.isNull()) // corrupt cache: the search cannot continue
846 break;
847 const int cmp = mime.compare(inputMime);
848 if (cmp < 0) {
849 begin = medium + 1;
850 } else if (cmp > 0) {
851 if (!medium)
852 break;
853 end = medium - 1;
854 } else {
855 const quint32 iconOffset = cacheFile->recordUint32(icons, medium, 4);
856 // if corrupt cache, returns QLatin1StringView()
857 return cacheFile->getLatin1String(iconOffset);
858 }
859 }
860 return QLatin1StringView();
861}
862
863QString QMimeBinaryProvider::icon(const QString &name)
864{
865 return iconForMime(m_cacheFile.get(), m_cacheFile->m_icons, name);
866}
867
868QString QMimeBinaryProvider::genericIcon(const QString &name)
869{
870 return iconForMime(m_cacheFile.get(), m_cacheFile->m_genericIcons, name);
871}
872
873////
874
875#if QT_CONFIG(mimetype_database)
876static QString internalMimeFileName()
877{
878 return QStringLiteral("<internal MIME data>");
879}
880
881QMimeXMLProvider::QMimeXMLProvider(QMimeDatabasePrivate *db, InternalDatabaseEnum)
882 : QMimeProviderBase(db, internalMimeFileName())
883{
884 static_assert(sizeof(mimetype_database), "Bundled MIME database is empty");
885 static_assert(sizeof(mimetype_database) <= MimeTypeDatabaseOriginalSize,
886 "Compressed MIME database is larger than the original size");
887 static_assert(MimeTypeDatabaseOriginalSize <= 16*1024*1024,
888 "Bundled MIME database is too big");
889 const char *data = reinterpret_cast<const char *>(mimetype_database);
890 qsizetype size = MimeTypeDatabaseOriginalSize;
891
892#ifdef MIME_DATABASE_IS_ZSTD
893 // uncompress with libzstd
894 std::unique_ptr<char []> uncompressed(new char[size]);
895 size = ZSTD_decompress(uncompressed.get(), size, mimetype_database, sizeof(mimetype_database));
896 Q_ASSERT(!ZSTD_isError(size));
897 data = uncompressed.get();
898#elif defined(MIME_DATABASE_IS_GZIP)
899 std::unique_ptr<char []> uncompressed(new char[size]);
900 z_stream zs = {};
901 zs.next_in = const_cast<Bytef *>(mimetype_database);
902 zs.avail_in = sizeof(mimetype_database);
903 zs.next_out = reinterpret_cast<Bytef *>(uncompressed.get());
904 zs.avail_out = size;
905
906 int res = inflateInit2(&zs, MAX_WBITS | 32);
907 Q_ASSERT(res == Z_OK);
908 res = inflate(&zs, Z_FINISH);
909 Q_ASSERT(res == Z_STREAM_END);
910 res = inflateEnd(&zs);
911 Q_ASSERT(res == Z_OK);
912
913 data = uncompressed.get();
914 size = zs.total_out;
915#endif
916
917 load(data, size);
918 sortMagicMatchers();
919}
920#else // !QT_CONFIG(mimetype_database)
921// never called in release mode, but some debug builds may need
922// this to be defined.
924 : QMimeProviderBase(db, QString())
925{
926 Q_UNREACHABLE();
927}
928#endif // QT_CONFIG(mimetype_database)
929
930QMimeXMLProvider::QMimeXMLProvider(QMimeDatabasePrivate *db, const QString &directory)
931 : QMimeProviderBase(db, directory)
932{
934}
935
936QMimeXMLProvider::~QMimeXMLProvider()
937{
938}
939
940bool QMimeXMLProvider::isValid()
941{
942 // If you change this method, adjust the logic in QMimeDatabasePrivate::loadProviders,
943 // which assumes isValid==false is only possible in QMimeBinaryProvider.
944 return true;
945}
946
947bool QMimeXMLProvider::isInternalDatabase() const
948{
949#if QT_CONFIG(mimetype_database)
950 return m_directory == internalMimeFileName();
951#else
952 return false;
953#endif
954}
955
956bool QMimeXMLProvider::knowsMimeType(const QString &name)
957{
958 return m_nameMimeTypeMap.contains(name);
959}
960
961void QMimeXMLProvider::addFileNameMatches(const QString &fileName, QMimeGlobMatchResult &result)
962{
963 auto filterFunc = [this](const QString &name) { return !isMimeTypeGlobsExcluded(name); };
964 m_mimeTypeGlobs.matchingGlobs(fileName, result, filterFunc);
965}
966
967void QMimeXMLProvider::findByMagic(const QByteArray &data, QMimeMagicResult &result)
968{
969 for (const QMimeMagicRuleMatcher &matcher : std::as_const(m_magicMatchers)) {
970 if (matcher.matches(data)) {
971 const int priority = matcher.priority();
972 if (priority > result.accuracy) {
973 result.accuracy = priority;
974 result.candidate = matcher.mimetype();
975 }
976 }
977 }
978}
979
981QMimeXMLProvider::checkMagicRules(const QString &mime, const QByteArray &data) const
982{
984 for (const QMimeMagicRuleMatcher &matcher : m_magicMatchers) {
985 if (matcher.mimetype() != mime)
986 continue;
987 result.hasRules = true;
988 if (matcher.matches(data)) {
989 result.matched = true;
990 break;
991 }
992 }
993 return result;
994}
995
996void QMimeXMLProvider::ensureLoaded()
997{
998 QStringList allFiles;
999 const QString packageDir = m_directory + QStringView(u"/packages");
1000 for (const auto &entry : QDirListing(packageDir, QDirListing::IteratorFlag::FilesOnly
1001 | QDirListing::IteratorFlag::ResolveSymlinks))
1002 allFiles.emplace_back(packageDir + u'/' + entry.fileName());
1003
1004 if (m_allFiles == allFiles)
1005 return;
1006 m_allFiles = allFiles;
1007
1008 m_nameMimeTypeMap.clear();
1009 m_aliases.clear();
1010 m_parents.clear();
1011 m_mimeTypeGlobs.clear();
1012 m_magicMatchers.clear();
1013
1014 //qDebug() << "Loading" << m_allFiles;
1015
1016 for (const QString &file : std::as_const(allFiles))
1017 load(file);
1018 sortMagicMatchers();
1019}
1020
1021// Same order as in mime.cache: highest priority first, then by mimetype name,
1022// so that both providers pick the same mimetype when several rules match.
1023void QMimeXMLProvider::sortMagicMatchers()
1024{
1025 std::stable_sort(m_magicMatchers.begin(), m_magicMatchers.end(),
1026 [](const QMimeMagicRuleMatcher &a, const QMimeMagicRuleMatcher &b) {
1027 if (a.priority() != b.priority())
1028 return a.priority() > b.priority();
1029 return a.mimetype() < b.mimetype();
1030 });
1031}
1032
1033QMimeTypePrivate::LocaleHash QMimeXMLProvider::localeComments(const QString &name)
1034{
1035 return m_nameMimeTypeMap.value(name).localeComments;
1036}
1037
1038bool QMimeXMLProvider::hasGlobDeleteAll(const QString &name)
1039{
1040 return m_nameMimeTypeMap.value(name).hasGlobDeleteAll;
1041}
1042
1043QStringList QMimeXMLProvider::globPatterns(const QString &name)
1044{
1045 return m_nameMimeTypeMap.value(name).globPatterns;
1046}
1047
1048QString QMimeXMLProvider::icon(const QString &name)
1049{
1050 return m_nameMimeTypeMap.value(name).iconName;
1051}
1052
1053QString QMimeXMLProvider::genericIcon(const QString &name)
1054{
1055 return m_nameMimeTypeMap.value(name).genericIconName;
1056}
1057
1058void QMimeXMLProvider::load(const QString &fileName)
1059{
1060 QString errorMessage;
1061 if (!load(fileName, &errorMessage))
1062 qWarning("QMimeDatabase: Error loading %ls\n%ls", qUtf16Printable(fileName), qUtf16Printable(errorMessage));
1063}
1064
1065bool QMimeXMLProvider::load(const QString &fileName, QString *errorMessage)
1066{
1067 QFile file(fileName);
1068 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1069 if (errorMessage)
1070 *errorMessage = "Cannot open "_L1 + fileName + ": "_L1 + file.errorString();
1071 return false;
1072 }
1073
1074 if (errorMessage)
1075 errorMessage->clear();
1076
1077 QMimeTypeParser parser(*this);
1078 return parser.parse(&file, fileName, errorMessage);
1079}
1080
1081#if QT_CONFIG(mimetype_database)
1082void QMimeXMLProvider::load(const char *data, qsizetype len)
1083{
1084 QBuffer buffer;
1085 buffer.setData(QByteArray::fromRawData(data, len));
1086 buffer.open(QIODevice::ReadOnly);
1087 QString errorMessage;
1088 QMimeTypeParser parser(*this);
1089 if (!parser.parse(&buffer, internalMimeFileName(), &errorMessage))
1090 qWarning("QMimeDatabase: Error loading internal MIME data\n%s", qPrintable(errorMessage));
1091}
1092#endif
1093
1094void QMimeXMLProvider::addGlobPattern(const QMimeGlobPattern &glob)
1095{
1096 m_mimeTypeGlobs.addGlob(glob);
1097}
1098
1099void QMimeXMLProvider::addMimeType(const QMimeTypeXMLData &mt)
1100{
1101 m_nameMimeTypeMap.insert(mt.name, mt);
1102}
1103
1104void QMimeXMLProvider::addParents(const QString &mime, QStringList &result)
1105{
1106 const QStringList parents = m_parents.value(mime);
1107 for (const QString &parent : parents) {
1108 if (!result.contains(parent))
1109 result.append(parent);
1110 }
1111}
1112
1113void QMimeXMLProvider::addParent(const QString &child, const QString &parent)
1114{
1115 m_parents[child].append(parent);
1116}
1117
1118void QMimeXMLProvider::addAliases(const QString &name, QStringList &result)
1119{
1120 // Iterate through the whole hash. This method is rarely used.
1121 for (const auto &[alias, mimeName] : std::as_const(m_aliases).asKeyValueRange()) {
1122 if (mimeName == name)
1123 appendIfNew(result, alias);
1124 }
1125}
1126
1127QString QMimeXMLProvider::resolveAlias(const QString &name)
1128{
1129 return m_aliases.value(name);
1130}
1131
1132void QMimeXMLProvider::addAlias(const QString &alias, const QString &name)
1133{
1134 m_aliases.insert(alias, name);
1135}
1136
1137void QMimeXMLProvider::addAllMimeTypes(QList<QMimeType> &result)
1138{
1139 if (result.isEmpty()) { // fast path
1140 for (auto it = m_nameMimeTypeMap.constBegin(), end = m_nameMimeTypeMap.constEnd();
1141 it != end; ++it) {
1142 result.append(QMimeType(QMimeTypePrivate(it.value().name)));
1143 }
1144 } else {
1145 for (auto it = m_nameMimeTypeMap.constBegin(), end = m_nameMimeTypeMap.constEnd() ; it != end ; ++it) {
1146 const QString newMime = it.key();
1147 if (std::find_if(result.constBegin(), result.constEnd(), [newMime](const QMimeType &mime) -> bool { return mime.name() == newMime; })
1148 == result.constEnd())
1149 result.append(QMimeType(QMimeTypePrivate(it.value().name)));
1150 }
1151 }
1152}
1153
1154void QMimeXMLProvider::addMagicMatcher(const QMimeMagicRuleMatcher &matcher)
1155{
1156 m_magicMatchers.append(matcher);
1157}
1158
1159QT_END_NAMESPACE
void addGlob(const QMimeGlobPattern &glob)
quint64 recordOffset(quint32 index) const
bool loadIndirect(const CacheFile *cacheFile, quint64 posListOffset, quint64 firstRecordPos, quint32 stride)
bool load(const CacheFile *cacheFile, quint64 posListOffset, quint32 stride)
void addFileNameMatches(const QString &fileName, QMimeGlobMatchResult &result) override
QString resolveAlias(const QString &name) override
void findByMagic(const QByteArray &data, QMimeMagicResult &result) override
bool knowsMimeType(const QString &name) override
QString icon(const QString &name) override
bool hasGlobDeleteAll(const QString &name) override
void ensureLoaded() override
bool isValid() override
void addAliases(const QString &name, QStringList &result) override
virtual ~QMimeBinaryProvider()
void addAllMimeTypes(QList< QMimeType > &result) override
void addParents(const QString &mime, QStringList &result) override
QMimeMagicCheckResult checkMagicRules(const QString &mime, const QByteArray &data) const override
QMimeBinaryProvider(QMimeDatabasePrivate *db, const QString &directory)
QStringList globPatterns(const QString &name) override
QString genericIcon(const QString &name) override
bool isInternalDatabase() const override
QMimeTypePrivate::LocaleHash localeComments(const QString &name) override
The QMimeGlobPattern class contains the glob pattern for file names for MIME type matching.
bool isMimeTypeGlobsExcluded(const QString &name) const
QMimeProviderBase * m_overrideProvider
QMimeProviderBase * overrideProvider() const
void setOverrideProvider(QMimeProviderBase *provider)
QMimeDatabasePrivate * m_db
\inmodule QtCore
QMimeTypeParser(QMimeXMLProvider &provider)
void addFileNameMatches(const QString &fileName, QMimeGlobMatchResult &result) override
void addGlobPattern(const QMimeGlobPattern &glob)
void addAliases(const QString &name, QStringList &result) override
void addAlias(const QString &alias, const QString &name)
QMimeTypePrivate::LocaleHash localeComments(const QString &name) override
QStringList globPatterns(const QString &name) override
bool load(const QString &fileName, QString *errorMessage)
QMimeXMLProvider(QMimeDatabasePrivate *db, const QString &directory)
void ensureLoaded() override
bool knowsMimeType(const QString &name) override
bool isValid() override
bool isInternalDatabase() const override
QString resolveAlias(const QString &name) override
void addMimeType(const QMimeTypeXMLData &mt)
QMimeMagicCheckResult checkMagicRules(const QString &mime, const QByteArray &data) const override
void findByMagic(const QByteArray &data, QMimeMagicResult &result) override
QString icon(const QString &name) override
void addParents(const QString &mime, QStringList &result) override
void addAllMimeTypes(QList< QMimeType > &result) override
QMimeXMLProvider(QMimeDatabasePrivate *db, InternalDatabaseEnum)
void addMagicMatcher(const QMimeMagicRuleMatcher &matcher)
bool hasGlobDeleteAll(const QString &name) override
void addParent(const QString &child, const QString &parent)
QString genericIcon(const QString &name) override
@ PosMagicListOffset
@ PosGenericIconsListOffset
@ PosIconsListOffset
@ PosParentListOffset
@ PosLiteralListOffset
@ PosAliasListOffset
@ PosGlobListOffset
@ PosReverseSuffixTreeOffset
@ AliasRecordSize
@ SuffixNodeSize
@ ParentRecordSize
@ IconRecordSize
@ GlobRecordSize
@ MagicMatchletSize
@ MagicMatchSize
@ PosFirstRootOffset
@ PosFirstMatchOffset
static void appendIfNew(QStringList &list, const QString &str)
std::optional< quint32 > getUint32(quint64 offset, quint64 extra=0) const
std::optional< quint16 > getUint16(quint64 offset) const
quint32 recordUint32(const RecordList &list, quint32 index, quint32 fieldOffset) const
QLatin1StringView getLatin1String(quint64 offset) const
CacheFile(const QString &fileName)
std::optional< quint64 > safeRecordOffset(quint64 base, quint64 headerSize, quint32 index, quint32 stride) const
const char * getData(quint64 offset, quint64 length) const