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 ((jsonValue.type() == QJsonValue::Double) && (types[i] == QJsonValue::Null)) {
82 // Null type signals a possible NaN on a double value.
83 continue;
84 }
85 if (jsonValue.type() != types[i]) {
86 errorString = QObject::tr("Incorrect value type - key:type:expected %1:%2:%3")
87 .arg(valueKey, jsonValueTypeToString(jsonValue.type()),
88 jsonValueTypeToString(types[i]));
89 return false;
90 }
91 }
92 }
93
94 return true;
95}
96
97bool isJsonFile(const QByteArray& bytes, QJsonDocument& jsonDoc, QString& errorString)
98{
99 QJsonParseError parseError;
100 jsonDoc = QGCCompression::parseCompressedJson(bytes, &parseError);
101
102 if (parseError.error == QJsonParseError::NoError) {
103 return true;
104 }
105
106 const int startPos = qMax(0, parseError.offset - 100);
107 const int length = qMin(bytes.length() - startPos, 200);
108 qCDebug(JsonParsingLog) << "Json read error" << bytes.mid(startPos, length).constData();
109 errorString = parseError.errorString();
110
111 return false;
112}
113
114bool isJsonFile(const QString& fileName, QJsonDocument& jsonDoc, QString& errorString)
115{
116 const QByteArray jsonBytes = QGCCompression::readFile(fileName, &errorString);
117 if (jsonBytes.isEmpty() && !errorString.isEmpty()) {
118 return false;
119 }
120
121 return isJsonFile(jsonBytes, jsonDoc, errorString);
122}
123
124double possibleNaNJsonValue(const QJsonValue& value)
125{
126 if (value.type() == QJsonValue::Null) {
127 return std::numeric_limits<double>::quiet_NaN();
128 }
129
130 return value.toDouble();
131}
132
133bool validateKeys(const QJsonObject& jsonObject, const QList<KeyValidateInfo>& keyInfo, QString& errorString)
134{
135 QStringList keyList;
136 QList<QJsonValue::Type> typeList;
137
138 for (const KeyValidateInfo& info : keyInfo) {
139 if (info.required) {
140 keyList.append(info.key);
141 }
142 }
143 if (!validateRequiredKeys(jsonObject, keyList, errorString)) {
144 return false;
145 }
146
147 keyList.clear();
148 for (const KeyValidateInfo& info : keyInfo) {
149 keyList.append(info.key);
150 typeList.append(info.type);
151 }
152
153 return validateKeyTypes(jsonObject, keyList, typeList, errorString);
154}
155
156bool validateKeysStrict(const QJsonObject& jsonObject, const QList<KeyValidateInfo>& keyInfo, QString& errorString)
157{
158 if (!validateKeys(jsonObject, keyInfo, errorString)) {
159 return false;
160 }
161
162 QSet<QString> expectedKeys;
163 expectedKeys.reserve(keyInfo.size());
164 for (const KeyValidateInfo &info : keyInfo) {
165 expectedKeys.insert(QLatin1String(info.key));
166 }
167
168 for (const QString &key : jsonObject.keys()) {
169 if (!expectedKeys.contains(key)) {
170 errorString = QStringLiteral("Unknown key: %1").arg(key);
171 return false;
172 }
173 }
174
175 return true;
176}
177
178} // namespace JsonParsing
179
180// ---------------------------------------------------------------------------
181// QGC json file header / translation utilities
182// ---------------------------------------------------------------------------
183
184namespace {
185
186constexpr const char *_translateKeysKey = "translateKeys";
187constexpr const char *_arrayIDKeysKey = "_arrayIDKeys";
188constexpr const char *_jsonGroundStationKey = "groundStation";
189constexpr const char *_jsonGroundStationValue = "QGroundControl";
190
191Q_APPLICATION_STATIC(QTranslator, s_jsonTranslator);
192
193QJsonObject translateObject(QJsonObject &jsonObject, const QString &translateContext, const QStringList &translateKeys);
194
195QJsonArray translateArray(QJsonArray &jsonArray, const QString &translateContext, const QStringList &translateKeys)
196{
197 for (qsizetype i = 0; i < jsonArray.count(); i++) {
198 QJsonObject childJsonObject = jsonArray[i].toObject();
199 jsonArray[i] = translateObject(childJsonObject, translateContext, translateKeys);
200 }
201 return jsonArray;
202}
203
204QJsonObject translateObject(QJsonObject &jsonObject, const QString &translateContext, const QStringList &translateKeys)
205{
206 for (const QString &key : jsonObject.keys()) {
207 if (jsonObject[key].isString()) {
208 QString locString = jsonObject[key].toString();
209 if (!translateKeys.contains(key)) {
210 continue;
211 }
212
213 QString disambiguation;
214 const QString disambiguationPrefix("#loc.disambiguation#");
215 if (locString.startsWith(disambiguationPrefix)) {
216 locString = locString.right(locString.length() - disambiguationPrefix.length());
217 const int commentEndIndex = locString.indexOf("#");
218 if (commentEndIndex != -1) {
219 disambiguation = locString.left(commentEndIndex);
220 locString = locString.right(locString.length() - disambiguation.length() - 1);
221 }
222 }
223
224 const QString xlatString = JsonParsing::translator()->translate(
225 translateContext.toUtf8().constData(),
226 locString.toUtf8().constData(),
227 disambiguation.toUtf8().constData());
228 if (!xlatString.isNull()) {
229 jsonObject[key] = xlatString;
230 }
231 } else if (jsonObject[key].isArray()) {
232 QJsonArray childJsonArray = jsonObject[key].toArray();
233 jsonObject[key] = translateArray(childJsonArray, translateContext, translateKeys);
234 } else if (jsonObject[key].isObject()) {
235 QJsonObject childJsonObject = jsonObject[key].toObject();
236 jsonObject[key] = translateObject(childJsonObject, translateContext, translateKeys);
237 }
238 }
239 return jsonObject;
240}
241
245QStringList resolveTranslateKeys(QJsonObject &jsonObject,
246 const QStringList &defaultTranslateKeys,
247 const QStringList &defaultArrayIDKeys)
248{
249 QString translateKeys;
250 if (jsonObject.contains(_translateKeysKey)) {
251 translateKeys = jsonObject[_translateKeysKey].toString();
252 } else if (!defaultTranslateKeys.isEmpty()) {
253 translateKeys = defaultTranslateKeys.join(",");
254 jsonObject[_translateKeysKey] = translateKeys;
255 }
256
257 if (!jsonObject.contains(_arrayIDKeysKey) && !defaultArrayIDKeys.isEmpty()) {
258 jsonObject[_arrayIDKeysKey] = defaultArrayIDKeys.join(",");
259 }
260
261 if (translateKeys.isEmpty()) {
262 return {};
263 }
264 return translateKeys.split(",");
265}
266
267} // namespace
268
269namespace JsonParsing {
270
271QTranslator *translator()
272{
273 return s_jsonTranslator();
274}
275
276void saveQGCJsonFileHeader(QJsonObject &jsonObject, const QString &fileType, int version)
277{
278 jsonObject[_jsonGroundStationKey] = _jsonGroundStationValue;
279 jsonObject[jsonFileTypeKey] = fileType;
280 jsonObject[jsonVersionKey] = version;
281}
282
283bool validateInternalQGCJsonFile(const QJsonObject &jsonObject, const QString &expectedFileType,
284 int minSupportedVersion, int maxSupportedVersion, int &version,
285 QString &errorString)
286{
287 static const QList<KeyValidateInfo> requiredKeys = {
288 {jsonFileTypeKey, QJsonValue::String, true},
289 {jsonVersionKey, QJsonValue::Double, true},
290 };
291
292 if (!validateKeys(jsonObject, requiredKeys, errorString)) {
293 return false;
294 }
295
296 const QString fileTypeValue = jsonObject[jsonFileTypeKey].toString();
297 if (fileTypeValue != expectedFileType) {
298 errorString = QObject::tr("Incorrect file type key expected:%1 actual:%2").arg(expectedFileType, fileTypeValue);
299 return false;
300 }
301
302 version = jsonObject[jsonVersionKey].toInt();
303 if (version < minSupportedVersion) {
304 errorString = QObject::tr("File version %1 is no longer supported").arg(version);
305 return false;
306 }
307
308 if (version > maxSupportedVersion) {
309 errorString = QObject::tr("File version %1 is newer than current supported version %2")
310 .arg(version)
311 .arg(maxSupportedVersion);
312 return false;
313 }
314
315 return true;
316}
317
318bool validateExternalQGCJsonFile(const QJsonObject &jsonObject, const QString &expectedFileType,
319 int minSupportedVersion, int maxSupportedVersion, int &version,
320 QString &errorString)
321{
322 static const QList<KeyValidateInfo> requiredKeys = {
323 {_jsonGroundStationKey, QJsonValue::String, true},
324 };
325
326 if (!validateKeys(jsonObject, requiredKeys, errorString)) {
327 return false;
328 }
329
330 return validateInternalQGCJsonFile(jsonObject, expectedFileType, minSupportedVersion, maxSupportedVersion, version,
332}
333
334QJsonObject openInternalQGCJsonFile(const QString &jsonFilename, const QString &expectedFileType,
335 int minSupportedVersion, int maxSupportedVersion, int &version,
336 QString &errorString,
337 const QStringList &defaultTranslateKeys,
338 const QStringList &defaultArrayIDKeys)
339{
340 const QByteArray bytes = QGCCompression::readFile(jsonFilename, &errorString);
341 if (bytes.isEmpty() && !errorString.isEmpty()) {
342 return {};
343 }
344
345 QJsonParseError jsonParseError;
346 const QJsonDocument doc = QGCCompression::parseCompressedJson(bytes, &jsonParseError);
347 if (jsonParseError.error != QJsonParseError::NoError) {
348 errorString = QObject::tr("Unable to parse json file: %1 error: %2 offset: %3")
349 .arg(jsonFilename, jsonParseError.errorString())
350 .arg(jsonParseError.offset);
351 return {};
352 }
353
354 if (!doc.isObject()) {
355 errorString = QObject::tr("Root of json file is not object: %1").arg(jsonFilename);
356 return {};
357 }
358
359 QJsonObject jsonObject = doc.object();
360 const bool success = validateInternalQGCJsonFile(jsonObject, expectedFileType, minSupportedVersion,
361 maxSupportedVersion, version, errorString);
362 if (!success) {
363 errorString = QObject::tr("Json file: '%1'. %2").arg(jsonFilename, errorString);
364 return {};
365 }
366
367 const QStringList translateKeys = resolveTranslateKeys(jsonObject, defaultTranslateKeys, defaultArrayIDKeys);
368 const QString context = QFileInfo(jsonFilename).fileName();
369 return translateObject(jsonObject, context, translateKeys);
370}
371
372} // 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.
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.