Revision control

++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++ #include "qqmldirparser_p.h"
++
++ #include <QtCore/QtDebug>
++
++ QT_BEGIN_NAMESPACE
++
++ static int parseInt(QStringView str, bool *ok)
++ {
++ int pos = 0;
++ int number = 0;
++ while (pos < str.size() && str.at(pos).isDigit()) {
++ if (pos != 0)
++ number *= 10;
++ number += str.at(pos).unicode() - '0';
++ ++pos;
++ }
++ if (pos != str.size())
++ *ok = false;
++ else
++ *ok = true;
++ return number;
++ }
++
++ static QTypeRevision parseVersion(const QString &str)
++ {
++ const int dotIndex = str.indexOf(QLatin1Char('.'));
++ if (dotIndex != -1 && str.indexOf(QLatin1Char('.'), dotIndex + 1) == -1) {
++ bool ok = false;
++ const int major = parseInt(QStringView(str).left(dotIndex), &ok);
++ if (!ok) return QTypeRevision();
++ const int minor = parseInt(QStringView(str).mid(dotIndex + 1, str.size() - dotIndex - 1), &ok);
++ return ok ? QTypeRevision::fromVersion(major, minor) : QTypeRevision();
++ }
++ return QTypeRevision();
++ }
++
++ void QQmlDirParser::clear()
++ {
++ _errors.clear();
++ _typeNamespace.clear();
++ _components.clear();
++ _dependencies.clear();
++ _imports.clear();
++ _exports.clear();
++ _scripts.clear();
++ _plugins.clear();
++ _designerSupported = false;
++ _typeInfos.clear();
++ _classNames.clear();
++ _linkTarget.clear();
++ }
++
++ inline static void scanSpace(const QChar *&ch) {
++ while (ch->isSpace() && !ch->isNull() && *ch != QLatin1Char('\n'))
++ ++ch;
++ }
++
++ inline static void scanToEnd(const QChar *&ch) {
++ while (*ch != QLatin1Char('\n') && !ch->isNull())
++ ++ch;
++ }
++
++ inline static QString scanWord(const QChar *&ch) {
++ const QChar *begin = ch;
++ while (!ch->isSpace() && !ch->isNull())
++ ++ch;
++ return QString(begin, ch - begin);
++ }
++
++ QString QQmlDirParser::scanQuotedWord(const QChar *&ch, quint16 lineNumber, quint16 columnNumber)
++ {
++ Q_ASSERT(*ch == QLatin1Char('"'));
++ ++ch;
++
++ QString result;
++
++ const QChar *begin = ch;
++ while (*ch != QLatin1Char('"')) {
++ if (ch->isNull()) {
++ reportError(lineNumber, columnNumber,
++ QStringLiteral("file ends inside a quoted string"));
++ result.append(begin, ch - begin);
++ return result;
++ }
++
++ if (*ch == QLatin1Char('\n') || *ch == QLatin1Char('\r')) {
++ reportError(lineNumber, columnNumber,
++ QStringLiteral("line breaks in quoted strings are not supported as they "
++ "are not portable between different operating systems"));
++ result.append(begin, ch - begin);
++ return result;
++ }
++
++ if (*ch == QLatin1Char('\\')) {
++ result.append(begin, ch - begin);
++ ++ch;
++ ++columnNumber;
++ if (*ch != QLatin1Char('"') && *ch != QLatin1Char('\\')) {
++ reportError(lineNumber, columnNumber,
++ QStringLiteral("only '\"' and '\\' can be escaped"));
++ return result;
++ }
++ begin = ch;
++ }
++
++ ++ch;
++ ++columnNumber;
++ }
++
++ result.append(begin, ch - begin);
++
++ Q_ASSERT(*ch == QLatin1Char('"'));
++ ++ch;
++
++ return result;
++ }
++
++ bool QQmlDirParser::parse(const QString &source)
++ {
++ quint16 lineNumber = 0;
++ bool firstLine = true;
++
++ auto readImport = [&](const QString *sections, int sectionCount, Import::Flags flags) {
++ Import import;
++ if (sectionCount == 2) {
++ import = Import(sections[1], QTypeRevision(), flags);
++ } else if (sectionCount == 3) {
++ if (sections[2] == QLatin1String("auto")) {
++ import = Import(sections[1], QTypeRevision(), flags | Import::Auto);
++ } else {
++ const auto version = parseVersion(sections[2]);
++ if (version.isValid()) {
++ import = Import(sections[1], version, flags);
++ } else {
++ reportError(lineNumber, 0,
++ QStringLiteral("invalid version %1, expected <major>.<minor>")
++ .arg(sections[2]));
++ return false;
++ }
++ }
++ } else {
++ reportError(lineNumber, 0,
++ QStringLiteral("%1 requires 1 or 2 arguments, but %2 were provided")
++ .arg(sections[0]).arg(sectionCount - 1));
++ return false;
++ }
++ if (sections[0] == QStringLiteral("import"))
++ _imports.append(import);
++ else
++ _dependencies.append(import);
++ return true;
++ };
++
++ auto readExport = [&](const QString *sections, int sectionCount) {
++ if (sectionCount < 2) {
++ reportError(lineNumber, 0,
++ QStringLiteral("export at least 1 argument, but none were provided"));
++ return false;
++ }
++
++ const QTypeRevision version = parseVersion(sections[1]);
++ if (!version.isValid()) {
++ reportError(lineNumber, 0,
++ QStringLiteral("invalid version %1, expected <major>.<minor>")
++ .arg(sections[1]));
++ return false;
++ }
++
++ _exports.append(version);
++ for (int i = 2; i < sectionCount; ++i) {
++ bool ok = false;
++ const int major = parseInt(sections[i], &ok);
++ if (!ok) {
++ reportError(lineNumber, 0,
++ QStringLiteral("invalid major version %1, expected integer")
++ .arg(sections[i]));
++ return false;
++ }
++
++ _exports.append(QTypeRevision::fromMajorVersion(major));
++ }
++
++ return true;
++ };
++
++ auto readPlugin = [&](const QString *sections, int sectionCount, bool isOptional) {
++ if (sectionCount < 2 || sectionCount > 3) {
++ reportError(lineNumber, 0, QStringLiteral("plugin directive requires one or two "
++ "arguments, but %1 were provided")
++ .arg(sectionCount - 1));
++ return false;
++ }
++
++ const Plugin entry(sections[1], sections[2], isOptional);
++ _plugins.append(entry);
++ return true;
++ };
++
++ const QChar *ch = source.constData();
++ while (!ch->isNull()) {
++ ++lineNumber;
++
++ bool invalidLine = false;
++ const QChar *lineStart = ch;
++
++ scanSpace(ch);
++ if (*ch == QLatin1Char('\n')) {
++ ++ch;
++ continue;
++ }
++ if (ch->isNull())
++ break;
++
++ QString sections[4];
++ int sectionCount = 0;
++
++ do {
++ if (*ch == QLatin1Char('#')) {
++ scanToEnd(ch);
++ break;
++ }
++
++ if (sectionCount >= 4) {
++ reportError(lineNumber, ch - lineStart, QLatin1String("unexpected token"));
++ scanToEnd(ch);
++ invalidLine = true;
++ break;
++ }
++
++ sections[sectionCount++] = (*ch == QLatin1Char('"'))
++ ? scanQuotedWord(ch, lineNumber, ch - lineStart)
++ : scanWord(ch);
++
++ scanSpace(ch);
++ } while (*ch != QLatin1Char('\n') && !ch->isNull());
++
++ if (!ch->isNull())
++ ++ch;
++
++ if (invalidLine) {
++ reportError(lineNumber, 0,
++ QStringLiteral("invalid qmldir directive contains too many tokens"));
++ continue;
++ } else if (sectionCount == 0) {
++ continue; // no sections, no party.
++
++ } else if (sections[0] == QLatin1String("module")) {
++ if (sectionCount != 2) {
++ reportError(lineNumber, 0,
++ QStringLiteral("module identifier directive requires one argument, but %1 were provided").arg(sectionCount - 1));
++ continue;
++ }
++ if (!_typeNamespace.isEmpty()) {
++ reportError(lineNumber, 0,
++ QStringLiteral("only one module identifier directive may be defined in a qmldir file"));
++ continue;
++ }
++ if (!firstLine) {
++ reportError(lineNumber, 0,
++ QStringLiteral("module identifier directive must be the first directive in a qmldir file"));
++ continue;
++ }
++
++ _typeNamespace = sections[1];
++
++ } else if (sections[0] == QLatin1String("plugin")) {
++ if (!readPlugin(sections, sectionCount, false))
++ continue;
++ } else if (sections[0] == QLatin1String("optional")) {
++ if (sectionCount < 2) {
++ reportError(lineNumber, 0, QStringLiteral("optional directive requires further "
++ "arguments, but none were provided."));
++ continue;
++ }
++
++ if (sections[1] == QStringLiteral("plugin")) {
++ if (!readPlugin(sections + 1, sectionCount - 1, true))
++ continue;
++ } else if (sections[1] == QLatin1String("import")) {
++ if (!readImport(sections + 1, sectionCount - 1, Import::Optional))
++ continue;
++ } else {
++ reportError(lineNumber, 0, QStringLiteral("only import and plugin can be optional, "
++ "not %1.").arg(sections[1]));
++ continue;
++ }
++ } else if (sections[0] == QLatin1String("default")) {
++ if (sectionCount < 2) {
++ reportError(lineNumber, 0,
++ QStringLiteral("default directive requires further "
++ "arguments, but none were provided."));
++ continue;
++ }
++ if (sections[1] == QLatin1String("import")) {
++ if (!readImport(sections + 1, sectionCount - 1,
++ Import::Flags({ Import::Optional, Import::OptionalDefault })))
++ continue;
++ } else {
++ reportError(lineNumber, 0,
++ QStringLiteral("only optional imports can have a default, "
++ "not %1.")
++ .arg(sections[1]));
++ }
++ } else if (sections[0] == QLatin1String("classname")) {
++ if (sectionCount < 2) {
++ reportError(lineNumber, 0,
++ QStringLiteral("classname directive requires an argument, but %1 were provided").arg(sectionCount - 1));
++
++ continue;
++ }
++
++ _classNames.append(sections[1]);
++
++ } else if (sections[0] == QLatin1String("internal")) {
++ if (sectionCount == 3) {
++ Component entry(sections[1], sections[2], QTypeRevision());
++ entry.internal = true;
++ _components.insert(entry.typeName, entry);
++ } else if (sectionCount == 4) {
++ const QTypeRevision version = parseVersion(sections[2]);
++ if (version.isValid()) {
++ Component entry(sections[1], sections[3], version);
++ entry.internal = true;
++ _components.insert(entry.typeName, entry);
++ } else {
++ reportError(lineNumber, 0,
++ QStringLiteral("invalid version %1, expected <major>.<minor>")
++ .arg(sections[2]));
++ continue;
++ }
++ } else {
++ reportError(lineNumber, 0,
++ QStringLiteral("internal types require 2 or 3 arguments, "
++ "but %1 were provided").arg(sectionCount - 1));
++ continue;
++ }
++ } else if (sections[0] == QLatin1String("singleton")) {
++ if (sectionCount < 3 || sectionCount > 4) {
++ reportError(lineNumber, 0,
++ QStringLiteral("singleton types require 2 or 3 arguments, but %1 were provided").arg(sectionCount - 1));
++ continue;
++ } else if (sectionCount == 3) {
++ // handle qmldir directory listing case where singleton is defined in the following pattern:
++ // singleton TestSingletonType TestSingletonType.qml
++ Component entry(sections[1], sections[2], QTypeRevision());
++ entry.singleton = true;
++ _components.insert(entry.typeName, entry);
++ } else {
++ // handle qmldir module listing case where singleton is defined in the following pattern:
++ // singleton TestSingletonType 2.0 TestSingletonType20.qml
++ const QTypeRevision version = parseVersion(sections[2]);
++ if (version.isValid()) {
++ const QString &fileName = sections[3];
++ Component entry(sections[1], fileName, version);
++ entry.singleton = true;
++ _components.insert(entry.typeName, entry);
++ } else {
++ reportError(lineNumber, 0, QStringLiteral("invalid version %1, expected <major>.<minor>").arg(sections[2]));
++ }
++ }
++ } else if (sections[0] == QLatin1String("typeinfo")) {
++ if (sectionCount != 2) {
++ reportError(lineNumber, 0,
++ QStringLiteral("typeinfo requires 1 argument, but %1 were provided").arg(sectionCount - 1));
++ continue;
++ }
++ _typeInfos.append(sections[1]);
++ } else if (sections[0] == QLatin1String("designersupported")) {
++ if (sectionCount != 1)
++ reportError(lineNumber, 0, QStringLiteral("designersupported does not expect any argument"));
++ else
++ _designerSupported = true;
++ } else if (sections[0] == QLatin1String("static")) {
++ if (sectionCount != 1)
++ reportError(lineNumber, 0, QStringLiteral("static does not expect any argument"));
++ else
++ _isStaticModule = true;
++ } else if (sections[0] == QLatin1String("system")) {
++ if (sectionCount != 1)
++ reportError(lineNumber, 0, QStringLiteral("system does not expect any argument"));
++ else
++ _isSystemModule = true;
++ } else if (sections[0] == QLatin1String("import")
++ || sections[0] == QLatin1String("depends")) {
++ if (!readImport(sections, sectionCount, Import::Default))
++ continue;
++ } else if (sections[0] == QLatin1String("export")) {
++ if (!readExport(sections, sectionCount))
++ continue;
++ } else if (sections[0] == QLatin1String("prefer")) {
++ if (sectionCount < 2) {
++ reportError(lineNumber, 0,
++ QStringLiteral("prefer directive requires one argument, "
++ "but %1 were provided").arg(sectionCount - 1));
++ continue;
++ }
++
++ if (!_preferredPath.isEmpty()) {
++ reportError(lineNumber, 0, QStringLiteral(
++ "only one prefer directive may be defined in a qmldir file"));
++ continue;
++ }
++
++ if (!sections[1].endsWith(u'/')) {
++ // Yes. People should realize it's a directory.
++ reportError(lineNumber, 0, QStringLiteral(
++ "the preferred directory has to end with a '/'"));
++ continue;
++ }
++
++ _preferredPath = sections[1];
++ } else if (sections[0] == QLatin1String("linktarget")) {
++ if (sectionCount < 2) {
++ reportError(lineNumber, 0,
++ QStringLiteral("linktarget directive requires an argument, "
++ "but %1 were provided")
++ .arg(sectionCount - 1));
++ continue;
++ }
++
++ if (!_linkTarget.isEmpty()) {
++ reportError(
++ lineNumber, 0,
++ QStringLiteral(
++ "only one linktarget directive may be defined in a qmldir file"));
++ continue;
++ }
++
++ _linkTarget = sections[1];
++ } else if (sectionCount == 2) {
++ // No version specified (should only be used for relative qmldir files)
++ insertComponentOrScript(sections[0], sections[1], QTypeRevision());
++ } else if (sectionCount == 3) {
++ const QTypeRevision version = parseVersion(sections[1]);
++ if (version.isValid()) {
++ insertComponentOrScript(sections[0], sections[2], version);
++ } else {
++ reportError(
++ lineNumber, 0,
++ QStringLiteral("invalid version %1, expected <major>.<minor>")
++ .arg(sections[1]));
++ }
++ } else {
++ reportError(lineNumber, 0,
++ QStringLiteral("a component declaration requires two or three arguments, but %1 were provided").arg(sectionCount));
++ }
++
++ firstLine = false;
++ }
++
++ return !hasError();
++ }
++
++ /* removes all file selector occurrences in path
++ firstPlus is the position of the initial '+' in the path
++ which we always have as we check for '+' to decide whether
++ we need to do some work at all
++ */
++ static QString pathWithoutFileSelectors(QString path, // we want a copy of path
++ qsizetype firstPlus)
++ {
++ do {
++ Q_ASSERT(path.at(firstPlus) == u'+');
++ const auto eos = path.size();
++ qsizetype terminatingSlashPos = firstPlus + 1;
++ while (terminatingSlashPos != eos && path.at(terminatingSlashPos) != u'/')
++ ++terminatingSlashPos;
++ path.remove(firstPlus, terminatingSlashPos - firstPlus + 1);
++ firstPlus = path.indexOf(u'+', firstPlus);
++ } while (firstPlus != -1);
++ return path;
++ }
++
++ static bool canDisambiguate(
++ const QString &fileName1, const QString &fileName2, QString *correctedFileName)
++ {
++ // If the entries are exactly the same we can delete one without losing anything.
++ if (fileName1 == fileName2)
++ return true;
++
++ // If we detect conflicting paths, we check if they agree when we remove anything
++ // looking like a file selector.
++
++ // ugly heuristic to deal with file selectors
++ const qsizetype file2PotentialFileSelectorPos = fileName2.indexOf(u'+');
++ const bool file2MightHaveFileSelector = file2PotentialFileSelectorPos != -1;
++
++ if (const qsizetype fileSelectorPos1 = fileName1.indexOf(u'+'); fileSelectorPos1 != -1) {
++ // existing entry was file selector entry, fix it up
++ // it could also be the case that _both_ are using file selectors
++ const QString baseName = file2MightHaveFileSelector
++ ? pathWithoutFileSelectors(fileName2, file2PotentialFileSelectorPos)
++ : fileName2;
++
++ if (pathWithoutFileSelectors(fileName1, fileSelectorPos1) != baseName)
++ return false;
++
++ *correctedFileName = baseName;
++ return true;
++ }
++
++ // new entry contains file selector (and we know that fileName1 did not)
++ if (file2MightHaveFileSelector
++ && pathWithoutFileSelectors(fileName2, file2PotentialFileSelectorPos) == fileName1) {
++ *correctedFileName = fileName1;
++ return true;
++ }
++
++ return false;
++ }
++
++ static void disambiguateFileSelectedComponents(QQmlDirComponents *components)
++ {
++ using ConstIterator = QQmlDirComponents::const_iterator;
++
++ // end iterator may get invalidated by the erasing below.
++ // Therefore, refetch it on each iteration.
++ for (ConstIterator cit = components->constBegin(); cit != components->constEnd();) {
++
++ // We can erase and re-assign cit if we immediately forget cit2.
++ // But we cannot erase cit2 without potentially invalidating cit.
++
++ bool doErase = false;
++ const ConstIterator cend = components->constEnd();
++ for (ConstIterator cit2 = ++ConstIterator(cit); cit2 != cend; ++cit2) {
++ if (cit2.key() != cit.key())
++ break;
++
++ Q_ASSERT(cit2->typeName == cit->typeName);
++
++ if (cit2->version != cit->version
++ || cit2->internal != cit->internal
++ || cit2->singleton != cit->singleton) {
++ continue;
++ }
++
++ // The two components may differ only by fileName now.
++
++ if (canDisambiguate(cit->fileName, cit2->fileName, &(cit2->fileName))) {
++ doErase = true;
++ break;
++ }
++ }
++
++ if (doErase)
++ cit = components->erase(cit);
++ else
++ ++cit;
++ }
++ }
++
++ static void disambiguateFileSelectedScripts(QQmlDirScripts *scripts)
++ {
++ using Iterator = QQmlDirScripts::iterator;
++
++ Iterator send = scripts->end();
++
++ for (Iterator sit = scripts->begin(); sit != send; ++sit) {
++ send = std::remove_if(++Iterator(sit), send, [sit](const QQmlDirParser::Script &script2) {
++ if (sit->nameSpace != script2.nameSpace || sit->version != script2.version)
++ return false;
++
++ // The two scripts may differ only by fileName now.
++ return canDisambiguate(sit->fileName, script2.fileName, &(sit->fileName));
++ });
++ }
++
++ scripts->erase(send, scripts->end());
++ }
++
++ void QQmlDirParser::disambiguateFileSelectors()
++ {
++ disambiguateFileSelectedComponents(&_components);
++ disambiguateFileSelectedScripts(&_scripts);
++ }
++
++ void QQmlDirParser::reportError(quint16 line, quint16 column, const QString &description)
++ {
++ QQmlJS::DiagnosticMessage error;
++ error.loc.startLine = line;
++ error.loc.startColumn = column;
++ error.message = description;
++ _errors.append(error);
++ }
++
++ void QQmlDirParser::insertComponentOrScript(
++ const QString &name, const QString &fileName, QTypeRevision version)
++ {
++ // A 'js' extension indicates a namespaced script import
++ if (fileName.endsWith(QLatin1String(".js")) || fileName.endsWith(QLatin1String(".mjs")))
++ _scripts.append(Script(name, fileName, version));
++ else
++ _components.insert(name, Component(name, fileName, version));
++ }
++
++ void QQmlDirParser::setError(const QQmlJS::DiagnosticMessage &e)
++ {
++ _errors.clear();
++ reportError(e.loc.startLine, e.loc.startColumn, e.message);
++ }
++
++ QList<QQmlJS::DiagnosticMessage> QQmlDirParser::errors(const QString &uri) const
++ {
++ QList<QQmlJS::DiagnosticMessage> errors;
++ const int numErrors = _errors.size();
++ errors.reserve(numErrors);
++ for (int i = 0; i < numErrors; ++i) {
++ QQmlJS::DiagnosticMessage e = _errors.at(i);
++ e.message.replace(QLatin1String("$$URI$$"), uri);
++ errors << e;
++ }
++ return errors;
++ }
++
++ QDebug &operator<< (QDebug &debug, const QQmlDirParser::Component &component)
++ {
++ const QString output = QStringLiteral("{%1 %2.%3}")
++ .arg(component.typeName).arg(component.version.majorVersion())
++ .arg(component.version.minorVersion());
++ return debug << qPrintable(output);
++ }
++
++ QDebug &operator<< (QDebug &debug, const QQmlDirParser::Script &script)
++ {
++ const QString output = QStringLiteral("{%1 %2.%3}")
++ .arg(script.nameSpace).arg(script.version.majorVersion())
++ .arg(script.version.minorVersion());
++ return debug << qPrintable(output);
++ }
++
++ QT_END_NAMESPACE