1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
5#include "qloggingregistry_p.h"
6
7#include <QtCore/qfile.h>
8#include <QtCore/qlibraryinfo.h>
9#include <QtCore/private/qlocking_p.h>
10#include <QtCore/qscopedvaluerollback.h>
11#include <QtCore/qstandardpaths.h>
12#include <QtCore/qstringtokenizer.h>
13#include <QtCore/qdir.h>
14#include <QtCore/qcoreapplication.h>
15#include <qplatformdefs.h>
16
17#if QT_CONFIG(settings)
18#include <QtCore/qsettings.h>
19#include <QtCore/private/qsettings_p.h>
20#endif
21
22// We can't use the default macros because this would lead to recursion.
23// Instead let's define our own one that unconditionally logs...
24#define debugMsg QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC, "qt.core.logging").debug
25#define warnMsg QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC, "qt.core.logging").warning
26
27QT_BEGIN_NAMESPACE
28
29using namespace Qt::StringLiterals;
30
31Q_GLOBAL_STATIC(QLoggingRegistry, qtLoggingRegistry)
32alignas(QLoggingCategory) static unsigned char defaultLoggingCategory[sizeof(QLoggingCategory)];
33
34/*!
35 \internal
36 Constructs a logging rule with default values.
37*/
38QLoggingRule::QLoggingRule()
39{
40}
41
42/*!
43 \internal
44 Constructs a logging rule.
45*/
46QLoggingRule::QLoggingRule(QStringView pattern, bool enabled) : enabled(enabled)
47{
48 parse(pattern);
49}
50
51/*!
52 \internal
53 Return value 1 means filter passed, 0 means filter doesn't influence this
54 category, -1 means category doesn't pass this filter.
55 */
56int QLoggingRule::pass(QLatin1StringView cat, QtMsgType msgType) const
57{
58 // check message type
59 if (messageType > -1 && messageType != msgType)
60 return 0;
61
62 if (flags == FullText) {
63 // full match
64 if (category == cat)
65 return (enabled ? 1 : -1);
66 else
67 return 0;
68 }
69
70 const qsizetype idx = cat.indexOf(s: category);
71 if (idx >= 0) {
72 if (flags == MidFilter) {
73 // matches somewhere
74 return (enabled ? 1 : -1);
75 } else if (flags == LeftFilter) {
76 // matches left
77 if (idx == 0)
78 return (enabled ? 1 : -1);
79 } else if (flags == RightFilter) {
80 // matches right
81 if (idx == (cat.size() - category.size()))
82 return (enabled ? 1 : -1);
83 }
84 }
85 return 0;
86}
87
88/*!
89 \internal
90 Parses \a pattern.
91 Allowed is f.ex.:
92 qt.core.io.debug FullText, QtDebugMsg
93 qt.core.* LeftFilter, all types
94 *.io.warning RightFilter, QtWarningMsg
95 *.core.* MidFilter
96 */
97void QLoggingRule::parse(QStringView pattern)
98{
99 QStringView p;
100
101 // strip trailing ".messagetype"
102 if (pattern.endsWith(s: ".debug"_L1)) {
103 p = pattern.chopped(n: 6); // strlen(".debug")
104 messageType = QtDebugMsg;
105 } else if (pattern.endsWith(s: ".info"_L1)) {
106 p = pattern.chopped(n: 5); // strlen(".info")
107 messageType = QtInfoMsg;
108 } else if (pattern.endsWith(s: ".warning"_L1)) {
109 p = pattern.chopped(n: 8); // strlen(".warning")
110 messageType = QtWarningMsg;
111 } else if (pattern.endsWith(s: ".critical"_L1)) {
112 p = pattern.chopped(n: 9); // strlen(".critical")
113 messageType = QtCriticalMsg;
114 } else {
115 p = pattern;
116 }
117
118 const QChar asterisk = u'*';
119 if (!p.contains(c: asterisk)) {
120 flags = FullText;
121 } else {
122 if (p.endsWith(c: asterisk)) {
123 flags |= LeftFilter;
124 p = p.chopped(n: 1);
125 }
126 if (p.startsWith(c: asterisk)) {
127 flags |= RightFilter;
128 p = p.mid(pos: 1);
129 }
130 if (p.contains(c: asterisk)) // '*' only supported at start/end
131 flags = PatternFlags();
132 }
133
134 category = p.toString();
135}
136
137/*!
138 \class QLoggingSettingsParser
139 \since 5.3
140 \internal
141
142 Parses a .ini file with the following format:
143
144 [rules]
145 rule1=[true|false]
146 rule2=[true|false]
147 ...
148
149 [rules] is the default section, and therefore optional.
150*/
151
152/*!
153 \internal
154 Parses configuration from \a content.
155*/
156void QLoggingSettingsParser::setContent(QStringView content, char16_t separator)
157{
158 _rules.clear();
159 for (auto line : qTokenize(h&: content, n&: separator))
160 parseNextLine(line);
161}
162
163/*!
164 \internal
165 Parses configuration from \a stream.
166*/
167void QLoggingSettingsParser::setContent(FILE *stream)
168{
169 _rules.clear();
170
171 constexpr size_t ChunkSize = 240;
172 QByteArray buffer(ChunkSize, Qt::Uninitialized);
173 auto readline = [&](FILE *stream) {
174 // Read one line into the buffer
175
176 // fgets() always writes the terminating null into the buffer, so we'll
177 // allow it to write to the QByteArray's null (thus the off by 1).
178 char *s = fgets(s: buffer.begin(), n: buffer.size() + 1, stream: stream);
179 if (!s)
180 return QByteArrayView{};
181
182 qsizetype len = strlen(s: s);
183 while (len == buffer.size()) {
184 // need to grow the buffer
185 buffer.resizeForOverwrite(size: buffer.size() + ChunkSize);
186 s = fgets(s: buffer.end() - ChunkSize, n: ChunkSize + 1, stream: stream);
187 if (!s)
188 break;
189 len += strlen(s: s);
190 }
191 QByteArrayView result(buffer.constBegin(), len);
192 if (result.endsWith(c: '\n'))
193 result.chop(n: 1);
194 return result;
195 };
196
197 QByteArrayView line;
198 while (!(line = readline(stream)).isNull())
199 parseNextLine(line: QString::fromUtf8(utf8: line));
200}
201
202/*!
203 \internal
204 Parses one line of the configuration file
205*/
206
207void QLoggingSettingsParser::parseNextLine(QStringView line)
208{
209 // Remove whitespace at start and end of line:
210 line = line.trimmed();
211
212 // comment
213 if (line.startsWith(c: u';'))
214 return;
215
216 if (line.startsWith(c: u'[') && line.endsWith(c: u']')) {
217 // new section
218 auto sectionName = line.mid(pos: 1).chopped(n: 1).trimmed();
219 m_inRulesSection = sectionName.compare(s: "rules"_L1, cs: Qt::CaseInsensitive) == 0;
220 return;
221 }
222
223 if (m_inRulesSection) {
224 const qsizetype equalPos = line.indexOf(c: u'=');
225 if (equalPos != -1) {
226 if (line.lastIndexOf(c: u'=') == equalPos) {
227 const auto key = line.left(n: equalPos).trimmed();
228#if QT_CONFIG(settings)
229 QString tmp;
230 QSettingsPrivate::iniUnescapedKey(key: key.toUtf8(), result&: tmp);
231 QStringView pattern = qToStringViewIgnoringNull(s: tmp);
232#else
233 QStringView pattern = key;
234#endif
235 const auto valueStr = line.mid(pos: equalPos + 1).trimmed();
236 int value = -1;
237 if (valueStr == "true"_L1)
238 value = 1;
239 else if (valueStr == "false"_L1)
240 value = 0;
241 QLoggingRule rule(pattern, (value == 1));
242 if (rule.flags != 0 && (value != -1))
243 _rules.append(t: std::move(rule));
244 else
245 warnMsg(msg: "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
246 } else {
247 warnMsg(msg: "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
248 }
249 }
250 }
251}
252
253/*!
254 \internal
255 QLoggingRegistry constructor
256 */
257QLoggingRegistry::QLoggingRegistry()
258 : categoryFilter(defaultCategoryFilter)
259{
260 using U = QLoggingCategory::UnregisteredInitialization;
261 Q_ASSERT_X(!self, "QLoggingRegistry", "Singleton recreated");
262 self = this;
263
264 // can't use std::construct_at here - private constructor
265 auto cat = new (defaultLoggingCategory) QLoggingCategory(U{}, defaultCategoryName);
266 categories.emplace(key: cat, args: QtDebugMsg);
267
268#if defined(Q_OS_ANDROID)
269 // Unless QCoreApplication has been constructed we can't be sure that
270 // we are on Qt's main thread. If we did allow logging here, we would
271 // potentially set Qt's main thread to Android's thread 0, which would
272 // confuse Qt later when running main().
273 if (!qApp)
274 return;
275#endif
276
277 initializeRules(); // Init on first use
278}
279
280static bool qtLoggingDebug()
281{
282 static const bool debugEnv = [] {
283 bool debug = qEnvironmentVariableIsSet(varName: "QT_LOGGING_DEBUG");
284 if (debug)
285 debugMsg(msg: "QT_LOGGING_DEBUG environment variable is set.");
286 return debug;
287 }();
288 return Q_UNLIKELY(debugEnv);
289}
290
291static QList<QLoggingRule> loadRulesFromFile(const QString &filePath)
292{
293 Q_ASSERT(!filePath.isEmpty());
294 if (qtLoggingDebug()) {
295 debugMsg(msg: "Checking \"%s\" for rules",
296 QDir::toNativeSeparators(pathName: filePath).toUtf8().constData());
297 }
298
299 // We bypass QFile here because QFile is a QObject.
300 if (Q_UNLIKELY(filePath.at(0) == u':')) {
301 if (qtLoggingDebug()) {
302 warnMsg(msg: "Attempted to load config rules from Qt resource path \"%ls\"",
303 qUtf16Printable(filePath));
304 }
305 return {};
306 }
307
308#ifdef Q_OS_WIN
309 // text mode: let the runtime do CRLF translation
310 FILE *f = _wfopen(reinterpret_cast<const wchar_t *>(filePath.constBegin()), L"rtN");
311#else
312 FILE *f = QT_FOPEN(filename: QFile::encodeName(fileName: filePath).constBegin(), modes: "re");
313#endif
314 if (f) {
315 QLoggingSettingsParser parser;
316 parser.setContent(f);
317 fclose(stream: f);
318 if (qtLoggingDebug())
319 debugMsg(msg: "Loaded %td rules from \"%ls\"", static_cast<ptrdiff_t>(parser.rules().size()),
320 qUtf16Printable(filePath));
321 return parser.rules();
322 } else if (int err = errno; err != ENOENT) {
323 warnMsg(msg: "Failed to load file \"%ls\": %ls", qUtf16Printable(filePath),
324 qUtf16Printable(qt_error_string(err)));
325 }
326 return QList<QLoggingRule>();
327}
328
329/*!
330 \internal
331 Initializes the rules database by loading
332 $QT_LOGGING_CONF, $QT_LOGGING_RULES, and .config/QtProject/qtlogging.ini.
333 */
334void QLoggingRegistry::initializeRules()
335{
336 if (qtLoggingDebug()) {
337 debugMsg(msg: "Initializing the rules database ...");
338 debugMsg(msg: "Checking %s environment variable", "QT_LOGGING_CONF");
339 }
340 QList<QLoggingRule> er, qr, cr;
341 // get rules from environment
342 if (QString rulesFilePath = qEnvironmentVariable(varName: "QT_LOGGING_CONF"); !rulesFilePath.isEmpty())
343 er = loadRulesFromFile(filePath: rulesFilePath);
344
345 if (qtLoggingDebug())
346 debugMsg(msg: "Checking %s environment variable", "QT_LOGGING_RULES");
347
348 const QString rulesSrc = qEnvironmentVariable(varName: "QT_LOGGING_RULES");
349 if (!rulesSrc.isEmpty()) {
350 QLoggingSettingsParser parser;
351 parser.setImplicitRulesSection(true);
352 parser.setContent(content: rulesSrc, separator: u';');
353
354 if (qtLoggingDebug())
355 debugMsg(msg: "Loaded %td rules", static_cast<ptrdiff_t>(parser.rules().size()));
356
357 er += parser.rules();
358 }
359
360 const QString configFileName = u"QtProject/qtlogging.ini"_s;
361 QStringView baseConfigFileName = QStringView(configFileName).sliced(pos: strlen(s: "QtProject"));
362 Q_ASSERT(baseConfigFileName.startsWith(u'/'));
363
364 // get rules from Qt data configuration path
365 qr = loadRulesFromFile(filePath: QLibraryInfo::path(p: QLibraryInfo::DataPath) + baseConfigFileName);
366
367 // get rules from user's/system configuration
368 // locateAll() returns the user's file (most overriding) first
369 const QStringList configPaths =
370 QStandardPaths::locateAll(type: QStandardPaths::GenericConfigLocation, fileName: configFileName);
371 for (qsizetype i = configPaths.size(); i > 0; --i)
372 cr += loadRulesFromFile(filePath: configPaths[i - 1]);
373
374 const QMutexLocker locker(&registryMutex);
375
376 ruleSets[EnvironmentRules] = std::move(er);
377 ruleSets[QtConfigRules] = std::move(qr);
378 ruleSets[ConfigRules] = std::move(cr);
379
380 if (!ruleSets[EnvironmentRules].isEmpty() || !ruleSets[QtConfigRules].isEmpty() || !ruleSets[ConfigRules].isEmpty())
381 updateRules();
382}
383
384/*!
385 \internal
386 Registers a category object.
387
388 This method might be called concurrently for the same category object.
389*/
390void QLoggingRegistry::registerCategory(QLoggingCategory *cat, QtMsgType enableForLevel)
391{
392 const auto locker = qt_scoped_lock(mutex&: registryMutex);
393
394 auto r = categories.tryEmplace(key: cat, args&: enableForLevel);
395 if (r.inserted) {
396 // new entry
397 (*categoryFilter)(cat);
398 }
399}
400
401/*!
402 \internal
403 Unregisters a category object.
404*/
405void QLoggingRegistry::unregisterCategory(QLoggingCategory *cat)
406{
407 const auto locker = qt_scoped_lock(mutex&: registryMutex);
408 categories.remove(key: cat);
409}
410
411/*!
412 \since 6.3
413 \internal
414
415 Registers the environment variable \a environment as the control variable
416 for enabling debugging by default for category \a categoryName. The
417 category name must start with "qt."
418*/
419void QLoggingRegistry::registerEnvironmentOverrideForCategory(const char *categoryName,
420 const char *environment)
421{
422 qtCategoryEnvironmentOverrides.insert_or_assign(k: categoryName, obj&: environment);
423}
424
425/*!
426 \internal
427 Installs logging rules as specified in \a content.
428 */
429void QLoggingRegistry::setApiRules(const QString &content)
430{
431 QLoggingSettingsParser parser;
432 parser.setImplicitRulesSection(true);
433 parser.setContent(content);
434
435 if (qtLoggingDebug())
436 debugMsg(msg: "Loading logging rules set by QLoggingCategory::setFilterRules ...");
437
438 const QMutexLocker locker(&registryMutex);
439
440 ruleSets[ApiRules] = parser.rules();
441
442 updateRules();
443}
444
445/*!
446 \internal
447 Activates a new set of logging rules for the default filter.
448
449 (The caller must lock registryMutex to make sure the API is thread safe.)
450*/
451void QLoggingRegistry::updateRules()
452{
453 for (auto it = categories.keyBegin(), end = categories.keyEnd(); it != end; ++it)
454 (*categoryFilter)(*it);
455}
456
457/*!
458 \internal
459 Installs a custom filter rule.
460*/
461QLoggingCategory::CategoryFilter
462QLoggingRegistry::installFilter(QLoggingCategory::CategoryFilter filter)
463{
464 const auto locker = qt_scoped_lock(mutex&: registryMutex);
465
466 if (!filter)
467 filter = defaultCategoryFilter;
468
469 QLoggingCategory::CategoryFilter old = categoryFilter;
470 categoryFilter = filter;
471
472 updateRules();
473
474 return old;
475}
476
477QLoggingRegistry *QLoggingRegistry::instance()
478{
479 Q_CONSTINIT thread_local bool recursionGuard = false;
480 if (recursionGuard)
481 return nullptr;
482 QScopedValueRollback<bool> rollback(recursionGuard, true);
483 return qtLoggingRegistry();
484}
485
486QLoggingCategory *QLoggingRegistry::defaultCategory()
487{
488 // Initialize the defaultLoggingCategory global static, if necessary. Note
489 // how it remains initialized forever, even if the QLoggingRegistry
490 // instance() is destroyed.
491 instance();
492
493 // std::launder() to be on the safe side, but it's unnecessary because the
494 // object is never recreated.
495 return std::launder(p: reinterpret_cast<QLoggingCategory *>(defaultLoggingCategory));
496}
497
498/*!
499 \internal
500 Updates category settings according to rules.
501
502 As a category filter, it is run with registryMutex held.
503*/
504void QLoggingRegistry::defaultCategoryFilter(QLoggingCategory *cat)
505{
506 const QLoggingRegistry *reg = self;
507 Q_ASSERT(reg->categories.contains(cat));
508 QtMsgType enableForLevel = reg->categories.value(key: cat);
509
510 // NB: note that the numeric values of the Qt*Msg constants are
511 // not in severity order.
512 bool debug = (enableForLevel == QtDebugMsg);
513 bool info = debug || (enableForLevel == QtInfoMsg);
514 bool warning = info || (enableForLevel == QtWarningMsg);
515 bool critical = warning || (enableForLevel == QtCriticalMsg);
516
517 // hard-wired implementation of
518 // qt.*.debug=false
519 // qt.debug=false
520 if (const char *categoryName = cat->categoryName()) {
521 // == "qt" or startsWith("qt.")
522 if (strcmp(s1: categoryName, s2: "qt") == 0) {
523 debug = false;
524 } else if (strncmp(s1: categoryName, s2: "qt.", n: 3) == 0) {
525 // may be overridden
526 auto it = reg->qtCategoryEnvironmentOverrides.find(x: categoryName);
527 if (it == reg->qtCategoryEnvironmentOverrides.end())
528 debug = false;
529 else
530 debug = qEnvironmentVariableIntValue(varName: it->second);
531 }
532 }
533
534 const auto categoryName = QLatin1StringView(cat->categoryName());
535
536 for (const auto &ruleSet : reg->ruleSets) {
537 for (const auto &rule : ruleSet) {
538 int filterpass = rule.pass(cat: categoryName, msgType: QtDebugMsg);
539 if (filterpass != 0)
540 debug = (filterpass > 0);
541 filterpass = rule.pass(cat: categoryName, msgType: QtInfoMsg);
542 if (filterpass != 0)
543 info = (filterpass > 0);
544 filterpass = rule.pass(cat: categoryName, msgType: QtWarningMsg);
545 if (filterpass != 0)
546 warning = (filterpass > 0);
547 filterpass = rule.pass(cat: categoryName, msgType: QtCriticalMsg);
548 if (filterpass != 0)
549 critical = (filterpass > 0);
550 }
551 }
552
553 cat->setEnabled(type: QtDebugMsg, enable: debug);
554 cat->setEnabled(type: QtInfoMsg, enable: info);
555 cat->setEnabled(type: QtWarningMsg, enable: warning);
556 cat->setEnabled(type: QtCriticalMsg, enable: critical);
557}
558
559
560QT_END_NAMESPACE
561

source code of qtbase/src/corelib/io/qloggingregistry.cpp