QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
JsonParsing.cc
Go to the documentation of this file.
1#include "JsonParsing.h"
2
3#include <limits>
4
5#include <QtCore/QApplicationStatic>
6#include <QtCore/QFileInfo>
7#include <QtCore/QJsonArray>
8#include <QtCore/QJsonParseError>
9#include <QtCore/QObject>
10#include <QtCore/QSet>
11#include <QtCore/QTranslator>
12
13#include "QGCCompression.h"
14#include "QGCLoggingCategory.h"
15
16QGC_LOGGING_CATEGORY(JsonParsingLog, "Utilities.Parsing.Json")
17
18namespace {
19
20QString jsonValueTypeToString(QJsonValue::Type type)
21{
22 struct TypeToString
23 {
24 QJsonValue::Type type;
25 const char* string;
26 };
27
28 static constexpr const TypeToString typeToStringMap[] = {
29 {QJsonValue::Null, "NULL"}, {QJsonValue::Bool, "Bool"}, {QJsonValue::Double, "Double"},
30 {QJsonValue::String, "String"}, {QJsonValue::Array, "Array"}, {QJsonValue::Object, "Object"},
31 {QJsonValue::Undefined, "Undefined"},
32 };
33
34 for (const TypeToString& entry : typeToStringMap) {
35 if (type == entry.type) {
36 return entry.string;
37 }
38 }
39
40 return QObject::tr("Unknown type: %1").arg(type);
41}
42
43} // namespace
44
45namespace JsonParsing {
46
47bool validateRequiredKeys(const QJsonObject& jsonObject, const QStringList& keys, QString& errorString)
48{
49 QString missingKeys;
50
51 for (const QString& key : keys) {
52 if (!jsonObject.contains(key)) {
53 if (!missingKeys.isEmpty()) {
54 missingKeys += QStringLiteral(", ");
55 }
56 missingKeys += key;
57 }
58 }
59
60 if (!missingKeys.isEmpty()) {
61 errorString = QObject::tr("The following required keys are missing: %1").arg(missingKeys);
62 return false;
63 }
64
65 return true;
66}
67
68bool validateKeyTypes(const QJsonObject& jsonObject, const QStringList& keys, const QList<QJsonValue::Type>& types,
69 QString& errorString)
70{
71 if (keys.count() != types.count()) {
72 errorString = QObject::tr("Mismatched key and type list sizes: keys=%1 types=%2")
73 .arg(keys.count()).arg(types.count());
74 return false;
75 }
76
77 for (qsizetype i = 0; i < types.count(); i++) {
78 const QString& valueKey = keys[i];
79 if (jsonObject.contains(valueKey)) {
80 const QJsonValue& jsonValue = jsonObject[valueKey];
81 if (types[i] == QJsonValue::Undefined) {
82 // Undefined signals any type is acceptable (e.g. "default" whose type follows the fact type).
83 continue;
84 }
85 if ((jsonValue.type() == QJsonValue::Double) && (types[i] == QJsonValue::Null)) {
86 // Null type signals a possible NaN on a double value.
87 continue;
88 }
89 if (jsonValue.type() != types[i]) {
90 errorString = QObject::tr("Incorrect value type - key:type:expected %1:%2:%3")
91 .arg(valueKey, jsonValueTypeToString(jsonValue.type()),
92 jsonValueTypeToString(types[i]));
93 return false;
94 }
95 }
96 }
97
98 return true;
99}
100
101bool isJsonFile(const QByteArray& bytes, QJsonDocument& jsonDoc, QString& errorString)
102{
103 QJsonParseError parseError;
104 jsonDoc = QGCCompression::parseCompressedJson(bytes, &parseError);
105
106 if (parseError.error == QJsonParseError::NoError) {
107 return true;
108 }
109
111 // parseError.offset refers to the decompressed text, so a slice of `bytes` would be garbage.
112 qCDebug(JsonParsingLog) << "Json read error (compressed input) offset" << parseError.offset;
113 } else {
114 const int startPos = qMax(0, parseError.offset - 100);
115 const int length = qMin(bytes.length() - startPos, 200);
116 qCDebug(JsonParsingLog) << "Json read error" << bytes.mid(startPos, length).constData();
117 }
118 errorString = QStringLiteral("%1 (offset %2)").arg(parseError.errorString()).arg(parseError.offset);
119
120 return false;
121}
122
123bool isJsonFile(const QString& fileName, QJsonDocument& jsonDoc, QString& errorString)
124{
125 const QByteArray jsonBytes = QGCCompression::readFile(fileName, &errorString);
126 if (jsonBytes.isEmpty() && !errorString.isEmpty()) {
127 jsonDoc = {};
128 return false;
129 }
130
131 return isJsonFile(jsonBytes, jsonDoc, errorString);
132}
133
134double possibleNaNJsonValue(const QJsonValue& value)
135{
136 if (value.type() == QJsonValue::Null) {
137 return std::numeric_limits<double>::quiet_NaN();
138 }
139
140 return value.toDouble();
141}
142
143bool validateKeys(const QJsonObject& jsonObject, const QList<KeyValidateInfo>& keyInfo, QString& errorString)
144{
145 QStringList keyList;
146 QList<QJsonValue::Type> typeList;
147
148 for (const KeyValidateInfo& info : keyInfo) {
149 if (info.required) {
150 keyList.append(info.key);
151 }
152 }
153 if (!validateRequiredKeys(jsonObject, keyList, errorString)) {
154 return false;
155 }
156
157 keyList.clear();
158 for (const KeyValidateInfo& info : keyInfo) {
159 keyList.append(info.key);
160 typeList.append(info.type);
161 }
162
163 return validateKeyTypes(jsonObject, keyList, typeList, errorString);
164}
165
166bool validateKeysStrict(const QJsonObject& jsonObject, const QList<KeyValidateInfo>& keyInfo, QString& errorString)
167{
168 if (!validateKeys(jsonObject, keyInfo, errorString)) {
169 return false;
170 }
171
172 QSet<QString> expectedKeys;
173 expectedKeys.reserve(keyInfo.size());
174 for (const KeyValidateInfo &info : keyInfo) {
175 expectedKeys.insert(QLatin1String(info.key));
176 }
177
178 for (const QString &key : jsonObject.keys()) {
179 if (!expectedKeys.contains(key)) {
180 errorString = QObject::tr("Unknown key: %1").arg(key);
181 return false;
182 }
183 }
184
185 return true;
186}
187
188} // namespace JsonParsing
189
190// ---------------------------------------------------------------------------
191// QGC json file header / translation utilities
192// ---------------------------------------------------------------------------
193
194namespace {
195
196constexpr const char *_translateKeysKey = "translateKeys";
197constexpr const char *_arrayIDKeysKey = "_arrayIDKeys";
198constexpr const char *_jsonGroundStationKey = "groundStation";
199constexpr const char *_jsonGroundStationValue = "QGroundControl";
200
201Q_APPLICATION_STATIC(QTranslator, s_jsonTranslator);
202
203QJsonObject translateObject(QJsonObject &jsonObject, const QString &translateContext, const QStringList &translateKeys);
204
205QJsonArray translateArray(QJsonArray &jsonArray, const QString &translateContext, const QStringList &translateKeys)
206{
207 for (qsizetype i = 0; i < jsonArray.count(); i++) {
208 QJsonObject childJsonObject = jsonArray[i].toObject();
209 jsonArray[i] = translateObject(childJsonObject, translateContext, translateKeys);
210 }
211 return jsonArray;
212}
213
214QJsonObject translateObject(QJsonObject &jsonObject, const QString &translateContext, const QStringList &translateKeys)
215{
216 for (const QString &key : jsonObject.keys()) {
217 if (jsonObject[key].isString()) {
218 QString locString = jsonObject[key].toString();
219 if (!translateKeys.contains(key)) {
220 continue;
221 }
222
223 QString disambiguation;
224 const QString disambiguationPrefix("#loc.disambiguation#");
225 if (locString.startsWith(disambiguationPrefix)) {
226 locString = locString.right(locString.length() - disambiguationPrefix.length());
227 const int commentEndIndex = locString.indexOf("#");
228 if (commentEndIndex != -1) {
229 disambiguation = locString.left(commentEndIndex);
230 locString = locString.right(locString.length() - disambiguation.length() - 1);
231 }
232 }
233
234 const QString xlatString = JsonParsing::translator()->translate(
235 translateContext.toUtf8().constData(),
236 locString.toUtf8().constData(),
237 disambiguation.toUtf8().constData());
238 if (!xlatString.isNull()) {
239 jsonObject[key] = xlatString;
240 }
241 } else if (jsonObject[key].isArray()) {
242 QJsonArray childJsonArray = jsonObject[key].toArray();
243 jsonObject[key] = translateArray(childJsonArray, translateContext, translateKeys);
244 } else if (jsonObject[key].isObject()) {
245 QJsonObject childJsonObject = jsonObject[key].toObject();
246 jsonObject[key] = translateObject(childJsonObject, translateContext, translateKeys);
247 }
248 }
249 return jsonObject;
250}
251
255QStringList resolveTranslateKeys(QJsonObject &jsonObject,
256 const QStringList &defaultTranslateKeys,
257 const QStringList &defaultArrayIDKeys)
258{
259 QString translateKeys;
260 if (jsonObject.contains(_translateKeysKey)) {
261 translateKeys = jsonObject[_translateKeysKey].toString();
262 } else if (!defaultTranslateKeys.isEmpty()) {
263 translateKeys = defaultTranslateKeys.join(",");
264 jsonObject[_translateKeysKey] = translateKeys;
265 }
266
267 if (!jsonObject.contains(_arrayIDKeysKey) && !defaultArrayIDKeys.isEmpty()) {
268 jsonObject[_arrayIDKeysKey] = defaultArrayIDKeys.join(",");
269 }
270
271 if (translateKeys.isEmpty()) {
272 return {};
273 }
274 return translateKeys.split(",");
275}
276
277} // namespace
278
279namespace JsonParsing {
280
281QTranslator *translator()
282{
283 return s_jsonTranslator();
284}
285
286void saveQGCJsonFileHeader(QJsonObject &jsonObject, const QString &fileType, int version)
287{
288 jsonObject[_jsonGroundStationKey] = _jsonGroundStationValue;
289 jsonObject[jsonFileTypeKey] = fileType;
290 jsonObject[jsonVersionKey] = version;
291}
292
293bool validateInternalQGCJsonFile(const QJsonObject &jsonObject, const QString &expectedFileType,
294 int minSupportedVersion, int maxSupportedVersion, int &version,
295 QString &errorString)
296{
297 static const QList<KeyValidateInfo> requiredKeys = {
298 {jsonFileTypeKey, QJsonValue::String, true},
299 {jsonVersionKey, QJsonValue::Double, true},
300 };
301
302 if (!validateKeys(jsonObject, requiredKeys, errorString)) {
303 return false;
304 }
305
306 const QString fileTypeValue = jsonObject[jsonFileTypeKey].toString();
307 if (fileTypeValue != expectedFileType) {
308 errorString = QObject::tr("Incorrect file type key expected:%1 actual:%2").arg(expectedFileType, fileTypeValue);
309 return false;
310 }
311
312 version = jsonObject[jsonVersionKey].toInt();
313 if (version < minSupportedVersion) {
314 errorString = QObject::tr("File version %1 is no longer supported").arg(version);
315 return false;
316 }
317
318 if (version > maxSupportedVersion) {
319 errorString = QObject::tr("File version %1 is newer than current supported version %2")
320 .arg(version)
321 .arg(maxSupportedVersion);
322 return false;
323 }
324
325 return true;
326}
327
328bool validateExternalQGCJsonFile(const QJsonObject &jsonObject, const QString &expectedFileType,
329 int minSupportedVersion, int maxSupportedVersion, int &version,
330 QString &errorString)
331{
332 static const QList<KeyValidateInfo> requiredKeys = {
333 {_jsonGroundStationKey, QJsonValue::String, true},
334 };
335
336 if (!validateKeys(jsonObject, requiredKeys, errorString)) {
337 return false;
338 }
339
340 return validateInternalQGCJsonFile(jsonObject, expectedFileType, minSupportedVersion, maxSupportedVersion, version,
342}
343
344QJsonObject openInternalQGCJsonFile(const QString &jsonFilename, const QString &expectedFileType,
345 int minSupportedVersion, int maxSupportedVersion, int &version,
346 QString &errorString,
347 const QStringList &defaultTranslateKeys,
348 const QStringList &defaultArrayIDKeys)
349{
350 QJsonDocument doc;
351 if (!isJsonFile(jsonFilename, doc, errorString)) {
352 errorString = QObject::tr("Unable to parse json file: %1 error: %2").arg(jsonFilename, errorString);
353 return {};
354 }
355
356 if (!doc.isObject()) {
357 errorString = QObject::tr("Root of json file is not object: %1").arg(jsonFilename);
358 return {};
359 }
360
361 QJsonObject jsonObject = doc.object();
362 const bool success = validateInternalQGCJsonFile(jsonObject, expectedFileType, minSupportedVersion,
363 maxSupportedVersion, version, errorString);
364 if (!success) {
365 errorString = QObject::tr("Json file: '%1'. %2").arg(jsonFilename, errorString);
366 return {};
367 }
368
369 const QStringList translateKeys = resolveTranslateKeys(jsonObject, defaultTranslateKeys, defaultArrayIDKeys);
370 const QString context = QFileInfo(jsonFilename).fileName();
371 return translateObject(jsonObject, context, translateKeys);
372}
373
374} // namespace JsonParsing
Q_APPLICATION_STATIC(ADSBVehicleManager, _adsbVehicleManager, SettingsManager::instance() ->adsbVehicleManagerSettings())
QString errorString
#define QGC_LOGGING_CATEGORY(name, categoryStr)
bool validateExternalQGCJsonFile(const QJsonObject &jsonObject, const QString &expectedFileType, int minSupportedVersion, int maxSupportedVersion, int &version, QString &errorString)
bool validateKeyTypes(const QJsonObject &jsonObject, const QStringList &keys, const QList< QJsonValue::Type > &types, QString &errorString)
bool validateInternalQGCJsonFile(const QJsonObject &jsonObject, const QString &expectedFileType, int minSupportedVersion, int maxSupportedVersion, int &version, QString &errorString)
bool validateKeys(const QJsonObject &jsonObject, const QList< KeyValidateInfo > &keyInfo, QString &errorString)
Validates that all required keys are present and that listed keys have the expected type.
bool isJsonFile(const QByteArray &bytes, QJsonDocument &jsonDoc, QString &errorString)
Determines whether an in-memory byte buffer contains parseable JSON content.
constexpr const char * jsonFileTypeKey
Definition JsonParsing.h:13
QJsonObject openInternalQGCJsonFile(const QString &jsonFilename, const QString &expectedFileType, int minSupportedVersion, int maxSupportedVersion, int &version, QString &errorString, const QStringList &defaultTranslateKeys, const QStringList &defaultArrayIDKeys)
double possibleNaNJsonValue(const QJsonValue &value)
Returns NaN if the value is null, or the value converted to double otherwise.
void saveQGCJsonFileHeader(QJsonObject &jsonObject, const QString &fileType, int version)
Saves the standard QGC file header (groundStation, fileType, version) into the json object.
bool validateKeysStrict(const QJsonObject &jsonObject, const QList< KeyValidateInfo > &keyInfo, QString &errorString)
Validates keys like validateKeys but also rejects any keys not listed in keyInfo.
QTranslator * translator()
Translator used by openInternalQGCJsonFile for localized strings.
constexpr const char * jsonVersionKey
Definition JsonParsing.h:12
bool validateRequiredKeys(const QJsonObject &jsonObject, const QStringList &keys, QString &errorString)
Validates that all listed keys are present in the object.
bool looksLikeCompressedData(const QByteArray &data)
QByteArray readFile(const QString &filePath, QString *errorString, qint64 maxBytes)
Read file contents, transparently decompressing .gz/.xz/.zst/.bz2/.lz4 files.
QJsonDocument parseCompressedJson(const QByteArray &data, QJsonParseError *error)
Parse JSON from data that may be compressed. Auto-detects gzip/xz/zstd/bzip2/lz4.