1/*
2 This file is part of the KDE libraries
3
4 SPDX-FileCopyrightText: 2003 Cornelius Schumacher <schumacher@kde.org>
5 SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
6 SPDX-FileCopyrightText: 2003 Zack Rusin <zack@kde.org>
7 SPDX-FileCopyrightText: 2006 Michaƫl Larouche <michael.larouche@kdemail.net>
8 SPDX-FileCopyrightText: 2008 Allen Winter <winter@kde.org>
9 SPDX-FileCopyrightText: 2020 Tomaz Cananbrava <tcanabrava@kde.org>
10
11 SPDX-License-Identifier: LGPL-2.0-or-later
12*/
13
14#include "KConfigXmlParser.h"
15
16#include <QDebug>
17#include <QDomAttr>
18#include <QDomElement>
19#include <QDomNode>
20#include <QFile>
21#include <QList>
22#include <QStringList>
23#include <QTextStream>
24#include <QTime>
25#include <iostream>
26// TODO: Move preprocessDefault to Header / CPP implementation.
27// it makes no sense for a parser to process those values and generate code.
28
29static void preProcessDefault(QString &defaultValue,
30 const QString &name,
31 const QString &type,
32 const CfgEntry::Choices &cfgChoices,
33 QString &code,
34 const KConfigParameters &cfg)
35{
36 if (type == QLatin1String("String") && !defaultValue.isEmpty()) {
37 defaultValue = literalString(s: defaultValue);
38
39 } else if (type == QLatin1String("Path") && !defaultValue.isEmpty()) {
40 defaultValue = literalString(s: defaultValue);
41 } else if (type == QLatin1String("Url") && !defaultValue.isEmpty()) {
42 // Use fromUserInput in order to support absolute paths and absolute urls, like KDE4's KUrl(QString) did.
43 defaultValue = QLatin1String("QUrl::fromUserInput( %1)").arg(args: literalString(s: defaultValue));
44 } else if ((type == QLatin1String("UrlList") || type == QLatin1String("StringList") || type == QLatin1String("PathList")) && !defaultValue.isEmpty()) {
45 QTextStream cpp(&code, QIODevice::WriteOnly | QIODevice::Append);
46 if (!code.isEmpty()) {
47 cpp << '\n';
48 }
49
50 if (type == QLatin1String("UrlList")) {
51 cpp << " QList<QUrl> default" << name << ";\n";
52 } else {
53 cpp << " QStringList default" << name << ";\n";
54 }
55 const QStringList defaults = defaultValue.split(sep: QLatin1Char(','));
56 for (const auto &val : defaults) {
57 cpp << " default" << name << ".append( ";
58 if (type == QLatin1String("UrlList")) {
59 cpp << "QUrl::fromUserInput(";
60 }
61 cpp << "QString::fromUtf8( \"" << val << "\" ) ";
62 if (type == QLatin1String("UrlList")) {
63 cpp << ") ";
64 }
65 cpp << ");\n";
66 }
67 defaultValue = QLatin1String("default") + name;
68
69 } else if (type == QLatin1String("Color") && !defaultValue.isEmpty()) {
70 static const QRegularExpression colorRe(QRegularExpression::anchoredPattern(QStringLiteral("\\d+,\\s*\\d+,\\s*\\d+(,\\s*\\d+)?")));
71
72 if (colorRe.match(subject: defaultValue).hasMatch()) {
73 defaultValue = QLatin1String("QColor( %1 )").arg(args&: defaultValue);
74 } else {
75 defaultValue = QLatin1String("QColor( \"%1\" )").arg(args&: defaultValue);
76 }
77
78 } else if (type == QLatin1String("Enum")) {
79 for (const auto &choice : cfgChoices.choices) {
80 if (choice.name == defaultValue) {
81 if (cfg.globalEnums && cfgChoices.name().isEmpty()) {
82 defaultValue.prepend(s: cfgChoices.prefix);
83 } else {
84 defaultValue.prepend(s: enumTypeQualifier(n: name, c: cfgChoices) + cfgChoices.prefix);
85 }
86 break;
87 }
88 }
89
90 } else if (type == QLatin1String("IntList")) {
91 QTextStream cpp(&code, QIODevice::WriteOnly | QIODevice::Append);
92 if (!code.isEmpty()) {
93 cpp << '\n';
94 }
95
96 cpp << " QList<int> default" << name << ";\n";
97 if (!defaultValue.isEmpty()) {
98 const QStringList defaults = defaultValue.split(sep: QLatin1Char(','));
99 for (const auto &defaultVal : defaults) {
100 cpp << " default" << name << ".append( " << defaultVal << " );\n";
101 }
102 }
103 defaultValue = QLatin1String("default") + name;
104 } else if (type == QLatin1String("Time")) {
105 if (!defaultValue.isEmpty()) {
106 const QTime time = QTime::fromString(string: defaultValue);
107 defaultValue = QStringLiteral("QTime(%1, %2, %3)").arg(a: time.hour()).arg(a: time.minute()).arg(a: time.second());
108 }
109 }
110}
111
112static QString dumpNode(const QDomNode &node)
113{
114 QString msg;
115 QTextStream s(&msg, QIODevice::WriteOnly);
116 node.save(s, 0);
117
118 msg = msg.simplified();
119 if (msg.length() > 40) {
120 return msg.left(n: 37) + QLatin1String("...");
121 }
122 return msg;
123}
124
125void KConfigXmlParser::readParameterFromEntry(CfgEntry &readEntry, const QDomElement &e)
126{
127 readEntry.param = e.attribute(QStringLiteral("name"));
128 readEntry.paramType = e.attribute(QStringLiteral("type"));
129
130 if (readEntry.param.isEmpty()) {
131 std::cerr << "Parameter must have a name: " << qPrintable(dumpNode(e)) << std::endl;
132 exit(status: 1);
133 }
134
135 if (readEntry.paramType.isEmpty()) {
136 std::cerr << "Parameter must have a type: " << qPrintable(dumpNode(e)) << std::endl;
137 exit(status: 1);
138 }
139
140 if ((readEntry.paramType == QLatin1String("Int")) || (readEntry.paramType == QLatin1String("UInt"))) {
141 bool ok;
142 readEntry.paramMax = e.attribute(QStringLiteral("max")).toInt(ok: &ok);
143 if (!ok) {
144 std::cerr << "Integer parameter must have a maximum (e.g. max=\"0\"): " << qPrintable(dumpNode(e)) << std::endl;
145 exit(status: 1);
146 }
147 } else if (readEntry.paramType == QLatin1String("Enum")) {
148 for (QDomElement e2 = e.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) {
149 if (e2.tagName() == QLatin1String("values")) {
150 for (QDomElement e3 = e2.firstChildElement(); !e3.isNull(); e3 = e3.nextSiblingElement()) {
151 if (e3.tagName() == QLatin1String("value")) {
152 readEntry.paramValues.append(t: e3.text());
153 }
154 }
155 break;
156 }
157 }
158 if (readEntry.paramValues.isEmpty()) {
159 std::cerr << "No values specified for parameter '" << qPrintable(readEntry.param) << "'." << std::endl;
160 exit(status: 1);
161 }
162 readEntry.paramMax = readEntry.paramValues.count() - 1;
163 } else {
164 std::cerr << "Parameter '" << qPrintable(readEntry.param) << "' has type " << qPrintable(readEntry.paramType)
165 << " but must be of type int, uint or Enum." << std::endl;
166 exit(status: 1);
167 }
168}
169
170bool KConfigXmlParser::hasDefaultCode(CfgEntry &readEntry, const QDomElement &element)
171{
172 Q_UNUSED(readEntry)
173
174 for (QDomElement e = element.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
175 if (e.attribute(QStringLiteral("param")).isEmpty()) {
176 if (e.attribute(QStringLiteral("code")) == QLatin1String("true")) {
177 return true;
178 }
179 }
180 }
181 return false;
182}
183
184void KConfigXmlParser::readChoicesFromEntry(CfgEntry &readEntry, const QDomElement &e)
185{
186 QList<CfgEntry::Choice> chlist;
187 const static QRegularExpression choiceNameRegex(QStringLiteral("\\w+"));
188
189 for (QDomElement e2 = e.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) {
190 if (e2.tagName() != QLatin1String("choice")) {
191 continue;
192 }
193 CfgEntry::Choice choice;
194 choice.name = e2.attribute(QStringLiteral("name"));
195 if (choice.name.isEmpty()) {
196 std::cerr << "Tag <choice> requires attribute 'name'." << std::endl;
197 } else if (!choiceNameRegex.match(subject: choice.name).hasMatch()) {
198 std::cerr << "Tag <choice> attribute 'name' must be compatible with Enum naming. name was '" << qPrintable(choice.name)
199 << "'. You can use attribute 'value' to pass any string as the choice value." << std::endl;
200 }
201 choice.val = e2.attribute(QStringLiteral("value"));
202 for (QDomElement e3 = e2.firstChildElement(); !e3.isNull(); e3 = e3.nextSiblingElement()) {
203 if (e3.tagName() == QLatin1String("label")) {
204 choice.label = e3.text();
205 choice.context = e3.attribute(QStringLiteral("context"));
206 }
207 if (e3.tagName() == QLatin1String("tooltip")) {
208 choice.toolTip = e3.text();
209 choice.context = e3.attribute(QStringLiteral("context"));
210 }
211 if (e3.tagName() == QLatin1String("whatsthis")) {
212 choice.whatsThis = e3.text();
213 choice.context = e3.attribute(QStringLiteral("context"));
214 }
215 }
216 chlist.append(t: choice);
217 }
218
219 QString name = e.attribute(QStringLiteral("name"));
220 QString prefix = e.attribute(QStringLiteral("prefix"));
221
222 readEntry.choices = CfgEntry::Choices(chlist, name, prefix);
223}
224
225void KConfigXmlParser::readGroupElements(CfgEntry &readEntry, const QDomElement &element)
226{
227 for (QDomElement e = element.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
228 QString tag = e.tagName();
229 if (tag == QLatin1String("label")) {
230 readEntry.label = e.text();
231 readEntry.labelContext = e.attribute(QStringLiteral("context"));
232 } else if (tag == QLatin1String("tooltip")) {
233 readEntry.toolTip = e.text();
234 readEntry.toolTipContext = e.attribute(QStringLiteral("context"));
235 } else if (tag == QLatin1String("whatsthis")) {
236 readEntry.whatsThis = e.text();
237 readEntry.whatsThisContext = e.attribute(QStringLiteral("context"));
238 } else if (tag == QLatin1String("min")) {
239 readEntry.min = e.text();
240 } else if (tag == QLatin1String("max")) {
241 readEntry.max = e.text();
242 } else if (tag == QLatin1String("code")) {
243 readEntry.code = e.text();
244 } else if (tag == QLatin1String("parameter")) {
245 readParameterFromEntry(readEntry, e);
246 } else if (tag == QLatin1String("default")) {
247 if (e.attribute(QStringLiteral("param")).isEmpty()) {
248 readEntry.defaultValue = e.text();
249 }
250 } else if (tag == QLatin1String("choices")) {
251 readChoicesFromEntry(readEntry, e);
252 } else if (tag == QLatin1String("emit")) {
253 Signal signal;
254 signal.name = e.attribute(QStringLiteral("signal"));
255 readEntry.signalList.append(t: signal);
256 }
257 }
258}
259
260void KConfigXmlParser::createChangedSignal(CfgEntry &readEntry)
261{
262 if (cfg.generateProperties && (cfg.allMutators || cfg.mutators.contains(str: readEntry.name))) {
263 Signal s;
264 s.name = changeSignalName(n: readEntry.name);
265 s.modify = true;
266 readEntry.signalList.append(t: s);
267 }
268}
269
270void KConfigXmlParser::validateNameAndKey(CfgEntry &readEntry, const QDomElement &element)
271{
272 bool nameIsEmpty = readEntry.name.isEmpty();
273 if (nameIsEmpty && readEntry.key.isEmpty()) {
274 std::cerr << "Entry must have a name or a key: " << qPrintable(dumpNode(element)) << std::endl;
275 exit(status: 1);
276 }
277
278 if (readEntry.key.isEmpty()) {
279 readEntry.key = readEntry.name;
280 }
281
282 if (nameIsEmpty) {
283 readEntry.name = readEntry.key;
284 readEntry.name.remove(c: QLatin1Char(' '));
285 } else if (readEntry.name.contains(c: QLatin1Char(' '))) {
286 std::cout << "Entry '" << qPrintable(readEntry.name) << "' contains spaces! <name> elements can not contain spaces!" << std::endl;
287 readEntry.name.remove(c: QLatin1Char(' '));
288 }
289
290 if (readEntry.name.contains(QStringLiteral("$("))) {
291 if (readEntry.param.isEmpty()) {
292 std::cerr << "Name may not be parameterized: " << qPrintable(readEntry.name) << std::endl;
293 exit(status: 1);
294 }
295 } else {
296 if (!readEntry.param.isEmpty()) {
297 std::cerr << "Name must contain '$(" << qPrintable(readEntry.param) << ")': " << qPrintable(readEntry.name) << std::endl;
298 exit(status: 1);
299 }
300 }
301}
302
303void KConfigXmlParser::readParamDefaultValues(CfgEntry &readEntry, const QDomElement &element)
304{
305 if (readEntry.param.isEmpty()) {
306 return;
307 }
308 // Adjust name
309 readEntry.paramName = readEntry.name;
310
311 readEntry.name.remove(QStringLiteral("$(") + readEntry.param + QLatin1Char(')'));
312 // Lookup defaults for indexed entries
313 for (int i = 0; i <= readEntry.paramMax; i++) {
314 readEntry.paramDefaultValues.append(t: QString());
315 }
316
317 for (QDomElement e = element.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
318 QString tag = e.tagName();
319 if (tag != QLatin1String("default")) {
320 continue;
321 }
322 QString index = e.attribute(QStringLiteral("param"));
323 if (index.isEmpty()) {
324 continue;
325 }
326
327 bool ok;
328 int i = index.toInt(ok: &ok);
329 if (!ok) {
330 i = readEntry.paramValues.indexOf(str: index);
331 if (i == -1) {
332 std::cerr << "Index '" << qPrintable(index) << "' for default value is unknown." << std::endl;
333 exit(status: 1);
334 }
335 }
336
337 if ((i < 0) || (i > readEntry.paramMax)) {
338 std::cerr << "Index '" << i << "' for default value is out of range [0, " << readEntry.paramMax << "]." << std::endl;
339 exit(status: 1);
340 }
341
342 QString tmpDefaultValue = e.text();
343
344 if (e.attribute(QStringLiteral("code")) != QLatin1String("true")) {
345 preProcessDefault(defaultValue&: tmpDefaultValue, name: readEntry.name, type: readEntry.type, cfgChoices: readEntry.choices, code&: readEntry.code, cfg);
346 }
347
348 readEntry.paramDefaultValues[i] = tmpDefaultValue;
349 }
350}
351
352CfgEntry *KConfigXmlParser::parseEntry(const QString &group, const QString &parentGroup, const QDomElement &element)
353{
354 CfgEntry readEntry;
355 readEntry.type = element.attribute(QStringLiteral("type"));
356 readEntry.name = element.attribute(QStringLiteral("name"));
357 readEntry.key = element.attribute(QStringLiteral("key"));
358 readEntry.hidden = element.attribute(QStringLiteral("hidden")) == QLatin1String("true");
359 ;
360 readEntry.group = group;
361 readEntry.parentGroup = parentGroup;
362
363 const bool nameIsEmpty = readEntry.name.isEmpty();
364
365 readGroupElements(readEntry, element);
366
367 validateNameAndKey(readEntry, element);
368
369 if (readEntry.label.isEmpty()) {
370 readEntry.label = readEntry.key;
371 }
372
373 if (readEntry.type.isEmpty()) {
374 readEntry.type = QStringLiteral("String"); // XXX : implicit type might be bad
375 }
376
377 readParamDefaultValues(readEntry, element);
378
379 if (!mValidNameRegexp.match(subject: readEntry.name).hasMatch()) {
380 if (nameIsEmpty) {
381 std::cerr << "The key '" << qPrintable(readEntry.key)
382 << "' can not be used as name for the entry because "
383 "it is not a valid name. You need to specify a valid name for this entry."
384 << std::endl;
385 } else {
386 std::cerr << "The name '" << qPrintable(readEntry.name) << "' is not a valid name for an entry." << std::endl;
387 }
388 exit(status: 1);
389 }
390
391 if (mAllNames.contains(str: readEntry.name)) {
392 if (nameIsEmpty) {
393 std::cerr << "The key '" << qPrintable(readEntry.key)
394 << "' can not be used as name for the entry because "
395 "it does not result in a unique name. You need to specify a unique name for this entry."
396 << std::endl;
397 } else {
398 std::cerr << "The name '" << qPrintable(readEntry.name) << "' is not unique." << std::endl;
399 }
400 exit(status: 1);
401 }
402
403 mAllNames.append(t: readEntry.name);
404
405 if (!hasDefaultCode(readEntry, element)) {
406 // TODO: Move all the options to CfgEntry.
407 preProcessDefault(defaultValue&: readEntry.defaultValue, name: readEntry.name, type: readEntry.type, cfgChoices: readEntry.choices, code&: readEntry.code, cfg);
408 }
409
410 // TODO: Try to Just return the CfgEntry we populated instead of
411 // creating another one to fill the code.
412 CfgEntry *result = new CfgEntry();
413 result->group = readEntry.group;
414 result->parentGroup = readEntry.parentGroup;
415 result->type = readEntry.type;
416 result->key = readEntry.key;
417 result->name = readEntry.name;
418 result->labelContext = readEntry.labelContext;
419 result->label = readEntry.label;
420 result->toolTipContext = readEntry.toolTipContext;
421 result->toolTip = readEntry.toolTip;
422 result->whatsThisContext = readEntry.whatsThisContext;
423 result->whatsThis = readEntry.whatsThis;
424 result->code = readEntry.code;
425 result->defaultValue = readEntry.defaultValue;
426 result->choices = readEntry.choices;
427 result->signalList = readEntry.signalList;
428 result->hidden = readEntry.hidden;
429
430 if (!readEntry.param.isEmpty()) {
431 result->param = readEntry.param;
432 result->paramName = readEntry.paramName;
433 result->paramType = readEntry.paramType;
434 result->paramValues = readEntry.paramValues;
435 result->paramDefaultValues = readEntry.paramDefaultValues;
436 result->paramMax = readEntry.paramMax;
437 }
438 result->min = readEntry.min;
439 result->max = readEntry.max;
440 createChangedSignal(readEntry&: *result);
441
442 return result;
443}
444
445// TODO: Change the name of the config variable.
446KConfigXmlParser::KConfigXmlParser(const KConfigParameters &cfg, const QString &inputFileName)
447 : cfg(cfg)
448 , mInputFileName(inputFileName)
449{
450 mValidNameRegexp.setPattern(QRegularExpression::anchoredPattern(QStringLiteral("[a-zA-Z_][a-zA-Z0-9_]*")));
451}
452
453void KConfigXmlParser::start()
454{
455 QFile input(mInputFileName);
456 if (!input.open(flags: QIODevice::ReadOnly)) {
457 qFatal(msg: "Could not open input file: %s", qUtf8Printable(mInputFileName));
458 }
459 QDomDocument doc;
460 const QDomDocument::ParseResult parseResult = doc.setContent(device: &input);
461 if (!parseResult) {
462 std::cerr << "Unable to load document." << std::endl;
463 std::cerr << "Parse error in " << qPrintable(mInputFileName) << ", line " << parseResult.errorLine << ", col " << parseResult.errorColumn << ": "
464 << qPrintable(parseResult.errorMessage) << std::endl;
465 exit(status: 1);
466 }
467
468 QDomElement cfgElement = doc.documentElement();
469 if (cfgElement.isNull()) {
470 std::cerr << "No document in kcfg file" << std::endl;
471 exit(status: 1);
472 }
473
474 for (QDomElement element = cfgElement.firstChildElement(); !element.isNull(); element = element.nextSiblingElement()) {
475 QString tag = element.tagName();
476
477 if (tag == QLatin1String("include")) {
478 readIncludeTag(element);
479 } else if (tag == QLatin1String("kcfgfile")) {
480 readKcfgfileTag(element);
481 } else if (tag == QLatin1String("group")) {
482 readGroupTag(element);
483 } else if (tag == QLatin1String("signal")) {
484 readSignalTag(element);
485 }
486 }
487}
488
489ParseResult KConfigXmlParser::getParseResult() const
490{
491 return mParseResult;
492}
493
494void KConfigXmlParser::readIncludeTag(const QDomElement &e)
495{
496 QString includeFile = e.text();
497 if (!includeFile.isEmpty()) {
498 mParseResult.includes.append(t: includeFile);
499 }
500}
501
502void KConfigXmlParser::readGroupTag(const QDomElement &e)
503{
504 QString group = e.attribute(QStringLiteral("name"));
505 if (group.isEmpty()) {
506 std::cerr << "Group without name" << std::endl;
507 exit(status: 1);
508 }
509
510 const QString parentGroup = e.attribute(QStringLiteral("parentGroupName"));
511
512 for (QDomElement e2 = e.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) {
513 if (e2.tagName() != QLatin1String("entry")) {
514 continue;
515 }
516 CfgEntry *entry = parseEntry(group, parentGroup, element: e2);
517 if (entry) {
518 mParseResult.entries.append(t: entry);
519 } else {
520 std::cerr << "Can not parse entry." << std::endl;
521 exit(status: 1);
522 }
523 }
524}
525
526void KConfigXmlParser::readKcfgfileTag(const QDomElement &e)
527{
528 mParseResult.cfgFileName = e.attribute(QStringLiteral("name"));
529 mParseResult.cfgStateConfig = e.attribute(QStringLiteral("stateConfig")).toLower() == QLatin1String("true");
530 mParseResult.cfgFileNameArg = e.attribute(QStringLiteral("arg")).toLower() == QLatin1String("true");
531 for (QDomElement e2 = e.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) {
532 if (e2.tagName() == QLatin1String("parameter")) {
533 Param p;
534 p.name = e2.attribute(QStringLiteral("name"));
535 p.type = e2.attribute(QStringLiteral("type"));
536 if (p.type.isEmpty()) {
537 p.type = QStringLiteral("String");
538 }
539 mParseResult.parameters.append(t: p);
540 }
541 }
542}
543
544void KConfigXmlParser::readSignalTag(const QDomElement &e)
545{
546 QString signalName = e.attribute(QStringLiteral("name"));
547 if (signalName.isEmpty()) {
548 std::cerr << "Signal without name." << std::endl;
549 exit(status: 1);
550 }
551 Signal theSignal;
552 theSignal.name = signalName;
553
554 for (QDomElement e2 = e.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) {
555 if (e2.tagName() == QLatin1String("argument")) {
556 Param argument;
557 argument.type = e2.attribute(QStringLiteral("type"));
558 if (argument.type.isEmpty()) {
559 std::cerr << "Signal argument without type." << std::endl;
560 exit(status: 1);
561 }
562 argument.name = e2.text();
563 theSignal.arguments.append(t: argument);
564 } else if (e2.tagName() == QLatin1String("label")) {
565 theSignal.label = e2.text();
566 }
567 }
568
569 mParseResult.signalList.append(t: theSignal);
570}
571

source code of kconfig/src/kconfig_compiler/KConfigXmlParser.cpp