Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmldirparser.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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
6
7#include <QtCore/QtDebug>
8
10
11static int parseInt(QStringView str, bool *ok)
12{
13 int pos = 0;
14 int number = 0;
15 while (pos < str.size() && str.at(pos).isDigit()) {
16 if (pos != 0)
17 number *= 10;
18 number += str.at(pos).unicode() - '0';
19 ++pos;
20 }
21 if (pos != str.size())
22 *ok = false;
23 else
24 *ok = true;
25 return number;
26}
27
28static QTypeRevision parseVersion(const QString &str)
29{
30 const int dotIndex = str.indexOf(QLatin1Char('.'));
31 if (dotIndex != -1 && str.indexOf(QLatin1Char('.'), dotIndex + 1) == -1) {
32 bool ok = false;
33 const int major = parseInt(QStringView(str).left(dotIndex), &ok);
34 if (!ok) return QTypeRevision();
35 const int minor = parseInt(QStringView(str).mid(dotIndex + 1, str.size() - dotIndex - 1), &ok);
36 return ok ? QTypeRevision::fromVersion(major, minor) : QTypeRevision();
37 }
38 return QTypeRevision();
39}
40
41void QQmlDirParser::clear()
42{
43 _errors.clear();
44 _typeNamespace.clear();
45 _components.clear();
46 _dependencies.clear();
47 _imports.clear();
48 _exports.clear();
49 _scripts.clear();
50 _plugins.clear();
51 _designerSupported = false;
52 _typeInfos.clear();
53 _classNames.clear();
54 _linkTarget.clear();
55}
56
57inline static void scanSpace(const QChar *&ch) {
58 while (ch->isSpace() && !ch->isNull() && *ch != QLatin1Char('\n'))
59 ++ch;
60}
61
62inline static void scanToEnd(const QChar *&ch) {
63 while (*ch != QLatin1Char('\n') && !ch->isNull())
64 ++ch;
65}
66
67inline static QString scanWord(const QChar *&ch) {
68 const QChar *begin = ch;
69 while (!ch->isSpace() && !ch->isNull())
70 ++ch;
71 return QString(begin, ch - begin);
72}
73
74QString QQmlDirParser::scanQuotedWord(const QChar *&ch, quint16 lineNumber, quint16 columnNumber)
75{
76 Q_ASSERT(*ch == QLatin1Char('"'));
77 ++ch;
78
79 QString result;
80
81 const QChar *begin = ch;
82 while (*ch != QLatin1Char('"')) {
83 if (ch->isNull()) {
84 reportError(lineNumber, columnNumber,
85 QStringLiteral("file ends inside a quoted string"));
86 result.append(begin, ch - begin);
87 return result;
88 }
89
90 if (*ch == QLatin1Char('\n') || *ch == QLatin1Char('\r')) {
91 reportError(lineNumber, columnNumber,
92 QStringLiteral("line breaks in quoted strings are not supported as they "
93 "are not portable between different operating systems"));
94 result.append(begin, ch - begin);
95 return result;
96 }
97
98 if (*ch == QLatin1Char('\\')) {
99 result.append(begin, ch - begin);
100 ++ch;
101 ++columnNumber;
102 if (*ch != QLatin1Char('"') && *ch != QLatin1Char('\\')) {
103 reportError(lineNumber, columnNumber,
104 QStringLiteral("only '\"' and '\\' can be escaped"));
105 return result;
106 }
107 begin = ch;
108 }
109
110 ++ch;
111 ++columnNumber;
112 }
113
114 result.append(begin, ch - begin);
115
116 Q_ASSERT(*ch == QLatin1Char('"'));
117 ++ch;
118
119 return result;
120}
121
122bool QQmlDirParser::parse(const QString &source)
123{
124 quint16 lineNumber = 0;
125 bool firstLine = true;
126
127 auto readImport = [&](const QString *sections, int sectionCount, Import::Flags flags) {
128 Import import;
129 if (sectionCount == 2) {
130 import = Import(sections[1], QTypeRevision(), flags);
131 } else if (sectionCount == 3) {
132 if (sections[2] == QLatin1String("auto")) {
133 import = Import(sections[1], QTypeRevision(), flags | Import::Auto);
134 } else {
135 const auto version = parseVersion(sections[2]);
136 if (version.isValid()) {
137 import = Import(sections[1], version, flags);
138 } else {
139 reportError(lineNumber, 0,
140 QStringLiteral("invalid version %1, expected <major>.<minor>")
141 .arg(sections[2]));
142 return false;
143 }
144 }
145 } else {
146 reportError(lineNumber, 0,
147 QStringLiteral("%1 requires 1 or 2 arguments, but %2 were provided")
148 .arg(sections[0]).arg(sectionCount - 1));
149 return false;
150 }
151 if (sections[0] == QStringLiteral("import"))
152 _imports.append(import);
153 else
154 _dependencies.append(import);
155 return true;
156 };
157
158 auto readExport = [&](const QString *sections, int sectionCount) {
159 if (sectionCount < 2) {
160 reportError(lineNumber, 0,
161 QStringLiteral("export at least 1 argument, but none were provided"));
162 return false;
163 }
164
165 const QTypeRevision version = parseVersion(sections[1]);
166 if (!version.isValid()) {
167 reportError(lineNumber, 0,
168 QStringLiteral("invalid version %1, expected <major>.<minor>")
169 .arg(sections[1]));
170 return false;
171 }
172
173 _exports.append(version);
174 for (int i = 2; i < sectionCount; ++i) {
175 bool ok = false;
176 const int major = parseInt(sections[i], &ok);
177 if (!ok) {
178 reportError(lineNumber, 0,
179 QStringLiteral("invalid major version %1, expected integer")
180 .arg(sections[i]));
181 return false;
182 }
183
184 // For PAST_MAJOR_VERSION entries we add the maximum minor version.
185 // Any minor version of that major can be imported.
186 _exports.append(QTypeRevision::fromVersion(major, 254));
187 }
188
189 return true;
190 };
191
192 auto readPlugin = [&](const QString *sections, int sectionCount, bool isOptional) {
193 if (sectionCount < 2 || sectionCount > 3) {
194 reportError(lineNumber, 0, QStringLiteral("plugin directive requires one or two "
195 "arguments, but %1 were provided")
196 .arg(sectionCount - 1));
197 return false;
198 }
199
200 const Plugin entry(sections[1], sections[2], isOptional);
201 _plugins.append(entry);
202 return true;
203 };
204
205 const QChar *ch = source.constData();
206 while (!ch->isNull()) {
207 ++lineNumber;
208
209 bool invalidLine = false;
210 const QChar *lineStart = ch;
211
212 scanSpace(ch);
213 if (*ch == QLatin1Char('\n')) {
214 ++ch;
215 continue;
216 }
217 if (ch->isNull())
218 break;
219
220 QString sections[4];
221 int sectionCount = 0;
222
223 do {
224 if (*ch == QLatin1Char('#')) {
225 scanToEnd(ch);
226 break;
227 }
228
229 if (sectionCount >= 4) {
230 reportError(lineNumber, ch - lineStart, QLatin1String("unexpected token"));
231 scanToEnd(ch);
232 invalidLine = true;
233 break;
234 }
235
236 sections[sectionCount++] = (*ch == QLatin1Char('"'))
237 ? scanQuotedWord(ch, lineNumber, ch - lineStart)
238 : scanWord(ch);
239
240 scanSpace(ch);
241 } while (*ch != QLatin1Char('\n') && !ch->isNull());
242
243 if (!ch->isNull())
244 ++ch;
245
246 if (invalidLine) {
247 reportError(lineNumber, 0,
248 QStringLiteral("invalid qmldir directive contains too many tokens"));
249 continue;
250 } else if (sectionCount == 0) {
251 continue; // no sections, no party.
252
253 } else if (sections[0] == QLatin1String("module")) {
254 if (sectionCount != 2) {
255 reportError(lineNumber, 0,
256 QStringLiteral("module identifier directive requires one argument, but %1 were provided").arg(sectionCount - 1));
257 continue;
258 }
259 if (!_typeNamespace.isEmpty()) {
260 reportError(lineNumber, 0,
261 QStringLiteral("only one module identifier directive may be defined in a qmldir file"));
262 continue;
263 }
264 if (!firstLine) {
265 reportError(lineNumber, 0,
266 QStringLiteral("module identifier directive must be the first directive in a qmldir file"));
267 continue;
268 }
269
270 _typeNamespace = sections[1];
271
272 } else if (sections[0] == QLatin1String("plugin")) {
273 if (!readPlugin(sections, sectionCount, false))
274 continue;
275 } else if (sections[0] == QLatin1String("optional")) {
276 if (sectionCount < 2) {
277 reportError(lineNumber, 0, QStringLiteral("optional directive requires further "
278 "arguments, but none were provided."));
279 continue;
280 }
281
282 if (sections[1] == QStringLiteral("plugin")) {
283 if (!readPlugin(sections + 1, sectionCount - 1, true))
284 continue;
285 } else if (sections[1] == QLatin1String("import")) {
286 if (!readImport(sections + 1, sectionCount - 1, Import::Optional))
287 continue;
288 } else {
289 reportError(lineNumber, 0, QStringLiteral("only import and plugin can be optional, "
290 "not %1.").arg(sections[1]));
291 continue;
292 }
293 } else if (sections[0] == QLatin1String("default")) {
294 if (sectionCount < 2) {
295 reportError(lineNumber, 0,
296 QStringLiteral("default directive requires further "
297 "arguments, but none were provided."));
298 continue;
299 }
300 if (sections[1] == QLatin1String("import")) {
301 if (!readImport(sections + 1, sectionCount - 1,
302 Import::Flags({ Import::Optional, Import::OptionalDefault })))
303 continue;
304 } else {
305 reportError(lineNumber, 0,
306 QStringLiteral("only optional imports can have a default, "
307 "not %1.")
308 .arg(sections[1]));
309 }
310 } else if (sections[0] == QLatin1String("classname")) {
311 if (sectionCount < 2) {
312 reportError(lineNumber, 0,
313 QStringLiteral("classname directive requires an argument, but %1 were provided").arg(sectionCount - 1));
314
315 continue;
316 }
317
318 _classNames.append(sections[1]);
319
320 } else if (sections[0] == QLatin1String("internal")) {
321 if (sectionCount == 3) {
322 Component entry(sections[1], sections[2], QTypeRevision());
323 entry.internal = true;
324 _components.insert(entry.typeName, entry);
325 } else if (sectionCount == 4) {
326 const QTypeRevision version = parseVersion(sections[2]);
327 if (version.isValid()) {
328 Component entry(sections[1], sections[3], version);
329 entry.internal = true;
330 _components.insert(entry.typeName, entry);
331 } else {
332 reportError(lineNumber, 0,
333 QStringLiteral("invalid version %1, expected <major>.<minor>")
334 .arg(sections[2]));
335 continue;
336 }
337 } else {
338 reportError(lineNumber, 0,
339 QStringLiteral("internal types require 2 or 3 arguments, "
340 "but %1 were provided").arg(sectionCount - 1));
341 continue;
342 }
343 } else if (sections[0] == QLatin1String("singleton")) {
344 if (sectionCount < 3 || sectionCount > 4) {
345 reportError(lineNumber, 0,
346 QStringLiteral("singleton types require 2 or 3 arguments, but %1 were provided").arg(sectionCount - 1));
347 continue;
348 } else if (sectionCount == 3) {
349 // handle qmldir directory listing case where singleton is defined in the following pattern:
350 // singleton TestSingletonType TestSingletonType.qml
351 Component entry(sections[1], sections[2], QTypeRevision());
352 entry.singleton = true;
353 _components.insert(entry.typeName, entry);
354 } else {
355 // handle qmldir module listing case where singleton is defined in the following pattern:
356 // singleton TestSingletonType 2.0 TestSingletonType20.qml
357 const QTypeRevision version = parseVersion(sections[2]);
358 if (version.isValid()) {
359 const QString &fileName = sections[3];
360 Component entry(sections[1], fileName, version);
361 entry.singleton = true;
362 _components.insert(entry.typeName, entry);
363 } else {
364 reportError(lineNumber, 0, QStringLiteral("invalid version %1, expected <major>.<minor>").arg(sections[2]));
365 }
366 }
367 } else if (sections[0] == QLatin1String("typeinfo")) {
368 if (sectionCount != 2) {
369 reportError(lineNumber, 0,
370 QStringLiteral("typeinfo requires 1 argument, but %1 were provided").arg(sectionCount - 1));
371 continue;
372 }
373 _typeInfos.append(sections[1]);
374 } else if (sections[0] == QLatin1String("designersupported")) {
375 if (sectionCount != 1)
376 reportError(lineNumber, 0, QStringLiteral("designersupported does not expect any argument"));
377 else
378 _designerSupported = true;
379 } else if (sections[0] == QLatin1String("static")) {
380 if (sectionCount != 1)
381 reportError(lineNumber, 0, QStringLiteral("static does not expect any argument"));
382 else
383 _isStaticModule = true;
384 } else if (sections[0] == QLatin1String("system")) {
385 if (sectionCount != 1)
386 reportError(lineNumber, 0, QStringLiteral("system does not expect any argument"));
387 else
388 _isSystemModule = true;
389 } else if (sections[0] == QLatin1String("import")
390 || sections[0] == QLatin1String("depends")) {
391 if (!readImport(sections, sectionCount, Import::Default))
392 continue;
393 } else if (sections[0] == QLatin1String("export")) {
394 if (!readExport(sections, sectionCount))
395 continue;
396 } else if (sections[0] == QLatin1String("prefer")) {
397 if (sectionCount < 2) {
398 reportError(lineNumber, 0,
399 QStringLiteral("prefer directive requires one argument, "
400 "but %1 were provided").arg(sectionCount - 1));
401 continue;
402 }
403
404 if (!_preferredPath.isEmpty()) {
405 reportError(lineNumber, 0, QStringLiteral(
406 "only one prefer directive may be defined in a qmldir file"));
407 continue;
408 }
409
410 if (!sections[1].endsWith(u'/')) {
411 // Yes. People should realize it's a directory.
412 reportError(lineNumber, 0, QStringLiteral(
413 "the preferred directory has to end with a '/'"));
414 continue;
415 }
416
417 _preferredPath = sections[1];
418 } else if (sections[0] == QLatin1String("linktarget")) {
419 if (sectionCount < 2) {
420 reportError(lineNumber, 0,
421 QStringLiteral("linktarget directive requires an argument, "
422 "but %1 were provided")
423 .arg(sectionCount - 1));
424 continue;
425 }
426
427 if (!_linkTarget.isEmpty()) {
428 reportError(
429 lineNumber, 0,
430 QStringLiteral(
431 "only one linktarget directive may be defined in a qmldir file"));
432 continue;
433 }
434
435 _linkTarget = sections[1];
436 } else if (sectionCount == 2) {
437 // No version specified (should only be used for relative qmldir files)
438 insertComponentOrScript(sections[0], sections[1], QTypeRevision());
439 } else if (sectionCount == 3) {
440 const QTypeRevision version = parseVersion(sections[1]);
441 if (version.isValid()) {
442 insertComponentOrScript(sections[0], sections[2], version);
443 } else {
444 reportError(
445 lineNumber, 0,
446 QStringLiteral("invalid version %1, expected <major>.<minor>")
447 .arg(sections[1]));
448 }
449 } else {
450 reportError(lineNumber, 0,
451 QStringLiteral("a component declaration requires two or three arguments, but %1 were provided").arg(sectionCount));
452 }
453
454 firstLine = false;
455 }
456
457 return !hasError();
458}
459
460/* removes all file selector occurrences in path
461 firstPlus is the position of the initial '+' in the path
462 which we always have as we check for '+' to decide whether
463 we need to do some work at all
464*/
465static QString pathWithoutFileSelectors(QString path, // we want a copy of path
466 qsizetype firstPlus)
467{
468 do {
469 Q_ASSERT(path.at(firstPlus) == u'+');
470 const auto eos = path.size();
471 qsizetype terminatingSlashPos = firstPlus + 1;
472 while (terminatingSlashPos != eos && path.at(terminatingSlashPos) != u'/')
473 ++terminatingSlashPos;
474 path.remove(firstPlus, terminatingSlashPos - firstPlus + 1);
475 firstPlus = path.indexOf(u'+', firstPlus);
476 } while (firstPlus != -1);
477 return path;
478}
479
480static bool canDisambiguate(
481 const QString &fileName1, const QString &fileName2, QString *correctedFileName)
482{
483 // If the entries are exactly the same we can delete one without losing anything.
484 if (fileName1 == fileName2)
485 return true;
486
487 // If we detect conflicting paths, we check if they agree when we remove anything
488 // looking like a file selector.
489
490 // ugly heuristic to deal with file selectors
491 const qsizetype file2PotentialFileSelectorPos = fileName2.indexOf(u'+');
492 const bool file2MightHaveFileSelector = file2PotentialFileSelectorPos != -1;
493
494 if (const qsizetype fileSelectorPos1 = fileName1.indexOf(u'+'); fileSelectorPos1 != -1) {
495 // existing entry was file selector entry, fix it up
496 // it could also be the case that _both_ are using file selectors
497 const QString baseName = file2MightHaveFileSelector
498 ? pathWithoutFileSelectors(fileName2, file2PotentialFileSelectorPos)
499 : fileName2;
500
501 if (pathWithoutFileSelectors(fileName1, fileSelectorPos1) != baseName)
502 return false;
503
504 *correctedFileName = baseName;
505 return true;
506 }
507
508 // new entry contains file selector (and we know that fileName1 did not)
509 if (file2MightHaveFileSelector
510 && pathWithoutFileSelectors(fileName2, file2PotentialFileSelectorPos) == fileName1) {
511 *correctedFileName = fileName1;
512 return true;
513 }
514
515 return false;
516}
517
518static void disambiguateFileSelectedComponents(QQmlDirComponents *components)
519{
520 using ConstIterator = QQmlDirComponents::const_iterator;
521
522 // end iterator may get invalidated by the erasing below.
523 // Therefore, refetch it on each iteration.
524 for (ConstIterator cit = components->constBegin(); cit != components->constEnd();) {
525
526 // We can erase and re-assign cit if we immediately forget cit2.
527 // But we cannot erase cit2 without potentially invalidating cit.
528
529 bool doErase = false;
530 const ConstIterator cend = components->constEnd();
531 for (ConstIterator cit2 = ++ConstIterator(cit); cit2 != cend; ++cit2) {
532 if (cit2.key() != cit.key())
533 break;
534
535 Q_ASSERT(cit2->typeName == cit->typeName);
536
537 if (cit2->version != cit->version
538 || cit2->internal != cit->internal
539 || cit2->singleton != cit->singleton) {
540 continue;
541 }
542
543 // The two components may differ only by fileName now.
544
545 if (canDisambiguate(cit->fileName, cit2->fileName, &(cit2->fileName))) {
546 doErase = true;
547 break;
548 }
549 }
550
551 if (doErase)
552 cit = components->erase(cit);
553 else
554 ++cit;
555 }
556}
557
558static void disambiguateFileSelectedScripts(QQmlDirScripts *scripts)
559{
560 using Iterator = QQmlDirScripts::iterator;
561
562 Iterator send = scripts->end();
563
564 for (Iterator sit = scripts->begin(); sit != send; ++sit) {
565 send = std::remove_if(++Iterator(sit), send, [sit](const QQmlDirParser::Script &script2) {
566 if (sit->nameSpace != script2.nameSpace || sit->version != script2.version)
567 return false;
568
569 // The two scripts may differ only by fileName now.
570 return canDisambiguate(sit->fileName, script2.fileName, &(sit->fileName));
571 });
572 }
573
574 scripts->erase(send, scripts->end());
575}
576
577void QQmlDirParser::disambiguateFileSelectors()
578{
579 disambiguateFileSelectedComponents(&_components);
580 disambiguateFileSelectedScripts(&_scripts);
581}
582
583void QQmlDirParser::reportError(quint16 line, quint16 column, const QString &description)
584{
585 QQmlJS::DiagnosticMessage error;
586 error.loc.startLine = line;
587 error.loc.startColumn = column;
588 error.message = description;
589 _errors.append(error);
590}
591
592void QQmlDirParser::insertComponentOrScript(
593 const QString &name, const QString &fileName, QTypeRevision version)
594{
595 // A 'js' extension indicates a namespaced script import
596 if (fileName.endsWith(QLatin1String(".js")) || fileName.endsWith(QLatin1String(".mjs")))
597 _scripts.append(Script(name, fileName, version));
598 else
599 _components.insert(name, Component(name, fileName, version));
600}
601
602void QQmlDirParser::setError(const QQmlJS::DiagnosticMessage &e)
603{
604 _errors.clear();
605 reportError(e.loc.startLine, e.loc.startColumn, e.message);
606}
607
608QList<QQmlJS::DiagnosticMessage> QQmlDirParser::errors(const QString &uri) const
609{
610 QList<QQmlJS::DiagnosticMessage> errors;
611 const int numErrors = _errors.size();
612 errors.reserve(numErrors);
613 for (int i = 0; i < numErrors; ++i) {
614 QQmlJS::DiagnosticMessage e = _errors.at(i);
615 e.message.replace(QLatin1String("$$URI$$"), uri);
616 errors << e;
617 }
618 return errors;
619}
620
621QDebug &operator<< (QDebug &debug, const QQmlDirParser::Component &component)
622{
623 const QString output = QStringLiteral("{%1 %2.%3}")
624 .arg(component.typeName).arg(component.version.majorVersion())
625 .arg(component.version.minorVersion());
626 return debug << qPrintable(output);
627}
628
629QDebug &operator<< (QDebug &debug, const QQmlDirParser::Script &script)
630{
631 const QString output = QStringLiteral("{%1 %2.%3}")
632 .arg(script.nameSpace).arg(script.version.majorVersion())
633 .arg(script.version.minorVersion());
634 return debug << qPrintable(output);
635}
636
637QT_END_NAMESPACE
Combined button and popup list for selecting options.
static void scanSpace(const QChar *&ch)
static void disambiguateFileSelectedScripts(QQmlDirScripts *scripts)
static QString scanWord(const QChar *&ch)
static QString pathWithoutFileSelectors(QString path, qsizetype firstPlus)
static void disambiguateFileSelectedComponents(QQmlDirComponents *components)
static void scanToEnd(const QChar *&ch)
static QTypeRevision parseVersion(const QString &str)
static bool canDisambiguate(const QString &fileName1, const QString &fileName2, QString *correctedFileName)
static QT_BEGIN_NAMESPACE int parseInt(QStringView str, bool *ok)
QDataStream & operator<<(QDataStream &stream, const QImage &image)
[0]
Definition qimage.cpp:4045