Revision control
++ // 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 <QtCore/qcoreapplication.h>
++ #include <QtCore/qfile.h>
++ #include <QtCore/qjsonarray.h>
++ #include <QtCore/qjsondocument.h>
++ #include <QtCore/qjsonobject.h>
++ #include <QtCore/qset.h>
++
++ #include <algorithm>
++ #include <functional>
++
++ 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<QString> requiredKeys = {
++ QStringLiteral("projectFile"),
++ };
++ static const QSet<QString> allowedKeys
++ = QSet<QString>(requiredKeys)
++ << QStringLiteral("codec")
++ << QStringLiteral("excluded")
++ << QStringLiteral("includePaths")
++ << QStringLiteral("sources")
++ << QStringLiteral("compileCommands")
++ << QStringLiteral("subProjects")
++ << QStringLiteral("translations");
++ QSet<QString> actualKeys;
++ for (auto it = project.constBegin(), end = project.constEnd(); it != end; ++it)
++ actualKeys.insert(it.key());
++ const QSet<QString> 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<QString> 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.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<QRegularExpression> wildcardsToRegExes(const QStringList &wildcardPatterns)
++ {
++ QVector<QRegularExpression> 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;
++ };
++
++ QT_BEGIN_NAMESPACE
++
++ Projects projectDescriptionFromFile(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;
++ }
++
++ Projects projectDescriptionFromJson(const QJsonArray &rawProjects, QString *errorString)
++ {
++ errorString->clear();
++ Validator validator(errorString);
++ if (!validator.isValidProjectDescription(rawProjects))
++ return {};
++ ProjectConverter converter(errorString);
++ Projects result = converter.convertProjects(rawProjects);
++ if (!errorString->isEmpty())
++ return {};
++ return result;
++ }
++
++ QT_END_NAMESPACE