1/****************************************************************************
2**
3** Copyright (C) 2018 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt Linguist of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include "projectdescriptionreader.h"
30
31#include <QtCore/qcoreapplication.h>
32#include <QtCore/qfile.h>
33#include <QtCore/qjsonarray.h>
34#include <QtCore/qjsondocument.h>
35#include <QtCore/qjsonobject.h>
36#include <QtCore/qset.h>
37
38#include <algorithm>
39#include <functional>
40
41using std::placeholders::_1;
42
43class FMT {
44 Q_DECLARE_TR_FUNCTIONS(Linguist)
45};
46
47class Validator
48{
49public:
50 Validator(QString *errorString)
51 : m_errorString(errorString)
52 {
53 }
54
55 bool isValidProjectDescription(const QJsonArray &projects)
56 {
57 return std::all_of(first: projects.begin(), last: projects.end(),
58 pred: std::bind(f: &Validator::isValidProjectObject, args: this, args: _1));
59 }
60
61private:
62 bool isValidProject(const QJsonObject &project)
63 {
64 static const QSet<QString> requiredKeys = {
65 QStringLiteral("projectFile"),
66 };
67 static const QSet<QString> allowedKeys
68 = QSet<QString>(requiredKeys)
69 << QStringLiteral("codec")
70 << QStringLiteral("excluded")
71 << QStringLiteral("includePaths")
72 << QStringLiteral("sources")
73 << QStringLiteral("subProjects")
74 << QStringLiteral("translations");
75 QSet<QString> actualKeys;
76 for (auto it = project.constBegin(), end = project.constEnd(); it != end; ++it)
77 actualKeys.insert(value: it.key());
78 const QSet<QString> missingKeys = requiredKeys - actualKeys;
79 if (!missingKeys.isEmpty()) {
80 *m_errorString = FMT::tr(sourceText: "Missing keys in project description: %1.").arg(
81 a: missingKeys.values().join(sep: QLatin1String(", ")));
82 return false;
83 }
84 const QSet<QString> unexpected = actualKeys - allowedKeys;
85 if (!unexpected.isEmpty()) {
86 *m_errorString = FMT::tr(sourceText: "Unexpected keys in project %1: %2").arg(
87 args: project.value(QStringLiteral("projectFile")).toString(),
88 args: unexpected.values().join(sep: QLatin1String(", ")));
89 return false;
90 }
91 return isValidProjectDescription(projects: project.value(QStringLiteral("subProjects")).toArray());
92 }
93
94 bool isValidProjectObject(const QJsonValue &v)
95 {
96 if (!v.isObject()) {
97 *m_errorString = FMT::tr(sourceText: "JSON object expected.");
98 return false;
99 }
100 return isValidProject(project: v.toObject());
101 }
102
103 QString *m_errorString;
104};
105
106static QJsonArray readRawProjectDescription(const QString &filePath, QString *errorString)
107{
108 errorString->clear();
109 QFile file(filePath);
110 if (!file.open(flags: QIODevice::ReadOnly)) {
111 *errorString = FMT::tr(sourceText: "Cannot open project description file '%1'.\n")
112 .arg(a: filePath);
113 return {};
114 }
115 QJsonParseError parseError;
116 QJsonDocument doc = QJsonDocument::fromJson(json: file.readAll(), error: &parseError);
117 if (doc.isNull()) {
118 *errorString = FMT::tr(sourceText: "%1 in %2 at offset %3.\n")
119 .arg(args: parseError.errorString(), args: filePath)
120 .arg(a: parseError.offset);
121 return {};
122 }
123 QJsonArray result = doc.isArray() ? doc.array() : QJsonArray{doc.object()};
124 Validator validator(errorString);
125 if (!validator.isValidProjectDescription(projects: result))
126 return {};
127 return result;
128}
129
130class ProjectConverter
131{
132public:
133 ProjectConverter(QString *errorString)
134 : m_errorString(*errorString)
135 {
136 }
137
138 Projects convertProjects(const QJsonArray &rawProjects)
139 {
140 Projects result;
141 result.reserve(n: rawProjects.size());
142 for (const QJsonValue &rawProject : rawProjects) {
143 Project project = convertProject(v: rawProject);
144 if (!m_errorString.isEmpty())
145 break;
146 result.push_back(x: std::move(project));
147 }
148 return result;
149 }
150
151private:
152 Project convertProject(const QJsonValue &v)
153 {
154 if (!v.isObject())
155 return {};
156 Project result;
157 QJsonObject obj = v.toObject();
158 result.filePath = stringValue(obj, key: QLatin1String("projectFile"));
159 result.codec = stringValue(obj, key: QLatin1String("codec"));
160 result.excluded = stringListValue(obj, key: QLatin1String("excluded"));
161 result.includePaths = stringListValue(obj, key: QLatin1String("includePaths"));
162 result.sources = stringListValue(obj, key: QLatin1String("sources"));
163 if (obj.contains(key: QLatin1String("translations")))
164 result.translations.reset(p: new QStringList(stringListValue(obj, key: QLatin1String("translations"))));
165 result.subProjects = convertProjects(rawProjects: obj.value(key: QLatin1String("subProjects")).toArray());
166 return result;
167 }
168
169 bool checkType(const QJsonValue &v, QJsonValue::Type t, const QString &key)
170 {
171 if (v.type() == t)
172 return true;
173 m_errorString = FMT::tr(sourceText: "Key %1 should be %2 but is %3.").arg(args: key, args: jsonTypeName(t),
174 args: jsonTypeName(t: v.type()));
175 return false;
176 }
177
178 static QString jsonTypeName(QJsonValue::Type t)
179 {
180 // ### If QJsonValue::Type was declared with Q_ENUM we could just query QMetaEnum.
181 switch (t) {
182 case QJsonValue::Null:
183 return QStringLiteral("null");
184 case QJsonValue::Bool:
185 return QStringLiteral("bool");
186 case QJsonValue::Double:
187 return QStringLiteral("double");
188 case QJsonValue::String:
189 return QStringLiteral("string");
190 case QJsonValue::Array:
191 return QStringLiteral("array");
192 case QJsonValue::Object:
193 return QStringLiteral("object");
194 case QJsonValue::Undefined:
195 return QStringLiteral("undefined");
196 }
197 return QStringLiteral("unknown");
198 }
199
200 QString stringValue(const QJsonObject &obj, const QString &key)
201 {
202 if (!m_errorString.isEmpty())
203 return {};
204 QJsonValue v = obj.value(key);
205 if (v.isUndefined())
206 return {};
207 if (!checkType(v, t: QJsonValue::String, key))
208 return {};
209 return v.toString();
210 }
211
212 QStringList stringListValue(const QJsonObject &obj, const QString &key)
213 {
214 if (!m_errorString.isEmpty())
215 return {};
216 QJsonValue v = obj.value(key);
217 if (v.isUndefined())
218 return {};
219 if (!checkType(v, t: QJsonValue::Array, key))
220 return {};
221 return toStringList(v, key);
222 }
223
224 QStringList toStringList(const QJsonValue &v, const QString &key)
225 {
226 QStringList result;
227 const QJsonArray a = v.toArray();
228 result.reserve(alloc: a.count());
229 for (const QJsonValue &v : a) {
230 if (!v.isString()) {
231 m_errorString = FMT::tr(sourceText: "Unexpected type %1 in string array in key %2.")
232 .arg(args: jsonTypeName(t: v.type()), args: key);
233 return {};
234 }
235 result.append(t: v.toString());
236 }
237 return result;
238 }
239
240 QString &m_errorString;
241};
242
243Projects readProjectDescription(const QString &filePath, QString *errorString)
244{
245 const QJsonArray rawProjects = readRawProjectDescription(filePath, errorString);
246 if (!errorString->isEmpty())
247 return {};
248 ProjectConverter converter(errorString);
249 Projects result = converter.convertProjects(rawProjects);
250 if (!errorString->isEmpty())
251 return {};
252 return result;
253}
254

source code of qttools/src/linguist/shared/projectdescriptionreader.cpp