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

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