// Copyright (C) 2018 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "projectdescriptionreader.h" #include "fmt.h" #include #include #include #include #include #include #include #include using std::placeholders::_1; using namespace Qt::Literals::StringLiterals; class Validator { public: Validator(QString *errorString) : m_errorString(errorString) { } bool isValidProjectDescription(const QJsonArray &projects) { return std::all_of(projects.begin(), projects.end(), std::bind(&Validator::isValidProjectObject, this, _1)); } private: bool isValidProject(const QJsonObject &project) { static const QSet requiredKeys = { QStringLiteral("projectFile"), }; static const QSet allowedKeys = QSet(requiredKeys) << QStringLiteral("codec") << QStringLiteral("excluded") << QStringLiteral("includePaths") << QStringLiteral("sources") << QStringLiteral("compileCommands") << QStringLiteral("subProjects") << QStringLiteral("translations"); QSet actualKeys; for (auto it = project.constBegin(), end = project.constEnd(); it != end; ++it) actualKeys.insert(it.key()); const QSet missingKeys = requiredKeys - actualKeys; if (!missingKeys.isEmpty()) { *m_errorString = FMT::tr("Missing keys in project description: %1.") .arg(missingKeys.values().join(", "_L1)); return false; } const QSet unexpected = actualKeys - allowedKeys; if (!unexpected.isEmpty()) { *m_errorString = FMT::tr("Unexpected keys in project %1: %2") .arg(project.value(QStringLiteral("projectFile")).toString(), unexpected.values().join(", "_L1)); return false; } return isValidProjectDescription(project.value(QStringLiteral("subProjects")).toArray()); } bool isValidProjectObject(const QJsonValue &v) { if (!v.isObject()) { *m_errorString = FMT::tr("JSON object expected."); return false; } return isValidProject(v.toObject()); } QString *m_errorString; }; static QJsonArray readRawProjectDescription(const QString &filePath, QString *errorString) { errorString->clear(); QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { *errorString = FMT::tr("Cannot open project description file '%1'.\n") .arg(filePath); return {}; } QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseError); if (doc.isNull()) { *errorString = FMT::tr("%1 in %2 at offset %3.\n") .arg(parseError.errorString(), filePath) .arg(parseError.offset); return {}; } QJsonArray result = doc.isArray() ? doc.array() : QJsonArray{doc.object()}; Validator validator(errorString); if (!validator.isValidProjectDescription(result)) return {}; return result; } class ProjectConverter { public: ProjectConverter(QString *errorString) : m_errorString(*errorString) { } Projects convertProjects(const QJsonArray &rawProjects) { Projects result; result.reserve(rawProjects.size()); for (const QJsonValue rawProject : rawProjects) { Project project = convertProject(rawProject); if (!m_errorString.isEmpty()) break; result.push_back(std::move(project)); } return result; } private: Project convertProject(const QJsonValue &v) { if (!v.isObject()) return {}; Project result; QJsonObject obj = v.toObject(); result.filePath = stringValue(obj, "projectFile"_L1); result.compileCommands = stringValue(obj, "compileCommands"_L1); result.codec = stringValue(obj, "codec"_L1); result.excluded = wildcardsToRegExes(stringListValue(obj, "excluded"_L1)); result.includePaths = stringListValue(obj, "includePaths"_L1); result.sources = stringListValue(obj, "sources"_L1); if (obj.contains("translations"_L1)) result.translations = stringListValue(obj, "translations"_L1); result.subProjects = convertProjects(obj.value("subProjects"_L1).toArray()); return result; } bool checkType(const QJsonValue &v, QJsonValue::Type t, const QString &key) { if (v.type() == t) return true; m_errorString = FMT::tr("Key %1 should be %2 but is %3.").arg(key, jsonTypeName(t), jsonTypeName(v.type())); return false; } static QString jsonTypeName(QJsonValue::Type t) { // ### If QJsonValue::Type was declared with Q_ENUM we could just query QMetaEnum. switch (t) { case QJsonValue::Null: return QStringLiteral("null"); case QJsonValue::Bool: return QStringLiteral("bool"); case QJsonValue::Double: return QStringLiteral("double"); case QJsonValue::String: return QStringLiteral("string"); case QJsonValue::Array: return QStringLiteral("array"); case QJsonValue::Object: return QStringLiteral("object"); case QJsonValue::Undefined: return QStringLiteral("undefined"); } return QStringLiteral("unknown"); } static QVector wildcardsToRegExes(const QStringList &wildcardPatterns) { QVector result; result.reserve(wildcardPatterns.size()); for (const QString &wildcardPattern : wildcardPatterns) result.append(wildcardToRegEx(wildcardPattern)); return result; } // Return a QRegularExpression object for a TR_EXCLUDE / QT_EXCLUDE_SOURCES_FROM_TRANSLATION // wildcard pattern. The regular expression is only anchored at the beginning to allow matching // subdirectories. static QRegularExpression wildcardToRegEx(const QString &wildcardPattern) { return QRegularExpression( "\\A"_L1 + QRegularExpression::wildcardToRegularExpression( wildcardPattern, QRegularExpression::UnanchoredWildcardConversion)); } QString stringValue(const QJsonObject &obj, const QString &key) { if (!m_errorString.isEmpty()) return {}; QJsonValue v = obj.value(key); if (v.isUndefined()) return {}; if (!checkType(v, QJsonValue::String, key)) return {}; return v.toString(); } QStringList stringListValue(const QJsonObject &obj, const QString &key) { if (!m_errorString.isEmpty()) return {}; QJsonValue v = obj.value(key); if (v.isUndefined()) return {}; if (!checkType(v, QJsonValue::Array, key)) return {}; return toStringList(v, key); } QStringList toStringList(const QJsonValue &v, const QString &key) { QStringList result; const QJsonArray a = v.toArray(); result.reserve(a.count()); for (const QJsonValue v : a) { if (!v.isString()) { m_errorString = FMT::tr("Unexpected type %1 in string array in key %2.") .arg(jsonTypeName(v.type()), key); return {}; } result.append(v.toString()); } return result; } QString &m_errorString; }; Projects readProjectDescription(const QString &filePath, QString *errorString) { const QJsonArray rawProjects = readRawProjectDescription(filePath, errorString); if (!errorString->isEmpty()) return {}; ProjectConverter converter(errorString); Projects result = converter.convertProjects(rawProjects); if (!errorString->isEmpty()) return {}; return result; }