// Copyright (C) 2021 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 "qqmltoolingsettings_p.h" #include #include #include #include #include #if QT_CONFIG(settings) #include #endif #include using namespace Qt::StringLiterals; QQmlToolingSettings::SearchOptions::SearchOptions() = default; QQmlToolingSettings::SearchOptions::SearchOptions( QString settingFileName, bool reportFoundSettingsFiles, bool isQmllintSilent) : settingsFileName(settingFileName), reportFoundSettingsFiles(reportFoundSettingsFiles), isQmllintSilent(isQmllintSilent) { } void QQmlToolingSettings::addOption(const QString &name, const QVariant &defaultValue) { if (defaultValue.isValid()) { m_values[name] = defaultValue; } } QQmlToolingSettings::QQmlToolingSettings(const QString &toolName, const QStringList &recognizedIniSections) : m_searcher(u".%1.ini"_s.arg(toolName), u"%1.ini"_s.arg(toolName)), m_recognizedIniSections(recognizedIniSections) { } QQmlToolingSettings::SearchResult QQmlToolingSettings::read(const QString &settingsFilePath, SearchOptions options) { #if QT_CONFIG(settings) Q_ASSERT(QFileInfo::exists(settingsFilePath)); if (m_currentSettingsPath == settingsFilePath) return { SearchResult::ResultType::Found, settingsFilePath }; QSettings settings(settingsFilePath, QSettings::IniFormat); if (!options.isQmllintSilent) { const QStringList sections = settings.childGroups() << QLatin1String("General"); for (const QString §ion : sections) { if (!m_recognizedIniSections.contains(section)) { qWarning().noquote() << "Unrecognized section \"%1\" in %2\n"_L1.arg(section).arg(settingsFilePath) + "Recognized sections are: ["_L1 + m_recognizedIniSections.join(", "_L1) + u']'; } } } for (const QString &key : settings.allKeys()) m_values[key] = settings.value(key).toString(); m_currentSettingsPath = settingsFilePath; return { SearchResult::ResultType::Found, settingsFilePath }; #else Q_UNUSED(settingsFilePath); return SearchResult(); #endif } bool QQmlToolingSettings::writeDefaults() const { #if QT_CONFIG(settings) const QString path = QFileInfo(m_searcher.localSettingsFile()).absoluteFilePath(); QSettings settings(path, QSettings::IniFormat); for (auto it = m_values.constBegin(); it != m_values.constEnd(); ++it) { settings.setValue(it.key(), it.value().isNull() ? QString() : it.value()); } settings.sync(); if (settings.status() != QSettings::NoError) { qWarning() << "Failed to write default settings to" << path << "Error:" << settings.status(); return false; } qInfo() << "Wrote default settings to" << path; return true; #else return false; #endif } QQmlToolingSettings::SearchResult QQmlToolingSettings::Searcher::searchCurrentDirInCache(const QString &dirPath) { const auto it = m_seenDirectories.constFind(dirPath); return it != m_seenDirectories.constEnd() ? SearchResult{ SearchResult::ResultType::Found, *it } : SearchResult{ SearchResult::ResultType::NotFound, {} }; } static QString findIniFile(const QString &local, const QString &global) { // If we reach here, we didn't find the settings file in the current directory or any parent // directories. Now we will try to locate the settings file in the standard locations. First try // to locate settings file with the standard name. const QString iniFile = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, local); if (!iniFile.isEmpty()) return iniFile; // If not found, try alternate name format return QStandardPaths::locate(QStandardPaths::GenericConfigLocation, global); } QQmlToolingSettings::SearchResult QQmlToolingSettings::Searcher::searchDefaultLocation(const QSet *visitedDirs) { QString iniFile = findIniFile(m_localSettingsFile, m_globalSettingsFile); // Update the seen directories cache unconditionally with the current result for (const QString &dir : *visitedDirs) m_seenDirectories[dir] = iniFile; const SearchResult::ResultType found = iniFile.isEmpty() ? SearchResult::ResultType::NotFound : SearchResult::ResultType::Found; return SearchResult { found, std::move(iniFile) }; } QQmlToolingSettings::SearchResult QQmlToolingSettings::Searcher::searchDirectoryHierarchy( QSet *visitedDirs, const QString &path) { const QFileInfo fileInfo(path); QDir dir(fileInfo.isDir() ? path : fileInfo.dir()); while (dir.exists() && dir.isReadable()) { const QString dirPath = dir.absolutePath(); // Check if we have already seen this directory // If we have, we can use the cached INI file path // to avoid unnecessary file system operations // This is useful for large directory trees where the settings file might be in a parent // directory if (const SearchResult result = searchCurrentDirInCache(dirPath); result.isValid()) return result; visitedDirs->insert(dirPath); // Check if the settings file exists in the current directory // If it does, read it and update the seen directories cache if (QString ini = dir.absoluteFilePath(m_localSettingsFile); QFileInfo::exists(ini)) { for (const QString &visitedDir : std::as_const(*visitedDirs)) m_seenDirectories[visitedDir] = ini; return { SearchResult::ResultType::Found, std::move(ini) }; } if (!dir.cdUp()) break; } return SearchResult(); } QQmlToolingSettings::SearchResult QQmlToolingSettings::Searcher::search(const QString &path) { QSet visitedDirs; // Try to find settings in directory hierarchy if (const SearchResult result = searchDirectoryHierarchy(&visitedDirs, path); result.isValid()) return result; // If we didn't find the settings file in the current directory or any parent directories, // try to locate the settings file in the standard locations if (const SearchResult result = searchDefaultLocation(&visitedDirs); result.isValid()) return result; return SearchResult(); } QQmlToolingSettings::SearchResult QQmlToolingSettings::search( const QString &path, const SearchOptions &options) { const auto maybeReport = qScopeGuard([&]() { if (options.reportFoundSettingsFiles) reportConfigForFiles({ path }); }); // If a specific settings file is provided, read it directly if (!options.settingsFileName.isEmpty()) { QFileInfo fileInfo(options.settingsFileName); return fileInfo.exists() ? read(fileInfo.absoluteFilePath(), options) : SearchResult(); } if (const SearchResult result = m_searcher.search(path); result.isValid()) return read(result.iniFilePath, options); return SearchResult(); } QVariant QQmlToolingSettings::value(const QString &name) const { return m_values.value(name); } QStringList QQmlToolingSettings::valueAsStringList(const QString &name) const { return value(name).toString().split(QDir::listSeparator()); } void QQmlToolingSettings::resolveRelativeImportPaths(const QString &filePath, QStringList *paths) { // transform relative paths in absolute paths starting from filePath's directory const QDir fileDir = QFileInfo(filePath).absoluteDir(); for (auto it = paths->begin(), end = paths->end(); it != end; ++it) { if (QFileInfo(*it).isAbsolute()) continue; *it = QDir::cleanPath(fileDir.filePath(*it)); } } QStringList QQmlToolingSettings::valueAsAbsolutePathList(const QString &name, const QString &baseForRelativePaths) const { QStringList paths = valueAsStringList(name); resolveRelativeImportPaths(baseForRelativePaths, &paths); return paths; } bool QQmlToolingSettings::isSet(const QString &name) const { if (!m_values.contains(name)) return false; QVariant variant = m_values[name]; // Unset is encoded as an empty string return !(variant.canConvert(QMetaType(QMetaType::QString)) && variant.toString().isEmpty()); } bool QQmlToolingSettings::reportConfigForFiles(const QStringList &files) { constexpr int maxAllowedFileLength = 255; constexpr int minAllowedFileLength = 40; bool headerPrinted = false; auto lengthForFile = [maxAllowedFileLength](const QString &file) { return std::min(int(file.length()), maxAllowedFileLength); }; int maxFileLength = std::accumulate(files.begin(), files.end(), 0, [&](int acc, const QString &file) { return std::max(acc, lengthForFile(file)); }); if (maxFileLength < minAllowedFileLength) maxFileLength = minAllowedFileLength; for (const auto &file : files) { if (file.isEmpty()) { qWarning().noquote() << "Error: Could not find file" << file; return false; } QString displayFile = file; if (displayFile.length() > maxAllowedFileLength) { displayFile = u"..." + displayFile.right(maxAllowedFileLength - 3); } const auto result = search(file); if (!headerPrinted) { QString header = QStringLiteral("%1 | %2").arg("File", -maxFileLength).arg("Settings File"); qWarning().noquote() << header; qWarning().noquote() << QString(header.length(), u'-'); headerPrinted = true; } QString line = QStringLiteral("%1 | %2").arg(displayFile, -maxFileLength).arg(result.iniFilePath); qWarning().noquote() << line; } return true; }