6 #include "staticcompressed_p.h"
8 #include <Cutelyst/Application>
9 #include <Cutelyst/Request>
10 #include <Cutelyst/Response>
11 #include <Cutelyst/Context>
12 #include <Cutelyst/Engine>
14 #include <QMimeDatabase>
17 #include <QStandardPaths>
18 #include <QCoreApplication>
19 #include <QCryptographicHash>
20 #include <QLoggingCategory>
21 #include <QDataStream>
24 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
28 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
29 #include <brotli/encode.h>
34 Q_LOGGING_CATEGORY(C_STATICCOMPRESSED,
"cutelyst.plugin.staticcompressed", QtWarningMsg)
37 Plugin(parent), d_ptr(new StaticCompressedPrivate)
40 d->includePaths.append(parent->config(QStringLiteral(
"root")).toString());
51 d->includePaths.clear();
52 for (
const QString &path : paths) {
53 d->includePaths.append(
QDir(path));
67 const QVariantMap config = app->
engine()->
config(QStringLiteral(
"Cutelyst_StaticCompressed_Plugin"));
69 d->cacheDir.setPath(config.value(QStringLiteral(
"cache_directory"), _defaultCacheDir).toString());
71 if (Q_UNLIKELY(!d->cacheDir.exists())) {
72 if (!d->cacheDir.mkpath(d->cacheDir.absolutePath())) {
73 qCCritical(C_STATICCOMPRESSED,
"Failed to create cache directory for compressed static files at \"%s\".", qPrintable(d->cacheDir.absolutePath()));
78 qCInfo(C_STATICCOMPRESSED,
"Compressed cache directory: %s", qPrintable(d->cacheDir.absolutePath()));
80 const QString _mimeTypes = config.value(QStringLiteral(
"mime_types"), QStringLiteral(
"text/css,application/javascript")).toString();
81 qCInfo(C_STATICCOMPRESSED,
"MIME Types: %s", qPrintable(_mimeTypes));
83 #
if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
89 const QString _suffixes = config.value(QStringLiteral(
"suffixes"), QStringLiteral(
"js.map,css.map,min.js.map,min.css.map")).toString();
90 qCInfo(C_STATICCOMPRESSED,
"Suffixes: %s", qPrintable(_suffixes));
92 #
if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
98 d->checkPreCompressed = config.value(QStringLiteral(
"check_pre_compressed"),
true).toBool();
99 qCInfo(C_STATICCOMPRESSED,
"Check for pre-compressed files: %s", d->checkPreCompressed ?
"true" :
"false");
101 d->onTheFlyCompression = config.value(QStringLiteral(
"on_the_fly_compression"),
true).toBool();
102 qCInfo(C_STATICCOMPRESSED,
"Compress static files on the fly: %s", d->onTheFlyCompression ?
"true" :
"false");
104 QStringList supportedCompressions{QStringLiteral(
"deflate"), QStringLiteral(
"gzip")};
107 d->zlibCompressionLevel = config.
value(QStringLiteral(
"zlib_compression_level"), 9).toInt(&ok);
108 if (!ok || (d->zlibCompressionLevel < -1) || (d->zlibCompressionLevel > 9)) {
109 d->zlibCompressionLevel = -1;
112 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
113 d->zopfliIterations = config.value(QStringLiteral(
"zopfli_iterations"), 15).toInt(&ok);
114 if (!ok || (d->zopfliIterations < 0)) {
115 d->zopfliIterations = 15;
117 d->useZopfli = config.value(QStringLiteral(
"use_zopfli"),
false).toBool();
118 supportedCompressions << QStringLiteral(
"zopfli");
121 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
122 d->brotliQualityLevel = config.value(QStringLiteral(
"brotli_quality_level"), BROTLI_DEFAULT_QUALITY).toInt(&ok);
123 if (!ok || (d->brotliQualityLevel < BROTLI_MIN_QUALITY) || (d->brotliQualityLevel > BROTLI_MAX_QUALITY)) {
124 d->brotliQualityLevel = BROTLI_DEFAULT_QUALITY;
126 supportedCompressions << QStringLiteral(
"brotli");
129 qCInfo(C_STATICCOMPRESSED,
"Supported compressions: %s", qPrintable(supportedCompressions.join(
QLatin1Char(
','))));
132 d->beforePrepareAction(c, skipMethod);
138 void StaticCompressedPrivate::beforePrepareAction(
Context *c,
bool *skipMethod)
144 const QString path = c->req()->path();
147 for (
const QString &dir : dirs) {
149 if (!locateCompressedFile(c, path)) {
153 res->
setBody(QStringLiteral(
"File not found: ") + path);
162 if (match.
hasMatch() && locateCompressedFile(c, path)) {
167 bool StaticCompressedPrivate::locateCompressedFile(
Context *c,
const QString &relPath)
const
169 for (
const QDir &includePath : includePaths) {
170 const QString path = includePath.absoluteFilePath(relPath);
172 if (fileInfo.exists()) {
174 const QDateTime currentDateTime = fileInfo.lastModified();
193 _mimeTypeName = QStringLiteral(
"application/json");
199 const QString acceptEncoding = c->req()->
header(QStringLiteral(
"Accept-Encoding"));
200 qCDebug(C_STATICCOMPRESSED) <<
"Accept-Encoding:" << acceptEncoding;
202 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
204 compressedPath = locateCacheFile(path, currentDateTime, Brotli) ;
205 if (!compressedPath.
isEmpty()) {
206 qCDebug(C_STATICCOMPRESSED,
"Serving brotli compressed data from \"%s\".", qPrintable(compressedPath));
207 contentEncoding = QStringLiteral(
"br");
212 compressedPath = locateCacheFile(path, currentDateTime, useZopfli ? Zopfli : Gzip);
213 if (!compressedPath.
isEmpty()) {
214 qCDebug(C_STATICCOMPRESSED,
"Serving %s compressed data from \"%s\".", useZopfli ?
"zopfli" :
"gzip", qPrintable(compressedPath));
215 contentEncoding = QStringLiteral(
"gzip");
218 compressedPath = locateCacheFile(path, currentDateTime, Deflate);
219 if (!compressedPath.
isEmpty()) {
220 qCDebug(C_STATICCOMPRESSED,
"Serving deflate compressed data from \"%s\".", qPrintable(compressedPath));
221 contentEncoding = QStringLiteral(
"deflate");
230 qCDebug(C_STATICCOMPRESSED) <<
"Serving" << path;
238 if (!_mimeTypeName.
isEmpty()) {
240 }
else if (mimeType.
isValid()) {
247 headers.
setHeader(QStringLiteral(
"CACHE_CONTROL"), QStringLiteral(
"public"));
249 if (!contentEncoding.
isEmpty()) {
254 headers.
pushHeader(QStringLiteral(
"Vary"), QStringLiteral(
"Accept-Encoding"));
260 qCWarning(C_STATICCOMPRESSED) <<
"Could not serve" << path << file->
errorString();
265 qCWarning(C_STATICCOMPRESSED) <<
"File not found" << relPath;
269 QString StaticCompressedPrivate::locateCacheFile(
const QString &origPath,
const QDateTime &origLastModified, Compression compression)
const
275 switch (compression) {
278 suffix = QStringLiteral(
".gz");
280 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
282 suffix = QStringLiteral(
".br");
286 suffix = QStringLiteral(
".deflate");
289 Q_ASSERT_X(
false,
"locate cache file",
"invalid compression type");
293 if (checkPreCompressed) {
294 const QFileInfo origCompressed(origPath + suffix);
295 if (origCompressed.exists()) {
296 compressedPath = origCompressed.absoluteFilePath();
297 return compressedPath;
301 if (onTheFlyCompression) {
306 if (info.exists() && (info.lastModified() > origLastModified)) {
307 compressedPath = path;
310 if (lock.tryLock(10)) {
311 switch (compression) {
312 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
314 if (compressBrotli(origPath, path)) {
315 compressedPath = path;
320 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
321 if (compressZopfli(origPath, path)) {
322 compressedPath = path;
327 if (compressGzip(origPath, path, origLastModified)) {
328 compressedPath = path;
332 if (compressDeflate(origPath, path)) {
333 compressedPath = path;
344 return compressedPath;
347 static const quint32 crc_32_tab[] = {
348 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
349 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
350 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
351 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
352 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
353 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
354 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
355 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
356 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
357 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
358 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
359 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
360 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
361 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
362 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
363 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
364 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
365 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
366 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
367 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
368 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
369 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
370 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
371 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
372 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
373 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
374 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
375 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
376 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
377 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
378 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
379 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
380 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
381 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
382 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
383 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
384 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
385 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
386 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
387 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
388 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
389 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
390 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
393 quint32 updateCRC32(
unsigned char ch, quint32 crc)
395 return (crc_32_tab[((crc) ^ (quint8(ch))) & 0xff] ^ ((crc) >> 8));
400 return ~std::accumulate(
404 [](quint32 oldcrc32,
char buf){
return updateCRC32(buf, oldcrc32); });
407 bool StaticCompressedPrivate::compressGzip(
const QString &inputPath,
const QString &outputPath,
const QDateTime &origLastModified)
const
409 qCDebug(C_STATICCOMPRESSED,
"Compressing \"%s\" with gzip to \"%s\".", qPrintable(inputPath), qPrintable(outputPath));
411 QFile input(inputPath);
413 qCWarning(C_STATICCOMPRESSED) <<
"Can not open input file to compress with gzip:" << inputPath;
418 if (Q_UNLIKELY(data.
isEmpty())) {
419 qCWarning(C_STATICCOMPRESSED) <<
"Can not read input file or input file is empty:" << inputPath;
424 QByteArray compressedData = qCompress(data, zlibCompressionLevel);
427 QFile output(outputPath);
429 qCWarning(C_STATICCOMPRESSED) <<
"Can not open output file to compress with gzip:" << outputPath;
433 if (Q_UNLIKELY(compressedData.
isEmpty())) {
434 qCWarning(C_STATICCOMPRESSED) <<
"Failed to compress file with gzip, compressed data is empty:" << inputPath;
435 if (output.exists()) {
436 if (Q_UNLIKELY(!output.remove())) {
437 qCWarning(C_STATICCOMPRESSED) <<
"Can not remove invalid compressed gzip file:" << outputPath;
445 compressedData.
remove(0, 6);
446 compressedData.
chop(4);
451 headerStream << quint16(0x1f8b)
453 #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
456 << quint32(origLastModified.
toTime_t())
458 #if defined Q_OS_UNIX
460 #elif defined Q_OS_WIN
462 #elif defined Q_OS_MACOS
472 footerStream << crc32buf(data)
473 << quint32(data.
size());
475 if (Q_UNLIKELY(output.write(header + compressedData + footer) < 0)) {
476 qCCritical(C_STATICCOMPRESSED,
"Failed to write compressed gzip file \"%s\": %s", qPrintable(inputPath), qPrintable(output.errorString()));
483 bool StaticCompressedPrivate::compressDeflate(
const QString &inputPath,
const QString &outputPath)
const
485 qCDebug(C_STATICCOMPRESSED,
"Compressing \"%s\" with deflate to \"%s\".", qPrintable(inputPath), qPrintable(outputPath));
487 QFile input(inputPath);
489 qCWarning(C_STATICCOMPRESSED) <<
"Can not open input file to compress with deflate:" << inputPath;
494 if (Q_UNLIKELY(data.
isEmpty())) {
495 qCWarning(C_STATICCOMPRESSED) <<
"Can not read input file or input file is empty:" << inputPath;
500 QByteArray compressedData = qCompress(data, zlibCompressionLevel);
503 QFile output(outputPath);
505 qCWarning(C_STATICCOMPRESSED) <<
"Can not open output file to compress with deflate:" << outputPath;
509 if (Q_UNLIKELY(compressedData.
isEmpty())) {
510 qCWarning(C_STATICCOMPRESSED) <<
"Failed to compress file with deflate, compressed data is empty:" << inputPath;
511 if (output.exists()) {
512 if (Q_UNLIKELY(!output.remove())) {
513 qCWarning(C_STATICCOMPRESSED) <<
"Can not remove invalid compressed deflate file:" << outputPath;
521 compressedData.
remove(0, 6);
522 compressedData.
chop(4);
524 if (Q_UNLIKELY(output.write(compressedData) < 0)) {
525 qCCritical(C_STATICCOMPRESSED,
"Failed to write compressed deflate file \"%s\": %s", qPrintable(inputPath), qPrintable(output.errorString()));
532 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
533 bool StaticCompressedPrivate::compressZopfli(
const QString &inputPath,
const QString &outputPath)
const
535 qCDebug(C_STATICCOMPRESSED,
"Compressing \"%s\" with zopfli to \"%s\".", qPrintable(inputPath), qPrintable(outputPath));
537 QFile input(inputPath);
539 qCWarning(C_STATICCOMPRESSED) <<
"Can not open input file to compress with zopfli:" << inputPath;
544 if (Q_UNLIKELY(data.
isEmpty())) {
545 qCWarning(C_STATICCOMPRESSED) <<
"Can not read input file or input file is empty:" << inputPath;
550 ZopfliOptions options;
551 ZopfliInitOptions(&options);
552 options.numiterations = zopfliIterations;
554 unsigned char* out = 0;
557 ZopfliCompress(&options, ZopfliFormat::ZOPFLI_FORMAT_GZIP,
reinterpret_cast<const unsigned char *
>(data.
constData()), data.
size(), &out, &outSize);
561 QFile output(outputPath);
563 qCWarning(C_STATICCOMPRESSED) <<
"Can not open output file to compress with zopfli:" << outputPath;
565 if (Q_UNLIKELY(output.write(
reinterpret_cast<const char *
>(out), outSize) < 0)) {
566 qCCritical(C_STATICCOMPRESSED,
"Failed to write compressed zopfli file \"%s\": %s", qPrintable(inputPath), qPrintable(output.errorString()));
567 if (output.exists()) {
568 if (Q_UNLIKELY(!output.remove())) {
569 qCWarning(C_STATICCOMPRESSED) <<
"Can not remove invalid compressed zopfli file:" << outputPath;
577 qCWarning(C_STATICCOMPRESSED) <<
"Failed to compress file with zopfli, compressed data is empty:" << inputPath;
586 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
587 bool StaticCompressedPrivate::compressBrotli(
const QString &inputPath,
const QString &outputPath)
const
589 qCDebug(C_STATICCOMPRESSED,
"Compressing \"%s\" with brotli to \"%s\".", qPrintable(inputPath), qPrintable(outputPath));
591 QFile input(inputPath);
593 qCWarning(C_STATICCOMPRESSED) <<
"Can not open input file to compress with brotli:" << inputPath;
598 if (Q_UNLIKELY(data.
isEmpty())) {
599 qCWarning(C_STATICCOMPRESSED) <<
"Can not read input file or input file is empty:" << inputPath;
607 size_t outSize = BrotliEncoderMaxCompressedSize(
static_cast<size_t>(data.
size()));
608 if (Q_LIKELY(outSize > 0)) {
609 const uint8_t *in = (
const uint8_t *) data.
constData();
611 out = (uint8_t *) malloc(
sizeof(uint8_t) * (outSize+1));
612 if (Q_LIKELY(out !=
nullptr)) {
613 BROTLI_BOOL status = BrotliEncoderCompress(brotliQualityLevel, BROTLI_DEFAULT_WINDOW, BROTLI_DEFAULT_MODE, data.
size(), in, &outSize, out);
614 if (Q_LIKELY(status == BROTLI_TRUE)) {
615 QFile output(outputPath);
617 if (Q_LIKELY(output.write(
reinterpret_cast<const char *
>(out), outSize) > -1)) {
620 qCWarning(C_STATICCOMPRESSED,
"Failed to write brotli compressed data to output file \"%s\": %s", qPrintable(outputPath), qPrintable(output.errorString()));
621 if (output.exists()) {
622 if (Q_UNLIKELY(!output.remove())) {
623 qCWarning(C_STATICCOMPRESSED) <<
"Can not remove invalid compressed brotli file:" << outputPath;
628 qCWarning(C_STATICCOMPRESSED,
"Failed to open output file for brotli compression: %s", qPrintable(outputPath));
631 qCWarning(C_STATICCOMPRESSED,
"Failed to compress \"%s\" with brotli.", qPrintable(inputPath));
635 qCWarning(C_STATICCOMPRESSED,
"Can not allocate needed output buffer of size %lu for brotli compression.",
sizeof(uint8_t) * (outSize+1));
638 qCWarning(C_STATICCOMPRESSED,
"Needed output buffer too large to compress input of size %lu with brotli.",
static_cast<size_t>(data.
size()));
645 #include "moc_staticcompressed.cpp"
The Cutelyst Application.
Engine * engine() const noexcept
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
Response * res() const noexcept
Response * response() const noexcept
QVariantMap config(const QString &entity) const
user configuration for the application
QString header(const QString &key) const
Headers headers() const noexcept
void setStatus(quint16 status) noexcept
Headers & headers() noexcept
void setBody(QIODevice *body)
void setContentType(const QString &type)
Deliver static files compressed on the fly or precompressed.
virtual ~StaticCompressed() override
void setIncludePaths(const QStringList &paths)
void setDirs(const QStringList &dirs)
virtual bool setup(Application *app) override
The Cutelyst namespace holds all public Cutelyst API.
QByteArray::iterator begin()
const char * constData() const const
QByteArray::iterator end()
bool isEmpty() const const
QByteArray & remove(int pos, int len)
QByteArray toHex() const const
QByteArray hash(const QByteArray &data, QCryptographicHash::Algorithm method)
uint toTime_t() const const
qint64 toSecsSinceEpoch() const const
virtual bool open(QIODevice::OpenMode mode) override
virtual qint64 size() const const override
QString errorString() const const
T value(int i) const const
QMimeType mimeTypeForFile(const QString &fileName, QMimeDatabase::MatchMode mode) const const
bool isValid() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QRegularExpressionMatch match(const QString &subject, int offset, QRegularExpression::MatchType matchType, QRegularExpression::MatchOptions matchOptions) const const
bool hasMatch() const const
QString writableLocation(QStandardPaths::StandardLocation type)
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString fromLatin1(const char *str, int size)
bool isEmpty() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QByteArray toUtf8() const const