Revision control

++ // Copyright (C) 2025 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
++ // Qt-Security score:significant
++
++ #include "qqmljscontextproperties_p.h"
++
++ #include <private/qqmljsutils_p.h>
++ #include <private/qtqmlglobal_p.h>
++
++ #include <QtCore/qtconfigmacros.h>
++ #include <QtCore/qregularexpression.h>
++ #include <QtCore/qdirlisting.h>
++ #include <QtCore/qfile.h>
++
++ #if QT_CONFIG(qmlcontextpropertydump)
++ # include <QtCore/qsettings.h>
++ #endif
++
++ #if QT_CONFIG(process)
++ # include <QtCore/qprocess.h>
++ #endif
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QQmlJS {
++
++ using namespace Qt::StringLiterals;
++
++ // There are many ways to set context properties without triggering the regexp in s_pattern,
++ // but its supposed to catch most context properties set via "setContextProperty".
++ static constexpr QLatin1StringView s_pattern =
++ R"x((\.|->)setContextProperty\s*\(\s*(QStringLiteral\s*\(|QString\s*\(|QLatin1String(View)?\s*\(|u)?\s*"([^"]*)")x"_L1;
++ static constexpr int s_contextPropertyNameIdxInPattern = 4;
++
++ static const QRegularExpression s_matchSetContextProperty{ s_pattern,
++ QRegularExpression::MultilineOption };
++
++ QList<HeuristicContextProperty>
++ HeuristicContextProperties::definitionsForName(const QString &name) const
++ {
++ const auto it = m_properties.find(name);
++ if (it != m_properties.end())
++ return it.value();
++ return {};
++ }
++
++ void HeuristicContextProperties::add(const QString &name, const HeuristicContextProperty &property)
++ {
++ if (const auto it = m_properties.find(name); it != m_properties.end()) {
++ it.value().append(property);
++ return;
++ }
++ m_properties.insert(name, { property });
++ }
++
++ void HeuristicContextProperties::collectFromFile(const QString &filePath)
++ {
++ QFile file(filePath);
++ if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
++ return;
++
++ const QString fileContent = QString::fromUtf8(file.readAll());
++ for (const auto &match : s_matchSetContextProperty.globalMatch(fileContent)) {
++ const quint32 offset = match.capturedStart(s_contextPropertyNameIdxInPattern);
++ const quint32 length = match.capturedLength(s_contextPropertyNameIdxInPattern);
++ const auto [row, column] = QQmlJS::SourceLocation::rowAndColumnFrom(fileContent, offset);
++ const QQmlJS::SourceLocation sourceLocation{ offset, length, row, column };
++
++ add(match.captured(s_contextPropertyNameIdxInPattern),
++ HeuristicContextProperty{ filePath, sourceLocation });
++ }
++ }
++
++ void HeuristicContextProperties::grepFallback(const QList<QString> &rootUrls)
++ {
++ const QStringList fileFilters{ QQmlJSUtils::cppFileFilters.begin(),
++ QQmlJSUtils::cppFileFilters.end() };
++
++ for (const QString &url : rootUrls) {
++ for (const auto &dirEntry : QDirListing{ url, fileFilters,
++ QDirListing::IteratorFlag::Recursive
++ | QDirListing::IteratorFlag::FilesOnly }) {
++
++ const QString filePath = dirEntry.filePath();
++ collectFromFile(filePath);
++ }
++ }
++ }
++
++ #if QT_CONFIG(process) && !defined(Q_OS_WINDOWS)
++ void HeuristicContextProperties::parseGrepOutput(const QString &output)
++ {
++ for (const auto line : QStringTokenizer{ output, "\n"_L1, Qt::SkipEmptyParts })
++ collectFromFile(line.toString());
++ }
++ #endif
++
++ HeuristicContextProperties
++ HeuristicContextProperties::collectFromCppSourceDirs(const QList<QString> &cppSourceDirs)
++ {
++ HeuristicContextProperties result;
++ result.collectFromDirs(cppSourceDirs);
++ return result;
++ }
++
++ /*!
++ \internal
++ Uses grep to find files that have setContextProperty()-calls, and then search matching files
++ with QRegularExpression to extract the location and name of the found context properties.
++ */
++ void HeuristicContextProperties::collectFromDirs(const QList<QString> &dirs)
++ {
++ if (dirs.isEmpty())
++ return;
++
++ #if QT_CONFIG(process) && !defined(Q_OS_WINDOWS)
++ if (qEnvironmentVariableIsSet("QT_QML_NO_GREP")) {
++ grepFallback(dirs);
++ return;
++ }
++
++ QProcess grep;
++ QStringList arguments{ "--recursive"_L1,
++ "--null-data"_L1, // match multiline patterns
++ "--files-with-matches"_L1,
++ "--extended-regexp"_L1, // the pattern is "extended"
++ "-e"_L1,
++ s_pattern };
++
++ // don't search non-cpp files
++ for (const auto &fileFilter : QQmlJSUtils::cppFileFilters)
++ arguments << "--include"_L1 << fileFilter;
++
++ arguments.append(dirs);
++ grep.start("grep"_L1, arguments);
++ grep.waitForFinished();
++ if (grep.exitStatus() == QProcess::NormalExit) {
++ switch (grep.exitCode()) {
++ case 0: { // success
++ const QString output = QString::fromUtf8(grep.readAllStandardOutput());
++ parseGrepOutput(output);
++ return;
++ }
++ case 1: // success but no context properties found
++ return;
++ default: // grep error
++ break;
++ }
++ }
++ #endif
++ grepFallback(dirs);
++ }
++
++ #if QT_CONFIG(qmlcontextpropertydump)
++ static SourceLocation deserializeSourceLocation(const QString &string)
++ {
++ constexpr int size = 4;
++ const QStringList bits = string.split(u',', Qt::SkipEmptyParts);
++ if (bits.length() != size)
++ return SourceLocation{};
++
++ SourceLocation result;
++ quint32 *destination[size] = { &result.offset, &result.length, &result.startLine,
++ &result.startColumn };
++
++ bool everythingOk = true;
++ for (int i = 0; i < size; ++i) {
++ bool ok = false;
++ *destination[i] = bits[i].toInt(&ok);
++ everythingOk &= ok;
++ }
++
++ if (everythingOk)
++ return result;
++ return SourceLocation{};
++ }
++
++ static QString serializeSourceLocation(const SourceLocation &location)
++ {
++ QString result;
++ result.append(QString::number(location.offset)).append(u',');
++ result.append(QString::number(location.length)).append(u',');
++ result.append(QString::number(location.startLine)).append(u',');
++ result.append(QString::number(location.startColumn));
++ return result;
++ }
++ #endif
++
++ #if QT_CONFIG(qmlcontextpropertydump)
++ static constexpr auto cachedHeuristicListKey = "cachedHeuristicList"_L1;
++ #endif
++
++ HeuristicContextProperties HeuristicContextProperties::collectFrom(QSettings *settings)
++ {
++ #if QT_CONFIG(qmlcontextpropertydump)
++ HeuristicContextProperties result;
++ std::vector<QString> names;
++
++ const int size = settings->beginReadArray(cachedHeuristicListKey);
++ for (int i = 0; i < size; ++i) {
++ settings->setArrayIndex(i);
++ names.push_back(settings->value("name").toString());
++ }
++ settings->endArray();
++
++ for (const auto &name : names) {
++ const int size = settings->beginReadArray(u"property_"_s.append(name));
++ for (int i = 0; i < size; ++i) {
++ settings->setArrayIndex(i);
++ result.add(
++ name,
++ HeuristicContextProperty{
++ settings->value("fileName").toString(),
++ deserializeSourceLocation(settings->value("sourceLocation").toString()),
++ });
++ }
++ settings->endArray();
++ }
++ return result;
++ #else
++ Q_UNUSED(settings);
++ return HeuristicContextProperties{};
++ #endif
++ }
++
++ void HeuristicContextProperties::writeCache(const QString &folder) const
++ {
++ #if QT_CONFIG(qmlcontextpropertydump)
++ QSettings settings(folder + "/.qt/contextPropertyDump.ini"_L1, QSettings::IniFormat);
++ settings.beginWriteArray(cachedHeuristicListKey);
++ int index = 0;
++ for (const auto &[name, _] : m_properties) {
++ settings.setArrayIndex(index++);
++ settings.setValue("name", name);
++ }
++ settings.endArray();
++
++ for (const auto &[name, definitions] : m_properties) {
++ settings.beginWriteArray(u"property_"_s.append(name));
++ for (int i = 0; i < definitions.size(); ++i) {
++ settings.setArrayIndex(i);
++ settings.setValue("fileName", definitions[i].filename);
++ settings.setValue("sourceLocation", serializeSourceLocation(definitions[i].location));
++ }
++ settings.endArray();
++ }
++ #else
++ Q_UNUSED(folder);
++ #endif
++ }
++ } // namespace QQmlJS
++
++ QT_END_NAMESPACE