19 #include "staticcompressed_p.h"
21 #include <Cutelyst/Application>
22 #include <Cutelyst/Request>
23 #include <Cutelyst/Response>
24 #include <Cutelyst/Context>
25 #include <Cutelyst/Engine>
27 #include <QMimeDatabase>
30 #include <QStandardPaths>
31 #include <QCoreApplication>
32 #include <QCryptographicHash>
33 #include <QLoggingCategory>
34 #include <QDataStream>
37 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
41 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
42 #include <brotli/encode.h>
47 Q_LOGGING_CATEGORY(C_STATICCOMPRESSED,
"cutelyst.plugin.staticcompressed", QtWarningMsg)
50 Plugin(parent), d_ptr(new StaticCompressedPrivate)
53 d->includePaths.append(parent->config(QStringLiteral(
"root")).toString());
64 d->includePaths.clear();
65 for (
const QString &path : paths) {
66 d->includePaths.append(
QDir(path));
80 const QVariantMap config = app->
engine()->
config(QStringLiteral(
"Cutelyst_StaticCompressed_Plugin"));
82 d->cacheDir.setPath(config.value(QStringLiteral(
"cache_directory"), _defaultCacheDir).toString());
84 if (Q_UNLIKELY(!d->cacheDir.exists())) {
85 if (!d->cacheDir.mkpath(d->cacheDir.absolutePath())) {
86 qCCritical(C_STATICCOMPRESSED,
"Failed to create cache directory for compressed static files at \"%s\".", qPrintable(d->cacheDir.absolutePath()));
91 qCInfo(C_STATICCOMPRESSED,
"Compressed cache directory: %s", qPrintable(d->cacheDir.absolutePath()));
93 const QString _mimeTypes = config.value(QStringLiteral(
"mime_types"), QStringLiteral(
"text/css,application/javascript")).toString();
94 qCInfo(C_STATICCOMPRESSED,
"MIME Types: %s", qPrintable(_mimeTypes));
96 #
if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
102 const QString _suffixes = config.value(QStringLiteral(
"suffixes"), QStringLiteral(
"js.map,css.map,min.js.map,min.css.map")).toString();
103 qCInfo(C_STATICCOMPRESSED,
"Suffixes: %s", qPrintable(_suffixes));
105 #
if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
111 d->checkPreCompressed = config.value(QStringLiteral(
"check_pre_compressed"),
true).toBool();
112 qCInfo(C_STATICCOMPRESSED,
"Check for pre-compressed files: %s", d->checkPreCompressed ?
"true" :
"false");
114 d->onTheFlyCompression = config.value(QStringLiteral(
"on_the_fly_compression"),
true).toBool();
115 qCInfo(C_STATICCOMPRESSED,
"Compress static files on the fly: %s", d->onTheFlyCompression ?
"true" :
"false");
117 QStringList supportedCompressions{QStringLiteral(
"deflate"), QStringLiteral(
"gzip")};
120 d->zlibCompressionLevel = config.
value(QStringLiteral(
"zlib_compression_level"), 9).toInt(&ok);
121 if (!ok || (d->zlibCompressionLevel < -1) || (d->zlibCompressionLevel > 9)) {
122 d->zlibCompressionLevel = -1;
125 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
126 d->zopfliIterations = config.value(QStringLiteral(
"zopfli_iterations"), 15).toInt(&ok);
127 if (!ok || (d->zopfliIterations < 0)) {
128 d->zopfliIterations = 15;
130 d->useZopfli = config.value(QStringLiteral(
"use_zopfli"),
false).toBool();
131 supportedCompressions << QStringLiteral(
"zopfli");
134 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
135 d->brotliQualityLevel = config.value(QStringLiteral(
"brotli_quality_level"), BROTLI_DEFAULT_QUALITY).toInt(&ok);
136 if (!ok || (d->brotliQualityLevel < BROTLI_MIN_QUALITY) || (d->brotliQualityLevel > BROTLI_MAX_QUALITY)) {
137 d->brotliQualityLevel = BROTLI_DEFAULT_QUALITY;
139 supportedCompressions << QStringLiteral(
"brotli");
142 qCInfo(C_STATICCOMPRESSED,
"Supported compressions: %s", qPrintable(supportedCompressions.join(
QLatin1Char(
','))));
145 d->beforePrepareAction(c, skipMethod);
151 void StaticCompressedPrivate::beforePrepareAction(
Context *c,
bool *skipMethod)
157 const QString path = c->req()->path();
160 for (
const QString &dir : dirs) {
162 if (!locateCompressedFile(c, path)) {
166 res->
setBody(QStringLiteral(
"File not found: ") + path);
175 if (match.
hasMatch() && locateCompressedFile(c, path)) {
180 bool StaticCompressedPrivate::locateCompressedFile(
Context *c,
const QString &relPath)
const
182 for (
const QDir &includePath : includePaths) {
183 const QString path = includePath.absoluteFilePath(relPath);
185 if (fileInfo.exists()) {
187 const QDateTime currentDateTime = fileInfo.lastModified();
206 _mimeTypeName = QStringLiteral(
"application/json");
212 const QString acceptEncoding = c->req()->
header(QStringLiteral(
"Accept-Encoding"));
213 qCDebug(C_STATICCOMPRESSED) <<
"Accept-Encoding:" << acceptEncoding;
215 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
217 compressedPath = locateCacheFile(path, currentDateTime, Brotli) ;
218 if (!compressedPath.
isEmpty()) {
219 qCDebug(C_STATICCOMPRESSED,
"Serving brotli compressed data from \"%s\".", qPrintable(compressedPath));
220 contentEncoding = QStringLiteral(
"br");
225 compressedPath = locateCacheFile(path, currentDateTime, useZopfli ? Zopfli : Gzip);
226 if (!compressedPath.
isEmpty()) {
227 qCDebug(C_STATICCOMPRESSED,
"Serving %s compressed data from \"%s\".", useZopfli ?
"zopfli" :
"gzip", qPrintable(compressedPath));
228 contentEncoding = QStringLiteral(
"gzip");
231 compressedPath = locateCacheFile(path, currentDateTime, Deflate);
232 if (!compressedPath.
isEmpty()) {
233 qCDebug(C_STATICCOMPRESSED,
"Serving deflate compressed data from \"%s\".", qPrintable(compressedPath));
234 contentEncoding = QStringLiteral(
"deflate");
243 qCDebug(C_STATICCOMPRESSED) <<
"Serving" << path;
251 if (!_mimeTypeName.
isEmpty()) {
253 }
else if (mimeType.
isValid()) {
260 headers.
setHeader(QStringLiteral(
"CACHE_CONTROL"), QStringLiteral(
"public"));
262 if (!contentEncoding.
isEmpty()) {
267 headers.
pushHeader(QStringLiteral(
"Vary"), QStringLiteral(
"Accept-Encoding"));
273 qCWarning(C_STATICCOMPRESSED) <<
"Could not serve" << path << file->
errorString();
278 qCWarning(C_STATICCOMPRESSED) <<
"File not found" << relPath;
282 QString StaticCompressedPrivate::locateCacheFile(
const QString &origPath,
const QDateTime &origLastModified, Compression compression)
const
288 switch (compression) {
291 suffix = QStringLiteral(
".gz");
293 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
295 suffix = QStringLiteral(
".br");
299 suffix = QStringLiteral(
".deflate");
302 Q_ASSERT_X(
false,
"locate cache file",
"invalid compression type");
306 if (checkPreCompressed) {
307 const QFileInfo origCompressed(origPath + suffix);
308 if (origCompressed.exists()) {
309 compressedPath = origCompressed.absoluteFilePath();
310 return compressedPath;
314 if (onTheFlyCompression) {
319 if (info.exists() && (info.lastModified() > origLastModified)) {
320 compressedPath = path;
323 if (lock.tryLock(10)) {
324 switch (compression) {
325 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
327 if (compressBrotli(origPath, path)) {
328 compressedPath = path;
333 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
334 if (compressZopfli(origPath, path)) {
335 compressedPath = path;
340 if (compressGzip(origPath, path, origLastModified)) {
341 compressedPath = path;
345 if (compressDeflate(origPath, path)) {
346 compressedPath = path;
357 return compressedPath;
360 static const quint32 crc_32_tab[] = {
361 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
362 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
363 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
364 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
365 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
366 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
367 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
368 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
369 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
370 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
371 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
372 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
373 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
374 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
375 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
376 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
377 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
378 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
379 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
380 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
381 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
382 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
383 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
384 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
385 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
386 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
387 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
388 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
389 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
390 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
391 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
392 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
393 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
394 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
395 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
396 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
397 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
398 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
399 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
400 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
401 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
402 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
403 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
406 quint32 updateCRC32(
unsigned char ch, quint32 crc)
408 return (crc_32_tab[((crc) ^ (quint8(ch))) & 0xff] ^ ((crc) >> 8));
413 return ~std::accumulate(
417 [](quint32 oldcrc32,
char buf){
return updateCRC32(buf, oldcrc32); });
420 bool StaticCompressedPrivate::compressGzip(
const QString &inputPath,
const QString &outputPath,
const QDateTime &origLastModified)
const
422 qCDebug(C_STATICCOMPRESSED,
"Compressing \"%s\" with gzip to \"%s\".", qPrintable(inputPath), qPrintable(outputPath));
424 QFile input(inputPath);
426 qCWarning(C_STATICCOMPRESSED) <<
"Can not open input file to compress with gzip:" << inputPath;
431 if (Q_UNLIKELY(data.
isEmpty())) {
432 qCWarning(C_STATICCOMPRESSED) <<
"Can not read input file or input file is empty:" << inputPath;
437 QByteArray compressedData = qCompress(data, zlibCompressionLevel);
440 QFile output(outputPath);
442 qCWarning(C_STATICCOMPRESSED) <<
"Can not open output file to compress with gzip:" << outputPath;
446 if (Q_UNLIKELY(compressedData.
isEmpty())) {
447 qCWarning(C_STATICCOMPRESSED) <<
"Failed to compress file with gzip, compressed data is empty:" << inputPath;
448 if (output.exists()) {
449 if (Q_UNLIKELY(!output.remove())) {
450 qCWarning(C_STATICCOMPRESSED) <<
"Can not remove invalid compressed gzip file:" << outputPath;
458 compressedData.
remove(0, 6);
459 compressedData.
chop(4);
464 headerStream << quint16(0x1f8b)
466 #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
469 << quint32(origLastModified.
toTime_t())
471 #if defined Q_OS_UNIX
473 #elif defined Q_OS_WIN
475 #elif defined Q_OS_MACOS
485 footerStream << crc32buf(data)
486 << quint32(data.
size());
488 if (Q_UNLIKELY(output.write(header + compressedData + footer) < 0)) {
489 qCCritical(C_STATICCOMPRESSED,
"Failed to write compressed gzip file \"%s\": %s", qPrintable(inputPath), qPrintable(output.errorString()));
496 bool StaticCompressedPrivate::compressDeflate(
const QString &inputPath,
const QString &outputPath)
const
498 qCDebug(C_STATICCOMPRESSED,
"Compressing \"%s\" with deflate to \"%s\".", qPrintable(inputPath), qPrintable(outputPath));
500 QFile input(inputPath);
502 qCWarning(C_STATICCOMPRESSED) <<
"Can not open input file to compress with deflate:" << inputPath;
507 if (Q_UNLIKELY(data.
isEmpty())) {
508 qCWarning(C_STATICCOMPRESSED) <<
"Can not read input file or input file is empty:" << inputPath;
513 QByteArray compressedData = qCompress(data, zlibCompressionLevel);
516 QFile output(outputPath);
518 qCWarning(C_STATICCOMPRESSED) <<
"Can not open output file to compress with deflate:" << outputPath;
522 if (Q_UNLIKELY(compressedData.
isEmpty())) {
523 qCWarning(C_STATICCOMPRESSED) <<
"Failed to compress file with deflate, compressed data is empty:" << inputPath;
524 if (output.exists()) {
525 if (Q_UNLIKELY(!output.remove())) {
526 qCWarning(C_STATICCOMPRESSED) <<
"Can not remove invalid compressed deflate file:" << outputPath;
534 compressedData.
remove(0, 6);
535 compressedData.
chop(4);
537 if (Q_UNLIKELY(output.write(compressedData) < 0)) {
538 qCCritical(C_STATICCOMPRESSED,
"Failed to write compressed deflate file \"%s\": %s", qPrintable(inputPath), qPrintable(output.errorString()));
545 #ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
546 bool StaticCompressedPrivate::compressZopfli(
const QString &inputPath,
const QString &outputPath)
const
548 qCDebug(C_STATICCOMPRESSED,
"Compressing \"%s\" with zopfli to \"%s\".", qPrintable(inputPath), qPrintable(outputPath));
550 QFile input(inputPath);
552 qCWarning(C_STATICCOMPRESSED) <<
"Can not open input file to compress with zopfli:" << inputPath;
557 if (Q_UNLIKELY(data.
isEmpty())) {
558 qCWarning(C_STATICCOMPRESSED) <<
"Can not read input file or input file is empty:" << inputPath;
563 ZopfliOptions options;
564 ZopfliInitOptions(&options);
565 options.numiterations = zopfliIterations;
567 unsigned char* out = 0;
570 ZopfliCompress(&options, ZopfliFormat::ZOPFLI_FORMAT_GZIP,
reinterpret_cast<const unsigned char *
>(data.
constData()), data.
size(), &out, &outSize);
574 QFile output(outputPath);
576 qCWarning(C_STATICCOMPRESSED) <<
"Can not open output file to compress with zopfli:" << outputPath;
578 if (Q_UNLIKELY(output.write(
reinterpret_cast<const char *
>(out), outSize) < 0)) {
579 qCCritical(C_STATICCOMPRESSED,
"Failed to write compressed zopfli file \"%s\": %s", qPrintable(inputPath), qPrintable(output.errorString()));
580 if (output.exists()) {
581 if (Q_UNLIKELY(!output.remove())) {
582 qCWarning(C_STATICCOMPRESSED) <<
"Can not remove invalid compressed zopfli file:" << outputPath;
590 qCWarning(C_STATICCOMPRESSED) <<
"Failed to compress file with zopfli, compressed data is empty:" << inputPath;
599 #ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
600 bool StaticCompressedPrivate::compressBrotli(
const QString &inputPath,
const QString &outputPath)
const
602 qCDebug(C_STATICCOMPRESSED,
"Compressing \"%s\" with brotli to \"%s\".", qPrintable(inputPath), qPrintable(outputPath));
604 QFile input(inputPath);
606 qCWarning(C_STATICCOMPRESSED) <<
"Can not open input file to compress with brotli:" << inputPath;
611 if (Q_UNLIKELY(data.
isEmpty())) {
612 qCWarning(C_STATICCOMPRESSED) <<
"Can not read input file or input file is empty:" << inputPath;
620 size_t outSize = BrotliEncoderMaxCompressedSize(
static_cast<size_t>(data.
size()));
621 if (Q_LIKELY(outSize > 0)) {
622 const uint8_t *in = (
const uint8_t *) data.
constData();
624 out = (uint8_t *) malloc(
sizeof(uint8_t) * (outSize+1));
625 if (Q_LIKELY(out !=
nullptr)) {
626 BROTLI_BOOL status = BrotliEncoderCompress(brotliQualityLevel, BROTLI_DEFAULT_WINDOW, BROTLI_DEFAULT_MODE, data.
size(), in, &outSize, out);
627 if (Q_LIKELY(status == BROTLI_TRUE)) {
628 QFile output(outputPath);
630 if (Q_LIKELY(output.write(
reinterpret_cast<const char *
>(out), outSize) > -1)) {
633 qCWarning(C_STATICCOMPRESSED,
"Failed to write brotli compressed data to output file \"%s\": %s", qPrintable(outputPath), qPrintable(output.errorString()));
634 if (output.exists()) {
635 if (Q_UNLIKELY(!output.remove())) {
636 qCWarning(C_STATICCOMPRESSED) <<
"Can not remove invalid compressed brotli file:" << outputPath;
641 qCWarning(C_STATICCOMPRESSED,
"Failed to open output file for brotli compression: %s", qPrintable(outputPath));
644 qCWarning(C_STATICCOMPRESSED,
"Failed to compress \"%s\" with brotli.", qPrintable(inputPath));
648 qCWarning(C_STATICCOMPRESSED,
"Can not allocate needed output buffer of size %lu for brotli compression.",
sizeof(uint8_t) * (outSize+1));
651 qCWarning(C_STATICCOMPRESSED,
"Needed output buffer too large to compress input of size %lu with brotli.",
static_cast<size_t>(data.
size()));
658 #include "moc_staticcompressed.cpp"
The Cutelyst Application.
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
Response * response() const
QVariantMap config(const QString &entity) const
user configuration for the application
QString header(const QString &key) const
void setBody(QIODevice *body)
void setStatus(quint16 status)
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