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