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